167 votes

Est-il possible de lire à partir d'un InputStream avec un délai d'attente ?

Plus précisément, le problème consiste à écrire une méthode comme celle-ci :

int maybeRead(InputStream in, long timeout)

où la valeur de retour est la même que in.read() si les données sont disponibles dans un délai de 'timeout' millisecondes, et -2 sinon. Avant le retour de la méthode, tous les threads créés doivent se terminer.

Pour éviter les disputes, le sujet ici java.io.InputStream, tel que documenté par Sun (toute version de Java). Veuillez noter que ce n'est pas aussi simple qu'il n'y paraît. Vous trouverez ci-dessous quelques faits qui sont directement soutenus par la documentation de Sun.

  1. La méthode in.read() peut être non interruptible.

  2. Envelopper l'InputStream dans un Reader ou un InterruptibleChannel ne sert à rien, car ces classes ne peuvent qu'appeler les méthodes de l'InputStream. S'il était possible d'utiliser ces classes, il serait possible d'écrire une solution qui exécute la même logique directement sur l'InputStream.

  3. Il est toujours acceptable que in.available() renvoie 0.

  4. La méthode in.close() peut se bloquer ou ne rien faire.

  5. Il n'y a pas de manière générale de tuer un autre fil.

93voto

Glen Best Points 11455

Utilisation de inputStream.available()

Il est toujours acceptable que System.in.available() renvoie 0.

J'ai constaté le contraire - il renvoie toujours la meilleure valeur pour le nombre d'octets disponibles. Javadoc pour 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.

Une estimation est inévitable en raison du calendrier/de l'instabilité. Le chiffre peut être une sous-estimation ponctuelle car de nouvelles données arrivent constamment. Cependant, il se "rattrape" toujours lors de l'appel suivant - il devrait tenir compte de toutes les données arrivées, sauf celles qui arrivent juste au moment du nouvel appel. Le fait de renvoyer en permanence 0 lorsqu'il y a des données ne remplit pas la condition ci-dessus.

Première mise en garde : les sous-classes concrètes de InputStream sont responsables de available()

InputStream est une classe abstraite. Elle n'a pas de source de données. Il est inutile qu'elle ait des données disponibles. Par conséquent, la javadoc pour available() déclare également :

The available method for class InputStream always returns 0.

This method should be overridden by subclasses.

Et en effet, les classes de flux d'entrée concrètes surchargent available(), fournissant des valeurs significatives, et non des 0 constants.

Deuxième mise en garde : veillez à utiliser le retour chariot lorsque vous saisissez des données sous Windows.

Si vous utilisez System.in votre programme ne reçoit des entrées que lorsque votre interpréteur de commandes vous les transmet. Si vous utilisez des redirections de fichiers ou des pipelines (par exemple, somefile > java myJavaApp ou somecommand | java myJavaApp ), les données d'entrée sont généralement transmises immédiatement. Toutefois, si vous tapez manuellement les données d'entrée, le transfert des données peut être retardé. Par exemple, avec l'interpréteur de commandes Windows cmd.exe, les données sont mises en mémoire tampon dans l'interpréteur de commandes cmd.exe. Les données ne sont transmises au programme java en cours d'exécution qu'après un retour de chariot (control-m ou <enter> ). C'est une limitation de l'environnement d'exécution. Bien sûr, InputStream.available() renvoie 0 tant que le shell met les données en mémoire tampon - c'est un comportement correct ; il n'y a pas de données disponibles à ce moment-là. Dès que les données sont disponibles auprès du shell, la méthode renvoie une valeur > 0. NB : Cygwin utilise également cmd.exe.

Solution la plus simple (pas de blocage, donc pas de délai d'attente nécessaire)

Utilisez juste ça :

    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.

OU de manière équivalente,

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

Solution plus riche (remplit au maximum le tampon dans le délai imparti)

Déclarez ceci :

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

Alors utilisez ceci :

    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.

72voto

Ian Jones Points 886

En supposant que votre flux n'est pas soutenu par une socket (vous ne pouvez donc pas utiliser la fonction Socket.setSoTimeout() ), je pense que la manière standard de résoudre ce type de problème est d'utiliser un Future.

Supposons que j'ai l'exécuteur et les flux suivants :

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

J'ai un rédacteur qui écrit des données puis attend 5 secondes avant d'écrire la dernière donnée et de fermer le flux :

    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 façon normale de lire ceci est la suivante. La lecture se bloque indéfiniment pour les données et se termine donc en 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");

qui sort :

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

S'il y avait un problème plus fondamental, comme le fait que l'auteur ne réponde pas, le lecteur bloquerait pour toujours. Si j'enveloppe la lecture dans un futur, je peux alors contrôler le timeout comme suit :

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

qui sort :

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)

Je peux attraper l'exception TimeoutException et faire le nettoyage que je veux.

24voto

Templar Points 2164

Si votre InputStream est soutenu par un Socket, vous pouvez définir un délai d'attente pour le Socket (en millisecondes) en utilisant la fonction setSoTimeout . Si l'appel read() ne se débloque pas dans le délai spécifié, il lèvera une SocketTimeoutException.

Assurez-vous simplement que vous appelez setSoTimeout sur le Socket avant d'effectuer l'appel read().

17voto

EJP Points 113412

Je remettrais en question l'énoncé du problème plutôt que de l'accepter aveuglément. Vous n'avez besoin de délais d'attente que depuis la console ou sur le réseau. Dans ce dernier cas, vous avez Socket.setSoTimeout() et HttpURLConnection.setReadTimeout() qui font tous deux exactement ce qui est requis, tant que vous les configurez correctement lorsque vous les construisez/acquérez. Laisser cela à un point arbitraire plus tard dans l'application quand tout ce que vous avez est l'InputStream est une mauvaise conception qui conduit à une mise en œuvre très maladroite.

6voto

jt. Points 3116

Je n'ai pas utilisé les classes du paquet NIO de Java, mais il semble ils pourraient être d'une certaine aide ici. Plus précisément, java.nio.channels.Canaux et java.nio.channels.InterruptibleChannel (canal interruptible) .

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X