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

147

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

int maybeRead(InputStream in, long timeout)

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

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

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

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

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

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

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

Серый
источник

Ответы:

83

Использование 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 при наличии данных не соответствует условию выше.

Первое предупреждение: за доступность отвечают конкретные подклассы InputStream

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

The available method for class InputStream always returns 0.

This method should be overridden by subclasses.

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

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

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

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

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

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

Richer Solution (максимально заполняет буфер в течение периода ожидания)

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

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.
Глен Бест
источник
1
Если is.available() > 1024это предложение не удастся. Конечно, есть потоки, которые возвращают ноль. SSLSockets например до недавнего времени. Вы не можете полагаться на это.
маркиз Лорн
Случай 'is.available ()> 1024' специально рассматривается через readLength.
Глен Бест
Комментарий re SSLSockets неверен - он возвращает 0 для доступных, если в буфере нет данных. Согласно моему ответу. Javadoc: «Если в сокете нет буферизованных байтов и сокет не был закрыт с помощью close, то available вернет 0.»
Glen Best
@GlenBest Мой комментарий о SSLSocket не является неправильным. До недавнего времени [мой акцент] он всегда возвращал ноль. Вы говорите о настоящем. Я говорю о всей истории JSSE, и я работал с ней до того, как она впервые была включена в Java 1.4 в 2002 году .
маркиз Лорн
Изменение условий цикла while на "while (is.available ()> 0 && System.currentTimeMillis () <maxTimeMillis && bufferOffset <b.length) {" сэкономило мне массу ресурсов ЦП.
Logic1
65

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

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

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

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

    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 и сделать любую очистку, какую захочу.

Ян Джонс
источник
14
Но как насчет блокирующей нити ?! Он останется в памяти, пока приложение не завершится? Если я прав, это может привести к бесконечным потокам, приложение сильно загружено и даже больше, заблокировать дальнейшие потоки от использования вашего пула, у которого заняты и заблокированы потоки. Пожалуйста, поправьте меня, если я ошибаюсь. Спасибо.
Мухаммед Гелбана
4
Мухаммед Гелбана, вы правы: блокирующий поток read () продолжает работать, и это не нормально. Я нашел способ предотвратить это, хотя: когда истечет время ожидания, закройте от вызывающего потока входной поток (в моем случае я закрываю Android-разъем Bluetooth, из которого поступает входной поток). Когда вы это сделаете, вызов read () немедленно вернется. В моем случае я использую перегрузку int read (byte []), и она сразу возвращается. Возможно, перегрузка int read () вызовет IOException, поскольку я не знаю, что он вернет ... На мой взгляд, это правильное решение.
Эммануэль Тузери
5
-1, поскольку чтение потоков остается заблокированным, пока приложение не завершится.
Ортвин Ангермейер
11
@ortang Это то, что я имел в виду под "ловить исключение TimeoutException и выполнять любую очистку ...". Например, я мог бы захотеть убить поток чтения: ... catch (TimeoutException e) {executor.shutdownNow (); }
Ян Джонс
12
executer.shutdownNowне убьет нить. Он будет пытаться прервать его, но безрезультатно. Очистка невозможна, и это серьезная проблема.
Марко Топольник
22

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

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

тамплиер
источник
18

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

Маркиз Лорн
источник
10
Существуют другие ситуации, когда чтение может потенциально блокироваться в течение значительного времени; например, при чтении с ленточного накопителя, с удаленно смонтированного сетевого диска или с HFS с ленточным роботом на заднем конце. (Но основной смысл вашего ответа - правильный.)
Стивен С.
1
@StephenC +1 за ваш комментарий и примеры. Чтобы добавить больше вашего примера, простой случай может быть, когда соединения с сокетом были сделаны правильно, но попытка чтения была заблокирована, поскольку данные должны были быть получены из БД, но это почему-то не произошло (допустим, БД не отвечает, и запрос пошел в заблокированном состоянии). В этом сценарии у вас должен быть способ явного тайм-аута операции чтения в сокете.
священное событие
1
Весь смысл абстракции InputStream заключается в том, чтобы не думать о базовой реализации. Справедливо спорить о плюсах и минусах опубликованных ответов. Но, чтобы поставить под сомнение постановку проблемы, это не поможет разногласия
pellucide
2
InputStream работает с потоком и блокируется, но не предоставляет механизм тайм-аута. Таким образом, абстракция InputStream не является удачно спроектированной абстракцией. Следовательно, просьба о способе ожидания в потоке не требует много. Таким образом, вопрос требует решения очень практической проблемы. Большинство базовых реализаций будут блокироваться. Это сама суть потока. Сокеты, файлы, каналы будут блокироваться, если другая сторона потока не готова с новыми данными.
pellucide
2
@EJP. Я не знаю, как ты это получил. Я не согласен с тобой. Утверждение проблемы «как установить тайм-аут на InputStream» действительно. Так как фреймворк не предоставляет возможности для тайм-аута, уместно задать такой вопрос.
pellucide
7

Я не использовал классы из пакета Java NIO, но, похоже, они могут быть здесь полезны. В частности, java.nio.channels.Channels и java.nio.channels.InterruptibleChannel .

JT.
источник
2
+1: я не верю, что есть надежный способ сделать то, что запрашивает ОП, только с InputStream. Тем не менее, Nio был создан для этой цели, среди других.
Эдди
2
ОП уже практически исключил это. InputStreams по своей сути блокируют и могут быть непрерываемыми.
Маркиз Лорн
5

Вот способ получить 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 () фактически необходим для уничтожения потоков зомби. Что происходит, так это то, что поток прерывания блокирует (!) При вызове reader.interrupt (). Другая тестовая программа (здесь не показана) дополнительно пытается закрыть канал, но это тоже не работает.


источник
не работает на Mac OS, ни с JDK 1.6, ни с JDK 1.7. Прерывание распознается только после нажатия возврата во время чтения.
Мостовский Свернуть
4

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

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

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


источник
5
Socket.setSoTimeout()одинаково правильное и гораздо более простое решение. Или HttpURLConnection.setReadTimeout().
маркиз Лорн
3
@EJP - это только «одинаково правильно» при определенных обстоятельствах; например, если входной поток является потоком сокета / потоком соединения HTTP.
Стивен С.
1
@Stephen C NIO только неблокируемый и выбираемый при тех же обстоятельствах. Например, нет неблокирующего файлового ввода-вывода.
Маркиз Лорн
2
@EJP, но есть неблокирующий канал ввода-вывода (System.in), неблокирующая
операция
1
@EJP В большинстве (всех?) Unices System.in на самом деле представляет собой канал (если вы не указали оболочке заменить его на файл), и в качестве канала он может быть неблокирующим.
Вики
0

Вдохновленный этим ответом, я придумал немного более объектно-ориентированное решение.

Это верно только в том случае, если вы собираетесь читать символы

Вы можете переопределить BufferedReader и реализовать что-то вроде этого:

public class SafeBufferedReader extends BufferedReader{

    private long millisTimeout;

    ( . . . )

    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
        try {
            waitReady();
        } catch(IllegalThreadStateException e) {
            return 0;
        }
        return super.read(cbuf, off, len);
    }

    protected void waitReady() throws IllegalThreadStateException, IOException {
        if(ready()) return;
        long timeout = System.currentTimeMillis() + millisTimeout;
        while(System.currentTimeMillis() < timeout) {
            if(ready()) return;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break; // Should restore flag
            }
        }
        if(ready()) return; // Just in case.
        throw new IllegalThreadStateException("Read timed out");
    }
}

Вот почти полный пример.

Я возвращаю 0 для некоторых методов, вы должны изменить его на -2 для удовлетворения ваших потребностей, но я думаю, что 0 больше подходит для контракта BufferedReader. Ничего плохого не произошло, просто прочитал 0 символов. Метод readLine - ужасный убийца производительности. Вы должны создать совершенно новый BufferedReader, если вы действительно хотите использовать readLin e. Прямо сейчас это не потокобезопасно. Если кто-то вызывает операцию, пока readLines ожидает строку, это приведет к неожиданным результатам.

Я не люблю возвращать -2 там, где я есть. Я бы бросил исключение, потому что некоторые люди могут просто проверять, если int <0, рассмотреть EOS. В любом случае, эти методы утверждают, что «не могут блокировать», вы должны проверить, является ли это утверждение истинным и просто не переопределять их.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.CharBuffer;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

/**
 * 
 * readLine
 * 
 * @author Dario
 *
 */
public class SafeBufferedReader extends BufferedReader{

    private long millisTimeout;

    private long millisInterval = 100;

    private int lookAheadLine;

    public SafeBufferedReader(Reader in, int sz, long millisTimeout) {
        super(in, sz);
        this.millisTimeout = millisTimeout;
    }

    public SafeBufferedReader(Reader in, long millisTimeout) {
        super(in);
        this.millisTimeout = millisTimeout;
    }



    /**
     * This is probably going to kill readLine performance. You should study BufferedReader and completly override the method.
     * 
     * It should mark the position, then perform its normal operation in a nonblocking way, and if it reaches the timeout then reset position and throw IllegalThreadStateException
     * 
     */
    @Override
    public String readLine() throws IOException {
        try {
            waitReadyLine();
        } catch(IllegalThreadStateException e) {
            //return null; //Null usually means EOS here, so we can't.
            throw e;
        }
        return super.readLine();
    }

    @Override
    public int read() throws IOException {
        try {
            waitReady();
        } catch(IllegalThreadStateException e) {
            return -2; // I'd throw a runtime here, as some people may just be checking if int < 0 to consider EOS
        }
        return super.read();
    }

    @Override
    public int read(char[] cbuf) throws IOException {
        try {
            waitReady();
        } catch(IllegalThreadStateException e) {
            return -2;  // I'd throw a runtime here, as some people may just be checking if int < 0 to consider EOS
        }
        return super.read(cbuf);
    }

    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
        try {
            waitReady();
        } catch(IllegalThreadStateException e) {
            return 0;
        }
        return super.read(cbuf, off, len);
    }

    @Override
    public int read(CharBuffer target) throws IOException {
        try {
            waitReady();
        } catch(IllegalThreadStateException e) {
            return 0;
        }
        return super.read(target);
    }

    @Override
    public void mark(int readAheadLimit) throws IOException {
        super.mark(readAheadLimit);
    }

    @Override
    public Stream<String> lines() {
        return super.lines();
    }

    @Override
    public void reset() throws IOException {
        super.reset();
    }

    @Override
    public long skip(long n) throws IOException {
        return super.skip(n);
    }

    public long getMillisTimeout() {
        return millisTimeout;
    }

    public void setMillisTimeout(long millisTimeout) {
        this.millisTimeout = millisTimeout;
    }

    public void setTimeout(long timeout, TimeUnit unit) {
        this.millisTimeout = TimeUnit.MILLISECONDS.convert(timeout, unit);
    }

    public long getMillisInterval() {
        return millisInterval;
    }

    public void setMillisInterval(long millisInterval) {
        this.millisInterval = millisInterval;
    }

    public void setInterval(long time, TimeUnit unit) {
        this.millisInterval = TimeUnit.MILLISECONDS.convert(time, unit);
    }

    /**
     * This is actually forcing us to read the buffer twice in order to determine a line is actually ready.
     * 
     * @throws IllegalThreadStateException
     * @throws IOException
     */
    protected void waitReadyLine() throws IllegalThreadStateException, IOException {
        long timeout = System.currentTimeMillis() + millisTimeout;
        waitReady();

        super.mark(lookAheadLine);
        try {
            while(System.currentTimeMillis() < timeout) {
                while(ready()) {
                    int charInt = super.read();
                    if(charInt==-1) return; // EOS reached
                    char character = (char) charInt;
                    if(character == '\n' || character == '\r' ) return;
                }
                try {
                    Thread.sleep(millisInterval);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // Restore flag
                    break;
                }
            }
        } finally {
            super.reset();
        }
        throw new IllegalThreadStateException("readLine timed out");

    }

    protected void waitReady() throws IllegalThreadStateException, IOException {
        if(ready()) return;
        long timeout = System.currentTimeMillis() + millisTimeout;
        while(System.currentTimeMillis() < timeout) {
            if(ready()) return;
            try {
                Thread.sleep(millisInterval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // Restore flag
                break;
            }
        }
        if(ready()) return; // Just in case.
        throw new IllegalThreadStateException("read timed out");
    }

}
DGoiko
источник