¿Es posible leer desde un InputStream con un tiempo de espera?

Resuelto asked hace 15 años • 0 respuestas

Específicamente, el problema es escribir un método como este:

int maybeRead(InputStream in, long timeout)

donde el valor de retorno es el mismo que in.read() si los datos están disponibles dentro de los milisegundos de 'tiempo de espera' y -2 en caso contrario. Antes de que el método regrese, todos los subprocesos generados deben salir.

Para evitar discusiones, el tema aquí es java.io.InputStream, según lo documentado por Sun (cualquier versión de Java). Tenga en cuenta que esto no es tan simple como parece. A continuación se muestran algunos datos respaldados directamente por la documentación de Sun.

  1. El método in.read() puede no ser interrumpible.

  2. Envolver el InputStream en un Reader o InterruptibleChannel no ayuda, porque lo único que esas clases pueden hacer es llamar a los métodos del InputStream. Si fuera posible utilizar esas clases, sería posible escribir una solución que simplemente ejecute la misma lógica directamente en InputStream.

  3. Siempre es aceptable que in.available() devuelva 0.

  4. El método in.close() puede bloquear o no hacer nada.

  5. No existe una forma general de eliminar otro hilo.

 avatar Apr 30 '09 07:04
Aceptado

Usando inputStream.disponible()

Siempre es aceptable que System.in.available() devuelva 0.

Descubrí lo contrario: siempre devuelve el mejor valor para la cantidad de bytes disponibles. Javadoc para InputStream.available():

Returns an estimate of the number of bytes that can be read (or skipped over) 
from this input stream without blocking by the next invocation of a method for 
this input stream.

Es inevitable realizar una estimación debido a cuestiones de tiempo o estancamiento. La cifra puede ser una subestimación puntual porque constantemente llegan nuevos datos. Sin embargo, siempre se "pone al día" en la siguiente llamada; debe tener en cuenta todos los datos llegados, excepto los que llegan justo en el momento de la nueva llamada. Devolver permanentemente 0 cuando hay datos no cumple la condición anterior.

Primera advertencia: las subclases concretas de InputStream son responsables de disponible()

InputStreames una clase abstracta. No tiene fuente de datos. No tiene sentido que tenga datos disponibles. Por lo tanto, javadoc para available()también establece:

The available method for class InputStream always returns 0.

This method should be overridden by subclasses.

Y, de hecho, las clases concretas del flujo de entrada anulan la disponibilidad(), proporcionando valores significativos, no ceros constantes.

Segunda advertencia: asegúrese de utilizar el retorno de carro al escribir datos en Windows.

Si usa System.in, su programa solo recibe información cuando su shell de comandos la entrega. Si está utilizando redirección/canalizaciones de archivos (por ejemplo, algún archivo > java myJavaApp o algún comando | java myJavaApp), los datos de entrada generalmente se entregan inmediatamente. Sin embargo, si ingresa datos manualmente, la transferencia de datos puede retrasarse. Por ejemplo, con el shell cmd.exe de Windows, los datos se almacenan en el shell cmd.exe. Los datos solo se pasan al programa Java en ejecución después del retorno de carro (control-m o <enter>). Esa es una limitación del entorno de ejecución. Por supuesto, InputStream.available() devolverá 0 mientras el shell almacene los datos en el buffer; ese es el comportamiento correcto; no hay datos disponibles en ese momento. Tan pronto como los datos estén disponibles desde el shell, el método devuelve un valor > 0. NB: Cygwin también usa cmd.exe.

La solución más sencilla (sin bloqueo, por lo que no se requiere tiempo de espera)

Solo usa esto:

    byte[] inputData = new byte[1024];
    int result = is.read(inputData, 0, is.available());  
    // result will indicate number of bytes read; -1 for EOF with no data read.

O equivalente,

    BufferedReader br = new BufferedReader(new InputStreamReader(System.in, Charset.forName("ISO-8859-1")),1024);
    // ...
         // inside some iteration / processing logic:
         if (br.ready()) {
             int readCount = br.read(inputData, bufferOffset, inputData.length-bufferOffset);
         }

Solución más rica (llena al máximo el búfer dentro del período de tiempo de espera)

Declara esto:

public static int readInputStreamWithTimeout(InputStream is, byte[] b, int timeoutMillis)
     throws IOException  {
     int bufferOffset = 0;
     long maxTimeMillis = System.currentTimeMillis() + timeoutMillis;
     while (System.currentTimeMillis() < maxTimeMillis && bufferOffset < b.length) {
         int readLength = java.lang.Math.min(is.available(),b.length-bufferOffset);
         // can alternatively use bufferedReader, guarded by isReady():
         int readResult = is.read(b, bufferOffset, readLength);
         if (readResult == -1) break;
         bufferOffset += readResult;
     }
     return bufferOffset;
 }

Entonces usa esto:

    byte[] inputData = new byte[1024];
    int readCount = readInputStreamWithTimeout(System.in, inputData, 6000);  // 6 second timeout
    // readCount will indicate number of bytes read; -1 for EOF with no data read.
Glen Best avatar May 01 '2013 06:05 Glen Best

Suponiendo que su transmisión no esté respaldada por un socket (por lo que no puede usar Socket.setSoTimeout()), creo que la forma estándar de resolver este tipo de problema es usar un Future.

Supongamos que tengo el siguiente ejecutor y transmisiones:

    ExecutorService executor = Executors.newFixedThreadPool(2);
    final PipedOutputStream outputStream = new PipedOutputStream();
    final PipedInputStream inputStream = new PipedInputStream(outputStream);

Tengo un escritor que escribe algunos datos y luego espera 5 segundos antes de escribir el último dato y cerrar la transmisión:

    Runnable writeTask = new Runnable() {
        @Override
        public void run() {
            try {
                outputStream.write(1);
                outputStream.write(2);
                Thread.sleep(5000);
                outputStream.write(3);
                outputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };
    executor.submit(writeTask);

La forma normal de leer esto es la siguiente. La lectura se bloqueará indefinidamente para los datos, por lo que se completa en 5 segundos:

    long start = currentTimeMillis();
    int readByte = 1;
    // Read data without timeout
    while (readByte >= 0) {
        readByte = inputStream.read();
        if (readByte >= 0)
            System.out.println("Read: " + readByte);
    }
    System.out.println("Complete in " + (currentTimeMillis() - start) + "ms");

que produce:

Read: 1
Read: 2
Read: 3
Complete in 5001ms

Si hubiera un problema más fundamental, como que el escritor no respondiera, el lector se bloquearía para siempre. Si finalizo la lectura en el futuro, puedo controlar el tiempo de espera de la siguiente manera:

    int readByte = 1;
    // Read data with timeout
    Callable<Integer> readTask = new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            return inputStream.read();
        }
    };
    while (readByte >= 0) {
        Future<Integer> future = executor.submit(readTask);
        readByte = future.get(1000, TimeUnit.MILLISECONDS);
        if (readByte >= 0)
            System.out.println("Read: " + readByte);
    }

que produce:

Read: 1
Read: 2
Exception in thread "main" java.util.concurrent.TimeoutException
    at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:228)
    at java.util.concurrent.FutureTask.get(FutureTask.java:91)
    at test.InputStreamWithTimeoutTest.main(InputStreamWithTimeoutTest.java:74)

Puedo detectar TimeoutException y hacer la limpieza que quiera.

Ian Jones avatar Mar 23 '2012 00:03 Ian Jones

Si su InputStream está respaldado por un Socket, puede establecer un tiempo de espera de Socket (en milisegundos) usando setSoTimeout . Si la llamada read() no se desbloquea dentro del tiempo de espera especificado, generará una excepción SocketTimeoutException.

Solo asegúrese de llamar a setSoTimeout en el Socket antes de realizar la llamada read().

Templar avatar Sep 06 '2011 05:09 Templar

Cuestionaría el planteamiento del problema en lugar de aceptarlo ciegamente. Sólo necesitas tiempos de espera desde la consola o a través de la red. Si tiene estos últimos Socket.setSoTimeout()y HttpURLConnection.setReadTimeout()ambos hacen exactamente lo que se requiere, siempre y cuando los configure correctamente cuando los construya/adquiera. Dejarlo en un punto arbitrario más adelante en la aplicación, cuando todo lo que tiene es InputStream, es un diseño deficiente que conduce a una implementación muy incómoda.

user207421 avatar Oct 19 '2011 23:10 user207421

No he usado las clases del paquete Java NIO, pero parece que podrían ser de alguna ayuda aquí. Específicamente, java.nio.channels.Channels y java.nio.channels.InterruptibleChannel .

jt. avatar Apr 30 '2009 01:04 jt.