2006年7月12日水曜日

TcpClientのメモ

.NET Frameworkに用意されている各種ネットワークアクセスクラス(WebClient, WebRequest/WebResponse, SmtpClient)ではなく、あえて低レベルのTcpClientを使うときもあります。そんなときのための注意点を。


ストリームの終わりをどのように判断するか、これは悩ましいところですが、NetworkStream.DataAvailableを使うのはやめておいたほうがいいです。送信元がデータを送るとき、いつでも全部まとめて送信するわけではないからです。たとえば、ある瞬間にはDataAvailableがfalseになっていても、1秒後にはtrueになったりします。「じゃあ、NetworkStream.DataAvailableがfalseならしばらく待ってみて、それでもfalseだったら読み取り終了にすればいいじゃないか」というのは筋が悪いです。いったい何秒まてば十分なのかは誰にも分からないですし、それに無駄な待ちが必ず発生することになりますしね。
結局のところ、規定の長さに達するまでStream.Readするか、またはデータの区切り(CrLfなど)が届くまでStream.ReadByteを繰り返すか、そのどちらかを選択するのがいいでしょう。もちろん、どちらを選ぶかはプロトコル次第ということになります。そのように実装すると、データの終わりを受け取るまではブロックすることになります。ですので、適切なタイムアウトは必須でしょう。


データの区切りが(CrLfなど)決まっている場合はStream.ReadByteを繰り返せと書きましたが、反論が2つ考えられます。1つ目は「区切りがCrLfなら、StreamReaderを使ってReadLineした方が簡単」という意見と、もうひとつは「1バイトずつStream.ReadByteするのは遅い」という意見です。


最初の意見ですが、もし送られてくるデータが全て文字列で、しかも最初から最後までエンコーディングが固定されているのなら、そのエンコーディングに対応したStreamReaderを使うのもいいでしょう。ですが、そうでないなら、つまりエンコーディングはデータを受け取るまで分からないとか、あるいは途中からは生のバイト列が必要になるとか、そういう場合はStreamReaderは向いていません。StreamReaderはストリームの先読みをして、内部でバッファリングします。ですので、「次の行からはエンコーディングの異なるStreamReaderを使う」とか「次の行からはバイト列」といったときに、ストリームの位置は次の行の先頭よりも先に進んでいます。要するに、StreamReaderを使った場合は、もう元のStreamは使えないと考えるべきなのです。(天邪鬼な人はISO-8859-1エンコーディングを持ち出すかもしれませんが、その議論はここではしません。「できるけど、ムダが多い」とだけ述べておきます。)


もうひとつの意見、つまりReadByteは遅いという意見ですが、それは生のNetworkStreamを1バイトずつ読み取るから非効率なので、そういう時はBufferedStreamでラップしてやればいいのです。もちろん、BufferedStreamは内部でバッファリングしますから、BufferedStreamを使った場合は、もうもとのStreamは使えないことに注意しなければなりませんが、大きな問題にはならないでしょう。