Чи можна читати з 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, коли є дані, не відповідає умові вище.

First Caveat: Бетонні підкласи InputStream відповідають за доступні ()

InputStream- абстрактний клас. У ньому немає джерела даних. Немає сенсу мати доступні дані. Отже, javadoc для available()також заявляє:

The available method for class InputStream always returns 0.

This method should be overridden by subclasses.

Дійсно, конкретні класи вхідного потоку переосмислюють наявні (), забезпечуючи значущі значення, а не постійні 0.

Другий застереження: переконайтеся, що ви використовуєте повернення каретки під час введення вводу в Windows.

Якщо використовується System.in, ваша програма отримує вхід лише тоді, коли ваша командна оболонка передасть її. Якщо ви використовуєте переадресацію файлів / файлів (наприклад, деякий файл> java myJavaApp або деякийкоманд | 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);
         }

Рішучіше рішення (максимально заповнює буфер протягом періоду очікування)

Заявіть про це:

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.
Глен Бест

Прокоментувати повторно SSLSockets неправильно - він повертає 0 для наявних iff, якщо в буфері немає даних. Відповідно до моєї відповіді. Javadoc: "Якщо в сокеті немає байтів, а сокет не закритий, використовуючи функцію close, доступний поверне 0."
Glen Best

@GlenBest Мій коментар щодо SSLSocket невірний. Донедавна [мій акцент] він завжди повертав нуль. Ви говорите про сучасність. Я говорю про всю історію JSSE, і з якою працював ще до того, як вона була вперше включена в Java 1.4 в 2002 році .
Маркіз Лорн

Змінивши умови циклу while на "while (is.available ()> 0 && System.currentTimeMillis () <maxTimeMillis && bufferOffset <b.length) {" врятував мені тонну центральних витрат на процесор.
Логіка1

65

Якщо припустити, що ваш потік не підтримується сокетом (тому ви не можете використовувати 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);

Нормальний спосіб читання цього полягає в наступному. Читання буде блокуватися нескінченно для даних, і це завершиться за 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 із стрічковим роботом на задньому кінці. (Але головна думка вашої відповіді правильна.)
Стівен C

1
@StephenC +1 для вашого коментаря та прикладів. Щоб додати більше вашого прикладу, простим випадком може бути те, коли з'єднання сокета було зроблено правильно, але спроба читання була заблокована, оскільки дані повинні були бути отримані з БД, але це якось не сталося (скажімо, БД не відповідає і запит пішов. у заблокованому стані). У цьому сценарії вам потрібно мати спосіб явно вичерпати операцію зчитування на сокет.
вересня 13:30

1
Вся суть абстракції InputStream полягає в тому, щоб не замислюватися над базовою реалізацією. Справедливо справедливо сперечатися про плюси і мінуси опублікованих відповідей. Але, на питання постановку задачі, не збирається допомагати disussion
pellucide

2
InputStream працює над потоком і блокує, але він не забезпечує механізм тайм-ауту. Отже абстракція InputStream не є влучно розробленою абстракцією. Отже, просити спосіб вичерпання потоку не вимагає багато. Тож питання вимагає вирішення дуже практичної проблеми. Більшість базових реалізацій буде заблоковано. У цьому сама суть потоку. Сокети, файли та труби блокуються, якщо інша сторона потоку не готова з новими даними.
пелюцид

2
@EJP. Я не знаю, як у вас це вийшло. Я не погодився з вами. Заява проблеми "як тайм-аут у InputStream" є дійсною. Оскільки рамка не передбачає способу очікування, доречно задати таке питання.
пелюцид

7

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


2
+1: Я не вірю в те, що існує надійний спосіб зробити те, про що вимагає ОП тільки з InputStream. Однак nio був створений для цієї мети, серед інших.
Едді

2
ОП вже в основному виключає це. Вхідні потоки по своїй суті блокують і можуть бути неперервними.
Маркіз Лорн

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 () насправді необхідний для вбивства потоків зомбі. Що відбувається, так це те, що нитка переривача блокує (!) Під час виклику читача.interrupt (). Інша програма тестування (не показана тут) додатково намагається закрити канал, але це також не працює.


не працює на mac os, ні з JDK 1.6, ні з JDK 1.7. Переривання розпізнається лише після натискання клавіші повернення під час читання.
Mostowski Згорнутись

4

Як сказав jt, NIO - найкраще (і правильне) рішення. Якщо ви насправді застрягли з InputStream, ви можете так чи інакше

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

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


5
Socket.setSoTimeout()є не менш правильним і набагато простішим рішенням. Або HttpURLConnection.setReadTimeout().
Маркіз Лорн

3
@EJP - це лише "однаково правильно" за певних обставин; наприклад, якщо вхідним потоком є ​​потік socket / HTTP-з'єднання.
Стівен С

1
@Stephen C NIO є лише блокуючим і вибирається за тих самих обставин. Наприклад, немає вводу / виводу файлів, що не блокуються.
Маркіз Лорн

2
@EJP, але IO не блокує IO (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");
    }

}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.