Winsock Programmer's FAQ
第7章: 論説記事: ザ・間違いリスト

ザ・間違いリスト

はじめに

「ザ・間違いリスト」(The Lame List)は、非常に有用なものであ るので、ここに再掲することにします。この文章は、Windows Sockets 2 アプリケーションプログラミングインターフェースバー ジョン 2.2.2 の付録C から直接カット&ペーストしたものです。このリ ストはもともと、Winsock スタックベンダ達が、お馬鹿なアプリケーショ ンの数々(名前を出すことは控えておきます)について文句を並べたもの が始まりです。にも関わらずこれらの内容は非常に有用なものです。と いうのは、新米の Winsock 屋さんは、やはり同じお馬鹿な間違いをし でかしてしまうからです。このリストに載っている内容を避けるように することが、あなたを Winsock の超プロに向かう長い道へと導くので す。

このリストのもともとのはしがき:

このリストを始めた功績は Microsoft の Keith Moore 氏によるものですが、他の面々達からも多くの貢献を頂きました。 sockets.com の Bob Quinn 氏は、これらの項目がなぜダメダメなのか、 またどう対処すべきか、という説明について労力を割いていただいた中 心人物です。このリストはあくまで、印刷に出したときのようなスナッ プショットです(ぎりぎり直前に詰め込んだ項目もいくつかあります)。

このバージョンのリストは、オリジナルのものとは若干違いがあり ます。句読点やちょっとした言い回しなどを修正してあります。そして もちろん、全部を HTML フォーマットできれいにしてあります。

ウィンドウズソケット・間違いリスト
(または「今週の大馬鹿大賞」)

ウィンドウズソケットベンダコミュニティの提供でお送りします

  1. 非ブロック型ソケットに対して connect() を呼び出 してWSAEWOULDBLOCK が返ってきた時、その直後に recv() を呼び出すと、必ずコネクションが確立する前の WSAEWOULDBLOCK が返されると期待すること。 間違い。
    理由: これは、アプリケーションが recv() を呼び出す までの間にコネクションが確立することはないだろう、という仮定に基 づいています。それは間違った仮定です。
    代案: そんなことはやらないこと。非ブロック型ソケットを使っている アプリケーションは WSAEWOULDBLOCK エラー値を処理し なければなりませんが、このエラーが絶対発生するだろうと当てにして はいけません。
  2. select()の引数に、三つの空fd_setと 正しいTIMEOUT構造体を与えて呼び出し、短時間のディレ イ関数として使うこと。許しがたい間違い。
    理由: select()関数はあくまでネットワーク関数であり、 一般的な目的のタイマ関数ではありません。
    代案: 正当なシステムタイマサービスを使用すること。
  3. 非ブロックソケットにおいて、コネクションが確立されたかどう かを判断するために connect() でポーリングすること。 新米の間違い。
    理由: Winsock 1.1 仕様では、非ブロックコネクションが確立途中のと きの connect() のエラーを規定していません。つまり返 却されるエラー値は異なることがありえます。
    代案: コネクション完了の非同期通知を使うのが推奨できる方法です。 同期操作モードを行いたいアプリケーションは select() 関数が使えます(が、項番 23 も参照して下さい)。
    代案じゃない案: 非ブロックソケットをブロックモードに変更して、 send()recv() でブロックするのは、 connect() でポーリングするよりもさらにもっとダメダ メです。
  4. ソケットハンドル値は必ず 16 以下だと仮定する こと。恐ろしく間違いの泥沼。
    理由: ソケットハンドル値として取りうることのない値は、 winsock.h で定義されている INVALID_SOCKET だけです。その他の SOCKET 型の取りうる値は全て正しい獲物であり、アプリ ケーションはこれを取り扱わなくてはなりません。いずれにせ よ、ソケットハンドルの値は隠蔽されるべきものであり、アプリケーショ ンはいかなる理由があっても、特定の値に依存してはいけません。
    代案: ソケットハンドルの値は、0 も含めて全ての値を取りうるものと して扱う。また、socket()WSASocket() の呼出しごとに違ったソケットハンドル値 が返ってくることを期待しないこと。ソケットハンドルは、Winsock の 実装によっては再利用されることが有り得ます。
  5. Win 16 の非プリエンプティブ環境において、 select() をタイムアウト 0 でポーリングすること。 吐き気がする間違い。
    理由: 0以外のタイムアウトであれば、select() はブロッ クフック関数を呼び出すので、イベントを受け付けるアプリケーション は、16ビットWindows環境上の他のプロセスに実行を移します。しかし、 タイムアウトが 0 であると、アプリケーションは他プロセスに実行を 譲ることはせず、ネットワーク操作さえも発生しないかもしれません (つまり無限ループになってしまう)。
    代案: 0 以外の小さな値のタイムアウトを使う。もっと良い方法は、 select() の代わりに非同期通知を使う。
  6. ソケットを非ブロックにするためだけに、 WSAAsyncSelect() を 0 イベントマスクで呼び出す。 間違い! 間違い! 間違い! 間違い!
    理由: WSAAsyncSelect() は、ネットワークイベント操作 をアプリケーションに非同期に通知させるように登録するために設計さ れたものです。Winsock 1.1 では 0 イベントマスクに対するエラーは 規定していませんが、不正な入力引数として扱われるかもしれませんし (つまりWSAEINVALを返却して失敗する)、要求が黙って無 視されてしまうだけかもしれません。
    代案: 非同期イベント通知を登録せずに、 ioctlsocket(FIONBIO) を使ってソケットを非ブロックに する。これは正にそのためのものです。
  7. Telnet アプリケーションで、 SO_OOBINLINE も有効にせず、OOB データを読み出すこと もしない。暴力的な間違い。
    理由: Telnet サーバが緊急データ(OOBデータ)を生成することは珍しい ことではありません。Telnet クライアントが Telnet BREAK コマンド やプロセス割り込みのコマンドを送るようなときです。このときサーバ は、 TCP 緊急通知と Telnet DATA MARK コマンドを組み合わせて、 「同期」をとります。もし telnet クライアントが緊急データを読み出 さないと、通常のデータもそれ以上読み込まれなくなってしまいます。 ずっと、絶対、永遠に。
    代案: いかなる telnet クライアントも、OOB データの読み込み、また は検出ができなくてはなりません。この方法として、 setsockopt() SO_OOBINLINE を呼び出してインライン OOB データを有効にするか、WSAAsyncSelect() (あるい は WSAEventSelect()) においてFD_OOB を 使うか、select() の呼出しでexcept_fds を使うかのいずれかの方法で OOB データの到着を検出し、それに応じ て recv()/WSARecv()MSG_OOB を与えて 呼び出します。
  8. 不正なソケットハンドル値が 0 であると仮定すること。手に負 えないほどの間違い。
    理由と代案:
    項番 4 を参照のこと。
  9. ブロッキングAPI の処理中にユーザがメインウィンドウを閉じた場 合に、きちんとシャットダウン処理を行わないアプリケーション。 完璧に間違い。
    理由: Winsock アプリケーションが、ソケットを close しないで WSACleanup() を呼び出した場合、Winsock の実装によっ ては、アプリケーションで使用されたリソースが正しく返却されないこ とがありえます。リソース漏れは結果として、他の全ての Winsock ア プリケーションから奪い合いとなるリソース不足(すなわち、ネットワー クシステムの異常)が引き起こされます。
    代案: 16ビット Winsock 1.1 アプリケーションにおいて、ブロッキン グAPI が処理中のときの正しい中断方法は以下のようになります。
    1. WSACancelBlockingCall() を呼び出す。
    2. 処理中の関数が帰ってくるまで待つ。もし処理が完了する前にキャ ンセルが発生したのであれば、その処理中の関数は WSAEINTR エラーを返します。しかし、キャンセル時の競 合状態によっては処理成功が返るので、アプリケーションは成功の場合 にも対応しなければなりません。
    3. 処理中のソケットもその他のソケットも全て閉じる。注: 接続済み のストリームソケットを正しく閉じるには、以下が必要になります。
      1. shutdown()how の引数を 1 に して呼び出す。
      2. recv() が 0 もしくは何らかのエラーを返 すまでループする。
      3. closesocket() を呼び出す。
      4. WSACleanup() を呼び出す。
    ここで述べた手順は、Winsock 2 アプリケーションでは関係ありません。 なぜなら、Winsock 2 では本当にブロックしてしまうので、同じスレッ ドから WSACancelBlockingCall() を呼び出すことは不可 能だからです(このため、WSACancelBlockingCall() 関数 は Winsock 2 では非推奨となりました)。ただし、上記の手順 3 の、 ソケットをきれいにシャットダウンする方法は今でも有効です。
  10. 帯域外データ。激しく間違い。
    理由: TCP の帯域外(OOB: Out of Band)データは信頼性がありません。 この説明で不十分であればこうです。実装によってはプロトコルレベル (緊急ポインタオフセットに関して)において互換性のない違いが存在し ます。バークレー(BSD) Unix は RFC 793 を字義どおり に実装しており、その他多くのものは修正版の RFC 1122 を実装して います(ある種のバージョンでは、MACフレームの開始位置をオフセット の開始位置として使うことで複数バイト数の OOB データを扱うことが できるものもあります)。もし二つの TCP ホストが違った OOB のバー ジョンを使っているとすると、これらのホスト間でお互いに OOB デー タを送信することはできないのです。
    代案: 理想としては、緊急データ用に別のソケットを使うことですが、 現実にはそういう逃げを打てないこともあります。プロトコルによって は OOB は必須であるので(項番 7 参照)、その 場合には依存性を極力最小化するか、もしくはユーザからの問い合わせ に答えられるようにテクニカルサポートのスタッフを増員するしかない でしょう。
  11. hostent 構造体の IP アドレスに対して strlen() を呼び出して、長さを 4 バイトに切り詰める。そして malloc() のヒープヘッダの一部を上書きする。長年 間違いを観測してきた私にとっても、ここまで大馬鹿な間違いはめった に見られない。
    理由: 理由なんて書く必要もないよねえ?
    代案: 代案はただ一つ、脳みそを入れ替えるしかない、というのは明白 です。
  12. メッセージが全部到着したかどうかを判断するた めに、recv(MSG_PEEK) でポーリングすること。間違 いの海でのたうちまわる。
    理由: ストリームソケット(TCP)はメッセージ境界を保存しません(
    項番 20 参照)。アプリケーションが、メッセージ が全部到着するのを待つために recv(MSG_PEEK)ioctlsocket(FIONREAD) を使うと、いつまでたっても成 功しないこともありえます。この理由の一つは、サービス提供側の内部 でバッファリングされるかもしれないということがあります。このとき 「メッセージ」のバイト列がシステムバッファの境界を跨っている場合、 Winsock は残りのバッファ側に入っているバイト数を数えないことがあ るからです。
    代案: peek 読み込みは使わない。常に自分のアプリケーションのバッ ファにデータを読み込んで、そこで必要なデータが来ているかどうかを 調べるようにする。
  13. 実際のバッファサイズ以上のデータを受け取ることがないとわかっ ている場合、実際のバッファサイズよりも大きな長さをバッファサイズ として与える。例外なく間違い。
    理由: Winsock の実装において、メモリ保護違反を避けるために、実際 にバッファを使う前にそこに読み書きができるかどうかをチェックする ことがよくあります。与えられたバッファサイズが実際のバッファサイ ズよりも大きい場合、このチェックに失敗するので、関数呼出しは WSAEFAULT を返して失敗してしまうでしょう。
    代案: 常に正しいバッファサイズを与えるようにする。
  14. 一連の Winsock 処理を実行するたびに毎回 WSAStartup()WSACleanup() を呼び出 す。間違いの包囲網を突破してる。
    理由: WSAStartup()WSACleanup() の 呼出しがそれぞれ対応している限りは、これは不正なことではありませ ん。しかし必要以上の処理を行っていることになります。
    代案: DLL、カスタムコントロール、クラスライブラリでは、ユニーク なタスクハンドルやプロセスIDを基にして、呼出し元クライアントを登 録することが可能です。これによって重複が起こらないように自動登録 を行うことができます。プロセスが最後のソケットを閉じると、自動登 録抹消を発生させることもできます。32ビット環境でのプロセス通知機 構を使っているのであれば、これはずっと簡単になります。
  15. API エラーを無視する。光り輝く間違い。
    理由: エラー値はあなたのお友達なんですよ! 関数が失敗したとき、 WSAGetLastError() で返却されるか、または非同期メッ セージ中に含まれるエラー値は、それがなぜ失敗したかを教え てくれます。失敗した関数やソケットの状態に応じて、何が、どうして 起こったか、そして次にどうするべきかを推測することができるのです。
    代案: エラー値をチェックし、それに備えてアプリケーションを書き、 必要なときにはそれを優雅に処理する。重大なエラーが起こったときは、 以下の内容を示すようなエラーメッセージを表示する。
    • 失敗した関数名
    • Winsock エラー番号、あるいはマクロ名
    • エラーメッセージの意味についての簡単な説明
    • 可能であれば、改善する方法についての提案

  16. 非同期通知メッセージの FD_READに対応して recv(MSG_PEEK) を呼び出す。大いに間違い。
    理由: 無駄無駄ァ。
    代案: FD_READ メッセージに対しては、普通の recv() 呼出しを行う。もし WSAEWOULDBLOCK が返ってきたとしても、このエラーを無 視するのは簡単ですし、また処理中のデータがまだあるので、後でもう 一度 FD_READ を受け取ることが保証されています。
  17. 単に FALSE を返す、空のブロッキ ングフックをインストールする。終わり亡き砂漠でのたうちまわる 間違い。
    理由: ブロッキングフック関数の一番の目的は、ブロッキング処理途中 のアプリケーションから処理を譲り渡すための機構を提供することでし た。ブロッキングフック関数から FALSE を返すというこ とは、この目的を無かったことにしてしまい、16ビット Windows の非 プリエンプティブ環境において、マルチタスクを行わないようにする、 ということです。これは、Winsock 実装によっては、処理中のネットワー ク操作が完了できなくなるものもあるのです。
    代案: 通常このハックは、再入可能メッセージを避けようとしてやって しまうことです。これを行うには、アクティブウインドウをサブクラス 化する、といったもっと良い方法があります。しかし再入可能メッセー ジを防ぐ、というのは、実のところ簡単な問題ではありません。
    なお念のためですが、Winsock 2 アプリケーションにおいてはこれは問 題にはなりません。なぜなら、ブロッキングフックはもはや過去の遺物 だからです(いい厄介払い)!
  18. クライアントアプリケーションにおいて特定ポー トに bind する。自分の首を締める間違い。
    理由: 定義上、クライアントアプリケーションは自分の方からネットワー ク通信を開始します。対照的に、受動的に通信を待つのがサーバアプリ ケーションです。サーバは、そのサービスを必要としているクライアン トに知られている、特定のポートに bind() しなければ なりません。しかし、クライアントは、サーバと通信するために、特定 のポートのソケットに bind() する必要はありません。
    これはごく僅かのアプリケーションプロトコルを除いて、全く不必要で あるばかりでなく、クライアントが特定のポート番号に bind() するのは危険でさえあります。すでに同じポート 番号を使っている他のソケットと衝突してしまい、 bind() 呼出しが WSAEADDRINUSE エラーで 失敗する危険があるのです。
    代案: 単純に、ローカルのポート番号は connect() (ス トリーム型ソケットおよびデータグラム型ソケット)、または sendto() (データグラムソケット) 呼出し時に、 Winsock 実装に割り当ててもらう。
  19. Nagle に異議を唱えるアプリケーション。 巨 大な深い割れ目の縁をふらふら歩くような間違い。
    理由: Nagle アルゴリズムは小さなネットワークトラフィックを減少さ せるものです。簡単に言うとこのアルゴリズムは、
    • 送信中の TCP セグメントの到達確認が全て行われた。または
    • 送信待ちのデータが TCP セグメント一杯になった
    のいずれかになるまで、 TCP セグメントの送信を行わないようにする、 というアルゴリズムです。 「Nagle に異議を唱えるアプリケーション」とは、時間の制約が厳しく 継続して送信しなければならないようなデータがあり、上記の条件が満 たされるまで待つことはできないようなアプリケーションのことをいい ます。このようなものは結果としてネットワークトラフィックを無駄遣 いしてしまいます。
    代案: 通信相手の TCP ホストから、データの反応がすぐに返ってくる ことに依存するようなアプリケーションを書かないこと。
  20. ストリームソケットで、メッセージフレームの区 切りが保持されると仮定すること。まさか、そんな、とても信じら れないような間違い。
    理由: ストリームソケット(TCP)は、どうしてストリームソケットと呼 ばれるかというと、えーと、それはデータストリームを提供するからで す(ああ、もう!)。そういうわけだから、アプリケーションが依存する ことのできるメッセージの長さは、最大でも一バイトの長さなのです。 それ以上でも以下でもありません。つまり、send() また は recv() の呼出しを行ったとき、Winsock 実装は指定 されたバッファ長よりも少ないバイト数しか転送しないということは、 いつでもありえるのです。
    代案: ブロック型ソケット、非ブロック型ソケットのどちらを使ってい るかに関わらず、send() または recv() の成功時に、その返却値と期待している値を比較するべきです。もし期 待している値よりも小さければ、バッファ長やポインタの位置を調整し て、次の関数呼出しに備える必要があるでしょう(もし非同期操作モー ドを使っているのであれば、これは非同期に行われるかもしれません)。
  21. WEPの中から WSACleanup() を呼び出す 16 ビット DLL。想像も及ばぬ間違い。
    理由: そもそも WEP() が間違い。ゆえに WEP() に依存するものも間違い。真面目な話、16ビット Windows は、WEP() が必ず呼び出されるとは保証してい ませんでした。そして、Windows サブシステムは、WEP() 中のいかなる処理も危機にさらされるような危なっかしい状態 に、しょっちゅう陥るのです。
    代案: WEP() に関わらぬこと。
  22. 一バイトずつ send() または recv() する。嫌になっちゃうほど間違い。
    理由: Nagle を無効にして一バイトずつ送信すると、オーバーヘッド対 データ比は、最大 40:1 にまでなります。帯域をどれくらい無駄にする かわかるかい? 君ならわかってくれると思うけど。
    一バイトごとの受信に関しては、ギネス・スタウトビールを点滴の注射 針で飲もうとしている、そのときの労力と非効率性を考えてみて欲しい。 それがまさに、データを一バイトずつ「飲んでいる」ときにアプリケー ションが感じることなのです。
    代案: Postel 氏が
    RFC 793 で述べた、以下の人生訓を尊重して欲しい。「自分のやること は保守的に、他人のすることには寛大に。」 言い方を替えれば、送信 の量はほどよく、受信は可能な限りたくさん、ということです。
  23. select()自虐的な間違い。.
    理由: select() を使うときに必要なステップを考えてみ てください。まずマクロを使って三つの fd_set をクリ アし、それぞれのソケットに対して適切な fd_set をセッ トし、そしてタイマーを設定し、そして select() を呼 び出す、という手順が必要です。
    select()から戻ると何かの処理が終わったソケットの数 が返されるので、その次に全ての fd_set を見て回って、 全てのソケットの中からマクロを使ってイベントの発生したソケットを 見つけ出す必要があり、そしてそのイベントが何であるかは、それまで のソケットの状態から推測するしかないという程度のことしかわからな いのです。
    代案: 非同期操作モードを使う(WSAAsyncSelect()WSAEventSelect() など)。
  24. inet_addr() を呼ぶ前に gethostbyname() を呼び出すアプリケーション。あま りの無駄さ加減に言葉も出ないほどの間違い。
    理由: 時にユーザは、ホスト名ではなくネットワークアドレスを使いた くなるときがあります。Winsock 1.1 仕様では、IPアドレスを標準的な ASCII ドット表記を与えた場合の gethostbyname() の動 作については何も定めていません。それを行った場合、処理が成功して (不必要な)逆引きを行うかもしれないし、処理に失敗するかもしれませ ん。
    代案: ユーザから入力された通信相手先 -- ホスト名であるかも知れないし、あるいはドット表 記のIPアドレスかもしれない -- に対しては、まず最初に inet_addr() を呼び出して IP アドレスかどうかをチェッ クして、もしこれが失敗したら gethostbyname() を呼び 出して、名前解決を試みるようにするべきです。
    さらに、アプリケーションによっては、入力された文字列がブロードキャ ストアドレス "255.255.255.255" であるかどうかを個別にチェックし た方がよいかもしれません。というのは、このブロードキャストアドレ スをinet_addr() に与えたときの返り値が SOCKET_ERROR と同じ値だからです。
  25. ブロッキングフックをインストールする Win32 アプリケーション。 激しく間違い。
    理由: ブロッキングフック関数は、処理を他のアプリケーションに譲り 渡す(
    項番 17 参照)ということを除いて、 本来一つのタスク内でのブロッキング処理の途中において、平行して他 の処理が行えるようにするために用意されたものです。しかし Win32 においては、スレッドというものがあるのです。
    代案: スレッドを使う。
  26. ソケットストリーム上で、「メッセージ」が全部到着するまで ioctlsocket(FIONREAD) でポーリングする。この世の ものとは思えぬ間違い。
    理由と代案: 項番 12 を参照のこと。
  27. どんな長さの UDP データグラムでも送信することができると思い 込む。犯罪的に間違い。
    理由: さまざまな種類のネットワークには全て、最大転送単位(MTU)と いう制限があります。その結果断片化(フラグメンテーション)が発生し、 データグラムが壊れてしまう可能性が増加してしまいます(断片が増え ると、その分喪失や転送誤りが発生する)。さらには、受信者側の TCP/IP サービス提供層では、断片化された巨大なデータグラムを再構 成する能力があるとは限らないのです。
    代案: 最大データグラムサイズを SO_MAX_MSG_SIZE ソケッ トオプションを使ってチェックし、それ以上の大きさを送信しないよう にする。さらにもっと余裕をとればなお良いです。経験則では最大 8K 程度までが良いです。
  28. UDP転送に信頼性があると思い込むこと(特にマルチキャスト転送の とき)。泥沼にはまる間違い。
    理由: UDP には信頼性を確保する機構はありません(だからこそ TCP が あるのです)。
    代案: TCP を使って、自分で独自にメッセージ境界を判断する。
  29. ベンダ依存の拡張を必要とし、それ無しでは実行できない(ひどい ときにはロードさえできない)アプリケーション。思わず言葉を失っ てしまうほどのどん底の間違い。
    理由: もし理由を自分で思いつけないというのなら、さっさとキーボー ドを捨てて足を洗ったほうが良いです。
    代案: 拡張機能が使えないときには、基本機能だけを使った代替方法等 を用意すること。
  30. UDPデータグラムが喪失したとき、送信者、受信者、途中のルータ のいずれかからエラーが通知されると期待すること。割れ目や亀裂 からじわじわと浸透してくる間違い。
    理由: UDP に信頼性はありません。TCP/IP スタックは、データグラム を捨ててしまっても、それをあなたに教えてくれる義務はないのです (送信者あるいは受信者で十分なバッファサイズが無かった場合や、受信者 側で巨大なデータグラムの断片を再構成できなかった場合などに、デー タグラムを捨ててしまうことがあります)。
    代案: データグラムは喪失してしまうことを想定し、それに対処する。 もし必要であれば、自分のアプリケーションプロトコルで独自に信頼性 を確保する実装を行う(あるいは、もし可能なら、TCP を使う)。

この文章の著作権は、「ザ・間違いリスト」の各項 目の著者に帰属します。これには、この文章の最初の前書きで触れた方々 も含みますが、この方々だけに限定するものでもありません。


<< TCP を有効に使うために TCP/IP のデバッグ >>
Last modified: $Id: lame-list.html,v 1.6 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