Winsock Programmer's FAQ
第3章: Winsock 中級者向けの議論

3.1 - Winsock において { HTTP, POP3, SMTP, FTP, Telnet, NNTP, など} はどうやって喋れば良いの?

Winsock 自身はこれらのプロトコルを話すための方法は提供してい ません。というのも Winsock はこれらアプリケーションレベルプロト コルのより下層を取り扱うものだからです。しかし、あなたのプログラ ムにこれらのプロトコルを話させる方法はたくさん存在します。

最も簡単な方法は、サードパーティのライブラリを使う方法です。リソースの章 にてこれらのいく つかを紹介しています。

もし HTTP、FTP、gopher プロトコルさえ使えればよいのであれば、 Microsoft Internet Explorer から提供されている WinInet ライブラ リを使用することができます。新し目のバージョンの Microsoft の開 発ツールには、WinInet に簡単にアクセスするためのコンポーネントが 含まれています。

最後に、全て自分で作り出すことも当然ながら可能です。実装しよ うとしているプロトコルの仕様を読むことから始めたほうが良いでしょ うね。ほとんどのインターネットのプロトコルは RFC に記述されています。重要な RFC のページには、 よく参照されるアプリケーションレベルのRFC へのリンクがあります。 プロトコルの複雑さは非常に様々であり、プロトコルを実装するための 難易度を測る唯一の方法は、関連するRFC (複数であることも)を読むし かありません。例として HTTP の場合は、非常に単純なプロトコルでは ありますが、この RFC の著者は 176 ページをもこの説明に費やしてい ます。ま、ほとんどの RFC ではこんなにもったいぶったりしてません けどね。よかった。

RFC を読んだけれどもまだよくそのプロトコルのことが理解できな いのであれば、Usenet で聞いてみるのも手です。特定のアプリケーショ ンプロトコルに特化したニュースグループがたくさん存在しています: その多くは comp.protocols.* 階層にあります。そこに無くても、その 他一般の Winsock、TCP/IP の メーリングリストとニュースグループ の一つで聞いてみることも できます。

3.2 - TCP ストリームを SSL/TLS で暗号化するにはどうすればよいの?

現時点では、Windows NT 4.0 SP4 以上、Windows 2000、Windows CE では組み込みの SSL 機能を持っています。その他のオペレーティング システムでは WinInet を使うか(さまざまな制約がありますが)、ある いはサードパーティ製の ライブ ラリ を使うことです。

Windows NT 4.0 SP4以降 と Windows 2000 は、セキュリティ API を通して SSL が提供されます。この機能がどのように動作するかを示 す簡単なサンプルコードが Win32 Platform SDK にあります。SSL のサンプルは、Platform SDK ディ レクトリの "Samples\WinBase\Security\SSL" 配下にありま す。

Windows CE では異なる SSL のメカニズムを持っています。この機 能をどのように使うのかを記述した MSDN の記事 があります。この記事は WinInet の方法にも触れています。

WinInet は Internet Explorer バージョン3 以降の機能で、これを 使うことで Intenet Explorer のネットワークの機能の一部を自分のプ ログラムで利用することができようになります。IE3 は Windows 95 OSR2 以降に載っており、Microsoft は IE を Windows の機能の一部と して残すことに多大な努力を払っているので、WinInet は妥当な選択肢 の一つです。WinInet の主な欠点は、SSL 機能は HTTP 上でしか動作し ないこと、また WinInet はあまり柔軟性がないということです。加え て、128-bit の IE は全世界で利用可能なわけではありません。MS Knowledge Base の記事 Q168151 ではこの機能を使う方法を示されています。

3.3 - Winsock プログラム で自分の IP アドレスを取得する方法は?

3種類の方法があり、それぞれに利点、欠点があります:

  1. 最も簡単な方法は、connect 済みのソケットに対して getsockname() を呼び出すことです。connect 済みのソ ケットが無ければ、その呼出しは失敗するか、あるいは無意味な情報が 返されます。
  2. ソケットを事前にオープンすることなしに自分のアドレスを取得す るには、gethostname() の返却値に対して gethostbyname() を呼び出すことです。この例に示すように、この呼出しはそ のホストが持つ全てのインターフェースのリストを返却します。 (この方式の問題点については、例のページを参照のこと。)
  3. 三番目の方法は Winsock 2 でのみ動作します。新しい WSAIoctl() API は SIO_GET_INTERFACE_LIST オプションをサポートしており、 返却される情報の一部として、システムの各々のネットワークインター フェースのアドレスが返却されます。[C++ の例] (これも、注意点につ いては例のページを参照のこと。)

後者二つの方式では、多くの TCP/IP ネットワークのマシンでは少 なくとも二つのアドレスが返却されます。一つは「普通の」ネットワー クインターフェースで、一つは「ループバック」ネットワークインター フェースになるはずです。通常、「普通の」ネットワークインターフェー スはモデムかイーサネットカードです。ループバックインターフェース (IPアドレス 127.0.0.1)は、同一のマシン上で動作する二つのプログラ ムが、オペレーティングシステムのネットワークハードウェアレイヤを 通過することなしに通信できるようにさせるものです。ループバックイ ンターフェースを使用した通信は少なくとも普通のネットワークインター フェースと同じ程度の速度であり、ネットワークスタックによってはは るかに高速になります。

一つのシステム上に二つ以上のネットワークインターフェースを持 つこともあり得ます。多くのサーバでは、例えば二つ以上のネットワー クインターフェースカードを持っていると、上記2番目、3番目の方法で は4つ以上のエントリーが表示されます。さらに複雑な例はサテライト 型インターネットルータで、これにはインターネットへの上流接続を行 うモデムコネクション、インターネットからの下流接続を行うサテライ トアダプタ、その他の LAN を通信するイーサネットカード、そしても ちろんループバックインターフェースが繋がっています。

もし同じマシン上で動作しているサーバとソケットを使って通信し たいのであれば、ループバックインターフェースを使ってください。そ うでなければ、普通のインターフェースのうちの一つを、頭をフル回転 させて選ばなくてはなりません。全ての目的で使える決まりきった方法 はありません。ほとんどのプログラムにおいては、実際にコネクション が使っている IP アドレスが返却されるので、上記の1の方法で十分で す。もしこれが使えないのであれば、インターフェースにリストを表示 して、ユーザに一つを選ばせる必要があるかもしれません。

PPPインターフェースのアドレスを知りたい、というときのように、 より詳細な条件がある場合があります。この場合には上記の 3 の方式 が使えます。これによって得られる情報の中には、PPP インターフェー スのフラグが含まれるので、これで「ポイントto ポイント」インター フェースかどうかを知ることができるのです。

3.4 - TCPのようなストリームプロトコルで、パケット単位の処理を強制するための正しい方法は?

よく使われる方法は、区切り文字を使う方式と、長さの接頭辞を付 加する方式の二つです。

区切り文字を使う方式の例は、例えばキャレット文字(^)を使ってパ ケットを区切る、ということです。もちろん、その区切り文字は通常デー タの中には決して出てきてはいけません。あるいは区切り文字を「エス ケープ」する何らかの方法を用意しなければなりません。

長さ接頭辞を付加する方式の例は、パケットサイズをあらわす2バイ トの整数値を、全てのパケットの先頭に付加する、というようなことで す。整数値をネットワーク経由で送信するための正しい方法については、 FAQ の記事 TCP を有効に使 うために を参照して下さい。

これら両者を組み合わせた方式もあります。例えば HTTP プロトコ ルでは、ヘッダ行を CRLF の組で分離しますが(区切り文字方式の一種)、 HTTP の応答としてバイナリデータの塊を含める場合には、サーバはデー タを送る前に Content-length ヘッダを送信します。これは長 さ接頭辞方式の一種です。

次に何バイトのデータが来るのか、長さ接頭辞の値を読み込めばす ぐわかるので、私は単純な長さ接頭辞方式の方が好きです。それに対し て区切り文字方式では、パケットの終わりが来たとわかるまで、闇雲に 読み出し続けなくてはならないのです。

3.5 - 今サーバを書いています。どのポート番号を使えばよいのですか?

今作っているサーバが、既存の一般的なインターネットプロトコル のサーバであれば、そのポート番号は既に割り当てられています。これ らのよく使われる番号については、Internet Assigned Numbers Authority (IANA) のウェブサイトで知ることができます。

もし新しいプロトコルのサーバを作っているのであれば、サーバの ポート番号を選ぶ上で、従うべきルールと慣習がいくつかあります。

  1. ポート番号 1 〜 1023 は、新しいプロトコルを作成する人は使っ てはいけません。この範囲は「標準」プロトコルのために IANA によっ て予約されています。POP3 や HTTP といった重要なプロトコルにおい ては小さい番号(それぞれ 110 と 80)が割り当てられますが、君の作っ た新しい K-RAD ゲームサーバで使うべきではないですね。id ソフトウェ アの DOOM ネットワークサーバが 666 番ポートを使っているのは地獄 に落ちるべきです。まあ Quake においては悪行を悔い改めたようです けど。

  2. ポート番号 1024 〜 49151 は登録用ポート番号で、この範囲から あなたのポート番号を選ぶのが正しいです。ただし、全世界の人がこの 範囲からポート番号を選ぶので、使いたいポート番号を 登録 するか、少なくとも 現在の登録 ポート番号のリスト のチェックくらいはするのが道理です。とは いえ、全ての人がその人のアプリケーションのポート番号を決 めるときに、このリストをチェックしてくれる保証などどこにもない、 ということには注意する必要があります。
  3. ポート番号 49152 〜 65535 は動的ポート番号で、オペレーティン グシステムがランダムにポート番号を選ぶときに、この範囲のポート番 号が使われます。(例えば FTP プロトコルでは、データ転送フェーズで はランダムに決定したポート番号が使用されます。)この範囲からポー トを選ぶのは良くない発想です。というのは、あなたのプログラムとOS がポート番号を奪い合う戦いが、いつかはかなりの確率で発生するであ ろうからです。

  4. 多くのOSでは、クライアントプログラムのローカルポート番号とし て、1024 〜 5000 の範囲から選びます。ですので、あなたのサーバ用 のポート番号は 5000 よりも大きい番号から選んだほうが良いのですが、 これは前項のルールほど厳密なルールではありません。
  5. ポート番号 5000 〜 49151 の「安全な」範囲の中で、IANA が 「未登録」としている番号がたくさんあります。この中で、パターン化 しているポート番号やすぐ思いつきそうな番号は避けたほうが無難です。 誰でも覚えやすい番号を選びがちなので、ポート番号が衝突する可能性 が大きくなるからです。例えば、ポート番号 6969、5150、22222 といっ た番号は悪い選択例です。

また、あなたのプログラムを動作させるマシン上で、別のサーバが 既にそのポート番号を使っている場合などがあり得るので、ポート番号 を設定で変更可能にしておくことも考えておいたほうが良いです。一つ の方法として、Winsock の getservbyname() 関数を使う 方法があります: この関数がポート番号を返したらその番号を使い、そ うでなければデフォルトのポート番号を使う、というようにします。す るとユーザは、このプログラムで使用するポート番号を、SERVICES ファ イルを編集することによって変更することができます。SERVICES ファ イルは Windows NT/2000 上では %WINSYSDIR%\DRIVERS\ETC に、Win9x マシン上では c:\Windows\ に存在します。

3.6 - UDP って何? どんな制限があるの?

UDP (User Datagram Protocol) は TCP/IP プロトコル群の一部で、TCP に対する別の選択肢となるもので す。("TCP/IP" と書いたときには UDP も含まれますが、時には"IP上の TCP"という意味でもあります。ここでの議論においては"UDP/IP" とい う用語も使います。) Winsock では、socket() の二番目 の引数として SOCK_DGRAM を与えると UDP ソケットを使 うことができます。

UDP は「信頼性のない」プロトコルです。スタック は、パケットの喪失、重複、 到着順序の前後に対して、一切の制御を行いません。UDP パケットはデー タが壊れているかのチェックはされますが、データの壊れた UDP パケッ トは黙って捨てられるだけです。

プロトコルスタックは、UDPデータグラムの大きさがネットワークの MTU より大きいときは、データグラ ムを分割します。通信相手側のスタックはその分割片から完全なデータ グラムを再び組み立ててから受信側のプログラムに渡します。もし分割 片の一部が失われたり壊れていたりしたときは、そのデータグラム全体 が捨て去られます。このため、巨大なデータグラムを送るのはあまり実 用的ではありません。例えば、8K の UDP データグラムは、Ethernet 上に送信されると、6つの塊に分割されます。もしこの6個の分割片のう ち一つでも喪失したり壊れたりすると、8K のデータグラム全体が捨て られてしまうのです。

データグラムの喪失は、送信側あるいは受信側のプロトコルスタッ ク内部で発生することもあります。たいていはバッファが足りなくなっ た場合などです。さらに、同じマシン上で動作する二つの通信プログラ ムでさえも、 UDPを使っている場合はデータを喪失してしまう可能性が あります。(Windows 上では、高負荷の状態において実際にこれが発生 します。スタック内のバッファが一杯になるとデータグラムが落とされ るからです。) これはつまり、ローカルの IPC(訳注: プロセス間通信) の機能として UDP を利用するのは限度がある、ということです。

これら、いずれの原因で喪失が発生した場合でも、送信側および受 信側には何の通知もされません。喪失がプロトコルスタック内部で発生 した場合であっても、です。

重複したデータグラムを捨てることはありません。全部受信側に送 られます。重複の問題を検出するのはアプリケーション側に任されてお り、重複したデータグラムをどう扱うかはそのプログラムが決めます。

UDPデータグラムはどんな順序で転送されるかわかりません。二つの データグラムが別々のルートを経由して転送されて、二番目のデータグ ラムのルートの方がたまたま早かった場合など、データグラムの到着順 序がネットワーク上で逆転することは時々発生します。

3.7 - UDPは何に使えるの?

上で議論してきたのを見ると、UDP は全く 使えないように見えます。でしょ? まあ、TCP のような信頼性のあるプロトコル に比べての利点も、いくつかはあります。

  1. UDPの方がよりスリムなプロトコルである: プロトコルヘッダの大 きさは TCP では最低 20 バイトでそれ以上になることもあるのに対し て、UDP では 8 バイトで固定です。
  2. UDP は輻輳制御もデータの集約転送も行いません。これにより、ACKの遅延Nagle アルゴリズムによって引き起こされる 遅延が発生することがなくなります。(もちろん、これは多くの場面に おいては欠点にもなり得ます。)
  3. プロトコルスタック中のコードの量は、UDP の部分の方が TCP の 部分よりも小さいです。つまり、パケットがネットワークに到着してか らアプリケーションに渡される間の遅延(レイテンシ)が小さくなるとい うことです。
  4. ブロードキャストやマルチキャストで は、UDP パケットしか使えません。

これらの特徴により、UDPは、時間的な制約と制御が信頼性よりも重 要であるようなアプリケーションに適しています。また、ある種のアプ リケーションにおいては、その性質柄、UDPの問題点を許容できるもの もあります: ストリーミングビデオのプログラムにおいては、データが 喪失しても単に一コマか二コマ落ちるだけのことです。

UDPにも利点はありますが、そのことで UDPの欠 点を忘れてしまうことがないように注意してください。非常に多く のアプリケーション作成者が、最初 UDP でプログラムを書き始め、そ して後で信頼性を確保する機能を無理矢理追加せざるを得なくなる、と いうことをやっているのです。UDP を使おうと検討しているときには、 最初から TCP を使ったほうが、それを再発明するよりも良いのではな いか、ということを自問自答してみてください。気をつけて欲しいこと は、Winsock のレイヤでは TCP を完全に再発明することはできない、 ということです。経路MTUディスカバリなどのいくつかのTCPの機能は、 OSのネットワークレイヤへの低レベルアクセスを必要とします。それ以 外のTCPの機能は UDP上でも複製することができるかも知れませんが、 正しく実装するのは難しいことです。TCP/IP とは20年来生き続けてき たものである、ということを忘れないで下さい。TCP/IP のプロトコル 群には、その信頼性と性能のチューニングのために並々ならぬ努力が費 やされてきているのです。

場合によっては RTP (RFC 1889)のようなものを使えば十分かも知れ ません。これは マルチキャストのマルチメディアアプリケーションの ために設計されたもので、UDPの上に信頼性を持つレイヤを設けるもの です。

3.8 - ブロードキャストパケットはどうやって送信するのですか?

UDP プロトコルを用いて、ネットワーク上の全てのワークステーショ ンが受け取ることができるパケットを送信することができます。(TCP はブロードキャストを使用することはできません。)

ブロードキャストパケットを送信するには、まず最初に setsockopt() 関数を使って SO_BROADCAST オプションを有効にしなければなりません。次に、「このLAN上に接続 されている全てのワークステーションにパケットを送れ」という意味の "directed broadcast" アドレスを得なくてはなりません。この directed broadcast アドレスを得るには、以下のような C コードを使 用します:

                u_long host_addr = inet_addr("172.16.77.88");   // local IP addr
                u_long net_mask = inet_addr("255.255.224.0");   // LAN netmask
                u_long net_addr = host_addr & net_mask;         // 172.16.64.0
                u_long dir_bcast_addr = net_addr | (~net_mask); // 172.16.95.255

潜在的な問題: ブロードキャストは便利なとき もありますが、ネットワーク上の全てのマシンに(パケットを受信しよ うとしていないマシンにおいても)負荷を発生させるということを意識 しておいてください。この理由は、プロトコルスタックの中でパケットの受信を拒 否することのできる部分は、レイヤが何層か下の部分にあるからです。 この結果、多くのルータでは単純なブロードキャストパケットは通過さ せないようになっていますし、場合によっては隣のネットワークからの directed broadcast も通過させないようになっています。(単純なブロー ドキャストパケットとは、アドレス 255.255.255.255 に送られたパケッ トです。) その現実的な結果としては、ブロードキャストが全く動作し ないこともあるし、また、たとえ動作はしてもネットワークに不必要な 負荷をかけてしまうことがあります。これらの問題を回避するために、 代わりに マ ルチキャスト の利用を検討しても良いかもしれません。

3.9 - Winsock はスレッドセーフですか?

Winsock の仕様上は、Winsock の実装がスレッドセーフであること を要求してはいませんが、Winsock のスレッドセーフバージョンを実装 することは許されています。

Bob Quinn 氏は、この件につい て以下のように述べています:

  • 「Winsock は、それがどんな実装であれ、実装した本人が スレッドセーフに作ったのであればスレッドセーフである (たまたま偶 然にスレッドセーフになるわけではない)。」
  • 「Microsoft (あるいはその他どのベンダでも)の実装が、スレッド セーフではないということは聞いたことが無い。」
  • 「Winsock アプリケーション開発者が、スレッド間でソケットを共 有するマルチスレッドアプリケーションを作った場合、スレッド間の同 期処理を行うのはその開発者の責任である。」

Bob氏のいう「同期処理」とは、例えば、二つのスレッドが連続して 同じソケットに send() を呼び出すと問題が起こるかも しれない、という意味だと思われます。Winsock の仕様においては、こ のような状況においてデータがどのように挿入されるかについて、何の 保証もありません。また、一方のスレッドがあるソケットに対して closesocket() を呼び出したとき、そのソケットを使っ ているもう一方のスレッドに対して、このソケットはもう無効であるこ とを何とかして通知しなければならない、ということについても同様の 問題があります。

一方のスレッドが send() を呼び出し、別のスレッド が同一のソケットに対して recv() を呼び出すのは安全 である、という伝説があります。しかし私はこれを確認したことはあり ません。より堅固な情報やデモンストレーション用のコード、さらなる 伝説などがあれば、教えていただけると幸いです。

複数スレッド間で一つのソケットにアクセスするよりも、一つのス レッドにソケットに対する単一の所有権を与えて、そしてネットワーク I/O 用のキューを用意することを考えたほうが良いかもしれません。こ のスレッドは I/O キューからのデータを送信し、受信したデータをキュー に入れることを行います。そしてその他のスレッドはそのキューに(適 宜同期処理を行いながら)アクセスするわけです。

非同期型ソケットの類を使うアプリケーションでは、通常なんらか の I/O キューを既に持っています。ここでの話に特に関係するのは、 オーバーラップ I/O あるいは I/O 完了ポートです。これらの I/O 戦 略はスレッドと親和性が良いからです。Winsock では、オーバーラップ したブロックをいくつか Winsock に与えると、Winsock はその一つを 送り終えてから次のブロックに移動します。これはつまり、オーバーラッ プしたブロックの連なりがそれぞれ別のスレッドによって追加されたも のだとしても、そのブロックの繋がりは崩れない、ということです。ま た、各スレッドは自分の追加したブロックに対して WSASend() を呼び出すこともできるので、メインループ をより簡単にすることができます。

3.10 - もし二つのスレッドが同じソケットに対して recv() を呼び出すと、それぞれにおいて同じデータが得られますか?

いいえ。Winsock はスレッド間でデータを複製するようなことはし ません。

二つの異なるスレッドから同一のソケットに対して同時に recv() 呼出しを 実行 してしまうと、大混乱が引 き起こされると思ってください。より詳細については、前項の質問を参照して下さい。

3.11 - あるソケットにおいて何かが起こったとき、それを二つのスレッドに通知する方法はありますか?

いいえ。同一のソケットに対して二つのスレッドが WSAAsyncSelect() を呼び出したとすると、最後に WSAAsyncSelect() を呼び出したほうのスレッドだけが、 それ以降の通知メッセージを受信することになります。同様に、同一の ソケットに対して二つのスレッドが WSAEventSelect() を呼び出した場合は、後から呼び出された方のイベントオブジェクトに 対してのみ、ソケットのイベントが発生が通知されます。また、一方の スレッドで WSAAsyncSelect() を呼び出し、もう一方の スレッドで同じソケットに WSAEventSelect() を呼び出 すことはできません。これらの関数は一つのソケットにおいて排他的な ものだからです。最後に、同一のソケットに対して二つのスレッドが select() を呼び出して、それぞれで同じ通知を受け取れ るかどうかを信頼することはできません。一方のスレッドでイベントを 発生させたりクリアしたりすると、他方のスレッドが監視しているイベ ントの状態を変化させてしまうかもしれないからです。

3.12 - インターネットに接続されているかどうかをどうやって検出できますか?

Winsockプログラムにとっては、コンピュータが既にインターネット に接続されているものとして処理をした方が便利なことがあります。多 くの場合「インターネットに接続されている」とは、ダイアルアップネッ トワーク接続がある、という意味になります。こういった接続をチェッ クしたいときには、このサンプル のコードを参照して下さい。

しかし、これは全ての状況で有効なわけではありません。まず最初 の問題は、全てのユーザがモデムを使ってインターネットに接続してい るわけではない、ということです。コンピュータはLANにつながってい て、LAN上のどれかのステーションがインターネットへのゲートウェイの役割を果たしてい る、という場合もよくあります。この場合、システムのネットワーク設 定を探し回ってゲートウェイが設定されているかどうかを調べることも できますが、今度はそのゲートウェイが単純にLANをインターネットに 接続しているとは限らない、という問題に突き当たります。LANがイン ターネットへゲートウェイ接続されていたとしても、そのゲー トウェイのインターネット接続が切れているかも知れませんし、アクセ スを制限するように設定されているかも知れないからです。

他にも、PCにインターネット接続用のモデムがあったとしても 、通常は切断されていて自動ダイアルするように設定されているか も知れない、という問題があります。この場合、モデムが現在接続され ていなくとも問題にはなりません。プログラムがとにかくコネクトしよ うとすれば、モデム接続が行なわれるからです。

この話の教訓は、普通はインターネット接続のチェックなどハナか らやらない方が良い、ということです。プログラムを起動することによっ て何をやろうとしているのか、ユーザは分かっているものだ、と単純に 仮定するのです。コネクションを張ろうとして、インターネット接続が 無かったためにそれが失敗したら、ユーザにそのことを伝えて、問題の 解決をユーザに委ねることができるのです。また、プログラムの接続制 御を、ユーザ設定可能にしても良いかも知れません。つまり、ユーザに ダイアルアップネットワーク接続をチェックした方が良いかどうかを尋 ねて、プログラムがやみくもにコネクションを張ろうとするのが良いか どうかを判断するのです。プログラムが推測するよりも、ユーザの方が ユーザ自身のシステムについてよく知っていることの方が多いのです。

3.13 - ローカルのユーザ名を取得するにはどうするのですか?

いくつかの方法があります。最も簡単な方法は、Win32 の GetUserName() 呼出しを使う方法です。[C++ の例]

他の方法として、Microsoft Knowledge Base の記事 Q155698 に示されている方法があります。これはやや複雑で、二つの全く違う方 法を説明しています。一つは Windows 9x/Windows 3.1 用で、もう一つ は Windows NT 用です。あなたが WIndows 3.1 をサポートする必要が あるか、あるいは LAN マネージャのドメイン名を(DNS のドメイン名で はなく)必要としているのでなければ、この記事の方法は避けたほうが 良いと思います。

3.14 - Windows 9x のダイアルアップ接続が、必要の無いときでも自動ダイヤルのウィンドウを出してきます。これを止めることはできますか?

ネットワークインタフェースを複数持っている Windows 9x のシス テムの中には、明らかに不必要のときでも、ダイアルアップ接続が自動 ダイヤルのウィンドウを出してくることがあります。その一例としては、 LAN に接続されていて、かつモデムを経由してもインターネットに接続 できるマシンのような場合があります。

ダイアルアップ接続のダイヤルウィンドウが出てくる引き金となる 原因として最も多いのは、Winsock プログラムが gethostbyname() を呼び出して DNS の参照を行おうとし たときです。たとえその名前が LAN 上のマシンで、DNS サーバが LAN 上に存在していたとしても、ダイアルアップ接続はまず Internet に接 続しようとしてしまうのです。

Win9x マシンをマルチホー ムにしようとしてDNS 設定をいじってみると、ネットワークのシス テムがリモートのDNS とローカルのDNSをサポートするようになんて設 計されていない、というのがはっきりわかるでしょう。結局、最適な解 決法は、IP アドレスを直接指定して、あなたのプログラムも IP アド レスを認識できるようにして、gethostbyname() を呼び 出さないようにすることです。

ダイアルアップ接続のバージョン 1.3 あるいは Winsock 2 では、 この問題が修正されている、と聞いたことがありますが、別の報告では それでも解決できない、と言っています。

3.15 - 非同期型ソケットは信頼できない、と聞いたことがあります。それって本当ですか?

あなたの書くプログラムが、Winsock 仕様に字義通り正しく従って いれば、非同期型ソケットは信頼することができます。

それでも時々、非同期の通知メッセージが喪失してしまうプログラ ムの話を聞いたことがあるでしょう。私が知っている限り、これらは全 て、文句をつけている人のプログラムのバグか、もしくは Winsock の ケチくさい通知ポリシーを誤解しているからです。

FD_WRITE の通知を考えてみましょう。FD_WRITE は、クライアント からのコネクションが通信相手に受理された時と、Winsock が WSAEWOULDBLOCK エラーを返した 後に出力バッファに空きができた時にのみ、送信されます。別 の言い方をすれば、FD_WRITE は「さっきまで、このソケットからデー タを送る準備ができていませんでした。でも今 OK になりましたよ。」 というときにだけ送られるのです。これを扱うための最も伝統的な方法 は、送りたいデータがあるときは、FD_WRITE を受け取っているかどう かに関わらず、常にデータを送信しようとすることです。そうすると WSAEWOULDBLOCK エラーが返されることがありますが、これは無害です し、扱うのは簡単です。FD_WRITE のハンドラは、単にキューに入って いるデータを全て送信しようとするだけで良く、これを全てのデータを 送信し終わるか、別の WSAEWOULDBLOCK が返されるまで繰り返すのです。

Win16 のメッセージキューは、長さは固定でかなり短いです。した がって、少なくとも 16ビットプログラムでは、通知を喪失してしまう 可能性があります。メッセージキューが一杯で Winsock が通知を送る ことができなかった場合、通知しようとし続けることになっているはず です。しかし経験的には必ずしもそうならないことが示されています。 なお、一口に「16ビットの Winsock」と言っても、そのプロトコルスタッ クには、さまざまなベンダーからそれぞれいくつものバージョンが何年 にも渡って出されているものがある、ということを忘れないでください。

私が関わってきた範囲では、ほとんど非同期型ソケットばかりを数 年に渡り使ってきましたが、何の問題もありませんでした。私よりも長 年、非同期型通知を使ってきた人たちも、これに同意してくれています。 もしどうしても通知が喪失されているようだ、と思うのであれば、我々 がプロトコルスタック内のバグを見落としているのか、あなたのプログ ラムにバグがあるのか、どちらであるのかをじっくりと考えてみてくだ さい。

3.16 - Nagle アルゴリズムとは何ですか?

Nagle アルゴリズムは、あるコネクションに送ったデータが全て確 認(ACK)されるまで、次のデータの送信を待たせる、という方法で TCP の最適化を行うものです。例外としては、ネットワークのフレームを満たすのに十分なデータ がキューに入っていれば、Nagle は ACK を待つようなことはしません。 (この例外が無いと、結果として Nagle アルゴリズムは TCP の移動ウィンドウアルゴリズムを無効にしてし まうからです。) Nagle アルゴリズムの全容については RFC 896 を参照して下さい。

さてそれで、君は Nagle アルゴリズムとは何のためのもの なの? ってことが聞きたいんだよね。

ネットワークの理想的な状態では、全てのプログラムは全ての send() の呼び出しにおいて、常にフレーム一杯のデータを送っていま す。これにより、パケット中の有効なデータの割合は最大になります。

TCP の基本ヘッダと IPv4 ヘッダは それぞれ 20 バイトです。したがって、プロトコルオーバーヘッドの最 悪値は 40/41 つまり 98% です。Ethernet の一フレームに入るデータ 量は最大 1500 バイトなので、プロトコルオーバーヘッドの最良値は 40/1500 つまり 3% 以下になります。

Nagle アルゴリズムは、データが通信相手から ACK されるまでプロ トコルスタックを待たせるので、その間ローカルのプログラムはさらに send() を呼び出すことができます。TCP はストリーム型プロトコルであるので、 これら複数の send() 呼出しのデータを一つの TCP パケッ トに集約させることができ、有効データの割合を増加させることができ るわけです。

単純な telnet のプログラムを考えてみましょう。telnet 通信の大 部分は、一個の文字を送って、その文字が通信相手からエコーバックさ れてくるのを受信する、という内容です。Nagle アルゴリズムが無けれ ば、これは TCP の最悪値: 数十バイトのプロトコルオーバーヘッドに 包まれた一バイトのユーザデータ、という状況を引き起こします。 Nagle アルゴリズムを有効にすると、TCPスタックは、前に送った文字 が確認(ACK)されるまで、telnet の一文字を送り出すことはしません。 そのときまでには、ユーザは他の文字を何文字かタイプしているでしょ うから、相対的にプロトコルオーバーヘッドは減少します。

この単純な効率化の仕掛けは、TCPプロトコル群の他の機能とも関係 します。

  • 多くのスタックでは遅延 ACK アルゴリズ ムを実装しています: これは特定の状況において、 相手側のスタックからの ACK の返事を遅らせるというもので、こちら 側で一個のパケット中にもう少しデータを "Nagle" する時間ができます。
  • Nagle アルゴリズムは、早いネットワークよりも遅いネットワーク の方が、パケット中の有効データの割合を向上させる傾向を持っていま す。なぜなら ACK が帰ってくるまでの時間が長いからです。
  • TCP では、ACK パケットにデータを持たせることを許しています。 もしこちら側のスタックが ACK パケットを送信する必要があると判断 した時、Nagle アルゴリズムが出力バッファにデータを溜めていたとす ると、その溜められていたデータは ACK パケットと一緒に送出されま す。

Nagle アルゴリズムは Winsock においてはデフォルトで有効になっ ていますが、setsockopt() の TCP_NODELAY オプション によって、ソケット単位で無効にすることができます。このオプション は、ごく一部の場面を除いては、無効にす るべきではありません

Nagle アルゴリズムにあまり頼りすぎないように注意してください。 send() はカーネル関数であるので、 send() の呼出しは通常の関数呼出しよりも重い処理なの です。アプリケーション側においても、自分でデータをできる限り集約 して、send() の呼出し回数を減らすようにするべきです。

3.17 - Nagle アルゴリズムをオフにすべきなのはどんなときでしょう?

一般論としては…ほとんど無いですよ。

未熟な Winsock 屋さんは、TCP データストリームでパケット単位の処理のようなことをさせよ うとして、Nagle アルゴリズムを無効にしようとしたがります。つまり、 例えば 40バイトと 60バイトの二つのパケットを送信して、受信側にお いても 40バイトのパケットと、次に 60バイトのパケットを別々に受信 させることを可能にしたい、というわけです。(Nagle アルゴリズムが 有効になっていると、TCP がこれら二つのパケットを一個の100バイト のパケットに集約してしまうことがあるでしょう。) 残念ながら、この 手法は全く無駄です。理由を以下に示します:

  1. たとえ送信側がこれらのパケットを別々に管理したとしても、受信 側の TCP/IP スタックが受信したパケットを一個のパケットに集約して しまうかもしれません。これは受信側が処理できる早さよりも送信側の 処理の方が早い場合にはいつでも起こりえます。
  2. Winsock の LSP(Layered Service Providers) が、ストリームデー タを分割したり集約したりすることがありえます。特に LSP が転送時 にデータを修正するときに起こりえます。
  3. クライアント側のプログラムで Nagle アルゴリズムを無効にして も、サーバ側が送ってくるパケットには影響がありません。逆もまた成 り立ちます。
  4. ルータやその他のネットワーク上の中継系がパケットを分割するか も知れませんし、ストリームプロトコル上で「正しく」再構成される保 証はありません。
  5. スタック中で利用可能なバッファサイズよりも大きなパケットが到 着したときに、バッファに入りきる分だけをキューに入れ、残りは捨て てしまう、というように分割されることがあります。(通信相手はその 残りのデータを後で再送することになります。)
  6. たとえ recv() 呼出し時に十分なバッファを Winsock に与えたとしても、ソケット上のキューに入っているデータを 全て返却するとは限りません。ソケット上のキューに入っているデータ を全て取得するには、呼出しを何度か行う必要があるかもしれないので す。

これらの問題点を除いても、Nagle アルゴリズムを無効にすると、 ほぼ間違いなくプログラムの性能が低下します。このアルゴリズムを無 効にしても良いのは、パケットのタイミング等、必要とされる要件が性 能よりも重要である場合のみです。

リアルタイムなユーザの入力を扱うプログラムでは、できる限りき びきびとした応答を得るために、ネットワークの帯域と引き換えに Nagle アルゴリズムを無効にすることがあります。例として、X Window のサーバと、複数プレイヤーのいるネットワークゲームの二つがありま す。これらの例では、ネットワーク帯域の節約よりも、パケットの遅延 をできる限り小さくすることの方がより重要なのです。

このトピックについてはさらに、ザ・間違いリスト と、 論説記事TCP を有効に使うた めに も参照して下さい。

3.18 - TCP の移動ウィンドウとは何ですか?

TCPを単純に実装すると、全ての パケットに対して、即時に ACK パケットによる確認を行うことに なります。受信側から ACK が届くまでは、送信側は次のパケットを送 信しません(あくまで単純な実装においては、ね)。ACK がある一定時間 内に返ってこなければ、送信側のスタックはパケットを再送信します。

この実装においては、そういった待ち時間のせいでネットワークの 性能が全く上がらない、という問題があります。この場合のパケット間 の最小間隔は、少なくともネットワーク上を往復する時間の二倍の時間、 すなわちパケットを送信する時間と受信側が ACK を送り返す時間にな ります。さらに両端での処理時間、一時的なハードウェアにおけるエラー (例えば Ethernet のコリジョンなど)、再送、ルーティングにおける遅 延、その他想像もつかない理由、などなどが加わることにより、データ を送信するための時間よりも、結局 ACK を待っている時間の方が長く なってしまうのです。つまり、一個のソケットでネットワークのパイプ を効率よく埋め尽くすことはできない、という問題が起こってしまうわ けです。

TCP ウィンドウはこの問題を解決するためのものです。TCPウィンド ウは、ある時点においていくつかの「飛行中の」未確認パケットが存在 することを許します。TCPコネクションが確立されるとき、お互いにコ ネクションに割り当てられたバッファ領域がどれだけあるかを教えあい ます。これが最大ウィンドウサイズとなります。ウィンドウが埋まって いくに合わせて、受信側は残りのウィンドウサイズを ACK パケットに よって送信側に送り返します。これによって、受信側のウィンドウがい つ溢れてデータが送信できなくなってしまうか、ということを送信側に 伝えます。送信側は、受信側のウィンドウが満杯になった、ということ がわかると、ウィンドウに空きができたよ、という意味の ACK を受け 取るまでデータの送信を停止します。

え?「どうして移動ウィンドウって呼ばれるのか」ですか? 長ーいバ イト列のTCPデータストリームがあると思ってください。送信側から受 信側のバッファを見ると、固定サイズの「窓」がバイト列に沿って移動 するように見えるので移動ウィンドウなのです。ウィンドウの一方の端 は、受信側が読み込んだ最後のバイトと次に読み込もうとしてるバイト の間に位置します。もう一方の端は、受信側入力バッファの最後のバイ トと送信側出力バッファから次に送り出されようとしてるバイトの間に なります。受信側がバッファからデータを読み出すのにしたがって、ウィ ンドウはストリームを移動していきます。ウィンドウが送信側のバッファ が移動してくると、送信側はそのウィンドウの空きを埋めるようにデー タを送信していくのです。

次の二つの質問でも関連する議論があります。

3.19 - お馬鹿なウィンドウ症候群(silly window syndrome)とは何ですか?

「お馬鹿なウィンドウ症候群」は、送信側が、受信者が扱える限度 を超えた速さでデータを送信し、さらに受信者が recv() を非常に小さいバッファサイズで呼び出したときに発生します。

高速な送信者は、受信者の TCP ウィンドウ をあっという間に埋め尽くしてしまいます。そして受信者が N バ イト読んだとします。ここで N は ネッ トワークフレームサイズに対して比較的小さい数字とします。単純 なスタックでは、すぐに送信者に ACK を送って TCP ウィンドウに N バイトの空きがあることを知らせる でしょう。これにより送信者は N バイトのデータを送信することにな りますが、N はフレームサイズよりも小さい値であるため、フレームが 一杯使われている状態に比べてプロトコルオーバヘッドは比較的大きく なってしまいます。そして受信側の処理が遅いので(かつ、なんと recv() を小さなバッファサイズでしか読み出さないくら いお馬鹿なので)、TCP ウィンドウの大きさは小さいままで留まり、結 果としてアプリケーションデータに対するプロトコルオーバヘッドが大 きくなり、性能に影響を及ぼすのです。

この問題に対する解決法が、遅延 ACK ア ルゴリズムというものです。これはウィンドウを通知する ACK を ちょっとだけ遅らせ、処理の遅い受信者にもう少し溜まっているデータ を受け取らせることが期待できます。この結果としてより大きなウィン ドウサイズの通知が行われるので、処理の早い送信者は一個のフレーム の中により多くのデータを詰め込むことができるのです。

なお、遅延ACKの解決法によっても、あなたのプログラムで小さなバッ ファでrecv()しても良くなるわけではありません。カー ネル空間とユーザ空間のコンテキスト切替の数を減らすためにも、やは り一度の呼び出しで、できる限り多くのデータを読み込むべきなのです。

3.20 - 遅延 ACK アルゴリズムとは何ですか?

TCPを単純に実装すると、受信したパケットに対して ACK パケットは すぐに返されます。(ACK は TCP が保証している信頼性を提供するため のものです。)

現在のスタックでは、ACK は少しの時間(概して最大 200ms 程度)遅 らせるようになっています。これには三つの理由があります。a) 「お馬鹿なウィンドウ症候群」を避ける。; b) ACK を返そうとしたときに返送するデータフレームの用意ができて いれば、ACK をその返送フレームに相乗りさせることができる。; c) 遅らせた期間内にいくつかフレームが届いたら、その分の ACK を一回 で返すことができる。

プロトコルスタックは、最大2フレーム分のデータまで遅らせること が許されています。

3.21 - 私のサーバをどのプラットフォーム上で運用させるべきでしょうか?

もしあなたが Windows をサーバとして使用すると決定したとすると、 高い負荷で運用するための現実の選択肢としては Windows NT サーバし かありません(なお、ここでの話題の範囲では、Windows 2000 と Windows NT 系に違いはありません)。

Windows NT ワークステーションと NT サーバは、まったく同一のカー ネルが使用されていることが暴露 されています。しかし NT ワークステーションのカーネルは、NT サー バの実行時の動作に関連して、自分自身の一部を起動時に使えなくして しまうのです。

最も大きな違いは、NTワークステーションでは、接続バックログが 5スロットに制 限されているということです。これはつまり、ネットワークスタック内 に接続バックログが5つ以上たまらないように、プログラムがさっさと accept() してあげなくてはならないということです。 キューが一杯になっている間は、スタックは新しい接続を拒否してしま います。きちんと作ってあるサーバであればこれは普通問題にはなりま せんが、一斉に攻撃(SYN flood とか)されるとキューが一杯になってし まい、正当なユーザへのサービスが妨害されてしまうということは発生 します。NT サーバ(動的バックログ機能が有効になっているとき)は、 SYN 攻撃に十分耐えられるように、バックログキューの長さは実質的に 無限になっています。

これら両者の違いについて、Microsoft が皆さんに聞いてほしいと 思っている点の一つは、ライセンスです。NT ワークステーション上で は、同時に 10 接続より多く accept するサーバを実行することはでき ません。現時点では、カーネルがこれを強制的に制限することはしてい ませんが、NT 4.0 ベータ期間中は制限されていました。(世間からの抗 議によって Microsoft がカーネルから制限を外したのです。) Microsoft は今後の製品において、これを強制的に制限しようとするか もしれません。SQL サーバなどの他の製品においては、Microsoft はこ のような制限を行う、ということが知られているのです。

その他のサーバの選択肢 -- Windows 95/98/ME -- は致命的なほどの制限があります。たとえば NT ワークステーションと 同様に 5 個までのバックログ制限があります。しかしもっと重要なこ とは、これらのカーネルは明らかに性能が劣っているのです。これを客 観的に証明するのは簡単なことです。FTP サーバかウェブサーバを設定 して、接続受付の速度やデータ転送速度を見てみればよいのです。 Win9x のカーネル上で動作しているサーバは、Windows NT 上で動作し ている同じプログラムに比べると、故意に不良品にしているかと思われ るほどです。

Win9x をサーバとすることは他にも問題があります。もっとも明ら かなものはその不安定性でしょう。また Win9x でサポートされている オーバーラップ I/O はカーネル外でエミュレーションされているもの ですし、I/O 完了ポートの機能は全くありません。これらの機能は、高 負荷の状況においてネットワーク帯域を最大にするために必須の機能な のです。もっと証拠が必要であるというのであれば、Win9x は複数のネッ トワークカードを入れるといくつか問題が出てくるということも挙げら れます。これは(ATM やギガビット Ethernet や FDDI といったカード を入れられないとき)安いネットワーク技術を使ってスループットを上 げるためによく使われるテクニックです。


<< Winsock 初心者のための情報 Winsock 上級者向けの議論 >>
Last modified: $Id: intermediate.html,v 1.6 2002/11/09 20:40:29 ksk Exp $ Go to the original FAQ page
< Go to the main FAQ page << Go to the Home Page