- PR -

InputStream#read() での例外処理

投稿者投稿内容
シュン
ぬし
会議室デビュー日: 2004/01/06
投稿数: 328
お住まい・勤務地: 東京都
投稿日時: 2004-11-11 17:49
う、そのサンプルコードからいろいろと危険なニオイが。

そのメソッドは、「SoketInputStreamの終端まで一気に読んで、文字列
化して返す」処理ですよね。

state3が起きるということは、
「そのメソッドを一つのSocketInputStreamに対して繰り返し呼び出す」
場合に発生しますが、そのようなことをしてはいけないはずです。
初回の呼び出しでソケットストリームは終端まで達しているので、それ
以降何度呼び出しても何も読めませんよ。state3は発生しないように
プログラムして、1と2だけ気にするようにしてください。

それと、ストリームから1バイトずつ読んで、1バイトを1キャラクタ化
していますが、ダブルバイト文字対応は大丈夫ですか?

それと、ソケットのストリームバッファから1バイトずつ読むことを繰
り返すのは、性能にとって良くないです。SocketInputStream#read()
は、その呼出し毎にnativeメソッドコールを発生しますし、おそらく
ネイティブのソケットAPIも、1バイトずつ読むというのは、纏めて読
むより性能が劣ります。
どうせ終端まで読みきってしまうのですから、大き目のバッファを利用
してバルクリードした方が良いですよ。

以上、シュンのソースコードレビューでした。
ねま
会議室デビュー日: 2004/11/10
投稿数: 18
投稿日時: 2004-11-11 18:23
未記入様、シュン様、ありがとうございます。

> case3. と case4. を分けているのはなぜ? case3. のあとには case4. になるでしょう。
自分自身の頭の中を整理するために分けました。

> とにかく -1 が返ってくるのはストリーム終端に達したからです。ストリーム終端に達した> のが正常にソケットを閉じたからなのかケーブルが抜けたからなのかは知りません。知りたいなら
> -1 を受け取ったあとに、Socket#isClosed() でも使って接続状態を確認したらどうですか?

Socket#isClosed() が利用できるか調べてみます。


> う、そのサンプルコードからいろいろと危険なニオイが。
はい。わざとシンプルに書きました。

> 「そのメソッドを一つのSocketInputStreamに対して繰り返し呼び出す」
ことをしないには、例えばどういう方法がありますか?
お知恵をいただきたいです。

先にも書いたとおり、現状は毎秒 readSample()をスレッドが呼び出しています。
それは、毎秒インプットストリームにデータがたまっていることを期待しているからです。
(実際には telnet なので、毎秒 write() と readSample() のセットを繰り返し呼んでいます)

一回 state2 が発生したとき、次が state1 なのか state2 なのかは
わからなくないですか?


> それと、ストリームから1バイトずつ読んで、1バイトを1キャラクタ化
> していますが、ダブルバイト文字対応は大丈夫ですか?
ascii 文字しか使用しないので考慮していません。

> それと、ソケットのストリームバッファから1バイトずつ読むことを繰
> り返すのは、性能にとって良くないです。SocketInputStream#read()
> は、その呼出し毎にnativeメソッドコールを発生しますし、おそらく
> ネイティブのソケットAPIも、1バイトずつ読むというのは、纏めて読
> むより性能が劣ります。
> どうせ終端まで読みきってしまうのですから、大き目のバッファを利用
> してバルクリードした方が良いですよ。

パフォーマンスが悪いのは認識しています。
が、telnet ですし一度に何百KBの通信をするわけではないので
今は特に問題視していません。


ちなみに、引用ってどうやるのでしょうか?
シュン
ぬし
会議室デビュー日: 2004/01/06
投稿数: 328
お住まい・勤務地: 東京都
投稿日時: 2004-11-11 18:54
疑問点は一つ、
何故ストリーム終端(行末じゃないですよ)に達したソケットに対して、
もう一度read()を試行するのですか?

あなたが作っているプログラムは、多分あなたの思っているとおりに
は動いていないような。あるいは、シンプルなサンプル記述になにか
変な間違いがあるかのどちらかですね。
ねま
会議室デビュー日: 2004/11/10
投稿数: 18
投稿日時: 2004-11-11 21:31
シュン様、ありがとうございます。
--------------------------------------------------------
public class TelnetSample {
  Socket socket = null;
  InputStream in = null;
  PrintStream out = null;

  pulic void do() {
    socket = new Socket(hostname, 23);
    socket.setSoTimeout(10000);

    in = socket.getInputStream();
    out = new PrintStream(socket.getOutputStream());

    // 無限ループ
    foo();
  }
  
  public String foo() {
    String data;

    try {
      while(true) {
        // Socketに対して、write と read を必ずセットで呼んでいる。
        out.print("ls")
        data = readSample();
        System.out.print(data);
        Thread.sleep(1000);
      }
    } catch(IOException e) {
      System.out.print("I/O Error!!");
    }
  }

  public String readSample() throws IOException {
    StringBuffer sb = new StringBuffer();

    while( true ) {
      int ch = in.read();
      if (ch < 0) {
        break;
      }
      sb.append((char) ch);
    }
    return sb.toString();
  }
}
--------------------------------------------------------
※即席ですが、こんなものを作りました。
 何がしたいのかだけを汲み取っていただくため、コンパイルは通らないです。
 TelnetSample#do() が実行された状態だと考えてください。
 全角空白でインデントしました。

> 疑問点は一つ、
> 何故ストリーム終端(行末じゃないですよ)に達したソケットに対して、
> もう一度read()を試行するのですか?

説明不足でしたが、毎秒あるコマンドを送信し、その受信結果を
readSample() で受け取っています。

よって、read() を続けて呼んでいるわけではありません。
-1 が返って来たかどうかに関係なく、
readSample() を抜けた後、次のコマンドを発信し、
また readSample() で受け取っているのです。

だから、
> 毎秒インプットストリームにデータがたまっていることを期待しているからです。
と表現したのです。


[ メッセージ編集済み 編集者: ねま 編集日時 2004-11-11 21:35 ]
komey
ベテラン
会議室デビュー日: 2003/11/27
投稿数: 76
投稿日時: 2004-11-11 22:27
引用:

シュンさんの書き込み (2004-11-11 18:54) より:
疑問点は一つ、
何故ストリーム終端(行末じゃないですよ)に達したソケットに対して、
もう一度read()を試行するのですか?



TelnetのようなTCPコネクションの場合は行って返っての通信ではなく、
リクエスト-レスポンスを何回も行うことになるので
一つのInputStreamに対してread()を実行する、ということかと。

Telnetが想像しにくかったらHTTPのKeep-Aliveを想像してみてください。


>ねまさん
-1を受信した場合、処理を実行して良いかどうかは
やはり細かく判断するしかないように思います。
# 実装に依存しそうなのであまり詳しくはわかりませんがSocket#isClosed()が有望?
その上でエラーの場合は例外を投げても良いように思いますよ。

老婆心ながらお聞きしますが、1回のリクエスト-レスポンスの終了と
Telnetセッションの終了をどうやって区別するかは検討済みでしょうか。
1回のリクエスト-レスポンスの終了は-1で検知できますが
Telnetセッションの終了は-1ではないですよね。

あと、実装では気にされているかもしれませんが、
read()でブロックされているうちに1秒が経過してしまい
処理が他のスレッドのread()とかち合わないように、
ノンブロッキングAPIを検討した方が良いですよ。


[ メッセージ編集済み 編集者: komey 編集日時 2004-11-11 22:28 ]
未記入
ぬし
会議室デビュー日: 2004/09/17
投稿数: 667
投稿日時: 2004-11-11 23:03
引用:

TelnetのようなTCPコネクションの場合は行って返っての通信ではなく、リクエスト-レスポンスを何回も行うことになるので一つのInputStreamに対してread()を実行する、ということかと。



私もシュンさんと同じ考えなんですが…。初回の readSample() でストリーム終端に達するまで読み出してしまうわけですから、さらに readSample() を呼ぶのであれば、(HTTP/1.0 のように) ソケットを再接続して InputStream を作り直さないと 2回目以降は何も得られないと思います。

メソッドを展開して、重要な制御構造だけ書き下すとこのようになると思いますが、なにか勘違いしてますかね。。
コード:

InputStream in = socket.getInputStream();
while(true) {
//ここでソケットの再接続/ストリームの再取得をしないとならないはず。
while(true) {
int ch = in.read();
if(ch < 0) {
break;
}
}
Thread.sleep(1000);
}



サーバー側が shutdownOutput() を実行してストリーム終端に達したあとで、ソケットを再接続せずに、再度、ストリームを有効にするなんてことが TCP/IP で可能なんでしょうか。

[ メッセージ編集済み 編集者: 未記入 編集日時 2004-11-11 23:04 ]
komey
ベテラン
会議室デビュー日: 2003/11/27
投稿数: 76
投稿日時: 2004-11-12 03:47
すみません、完全に勘違いしてました。
リクエスト−レスポンス1回が終わったからと言って-1が返るわけではないんですね。
先ほど実際に動かしてみてわかりました。
HTTPのKeep-Aliveのクライアント役をSocketクラスを使ってやってみたのですが
レスポンスをreadしてから次のレスポンスが到着するまではブロッキング状態で、Keep-Aliveのタイムアウトをもって-1が出力されました。

Telnetに置き換えると、Telnetセッションの終了は-1で検知できますが
1回のリクエスト-レスポンスの終了は-1ではない、ということですね。


混乱させることになってしまい、申し訳ありません。




ねま
会議室デビュー日: 2004/11/10
投稿数: 18
投稿日時: 2004-11-12 13:46
情報を小出しにしているようで恐縮です。
通信には、Jakarta Commons Net API を利用しています。

具体的には、
org.apache.commons.net.telnet.TelnetClient
を使用しています。サンプルが下記サイトにあり参考にしました。

Jakarta Commonsによるネットワークプログラミング
http://www.salicaceae.net/net/net05.html

さらには、TelnetClient をラップした Telnet クラスと
コネクションプーリングを実装した TelnetFactory クラスがいます。
Telnet のセッションは TelnetFactory が管理しています。

> あと、実装では気にされているかもしれませんが、
> read()でブロックされているうちに1秒が経過してしまい
> 処理が他のスレッドのread()とかち合わないように、
> ノンブロッキングAPIを検討した方が良いですよ。

先の readSample()は synchronized 指定をしています。

> Socket#isClosed()が有望?

org.apache.commons.net.telnet.TelnetClient
には、isConnected()はありますが、isClosed()はカプセル化されていて見えません。
また、isConnected() のチェックは readSample()の前で必ず行っています。

実装を見てみたのですが isClosed() は close()を呼んだとき、
isConnected()はconnect()を呼んだときにフラグが切り替わるようです。
InputStream#read()でExceptionが発生したときや -1 が発生したときには
フラグが切り替わらないのであまり信用できるフラグだとは思いません。


-----
私の理解不足で申し訳ありませんが、
 ○フロー
 1. ls コマンド送信
 2. 結果を受信
 3. 1秒待つ
 4. ls コマンド送信
 5. 結果を受信
 6. 1秒待つ
 〜以降繰り返し〜

といったフローがあるとき、
2. で -1 を検出したら 4. でコマンド送信しても 4. の結果を
5. で受け取ることは絶対にないのでしょうか?
言い換えると、一度 -1 になった InputStream の中には再接続しない限り
二度とデータがたまることはない。ということなんですね?
 (テストをこれからしてみます)

-----
> Telnetに置き換えると、Telnetセッションの終了は-1で検知できますが
> 1回のリクエスト-レスポンスの終了は-1ではない、ということですね。

まさに言いたいことはそれです。
-1 を検出した後でも、コマンドを送信すればまた InputStream にデータが
溜まってくれるものと思い込んでいました。
そこに私の勘違いがあったのですね。

さらには、readSample() をシンプルにしすぎたせいで皆様にも混乱を招いたようです。
本当の readSample() は以下のように、ストリームを最後まで読み出して抜けるのではなく、
プロンプトが出力されたら抜けるようになっており、正常系なら@に来ることは
(絶対に?)ありません。

--------------------------------------------------------
  public String readSample() throws IOException {
    StringBuffer sb = new StringBuffer();

    while( true ) {
      int ch = in.read();
      if (ch < 0) {
        break; // -----@
      }
      sb.append((char) ch);

      // プロンプトが出力されたらここで抜ける -----A
    }
    return sb.toString();
  }
--------------------------------------------------------
※丸付き数字でごめんなさい

-----
で、本題に戻ります。

ソケットを再接続しなければならない、ということは
Actor からみたら通信が切れているのと同じじゃないでしょうか?
コマンドを投げ続けても何も返ってこないわけですよね?

そうであるなら『通信が切れたから再接続してください』
と伝えなければならないと思うのです。

だったら、少なくとも state3. のときは例外で良いんじゃないか?
と思って皆様の意見をお聞きした次第なのです。

-----
蛇足ですが、
-1 を検出したとき、netstat を実行してみると

PC-A では、コネクションは切れていました。
PC-B では、ESTABLISHED のままでした。

なので、-1 を検出したときに、PC-A側からdisconnect()をちゃんと
呼んでやらないと、PC-B側 の仮想コンソールを使い切ってしまい、
そっちの方が問題でした。



[ メッセージ編集済み 編集者: ねま 編集日時 2004-11-13 03:37 ]

スキルアップ/キャリアアップ(JOB@IT)