简体   繁体   English

是否可以从带有超时的 InputStream 中读取?

[英]Is it possible to read from a InputStream with a timeout?

Specifically, the problem is to write a method like this:具体来说,问题是写一个这样的方法:

int maybeRead(InputStream in, long timeout)

where the return value is the same as in.read() if data is available within 'timeout' milliseconds, and -2 otherwise.如果数据在 'timeout' 毫秒内可用,则返回值与 in.read() 相同,否则为 -2。 Before the method returns, any spawned threads must exit.在该方法返回之前,任何产生的线程都必须退出。

To avoid arguments, the subject here java.io.InputStream, as documented by Sun (any Java version).为了避免争论,这里的主题是 java.io.InputStream,如 Sun(任何 Java 版本)所记录的那样。 Please note this is not as simple as it looks.请注意,这并不像看起来那么简单。 Below are some facts which are supported directly by Sun's documentation.以下是 Sun 文档直接支持的一些事实。

  1. The in.read() method may be non-interruptible. in.read() 方法可能是不可中断的。

  2. Wrapping the InputStream in a Reader or InterruptibleChannel doesn't help, because all those classes can do is call methods of the InputStream.将 InputStream 包装在 Reader 或 InterruptibleChannel 中没有帮助,因为所有这些类都可以调用 InputStream 的方法。 If it were possible to use those classes, it would be possible to write a solution that just executes the same logic directly on the InputStream.如果可以使用这些类,就可以编写一个直接在 InputStream 上执行相同逻辑的解决方案。

  3. It is always acceptable for in.available() to return 0. in.available() 返回 0 总是可以接受的。

  4. The in.close() method may block or do nothing. in.close() 方法可能会阻塞或什么都不做。

  5. There is no general way to kill another thread.没有通用的方法可以杀死另一个线程。

Using inputStream.available()使用 inputStream.available()

It is always acceptable for System.in.available() to return 0. System.in.available() 返回 0 总是可以接受的。

I've found the opposite - it always returns the best value for the number of bytes available.我发现相反 - 它总是返回可用字节数的最佳值。 Javadoc for InputStream.available() : InputStream.available() Javadoc:

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.

An estimate is unavoidable due to timing/staleness.由于时间/过时,估计是不可避免的。 The figure can be a one-off underestimate because new data are constantly arriving.这个数字可能是一次性的低估,因为新数据不断到来。 However it always "catches up" on the next call - it should account for all arrived data, bar that arriving just at the moment of the new call.然而,它总是在下一次调用时“赶上”——它应该考虑到所有到达的数据,除非在新调用的那一刻到达。 Permanently returning 0 when there are data fails the condition above.当有数据不符合上述条件时,永久返回 0。

First Caveat: Concrete subclasses of InputStream are responsible for available()第一个警告:InputStream 的具体子类负责 available()

InputStream is an abstract class. InputStream是一个抽象类。 It has no data source.它没有数据源。 It's meaningless for it to have available data.拥有可用数据是没有意义的。 Hence, javadoc for available() also states:因此, available() javadoc 也指出:

The available method for class InputStream always returns 0.

This method should be overridden by subclasses.

And indeed, the concrete input stream classes do override available(), providing meaningful values, not constant 0s.事实上,具体的输入流类确实覆盖了 available(),提供了有意义的值,而不是常量 0。

Second Caveat: Ensure you use carriage-return when typing input in Windows.第二个警告:确保在 Windows 中键入输入时使用回车符。

If using System.in , your program only receives input when your command shell hands it over.如果使用System.in ,您的程序仅在您的命令外壳程序移交时接收输入。 If you're using file redirection/pipes (eg somefile > java myJavaApp or somecommand | java myJavaApp ), then input data are usually handed over immediately.如果您使用文件重定向/管道(例如 somefile > java myJavaApp 或 somecommand | java myJavaApp ),则输入数据通常会立即移交。 However, if you manually type input, then data handover can be delayed.但是,如果您手动输入,则数据切换可能会延迟。 Eg With windows cmd.exe shell, the data are buffered within cmd.exe shell.例如,使用 windows cmd.exe shell,数据在 cmd.exe shell 中缓冲。 Data are only passed to the executing java program following carriage-return (control-m or <enter> ).数据仅在回车(control-m 或<enter> )后传递给正在执行的 java 程序。 That's a limitation of the execution environment.这是执行环境的限制。 Of course, InputStream.available() will return 0 for as long as the shell buffers the data - that's correct behaviour;当然,只要 shell 缓冲数据, InputStream.available() 就会返回 0 - 这是正确的行为; there are no available data at that point.那时没有可用数据。 As soon as the data are available from the shell, the method returns a value > 0. NB: Cygwin uses cmd.exe too.一旦 shell 中的数据可用,该方法就会返回一个 > 0 的值。注意:Cygwin 也使用 cmd.exe。

Simplest solution (no blocking, so no timeout required)最简单的解决方案(无阻塞,因此不需要超时)

Just use this:只需使用这个:

    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.

OR equivalently,或等效地,

    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 (maximally fills buffer within timeout period)更丰富的解决方案(在超时时间内最大程度地填充缓冲区)

Declare this:声明如下:

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

Then use this:然后使用这个:

    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.

Assuming your stream is not backed by a socket (so you can't use Socket.setSoTimeout() ), I think the standard way of solving this type of problem is to use a Future.假设您的流不受套接字支持(因此您不能使用Socket.setSoTimeout() ),我认为解决此类问题的标准方法是使用 Future。

Suppose I have the following executor and streams:假设我有以下执行程序和流:

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

I have writer that writes some data then waits for 5 seconds before writing the last piece of data and closing the stream:我有写入一些数据然后等待 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);

The normal way of reading this is as follows.正常的阅读方式如下。 The read will block indefinitely for data and so this completes in 5s:读取将无限期阻塞数据,因此这将在 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");

which outputs:输出:

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

If there was a more fundamental problem, like the writer not responding, the reader would block for ever.如果有更基本的问题,比如作者没有回应,读者就会永远阻塞。 If I wrap the read in a future, I can then control the timeout as follows:如果我在将来包装读取,则可以按如下方式控制超时:

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

which outputs:输出:

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)

I can catch the TimeoutException and do whatever cleanup I want.我可以捕获 TimeoutException 并进行我想要的任何清理。

If your InputStream is backed by a Socket, you can set a Socket timeout (in milliseconds) using setSoTimeout .如果您的 InputStream 由 Socket 支持,您可以使用setSoTimeout设置 Socket 超时(以毫秒为单位)。 If the read() call doesn't unblock within the timeout specified, it will throw a SocketTimeoutException.如果 read() 调用没有在指定的超时时间内解除阻塞,它将抛出 SocketTimeoutException。

Just make sure that you call setSoTimeout on the Socket before making the read() call.只需确保在进行 read() 调用之前在 Socket 上调用 setSoTimeout。

I would question the problem statement rather than just accept it blindly.我会质疑问题陈述而不是盲目地接受它。 You only need timeouts from the console or over the network.您只需要从控制台或通过网络超时。 If the latter you have Socket.setSoTimeout() and HttpURLConnection.setReadTimeout() which both do exactly what is required, as long as you set them up correctly when you construct/acquire them.如果后者你有Socket.setSoTimeout()HttpURLConnection.setReadTimeout()它们都完全符合要求,只要你在构造/获取它们时正确设置它们。 Leaving it to an arbitrary point later in the application when all you have is the InputStream is poor design leading to a very awkward implementation.当您拥有的只是 InputStream 时,将它留在应用程序中的任意点是糟糕的设计,导致非常尴尬的实现。

I have not used the classes from the Java NIO package, but it seems they might be of some help here.我没有使用过 Java NIO 包中的类,但看起来它们在这里可能会有所帮助。 Specifically, java.nio.channels.Channels and java.nio.channels.InterruptibleChannel .具体来说, java.nio.channels.Channelsjava.nio.channels.InterruptibleChannel

Here is a way to get a NIO FileChannel from System.in and check for availability of data using a timeout, which is a special case of the problem described in the question.这是一种从 System.in 获取 NIO FileChannel 并使用超时检查数据可用性的方法,这是问题中描述的问题的特例。 Run it at the console, don't type any input, and wait for the results.在控制台运行它,不要输入任何输入,然后等待结果。 It was tested successfully under Java 6 on Windows and Linux.它在 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];
    }
}

Interestingly, when running the program inside NetBeans 6.5 rather than at the console, the timeout doesn't work at all, and the call to System.exit() is actually necessary to kill the zombie threads.有趣的是,当在 NetBeans 6.5 中而不是在控制台中运行程序时,超时根本不起作用,实际上需要调用 System.exit() 来杀死僵尸线程。 What happens is that the interruptor thread blocks (!) on the call to reader.interrupt().发生的情况是中断线程阻塞 (!) 调用 reader.interrupt()。 Another test program (not shown here) additionally tries to close the channel, but that doesn't work either.另一个测试程序(此处未显示)也尝试关闭通道,但这也不起作用。

As jt said, NIO is the best (and correct) solution.正如 jt 所说,NIO 是最好的(也是正确的)解决方案。 If you really are stuck with an InputStream though, you could either如果你真的坚持使用 InputStream,你可以

  1. Spawn a thread who's exclusive job is to read from the InputStream and put the result into a buffer which can be read from your original thread without blocking.生成一个线程,该线程的唯一工作是从 InputStream 读取并将结果放入可以从原始线程读取而不会阻塞的缓冲区中。 This should work well if you only ever have one instance of the stream.如果您只有一个流实例,这应该很有效。 Otherwise you may be able to kill the thread using the deprecated methods in the Thread class, though this may cause resource leaks.否则,您可以使用 Thread 类中已弃用的方法终止线程,尽管这可能会导致资源泄漏。

  2. Rely on isAvailable to indicate data that can be read without blocking.依靠 isAvailable 表示可以无阻塞读取的数据。 However in some cases (such as with Sockets) it can take a potentially blocking read for isAvailable to report something other than 0.但是,在某些情况下(例如使用 Sockets),它可能需要对 isAvailable 进行潜在的阻塞读取才能报告 0 以外的值。

Inspired in this answer I came up with a bit more object-oriented solution.受到这个答案的启发,我想出了一个更面向对象的解决方案。

This is only valid if you're intending to read characters这仅在您打算阅读字符时才有效

You can override BufferedReader and implement something like this:您可以覆盖 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");
    }
}

Here's an almost complete example.这是一个几乎完整的示例。

I'm returning 0 on some methods, you should change it to -2 to meet your needs, but I think that 0 is more suitable with BufferedReader contract.我在某些方法上返回 0,您应该将其更改为 -2 以满足您的需要,但我认为 0 更适合 BufferedReader 合同。 Nothing wrong happened, it just read 0 chars.没有发生任何错误,它只是读取了 0 个字符。 readLine method is a horrible performance killer. readLine 方法是一个可怕的性能杀手。 You should create a entirely new BufferedReader if you actually want to use readLin e.如果你真的想使用 readLine e,你应该创建一个全新的 BufferedReader Right now, it is not thread safe.现在,它不是线程安全的。 If someone invokes an operation while readLines is waiting for a line, it will produce unexpected results如果有人在 readLines 等待一行的时候调用一个操作,会产生意想不到的结果

I don't like returning -2 where I am.我不喜欢在我所在的地方返回 -2。 I'd throw an exception because some people may just be checking if int < 0 to consider EOS.我会抛出一个异常,因为有些人可能只是在检查 int < 0 是否考虑 EOS。 Anyway, those methods claim that "can't block", you should check if that statement is actually true and just don't override'em.无论如何,这些方法声称“无法阻止”,您应该检查该声明是否真的正确,并且不要覆盖它们。

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

}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM