web-dev-qa-db-fra.com

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 le délai d'expiration "milliseconds" et -2 dans le cas contraire. Avant le retour de la méthode, tous les threads générés doivent quitter.

Pour éviter les arguments, le sujet ici est Java.io.InputStream, documenté par Sun (any Java)). Veuillez noter que ce n'est pas aussi simple qu'il y paraît. Vous trouverez ci-dessous certains faits pris en charge. directement par la documentation de Sun.

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

  2. Envelopper le InputStream dans un Reader ou dans InterruptibleChannel n'aide pas, car toutes ces classes peuvent faire, c'est appeler les méthodes du InputStream. S'il était possible d'utiliser ces classes, il serait possible d'écrire une solution qui exécute simplement la même logique directement sur InputStream.

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

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

  5. Il n'y a pas de moyen général de tuer un autre thread.

140
JKL

tilisation de inputStream.available ()

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

J'ai trouvé 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 moment opportun. Le chiffre peut être une sous-estimation ponctuelle car de nouvelles données arrivent constamment. Cependant, il "rattrape" toujours le prochain appel - il devrait prendre en compte toutes les données arrivées, sauf celles qui arrivent au moment du nouvel appel. Renvoyer définitivement 0 lorsqu'il y a des données échoue à 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. Il n'a pas de source de données. Cela n'a pas de sens d'avoir des données disponibles. Par conséquent, javadoc for available() indique é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 substituent effectivement available (), fournissant des valeurs significatives, et non des 0 constants.

Deuxième mise en garde: Assurez-vous d'utiliser le retour chariot lors de la saisie avec Windows.

Si vous utilisez System.in, Votre programme ne reçoit une entrée que lorsque votre commande Shell le lui transmet. Si vous utilisez une redirection de fichiers/tubes (par exemple, un fichier> Java monJavaApp ou une de ses commandes | Java monJavaApp)], les données d'entrée sont généralement immédiatement transmises. Cependant, Par exemple, avec Windows cmd.exe Shell, les données sont mises en mémoire tampon dans le cmd.exe Shell. Les données ne sont transmises qu'au programme en cours d’exécution Java). suite au retour-chariot (control-m ou <enter>). C'est une limitation de l'environnement d'exécution. Bien sûr, InputStream.available () retournera 0 tant que le shell tamponnera les données - c'est un comportement correct; il n'y a pas de données disponibles à ce stade. Dès que les données sont disponibles à partir du shell, la méthode renvoie une valeur> 0. NB: Cygwin utilise également cmd.exe.

La solution la plus simple (pas de blocage, donc pas de délai d'attente requis)

Il suffit d'utiliser ceci:

    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 équivalent,

    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 la mémoire 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.
79
Glen Best

En supposant que votre flux ne soit pas sauvegardé par un socket (vous ne pouvez donc pas utiliser Socket.setSoTimeout()), je pense que la façon habituelle de résoudre ce type de problème consiste à utiliser un futur.

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 écrivain qui écrit des données puis attend 5 secondes avant d'écrire le dernier élément de données 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 bloque indéfiniment pour les données et se termine en 5 secondes:

    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 produit:

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

S'il y avait un problème plus fondamental, tel que l'auteur ne répondait pas, le lecteur bloquerait pour toujours. Si j'emballe la lecture dans un futur, je peux alors contrôler le délai d'attente 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 produit:

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 la TimeoutException et faire le nettoyage que je veux.

65
Ian Jones

Je remettrais en question l'énoncé du problème plutôt que de l'accepter aveuglément. Vous n'avez besoin que de délais d'attente à partir de la console ou du réseau. Si ce dernier vous avez Socket.setSoTimeout() et HttpURLConnection.setReadTimeout(), ils font tous les deux exactement ce qui est requis, tant que vous les configurez correctement lorsque vous les construisez/les achetez. Laissé à un point arbitraire plus tard dans l'application quand tout ce que vous avez est que le InputStream est mauvaise conception conduisant à une implémentation très maladroite.

18
user207421

Si votre InputStream est soutenu par un Socket, vous pouvez définir un délai d'attente de Socket (en millisecondes) à l'aide de setSoTimeout . Si l'appel read () ne débloque pas dans le délai spécifié, il lève une exception SocketTimeoutException.

Assurez-vous simplement que vous appelez setSoTimeout sur le Socket avant de lancer l'appel à read ().

18
Templar

Je n'ai pas utilisé les classes du package Java NIO, mais il semble qu'elles puissent être utiles ici. Plus précisément, Java.nio.channels.Channels et Java.nio.channels.InterruptibleChannel .

7
jt.

Voici un moyen d'obtenir un FileChannel NIO à partir de System.in et de vérifier la disponibilité des données à l'aide d'un délai d'attente, ce qui est un cas particulier du problème décrit dans la question. Exécutez-le sur la console, ne tapez aucune entrée et attendez les résultats. Il a été testé avec succès sous Java 6 sous Windows et 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];
    }
}

Fait intéressant, lors de l'exécution du programme dans NetBeans 6.5 plutôt que sur la console, le délai d'attente ne fonctionne pas du tout et l'appel à System.exit () est réellement nécessaire pour tuer les threads zombie. En réalité, le thread d'interruption bloque (!) Lors de l'appel de reader.interrupt (). Un autre programme de test (non illustré ici) tente en outre de fermer le canal, mais cela ne fonctionne pas non plus.

5
JKL

Comme je l'ai dit, NIO est la meilleure (et correcte) solution. Si vous êtes vraiment bloqué avec un InputStream, vous pouvez soit

  1. Créer un thread dont le travail exclusif consiste à lire à partir de InputStream et à placer le résultat dans un tampon pouvant être lu à partir de votre thread d'origine sans blocage. Cela devrait bien fonctionner si vous ne possédez qu'une seule instance du flux. Sinon, vous pourrez peut-être tuer le thread à l'aide des méthodes obsolètes de la classe Thread, bien que cela puisse provoquer des fuites de ressources.

  2. Comptez sur isAvailable pour indiquer les données pouvant être lues sans blocage. Cependant, dans certains cas (comme avec Sockets), une lecture potentiellement bloquante peut être considérée comme impossible à signaler pour indiquer autre chose que 0.

4
Merzbow