È ansible leggere da un InputStream con un timeout?

In particolare, il problema è scrivere un metodo come questo:

int maybeRead(InputStream in, long timeout) 

dove il valore di ritorno è uguale a in.read () se i dati sono disponibili entro millisecondi “timeout” e -2 in caso contrario. Prima che il metodo ritorni, tutti i thread spawn devono uscire.

Per evitare argomenti, l’argomento qui java.io.InputStream, come documentato da Sun (qualsiasi versione di Java). Si prega di notare che questo non è così semplice come sembra. Di seguito sono riportati alcuni fatti supportati direttamente dalla documentazione di Sun.

  1. Il metodo in.read () può essere non interrompibile.

  2. Avvolgere InputStream in un Reader o InterruptibleChannel non aiuta, perché tutte quelle classi possono fare i metodi call di InputStream. Se fosse ansible utilizzare tali classi, sarebbe ansible scrivere una soluzione che esegue la stessa logica direttamente su InputStream.

  3. È sempre accettabile che in.available () restituisca 0.

  4. Il metodo in.close () può bloccare o non fare nulla.

  5. Non esiste un modo generale per uccidere un altro thread.

Utilizzo di inputStream.available ()

È sempre accettabile che System.in.available () restituisca 0.

Ho trovato il contrario: restituisce sempre il valore migliore per il numero di byte disponibili. Javadoc per 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. 

Una stima è inevitabile a causa di tempismo / intorpidimento. La cifra può essere sottovalutata una tantum perché i nuovi dati arrivano costantemente. Tuttavia, sempre “recupera” alla prossima chiamata: dovrebbe tenere conto di tutti i dati arrivati, escludendo quelli che arrivano proprio al momento della nuova chiamata. Restituisce in modo permanente 0 quando ci sono dati che non soddisfano la condizione di cui sopra.

Primo avvertimento: le sottoclassi concrete di InputStream sono responsabili della disponibilità ()

InputStream è una class astratta. Non ha una fonte di dati. Non ha senso che abbia dati disponibili. Quindi, javadoc per available() indica anche:

 The available method for class InputStream always returns 0. This method should be overridden by subclasss. 

In effetti, le classi concrete del stream di input sostituiscono available (), fornendo valori significativi, non 0s costanti.

Secondo avvertimento: assicurarsi di utilizzare carriage-return quando si digita l’input in Windows.

Se si utilizza System.in , il programma riceve solo input quando la shell di comando lo consegna. Se stai usando il reindirizzamento dei file / pipe (ad esempio somefile> java myJavaApp o somecommand | java myJavaApp), i dati di input vengono di solito consegnati immediatamente. Tuttavia, se si digita manualmente l’input, la consegna dei dati può essere ritardata. Ad esempio con la shell di windows cmd.exe, i dati vengono memorizzati nella shell di cmd.exe. I dati vengono passati al programma java in esecuzione dopo il carriage-return (control-m o ). Questa è una limitazione dell’ambiente di esecuzione. Ovviamente, InputStream.available () restituirà 0 per tutto il tempo in cui la shell bufferizza i dati – questo è il comportamento corretto; non ci sono dati disponibili a quel punto. Non appena i dati sono disponibili dalla shell, il metodo restituisce un valore> 0. Nota: Cygwin utilizza anche cmd.exe.

La soluzione più semplice (nessun blocco, quindi nessun timeout richiesto)

Basta usare questo:

  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 equivalentemente,

  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); } 

Soluzione più ricca (riempie il buffer al massimo entro il periodo di timeout)

Dichiara questo:

 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; } 

Quindi utilizzare questo:

  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. 

Supponendo che il tuo stream non sia supportato da un socket (quindi non puoi usare Socket.setSoTimeout() ), penso che il modo standard per risolvere questo tipo di problema sia usare un futuro.

Supponiamo che abbia il seguente esecutore e stream:

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

Ho uno scrittore che scrive alcuni dati quindi aspetta 5 secondi prima di scrivere l’ultimo pezzo di dati e chiudere lo stream:

  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); 

Il modo normale di leggere questo è il seguente. La lettura bloccherà a tempo indefinito per i dati e quindi questo si completa in 5s:

  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"); 

quali uscite:

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

Se ci fosse un problema più fondamentale, come se lo scrittore non rispondesse, il lettore bloccherebbe per sempre. Se avvolgo la lettura in un futuro, posso quindi controllare il timeout come segue:

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

quali uscite:

 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) 

Posso prendere il TimeoutException e fare qualsiasi operazione di pulizia che voglio.

Vorrei mettere in discussione la dichiarazione del problema piuttosto che accettarla alla cieca. Hai solo bisogno di timeout dalla console o dalla rete. Se si dispone di Socket.setSoTimeout() e HttpURLConnection.setReadTimeout() entrambi eseguono esattamente ciò che è necessario, a condizione che vengano impostati correttamente quando li si costruisce / acquisisce. Lasciandolo in un punto arbitrario più avanti nell’applicazione quando tutto ciò che si ha è InputStream è un progetto scadente che porta a un’implementazione molto complessa.

Se InputStream è supportato da un Socket, è ansible impostare un timeout Socket (in millisecondi) utilizzando setSoTimeout . Se la chiamata read () non si sblocca entro il timeout specificato, verrà lanciata una SocketTimeoutException.

Assicurati di chiamare setSoTimeout sul socket prima di effettuare la chiamata a read ().

Non ho usato le classi dal pacchetto NIO Java, ma sembra che potrebbero esserci di aiuto qui. Nello specifico, java.nio.channels.Channels e java.nio.channels.InterruptibleChannel .

Ecco un modo per ottenere un NIO FileChannel da System.in e verificare la disponibilità dei dati utilizzando un timeout, che è un caso speciale del problema descritto nella domanda. Eseguirlo sulla console, non digitare alcun input e attendere i risultati. È stato testato con successo su Java 6 su Windows e Linux.

 import java.io.FileInputStream; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.nio.channels.ClosedByInterruptException; public class Main { static final ByteBuffer buf = ByteBuffer.allocate(4096); public static void main(String[] args) { long timeout = 1000 * 5; try { InputStream in = extract(System.in); if (! (in instanceof FileInputStream)) throw new RuntimeException( "Could not extract a FileInputStream from STDIN."); try { int ret = maybeAvailable((FileInputStream)in, timeout); System.out.println( Integer.toString(ret) + " bytes were read."); } finally { in.close(); } } catch (Exception e) { throw new RuntimeException(e); } } /* unravels all layers of FilterInputStream wrappers to get to the * core InputStream */ public static InputStream extract(InputStream in) throws NoSuchFieldException, IllegalAccessException { Field f = FilterInputStream.class.getDeclaredField("in"); f.setAccessible(true); while( in instanceof FilterInputStream ) in = (InputStream)f.get((FilterInputStream)in); return in; } /* Returns the number of bytes which could be read from the stream, * timing out after the specified number of milliseconds. * Returns 0 on timeout (because no bytes could be read) * and -1 for end of stream. */ public static int maybeAvailable(final FileInputStream in, long timeout) throws IOException, InterruptedException { final int[] dataReady = {0}; final IOException[] maybeException = {null}; final Thread reader = new Thread() { public void run() { try { dataReady[0] = in.getChannel().read(buf); } catch (ClosedByInterruptException e) { System.err.println("Reader interrupted."); } catch (IOException e) { maybeException[0] = e; } } }; Thread interruptor = new Thread() { public void run() { reader.interrupt(); } }; reader.start(); for(;;) { reader.join(timeout); if (!reader.isAlive()) break; interruptor.start(); interruptor.join(1000); reader.join(1000); if (!reader.isAlive()) break; System.err.println("We're hung"); System.exit(1); } if ( maybeException[0] != null ) throw maybeException[0]; return dataReady[0]; } } 

È interessante notare che, quando si esegue il programma in NetBeans 6.5 anziché nella console, il timeout non funziona affatto e la chiamata a System.exit () è effettivamente necessaria per terminare i thread di zombie. Quello che succede è che il thread dell’interruttore blocca (!) Sulla chiamata a reader.interrupt (). Un altro programma di test (non mostrato qui) tenta inoltre di chiudere il canale, ma non funziona neanche.

Come ho detto, NIO è la soluzione migliore (e corretta). Se sei davvero bloccato con un InputStream, potresti farlo entrambi

  1. Crea un thread il cui lavoro esclusivo è leggere da InputStream e mettere il risultato in un buffer che può essere letto dalla tua discussione originale senza bloccare. Questo dovrebbe funzionare bene se hai sempre una sola istanza dello stream. Altrimenti potresti essere in grado di uccidere il thread utilizzando i metodi deprecati nella class Thread, sebbene ciò possa causare perdite di risorse.

  2. Affidatevi a IsDisponibile per indicare i dati che possono essere letti senza bloccare. Tuttavia in alcuni casi (come con Sockets) può richiedere una lettura potenzialmente bloccante per isAvailable per segnalare qualcosa di diverso da 0.