web-dev-qa-db-ja.com

タイムアウトでInputStreamから読み取ることは可能ですか?

具体的には、問題は次のようなメソッドを記述することです。

int maybeRead(InputStream in, long timeout)

戻り値は、データが 'timeout'ミリ秒以内に利用可能な場合はin.read()と同じであり、そうでない場合は-2です。メソッドが戻る前に、生成されたスレッドはすべて終了する必要があります。

引数を回避するために、Sunによって文書化されているJava.io.InputStreamの件名(任意のJavaバージョン)。これは見た目ほど単純ではないことに注意してください。以下は、Sunのドキュメントで直接サポートされているいくつかの事実です。

  1. In.read()メソッドは割り込み不可能な場合があります。

  2. InputStreamをReaderまたはInterruptibleChannelにラップすることは役に立ちません。これらのクラスができることはすべて、InputStreamのメソッドを呼び出すだけだからです。これらのクラスを使用できる場合、同じストリームをInputStreamで直接実行するだけのソリューションを作成できます。

  3. In.available()が0を返すことは常に許容されます。

  4. In.close()メソッドは、ブロックするか、何もしません。

  5. 別のスレッドを強制終了する一般的な方法はありません。

140
JKL

inputStream.available()を使用

System.in.available()が0を返すことは常に許容されます。

私は反対を見つけました-それは常に利用可能なバイト数の最適な値を返します。 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.

タイミング/陳腐化のため、見積もりは避けられません。新しいデータが絶えず到着しているため、この数字は1回限りの過小評価になる可能性があります。ただし、次の呼び出しでは常に「キャッチアップ」します。到着したすべてのデータを考慮し、新しい呼び出しの瞬間に到着することを禁止する必要があります。データが存在するときに永続的に0を返すと、上記の条件に失敗します。

最初の警告:InputStreamの具象サブクラスがavailable()を担当します

InputStreamは抽象クラスです。データソースはありません。利用可能なデータがあることは無意味です。したがって、available()のjavadocも次のように述べています。

The available method for class InputStream always returns 0.

This method should be overridden by subclasses.

実際、具体的な入力ストリームクラスはavailable()をオーバーライドし、定数0ではなく意味のある値を提供します。

2番目の警告:Windowsで入力を入力するときにキャリッジリターンを使用していることを確認してください。

System.inを使用している場合、プログラムはコマンドシェルがそれを引き渡すときにのみ入力を受け取ります。ファイルのリダイレクト/パイプを使用している場合(例:somefile> Java myJavaAppまたはsomecommand | Java myJavaApp)、入力データは通常すぐに引き渡されます。ただし、入力を手動で入力すると、データの引き継ぎが遅れることがあります。例えば。 Windowsのcmd.exeシェルでは、データはcmd.exeシェル内でバッファリングされます。データは、キャリッジリターン(control-mまたは<enter>)に続く実行中のJavaプログラムにのみ渡されます。それは実行環境の制限です。もちろん、シェルがデータをバッファリングしている限り、InputStream.available()は0を返します。これは正しい動作です。その時点で利用可能なデータはありません。シェルからデータが利用可能になるとすぐに、メソッドは0より大きい値を返します。注意: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.
79
Glen Best

ストリームがソケットに支えられていない(つまり[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をキャッチして、必要なクリーンアップを実行できます。

65
Ian Jones

盲目的に受け入れるのではなく、問題文に疑問を投げかけます。コンソールまたはネットワーク経由でのみタイムアウトが必要です。後者の場合は、Socket.setSoTimeout()HttpURLConnection.setReadTimeout()があり、どちらも必要なことを正確に行います。ただし、それらを作成/取得するときに正しく設定する必要があります。 InputStreamだけを持っているときにアプリケーションの任意のポイントにそれを残すと、非常に厄介な実装につながる不十分な設計になります。

18
user207421

InputStreamがSocketによってサポートされている場合、 setSoTimeout を使用してSocketタイムアウト(ミリ秒単位)を設定できます。 read()呼び出しが指定されたタイムアウト内にブロック解除されない場合、SocketTimeoutExceptionがスローされます。

Read()呼び出しを行う前に、必ずSocketでsetSoTimeoutを呼び出してください。

18
Templar

私はJava NIOパッケージのクラスを使用していませんが、seemsここで役立つかもしれません。具体的には、 Java.nio.channels.Channels および Java.nio.channels.InterruptibleChannel

7
jt.

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()の呼び出しでブロック(!)することです。別のテストプログラム(ここには表示されていません)は、さらにチャネルを閉じようとしますが、それも機能しません。

5
JKL

Jtが言ったように、NIOは最良の(そして正しい)ソリューションです。ただし、実際にInputStreamで動けない場合は、次のいずれかを実行できます。

  1. 排他的な仕事であるスレッドを生成するには、InputStreamから読み取り、ブロックせずに元のスレッドから読み取り可能なバッファーに結果を書き込みます。ストリームのインスタンスが1つしかない場合、これはうまく機能するはずです。そうしないと、Threadクラスの非推奨のメソッドを使用してスレッドを強制終了できますが、これによりリソースリークが発生する可能性があります。

  2. IsAvailableに依存して、ブロックせずに読み取れるデータを示します。ただし、場合によっては(Socketsなど)、isAvailableの潜在的なブロッキング読み取りを使用して、0以外の何かを報告できます。

4
Merzbow