Можно ли читать из InputStream с таймаутом?

В частности, проблема заключается в том, чтобы написать такой метод:

int maybeRead(InputStream in, long timeout)

где возвращаемое значение такое же, как и in.read(), если данные доступны в течение "тайм-аута" миллисекунд и -2 в противном случае. Прежде чем метод вернется, все порожденные потоки должны выйти.

Чтобы избежать аргументов, тема здесь java.io.InputStream, как описано Sun (любая версия Java). Обратите внимание, что это не так просто, как кажется. Ниже приведены некоторые факты, которые напрямую поддерживаются документацией Sun.

  • Метод in.read() может быть не прерываемым.

  • Обтекание InputStream в Reader или InterruptibleChannel не помогает, потому что все эти классы могут выполнять вызовы методов InputStream. Если бы можно было использовать эти классы, можно было бы написать решение, которое просто выполнит ту же логику непосредственно в InputStream.

  • Это всегда приемлемо для in.available() для возврата 0.

  • Метод in.close() может блокировать или ничего не делать.

  • Нет общего способа убить другой поток.

Ответ 1

Использование inputStream.available()

Для System.in.available() всегда допустимо возвращать 0.

Я нашел обратное - он всегда возвращает наилучшее значение для количества доступных байтов. Javadoc для 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.

Оценка неизбежна из-за хронометража/неподвижности. Эта цифра может быть разовой недооценкой, потому что постоянно поступают новые данные. Однако он всегда "догоняет" при следующем вызове - он должен учитывать все полученные данные, бар, который прибывает только в момент нового вызова. Постоянное возвращение 0, когда есть данные, не соответствует указанному выше условию.

First Caveat: Конкретные подклассы InputStream отвечают за доступные()

InputStream - абстрактный класс. У него нет источника данных. Для него бессмысленно иметь доступные данные. Следовательно, javadoc для available() также заявляет:

The available method for class InputStream always returns 0.

This method should be overridden by subclasses.

И действительно, конкретные классы потока ввода переопределяют доступные(), предоставляя значимые значения, а не константу 0.

Second Caveat: убедитесь, что вы используете возврат каретки при вводе ввода в Windows.

Если вы используете System.in, ваша программа получает только вход, когда ваша командная оболочка передает ее. Если вы используете перенаправление/каналы файлов (например, somefile > java myJavaApp или somecommand | java myJavaApp), тогда входные данные обычно передаются немедленно. Однако, если вы вручную вводите ввод, тогда передача данных может быть отложена. Например. С оболочкой windows cmd.exe данные буферизуются в оболочке cmd.exe. Данные передаются только исполняющей java-программе после возврата каретки (control-m или <enter>). Это ограничение среды исполнения. Конечно, InputStream.available() будет возвращать 0 до тех пор, пока оболочка буферизует данные - это правильное поведение; в этот момент нет доступных данных. Как только данные будут доступны из оболочки, метод возвращает значение > 0. NB: Cygwin также использует cmd.exe.

Простейшее решение (без блокировки, поэтому не требуется тайм-аут)

Просто используйте это:

    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.

ИЛИ эквивалентно,

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

Более богатое решение (максимально заполняет буфер в период ожидания)

Объявите это:

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

Затем используйте это:

    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.

Ответ 2

Предполагая, что ваш поток не поддерживается сокетом (поэтому вы не можете использовать Socket.setSoTimeout()), я думаю, что стандартным способом решения этой проблемы является использование Будущего.

Предположим, что у меня есть следующий исполнитель и потоки:

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

У меня есть запись, которая записывает некоторые данные, а затем ждет 5 секунд, прежде чем записывать последний фрагмент данных и закрывать поток:

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

Обычный способ чтения этого заключается в следующем. Чтение будет блокироваться бесконечно для данных, и поэтому это завершается в 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");

который выводит:

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

Если возникла более фундаментальная проблема, как писатель, не отвечающий, читатель блокировал бы навсегда. Если я завершу чтение в будущем, я смогу контролировать таймаут следующим образом:

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

который выводит:

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)

Я могу поймать TimeoutException и выполнить любую очистку, которую хочу.

Ответ 3

Я бы поставил под сомнение утверждение проблемы, а не просто принять его вслепую. Вам нужны только таймауты с консоли или по сети. Если у последнего есть Socket.setSoTimeout() и HttpURLConnection.setReadTimeout(), которые выполняют точно то, что требуется, до тех пор, пока вы правильно их настроили при их создании/приобретении. Оставляя его в произвольной точке позже в приложении, когда все, что у вас есть, это InputStream, это плохой дизайн, что приводит к очень неудобной реализации.

Ответ 4

Если ваш InputStream поддерживается Socket, вы можете установить тайм-аут Socket (в миллисекундах) с помощью setSoTimeout. Если вызов read() не разблокируется в течение указанного таймаута, он выдает исключение SocketTimeoutException.

Просто убедитесь, что вы вызываете setSoTimeout на Socket перед вызовом read().

Ответ 6

Вот способ получить NIO FileChannel из System.in и проверить доступность данных с использованием тайм-аута, что является особым случаем проблемы, описанной в вопросе. Запустите его на консоли, не вводите ввода и не ждите результатов. Он был успешно протестирован в Java 6 на Windows и 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];
    }
}

Интересно, что при запуске программы внутри NetBeans 6.5, а не на консоли, тайм-аут не работает вообще, и вызов System.exit() на самом деле необходим для уничтожения потоков зомби. Случается, что поток прерывания блокирует (!) Вызов call.interrupt(). Другая тестовая программа (не показана здесь) дополнительно пытается закрыть канал, но это тоже не работает.

Ответ 7

Как сказано выше, NIO - лучшее (и правильное) решение. Если вы действительно застряли в InputStream, вы можете либо

  • Создайте поток, эксклюзивное задание которого должно считываться из InputStream и помещать результат в буфер, который можно прочитать из исходного потока без блокировки. Это должно хорошо работать, если у вас только один экземпляр потока. В противном случае вы можете убить поток, используя устаревшие методы в классе Thread, хотя это может привести к утечкам ресурсов.

  • Положитесь на isAvailable, чтобы указать данные, которые можно читать без блокировки. Однако в некоторых случаях (например, с сокетами) может произойти потенциальная блокировка чтения для isAvailable, чтобы сообщить что-либо, отличное от 0.