是否可以从InputStream读取超时?


147

具体来说,问题是编写这样的方法:

int maybeRead(InputStream in, long timeout)

如果数据在“超时”毫秒内可用,则返回值与in.read()相同,否则为-2。在方法返回之前,所有产生的线程必须退出。

为避免自变量,此处的主题为java.io.InputStream,由Sun(任何Java版本)记录。请注意,这并不像看起来那么简单。以下是Sun文档直接支持的一些事实。

  1. in.read()方法可能是不可中断的。

  2. 将InputStream包装在Reader或InterruptibleChannel中没有帮助,因为所有这些类都可以做的是InputStream的调用方法。如果可以使用这些类,则可以编写一个直接在InputStream上执行相同逻辑的解决方案。

  3. in.available()返回0始终是可接受的。

  4. in.close()方法可能会阻止或不执行任何操作。

  5. 没有杀死另一线程的一般方法。

Answers:


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的具体子类负责available()

InputStream是一个抽象类。它没有数据源。拥有可用数据毫无意义。因此,javadoc用于available()也指出:

The available method for class InputStream always returns 0.

This method should be overridden by subclasses.

实际上,具体的输入流类确实会覆盖available(),提供有意义的值,而不是恒定的0。

第二个警告:确保在Windows中键入输入时使用回车符。

如果使用 System.in,则程序仅在命令外壳程序移交时才接收输入。如果您使用文件重定向/管道(例如somefile> java myJavaApp或somecommand | java myJavaApp),则通常会立即移交输入数据。但是,如果您手动键入输入,则数据切换可能会延迟。例如,使用Windows cmd.exe Shell,数据将缓存在cmd.exe Shell中。数据仅在回车(控制-m或<enter>)后才传递到正在执行的Java程序。那是执行环境的限制。当然,只要Shell缓冲数据,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处理。
Glen Best

注释re SSLSockets不正确-如果缓冲区中没有数据,则返回0表示可用。根据我的回答。Javadoc:“如果套接字上没有缓冲字节,并且尚未使用close关闭套接字,那么可用套接字将返回0。”
Glen Best

@GlenBest我的评论关于SSLSocket不正确。 直到最近 [我的重点],它一直都返回零。您在谈论现在。我正在谈论JSSE的整个历史,从2002年首次将其包含在Java 1.4中以来,我就一直在与之合作。
罗恩侯爵,2015年

通过将while循环条件更改为“ while(is.available()> 0 && System.currentTimeMillis()<maxTimeMillis && bufferOffset <b.length){”,我节省了大量CPU开销。
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
Muhammad Gelbana,您是对的:阻塞的read()线程保持运行状态,这不行。我发现了一种防止这种情况的方法:当超时发生时,从调用线程关闭输入流(在我的情况下,我关闭输入流来自的android蓝牙套接字)。当您这样做时,read()调用将立即返回。在我的情况下,我使用了int read(byte [])重载,并且那个立即返回。也许int read()重载会引发IOException,因为我不知道它将返回什么……在我看来,这是正确的解决方案。
伊曼纽尔·图扎里

5
-1,因为线程读取保持阻塞,直到应用程序终止。
Ortwin Angermeier

11
@ortang这就是我所说的“捕获TimeoutException并进行任何清理...”的意思,例如,我可能想杀死读取线程:... catch(TimeoutException e){executor.shutdownNow(); }
Ian Jones

12
executer.shutdownNow不会杀死线程。它将尝试中断它,但没有任何效果。无法清除,这是一个严重的问题。
Marko Topolnik

22

如果您的InputStream由套接字支持,则可以使用setSoTimeout设置套接字超时(以毫秒为单位)。如果read()调用未在指定的超时时间内取消阻止,则将引发SocketTimeoutException。

只需确保在调用read()之前在Socket上调用setSoTimeout。


18

我会质疑问题陈述,而不是一味地接受。您只需要从控制台或通过网络超时。如果您拥有后者Socket.setSoTimeout()HttpURLConnection.setReadTimeout()并且两者都完全满足要求,则在构造/获取它们时只要正确设置它们即可。当您只剩下InputStream时,在应用程序中将其留给任意点是糟糕的设计,从而导致非常尴尬的实现。


10
在其他情况下,读取可能会长时间阻塞。例如,当从磁带驱动器,远程安装的网络驱动器或后端有磁带机械手的HFS读取数据时。(但您回答的主要重点是正确的。)
Stephen C

1
@StephenC +1为您的评论和示例。要添加更多示例,一个简单的情况可能是正确建立了套接字连接,但由于要从DB提取数据而阻止了读取尝试,但是却以某种方式没有发生(让我们说,DB没有响应并且查询已经执行了)处于锁定状态)。在这种情况下,您需要一种方法来显式超时套接字上的读取操作。
sactiw

1
InputStream抽象的重点是不考虑底层实现。公平地讨论发布答案的利弊。但是,质疑问题陈述,将无助于讨论
pellucide

2
InputStream在流上工作并阻塞,但不提供超时机制。因此,InputStream抽象不是经过适当设计的抽象。因此,要求一种在流上超时的方法并不需要太多。因此,问题是要解决一个非常实际的问题。大多数基础实现将被阻止。那就是流的本质。如果流的另一端尚未准备好新数据,则套接字,文件,管道将阻塞。
pellucide

2
@EJP。我不知道你是怎么得到的。我不同意你的看法。问题语句“如何在InputStream上超时”有效。由于框架没有提供超时的方法,因此提出这样的问题是适当的。
pellucide


5

这是从System.in获取NIO FileChannel并使用超时检查数据可用性的方法,这是问题中所述问题的特例。在控制台上运行它,不要键入任何输入,然后等待结果。它已在Windows和Linux上的Java 6下成功测试。

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()的调用中阻塞了(!)。另外,另一个测试程序(此处未显示)尝试关闭通道,但这也不起作用。


在JDK 1.6和JDK 1.7上均不适用于Mac OS。仅在读取过程中按回车后才能识别中断。
Mostowski

4

正如jt所说,NIO是最好的(也是正确的)解决方案。如果您确实对InputStream感到困惑,则可以

  1. 产生一个线程的唯一工作是从InputStream读取并将结果放入缓冲区,该缓冲区可以从原始线程读取而不会阻塞。如果您只有一个流实例,这应该很好用。否则,您可以使用Thread类中不赞成使用的方法杀死线程,尽管这可能会导致资源泄漏。

  2. 依靠isAvailable指示可以读取而不会阻塞的数据。但是,在某些情况下(例如使用Sockets),isAvailable可能需要阻塞读取才能报告非0的值。


5
Socket.setSoTimeout()是同样正确且简单得多的解决方案。或者HttpURLConnection.setReadTimeout()
洛恩侯爵

3
@EJP-在某些情况下,这些只是“同等正确”;例如,如果输入流是套接字流/ HTTP连接流。
斯蒂芬·C

1
@Stephen C NIO在相同情况下仅是非阻塞性的并且可以选择。例如,没有非阻塞文件I / O。
罗恩侯爵,

2
@EJP,但是有一个非阻塞的管道IO(System.in),文件的非阻塞I / O(在本地磁盘上)是无意义的
12

1
@EJP在大多数(所有?)Unices System.in上,实际上是一个管道(如果您不告诉shell用文件替换它),并且作为管道,它可以是非阻塞的。
woky 2012年

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方法是一个可怕的性能杀手。如果您确实想使用readLin,则应该创建一个全新的BufferedReader 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");
    }

}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.