Winsock Programmer's FAQ
第7章: 論説記事: TCP を有効に使うために

TCP を有効に使うために

Warren Young 著

ネットワークプログラミングの初心者は、はじめの頃にほとんど必 ずと言っていいほど、ネットワークもしくは TCP/IP スタックがデータ を壊しているのではないかと思ってしまう問題に突き当たります。これ は初心者にとってかなり驚くべきことでしょう。TCP とは信頼性のある トランスポートプロトコルである、と教えられているでしょうから。事 実、TCP と Winsock は、正しく使えば全く信頼性があるものです。こ のチュートリアルでは、TCP の使い方を学んでいるときによく出くわす 問題について論じたいと思います。

問題 1: パケットは幻影である

この問題はさまざまな形をとって現れます。

  • 「クライアントプログラムは 100 バイト送信してるのに、サーバ プログラムはたった 50 バイトしか受け取らないんだ。」
  • 「僕のクライアントプログラムは、小さなパケットを何個か送って るんだけど、サーバプログラムじゃ一個の大きなパケットしか受け取ら ないんだよ。」
  • 「指定したソケットにデータが何バイトあるのかどうやったらわか るの? 受信バッファをパケットのサイズ分だけ準備したいんだ。」

この問題を理解することは、TCP/IP の通過儀礼の一つだと私は思い ます。

あなたがきちんと理解しなければならない概念の核心は、TCP は ストリーム型プロトコルである、ということです。その意味すると ころは、もし 100 バイト送信したとすると、受信側は 100 バイト全部 を一度に受信することもあるし、1バイトを100個ばらばらに受信するこ とも、25バイトの塊を4つ受信することもありえるということです。あ るいは受信側ではその 100 バイトの他にも、その直前に送られたデー タや直後に続いて送られたデータも一緒に受信することだってあるので す。

じゃあ、パケット全体だけを受信させるにはどうするの? と聞きた いんだよね。いちばん簡単な方法は、私の経験では、各パケットの頭に パケット長の値をプレフィックスとしてつけることです。例えば、各パ ケットに、パケットの長さがいくらあるかを示す2バイトの符号無し整 数のプレフィックスをつける、ということです(ネットワーク越しに整 数を正しく送信するためには、問題2の項を 参照して下さい)。長さプレフィックス方式は、例えばバイナリデータ のような、各プロトコルパケットが特定の構造を持っていないデータで あるときに特に効果があります。TCPストリームから長さプレフィック スを読み込むプログラムの例として、こちらの例を参照して下さい。

ストリームプロトコルの上でパケットを組み上げるもう一つの方法 は、「区切り文字方式」と呼ばれます。この方式では、送信する各パケッ トの後ろに独自の区切り文字を付けます。良い区切り文字を選ぶことが この方式のキモです。区切り文字は、パケット中には絶対現れない 一文字もしくは文字列でなければなりません。区切り文字プロトコ ルの良い例としては NNTP、POP3、SMTP、HTTP などがあり、これらでは 区切り文字として、復帰/改行("CRLF")を使っています。一般的に区切 り文字方式は、テキストベースのプロトコルでしかうまく動きません。 というのは、その設計上、使われるデータは全文字の内のサブセットに 制限されるためで、区切り文字として使うことができる文字がたくさん 残っているからです。なお、前述したプロトコルの中には、長さプレ フィックスの様相も同時に持つものもあります。例えば HTTP では、応 答の中で "Content-length:" ヘッダーを送信します。

この二つの方式のうちでは、私は長さプレフィックス方式の方が好 きです。それは、区切り文字方式の方はパケットの終了を見つけるまで 闇雲に読み込むようにようにプログラムを作らなければならないのに対 し、長さプレフィックス方式は長さのプレフィックスを読み込んだ時点 ですぐに、そのパケットの取り扱いを始められるからです。一方、区切 り文字方式の方は柔軟性があり、プロトコル自身をコンピュータ言語で あるかのように設計することもできます。まあそのときはプロトコルパー サーも複雑になってしまうでしょうけれども。

TCP上でパケットを正しく扱うために、他にもいくつか考慮すべき点 があります。一つ目は、recv() の返却値を必ずチェック することです。この値は、バッファ内にデータが何バイト占めているか を示しています -- あなたが思っているより小さい場合がよ くありえるのです。二つ目に、パケットが全部到着したかどうかを調べ たいときに、Winsock スタックのバッファの中を覗き見する(peek) ことはしない ということです。様々な理由で、ピークを行うことは問題があるのです。 その代わりに、データを全てあなたのアプリケーションバッファに読み 込んで、そこで処理をするようにしてください。

問題 2: バイト順序

Winsock プログラミングにおいて、ntohs()htonl() の呼出しが必須であるということは既にお気づ きのことかと思いますが、それがなぜ必要であるかはご存知な いかもしれません。この理由は、コンピュータに整数値を格納するため の方法には、大きく分けて二種類存在するため、なのです。その二種類 とは、ビッ グ・エンディアン リトル・エンディアン と呼ばれています。ビッグ・エンディアン は、数値の上のほうのバイトを、メモリ上で低い位置に格納します(大 きいほうが先)。一方、リトル・エンディアンのシステムではその逆で す(ちなみに「ミドル・エンディアン」という奇妙なシステムも存在し ます!)。これらの二つのコンピュータ間で通信しようとするときは、数 値フォーマットが両者で一致していなくてはならない、というのは明ら かです。このため TCP/IP の仕様では「ネットワークバイト順序」を定 義し、そのヘッダ中では(もちろん Winsock でも)全てこの順序が使わ れるのです。

もしネットワークプロトコルの一部として生の整数を送ろうとして いて、それを別の整数表現形式を持つプラットフォーム上で受信しよう とすると、その結果として、受信側ではゴミのデータを受け取ったよう になってしまいます。この問題を解決するために、TCPプロトコルの 教えに従い、ネットワークバイト順序を使いましょう。いつでも。

同じ原理が、浮動小数点など、他のプラットフォーム依存のデータ 形式についても言えます。Winsock では整数以外のデータについては、 プラットフォーム非依存の表現形式を生成する関数は定義されていませ んが、これを取り扱うために 外部データ表現形式 (External Data Representation, XDR) というプロトコルが存在します。 XDR は、二つのコンピュータ間でさまざまな型のデータを送り合うため の方法を、プラットフォームに依存しない形で定式化したものです。 XDR はとてもシンプルなので、自分で実装することもできるでしょう。 あるいは、ライブラリの ページで XDR プロトコルを実装しているライブラリを探してみてくだ さい。

ちなみに、ネットワークバイト順序は実はビッグエンディアンです。 しかし、決してこの事実を利用してはいけません。ビッグエンディアン のマシンで開発しているプログラマーの中にはバイト順序の問題を無視 してしまう人もいますが、これは悪いスタイルです。そのような悪い習 慣はいずれ自分の身に災難として降りかかってくるから、というこれ以 上の理由はありません。もう一つ、つまらない雑学を。リトルエンディ アンのマシンとして最も有名なものは、Intel x86 と Digital の Alpha でしょう。その他のほとんど全てはビッグエンディアンです。例 えば Motorola 680x0、Sun SPARC、MIPS Rx000 などがあります。面白 い話ですが、両方のモードで動作する「バイ・エンディアン」デバイス も僅かに存在します。例えば PowerPC や HP PA-RISC 8000 などです。 ただし PowerPC はほとんど常にビッグエンディアンモードで実行され ているようです。PA-RISC でも同様ではないかと私は思っています。

問題 3: 構造体のパディング

構造体のパディングの問題を説明するために、以下の C 宣言を考え てみましょう:

                struct foo {
                    char a;
                    int b;
                    char c;
                } foo_instance;

int が 32ビットと仮定すると、この構造体は 6 バイト占有すると 思うかもしれません。しかし問題は、多くのコンパイラは構造体の各メ ンバが4バイトの境界に並ぶように「詰め物」を入れる、ということで す。コンパイラがこのようなことを行うのは、近代的なCPUでは、メモ リ位置の境界に正しく整列されたデータを取ってくるほうが、境界に整 列されていないメモリから取ってくるよりも早いからなのです。上記の 構造体で 4 バイトの詰め物を入れると、実際には 12 バイトにまでな ります。この問題は、構造体の全体を、Winsock を通して送信しようと して、こんな風にしたときに初めて顕在化します:

                send(sd, (char*)&foo_instance, sizeof(foo), 0);

受信側のプログラムが、同じマシンアーキティクチャで同じコンパ イラで同じコンパイルオプションでコンパイルされたものでない限り、 相手側のマシンでデータを正しく受信できる保証はないのです。

解決法としては、構造体を送るときはいつでも、データメンバを一 つずつ「梱包」して送るということです。あるいは、コンパイラに強制 的に構造体をパックさせるという方法もあります。Visual C++ では、 コマンドラインオプションの /Zp か、#pragma pack ディレクティブでこれを行うことができます。また Borland C++ では -a コマンドラインオプションででき ます。ただし、バイト順序の問題は忘れないでください: パックした構 造体をそのまま送るときでも、送信前にバイト順序を正しく並べなおす ことは忘れないように。

まとめ

この話の教訓は、Winsock はあなたのデータを正しく送信しますが、 あなたが考えている通りの方法で送信するのだとは思わないことです!

Copyright © 1998-2001 by Warren Young. All rights reserved.


<< どの I/O 戦略を使うべきか? ザ・間違いリスト >>
Last modified: $Id: effective-tcp.html,v 1.5 2002/11/09 20:40:33 ksk Exp $ Go to the original FAQ page
< Go to the main FAQ page << Go to the Home Page