Andrew Gierth 氏 ( andreより:
私の知る限り…
相手側が (SO_LINGER
を使ったややこしいことをしないで) close()
するか終了したとすると、こちらの read()
の呼び 出しは 0 を返すはずです。同じ場合で、write()
呼び出しで何が 起こるかは、もうちょっとわかりづらいです。直後の呼び出し時ではな く、その次の呼び出し時にEPIPE
が返るでしょう。
もし相手が再起動するか l_onoff = 1, l_linger = 0
を設定して から閉じたとすると、read()
からは(最終的に) ECONNRESET
が返るか、write()
からは EPIPE
が返ることになるでしょ う。
さらに、write()
が EPIPE
を返すときは、同時に SIGPIPE
シグナルも発生することを指摘しておきます。すなわち、 このシグナルをハンドルするか無視しない限り、 EPIPE
エラーを 受け取ることは決してありません。
相手側に到達できないままになっている場合には、他のエラーが起こる でしょう。
write()
が 0 を返すことは論理的にはないと思います。 read()
は、相手側から FIN を受け取ったとき、そしてそれ以後 の呼び出しにおいては 0 を返すでしょう。
そうです、read()
が 0 を返すことに対応 しなければなりま せん。
例として、TCP 路からファイルを受け取っていると仮定しましょう。 read()
からの返却値はこのように取り扱ってください:
rc = read(sock,buf,sizeof(buf)); if (rc > 0) { write(file,buf,rc); /* ファイルに対するエラーチェックは省略 */ } else if (rc == 0) { close(file); close(sock); /* ファイルの取得に成功した */ } else /* rc < 0 */ { /* ファイルを閉じて削除する(完全なデータではないので) エラーを報告する、など */ }
man ページには "struct sockaddr *my_addr
" と示されています。 しかし sockaddr struct
は、実際に必要な構造体の単なるプレー スホルダーに過ぎず、どの種類のソケットであるかに応じて違った構造 体を渡さなくてはなりません。AF_INET
ソケットに対しては、 sockaddr_in 構造体が必要です。これには三つの重要なフィールドがあ ります。
- sin_family
これを
AF_INET
に設定する。- sin_port
ネットワークバイト順の 16 ビットのポート番号。
- sin_addr
ホストの IP アドレス。 これは
struct in_addr
であり、それはs_addr
という一つのフィールドのみを 含み、それはu_long
です。
getservbyname()
関数を使ってください。これは servent
構造体へのポインタを返します。あなたの興味があるのは s_port
フィールドでしょう。これにはポート番号が、正しいバイト順序で入っ ています(つまり htons()
を呼び出す必要はない)。サンプルルー チンを以下に示します。
/* サービス名とサービス種別と取って、ポート番号を返す。もしサー ビス名が見つからなければ、それを十進数として使おうとする。ポー ト番号はネットワーク用のバイト順序で返される。*/ int atoport(char *service, char *proto) { int port; long int lport; struct servent *serv; char *errpos; /* 最初に /etc/services から読もうとする */ serv = getservbyname(service, proto); if (serv != NULL) port = serv->s_port; else { /* services にはなかった。 数字なのかな? */ lport = strtol(service,&errpos,0); if ( (errpos[0] != 0) || (lport < 1) || (lport > 5000) ) return -1; /* 不正なポート番号 */ port = htons(lport); } return port; }
もし終了しようとしているのであれば、全ての UNIX では開いたファイ ルディスクリプタを終了時に閉じてくれる、ということを Andrew が保 証してくれました。終了するのでなければ、通常のclose()
呼び 出しによって閉じることができます。
この質問はよく、close()
しようとしている人達から尋ねられます。 なぜなら、その人達はそれがするべきことだと思っていて、そして netstat を実行してそのソケットがまだ生きていることを見つけるから です。そう、close()
は正しい方法です。TIME_WAIT 状態につい て、そしてなぜそれが重要であるかを読みたければ、 2.7 TIME_WAIT 状態について説明してください。 を参照して ください。
Michael Hunter ( mphunte氏より:
shutdown()
は、TCP を使ってサーバへ要求を送ることをいつ終了 したのか、の線引きをするのに便利です。典型的な使い方は、サーバに 要求を送り、続けて shutdown()
を行なうことです。サーバはあ なたの要求を受け取り、続けて EOF
(ほとんどの UNIX の実装で は 0 バイトの read)を受け取るでしょう。これは、それであなたの要 求が全てである、ということをサーバに伝えます。そしてあなたはその ソケットの読み出しでブロックします。サーバはあなたの要求を処理し、 必要なデータをあなたに送り返し、続けて close します。あなたがそ の要求に対する応答を全て読み出した後は、あなたは全ての応答を受け 取ったということを示す EOF
を読み出すことになるでしょう。 TTCP (TCP for Transactions -- R.Stevens 氏のホームページを参照) は TCP トランザクションの管理のより良い方法を提供しているという ことは憶えておくべきです。
S.Degtyarev ( de氏はこれについて、とても良い徹底的 なメッセージを書いてきてくれました。彼は、一方が「読み出し」プロ セス、もう一方が「書き込み」プロセスであるときのクライアントプロ セスの同期を手助けする shutdown() の使い方の実用的な例を示してく れました。彼のメッセージの一部を以下に示します。
ソケットは、データ転送とクライアント、サーバ間のトランザクション に使われるという点でパイプと非常に似ていますが、双方向であるとこ ろがパイプと異なっています。ソケットを使うプログラムはよく fork()
し、各プロセスはソケットディスクリプタを継承します。 パイプベースのプログラムでは、データの喪失とデッドロックを避ける ために、パイプの使用されていない側の終端を全て閉じて、そのパイプ ラインを一方向にすることが強く推奨されています。ソケットでは、一 方のプロセスに送信だけを許し他方を受信だけを許す、という方法はな いので、常に順序関係を心に留めておく必要があります。
一般的に close()
と shutdown()
との違いは以下のような 点です。close()
はそのプロセスのソケット ID は閉じますが、 他方のプロセスがそのソケット ID を共有しているならば、このコネク ションは開いたままです。このコネクションは読み出し、書き込みの両 方に対して開いたままであり、そしてこれが非常に重要になる時がある のです。shutdown()
はそのソケット ID を共有している全てのプ ロセスのコネクションを破棄します。read しようとしたものは EOF
を検出し、write しようとしたものは SIGPIPE
を受け 取るでしょう。これはおそらくカーネルのソケットバッファが一杯になっ てから遅れて発生します。それに加えて、shutdown()
にはどのよ うにコネクションを閉じるかを示す二番目の引数があります: 0 はそれ 以降の読み出しを無効にする意味であり、1 は書き込みを無効にし、2 は両方を無効にします。
以下の簡単な例は、非常に単純なクライアントプロセスの一部です。こ れはサーバとのコネクションを確立した後に fork します。そして子は EOF
を受け取るまでキーボード入力をサーバに送り、親はサーバ からの返事を受け取ります。
/* * サンプルクライアントの断片 * 変数宣言とエラー処理は省略 */ s=connect(...); if( fork() ){ /* 子は、標準入力を ソケットにコピーする */ while( gets(buffer) >0) write(s,buf,strlen(buffer)); close(s); exit(0); } else { /* 親は、返事を受け取る */ while( (l=read(s,buffer,sizeof(buffer)){ do_something(l,buffer); /* サーバからのコネクション切断を期待している */ /* 注意: ここでデッドロックする */ wait(0); /* 子の終了を待つ */ exit(0); }
これは何を期待しているのでしょうか? 子は標準入力から EOF
を 検出し、そのソケットを close し(コネクションが破棄されると仮定す る)、そして終了します。一方サーバは EOF
を検出し、コネクショ ンを closeして終了します。しかしその代わりに何を見ることになるで しょうか? 親プロセスのソケットインスタンスは、その親が書き込むこ とは無いにも関わらず、書き込み、読み出しに対して開いたままになっ てしまいます。サーバは EOF を検出することは決して無く、クライア ントからのさらなるデータを永遠に待ち続け、サーバもハングします。 なんと予想外のデッドロック! (まあ、どんなデッドロックも予想外の ものだけど :-)
このクライアントの一部は以下のように変更すべきです。
あなたは良いが浮気ですか?
if( fork() ) { /* 子 */ while( gets(buffer) } write(s,buffer,strlen(buffer)); shutdown(s,1); /* 書き込み用のコ ネクションを切断する。ここでサーバは EOF を検出する。注: ソケットからの読み出しはまだできる。サーバは EOF 受信後 にももっと何かデータを送ってくるかもしれない、でしょ? */ exit(0); }
この大まかな例で、クライアント、サーバの同期について起こりうるト ラブルの説明になっていることを願っています。一般に、ソケットを共 有する全てのプロセスにおいて、ある特定のソケットのインスタンス全 てを常に憶えておいて、close() を使いたいときは全部同時に閉じるか、 あるいはコネクションを破棄するためにあるプロセスの中でshutdown() を使わなければなりません。
TCP は、全ての転送データを可能な限り配送することが保証されている、 ということを思い出してください。ソケットを閉じたとき、サーバは、 全てのデータが届いたことを本当に本当に間違いなく確認するために、 TIME_WAIT 状態に入ります。ソケットが閉じられると、両側からお互い にメッセージを送りあうことによって、これ以上はもう送るデータが無 いということを合意します。私にはそれで十分だと思えたのですが、こ のハンドシェイクが終わった後にソケットが閉じられなければならない のです。問題は二つにまとめられます。第一に、最後の ACK が通信に 成功したことを確認する方法がないこと、第二に、「漂流中の重複パケッ ト」がネットワーク上に残っているかも知れないことで、これが配送さ れたときに対処しなければならないのです。
Andrew Gierth 氏 ( andreから以下に示す usenet 投稿で、 クローズ手順の説明を補足していただきました:
コネクションが ESTABLISHED 状態で、クライアントが通常の開放を行 なおうとしていると仮定しましょう。クライアントのシーケンス番号は Sc で、サーバのシーケンス番号は Ss です。パイプはどちらの向きも 空です。
Client Server ====== ====== ESTABLISHED ESTABLISHED (client が close する) ESTABLISHED ESTABLISHED ------->> FIN_WAIT_1 <<-------- FIN_WAIT_2 CLOSE_WAIT <<-------- (server が close する) LAST_ACK , ------->> TIME_WAIT CLOSED (2*msl 時間経過...) CLOSED
注: シーケンス番号の +1 は FIN が 1 バイトのデータであると数えら れるため(上記の図は RFC 793 の図13 と同一のものです)。
さてここで、この中の最後のパケットがネットワーク中で落ちてしまっ たときに何が起こるかを考えてみましょう。クライアントはコネクショ ンを終了しています。すなわちこれ以上送るべきデータも制御情報もな く、受け取ることもありません。しかしサーバは、クライアントが全て のデータを正しく受け取ったかどうかを知らないのです。最後の ACKセ グメントはそのためのものです。さて、サーバにとっては、クライアン トがデータを受け取ったかどうかは 気にしない かも知れません が、それは TCP の関知するところではありません。TCP は信頼性のあ るプロトコルですから、全てのデータが転送された通常のコネクション クローズ と、データが失われたかもしれないコネクション中 断 は区別 しなければなりません。
ですから、最後のパケットが落ちてしまうと、サーバはそれを再送し (つまりそれが確認されていないセグメントだから)、正しい ACKセグメ ントの応答がくることを期待します。もしクライアントがCLOSED 状態 に直行していたとすると、その再送セグメントに対するただ一つの可能 な応答は RST であり、これは実際にはデータが失われていないのに、 データが失われたとサーバに示してしまうのです。
(サーバからの FIN セグメントには、データが付加されているかも知れ ないということを思い出してください。)
免責: これは RFC (私が見つけた TCP 関連のもの全て) に対する私の 解釈で、これを確認するために実装のソースコードを調べたり、実際の コネクションをトレースしたりしたことはありません。ですが、私はこ のロジックが正しいことで満足しています。
さらに Vic からの注釈:
二番目の問題について Richard Stevens 氏( rsteven"Unix Network Programming" の著者。 1.6 [あ る本の題名] という本のソースコードはどこから取得できますか? を参照) からお話を頂いています。これについて説明している彼の 投稿とメールからの引用を一緒に掲載しておきます。別々の投稿記事か らの段落を一緒にしてますが、できる限り原文のままにしてあります。
Richard Stevens 氏 ( rstevenより:
TIME_WAIT 状態の期間が単に TCP の全二重クローズを扱うためだけで あったのなら、その時間はもっと短くなるでしょう。それは MSL(パケッ ト生存時間) ではなく、現在の RTO (再送タイムアウト) の機能になり ます。
TIME_WAIT 状態についていくつか論点を述べます。
- 最初に FIN を送った方の端が TIME_WAIT 状態に入ります。それ はそっちの端が最後の ACK を送る方の端になるからです。もし他方の 端からの FIN が失われた場合、あるいは最後の ACK が失われた場合は、 最初の FIN を送った側の端にコネクションの状態を管理させ、最後の ACK を再送させるために必要な情報を持っていることを保証します。
- TCP のシーケンス番号は、 2**32 バイトが転送されると一周し てしまうことを十分理解してください。例えば A.1500 (ホスト A、ポー トが 1500) と B.2000 の間にコネクションがあるとします。このコネ クションの途中で、あるセグメントが失われて再送されました。しかし そのセグメントは本当に失われたのではなく、どこか途中のルータで保 留され、そしてネットワークに再注入されるのです(これは「漂流中の 重複」 "wandering duplicate" と呼ばれます)。しかしパケットが失わ れているときと再送したときの間の時間に、再びそれが現れてコネクショ ンが(何の問題もなく)クローズされ、そして別のコネクションが同じホ スト、同じポートの間で確立してしまいます(つまり A.1500 と B.2000 の間。これはそのコネクションの「生まれ変わり」と呼ばれます)。し かし、その新しい生まれ変わりのコネクション用のシーケンス番号に、 まさに再登場しようとしている漂流中の重複パケットのシーケンス番号 と重なるものが選ばれてしまいました(これは実際に、TCP コネクショ ンにシーケンス番号を与えるある方法では起こり得ます)。その通り、 あなたは今、漂流中の重複(そのコネクションの前世)から、新しいコネ クションにデータを受け渡そうとしているのです。これを避けるには、 同じコネクションの生まれ変わりの再確立を、TIME_WAIT 状態が終わる まで許してはいけません。 TIME_WAIT 状態はこの二番目の問題を完全には解決してくれないけれど も、TIME_WAIT 抹殺と呼ばれるものを与えてくれます。RFC 1337 に詳 細があります。
- TIME_WAIT 状態の期間が 2*MSL である理由は、パケットがネッ トワーク中を漂っている最大時間が MSL 秒であると仮定されているか らです。2 という係数は往復時間のためです。MSL の推奨値は 120 秒 ですが、Berkeley 派生の実装では通常 30 秒が使われています。これ はつまり TIME_WAIT 遅延は 1 分から 4 分の間ということです。 Solaris 2.x では実際、120 秒の推奨 MSL を使用しています。
漂流中の重複パケットは、喪失したと思われて再送されたパケットです。 しかしそれは本当に喪失したのではなく…どこかのルータに問題が起き て、そのパケットをしばらくの間(秒のオーダ、TTL が十分大きければ 一分も有り得る)保留して、そしてそのパケットをネットワークに再注 入します。しかしそれが再登場した時にはすでに、元のパケットを送 信したアプリケーションは、そのパケットに含まれているデータを再送 しているのです。
TIME_WAIT 抹殺に関するこれらの潜在的な問題のために、TIME_WAIT 状 態を避けようとしては いけません 。つまり、通常の TCP コネク ション終了 (FIN/ACK/FIN/ACK) の代わりに SO_LINGER
オプショ ンを設定して RST を送ってはいけません。TIME_WAIT 状態があるのに は理由があるのです。それはあなたのお友達であって、あなたを助ける ためにあるのです :-)。
私はこの話題だけのために、出版されたばかりの私の "TCP/IP Illustrated, Volume 3" の中で長い議論を行なっています。TIME_WAIT 状態は実際、TCP の機能の中で最も誤解されているものの一つです。
私は現在 "Unix Network Programming" ( 1.6 [ある本の題名] という本のソースコードはどこから 取得できますか? を参照) を書き直しています。そして、しばしば混 乱し誤解されるこの話題についてさらに多く触れるつもりです。
Andrew による追記:
ソケットのクローズについて: もし SO_LINGER
がソケットに 対して呼び出されていなければ、close()
はデータを捨てること はないはずです。これは SVR4.2 では(そしておそらく、SVR4 以外の全 てのシステムでも)真実ですが、SVR4 ではおそらくそうでは ない ようです。すなわち、全てのデータの配送を保証するためには、 shutdown()
か SO_LINGER
のどちらかを使用する必要がある ようです。
Andrew Gierth 氏 ( andreより:
それはデフォルトでは、送るべきデータや確認がない限り、TCP コネク ションに送られるパケットはないからです。
出会いクラマスフォールズ
つまり、もしあなたが相手からのデータを単純に待っているとすると、 相手側が黙ってどこかに行ってしまったのか、単に次のデータを送る準 備がまだできていないのかを知る方法はないのです(特に相手側が PC で、ユーザがあの「でっかいスイッチ」にぶつかってしまったりする と…)。
一つの解決法は SO_KEEPALIVE
オプションを使うことです。この オプションはコネクションの定期的な診断を有効にし、相手側が存在す ることを保証します。警告: このオプションのデフォルトのタイ ムアウトは 最低 2 時間 です。このタイムアウトは(システム依 存の方法で)変更できることもよくありますが、しかしそれは通常コネ クション毎ではありません(私の知る限り)。
RFC1122 は、このタイムアウトを(もし存在すれば)変更可能とすべきだ と規定しています。メジャーな UNIX の種類においては、この変更は大 域的にしか行なうことできず、keepalive が有効になっている全ての TCP コネクションに影響があります。さらに、この値を変更する方法は、 たいてい難しく、文書化もあまりされておらず、とにかく存在するバー ジョン毎に対してさえも異なっているのです。
もしあなたがこの値を変更しなければならないのなら、カーネルコンフィ グレーションかネットワークオプションコンフィグレーションの中から、 tcp_keepidle
かそれに似たようなものを探してみてください。
しかし、もしあなたが相手側に 送信して いるのであれば、もっ とよい保証があります。それは、データを送るということは相手側から の ACK を受け取るということを意味しているので、相手側がまだ生き ているかどうかは、再送タイムアウトが過ぎた後に知ることができるで しょう。しかし再送タイムアウトはさまざまな不測の事態があることを 想定して設計されているので、ちょっとしたネットワークの混乱だけで は単純に TCP コネクションが落ちないようになっています。ですから 送信失敗の通知を受け取るには、それでも数分の遅延があると思ってく ださい。
現在 Internet 上で使用されている多くのアプリケーションプロトコル (例えば FTPや SMTP など)で取られている方法は、サーバ側で読み出し タイムアウトを実装することです。つまりサーバは、ある与えられた時 間(しばしば 15分の単位で)クライアントからの要求を受け取らなかっ た場合には、単純にそのクライアントに見切りをつけます。長い期間の アイドル時間を維持するコネクションをもつプロトコルであっても、二 つの選択があります:
SO_KEEPALIVE
を使う- 上位レベルでの keepalive 機構を使う(時折サーバに空要求を送 るなど)
非ブロック I/O を使うということは、ソケットから読み出すべきデー タがあるかどうかを見るために、ソケットをポーリングしなければなら ないということです。
SIGIO
を使うことで、あなたのアプリケーションにするべきこと をさせ、オペレーティングシステムにソケットに待っているデータがあ るということを(シグナルによって)教えさせることができます。この解 決法の唯一の欠点は、混乱するするかもしれないことと、複数のソケッ トを扱っている場合は、どのソケットからの読み出し準備ができている かを探すためにいずれにせよ select()
を使わなければならない、 ということです。
select()
を使う方法は、アプリケーションが複数のソケットから のデータを同時に受付けなければならない場合には最良の方法です。こ れは複数ソケットのうちどれか一つのデータの準備ができるまでブロッ クするからです。select()
を使うもう一つの利点は、いずれかの ソケットにデータがあるかどうかに関わらず、あなたに制御が戻ってく るまでのタイムアウト値を設定することができる、という点です。
Steve Rago 氏 ( saより:
EPROTO
はその終端において、そのプロトコルが回復不可能なエラー に出会ったことを意味しています。EPROTO
は、STREAMS ベースの ドライバにおいて、他に良いコードがない場合に返される「何でもあり」 のエラーコードの一つです。
Andrew ( andreからの追記:
read()
からの EPROTO
に対してできることはほとんどあり ませんが、ある STREAMS ベースの実装において、accept 処理完了前に コネクションがリセットされたときに、 accept()
から EPROTO
が返されるというものを見つけたことがあります。
別のある実装においては、もしそれが起こった場合には accept がブロッ クする可能性があるようでした。これは重要なことです。なぜなら、も し select()
が listen 中のソケットが読み出し可能であると返 してきたのなら、あなたは普通 accept()
呼び出しの中でブロッ クするとは予期 しない でしょうから。この修正はもちろん、 listen 中のソケットに対して select()
を使おうとしている場合 には、そのソケットを非ブロックモードに設定することです。
Richard Stevens ( rsteven氏より:
強制的に送ることはできません。以上。TCP がいつデータを送ることが できるかは、TCP 自身が決めることです。さて、通常 TCP ソケッ トに対して write()
を呼び出したときは、TCP は実際にそのセグ メントを送信するでしょう。しかし、それは保証されてはいませんし、 強制する方法もありません。なぜ TCP がセグメントを送信しないかは たくさんの 理由があります。すぐ思い付くことは、ウインドウが 一杯のときと Nagle アルゴリズムの二つです。
(TCP_NODELAY
を使用するという Andrew Gierth の提案から一部引用)
これを設定することは、多くのテストのうちの Nagle アルゴリズムを 無効にするだけです。ですが、元記事を書いた人の問題がこれであれば、 このソケットオプションを設定することが役に立ちます。
tcp_output() をちょっと眺めたところでは、セグメントを送るか否か を決めるために TCP が行なわなければならないテストは 11 くらいあ りました。
次は Dr. Charles E. Campbell Jr. ( ce氏より:
ご推察の通り、私は Nagle アルゴリズムを無効にしても何の問題もあ りませんでした。これは基本的にはバッファリングの方法です。どんな に小さなパケットであっても、全てのパケットに対して一定のオーバー ヘッドがあります。これのために Nagle アルゴリズムは、小さいパケッ トを(0.2 秒以上の遅延はないように)一緒に集めることによって、転送 されるバイト数のオーバーヘッドを減らしているのです。この方法は rcp に対しては良く働きます。例えば、0.2 秒の遅延は人間にとっては 気がつかないし、複数のユーザからの小さいパケットはより効率よく転 送されます。ほとんどのネットワーク利用者たちが、rcp や ftpといっ た標準のツールや、 telnet などのプログラムを使っているような、大 学での環境の従業員は使うかもしれませんね。
しかしながら、Nagle アルゴリズムは実時間制御に対しては完全にぶち 壊しであり、キー入力対話型アプリケーション(コントロール C とか、 他にもある?) に対しては良くありません。私には、人々が通常ソケッ トを使って新しく書く類のプログラムでは、この小パケット遅延は問題 であるように思えます。Nagle アルゴリズムを選択的に無視する一つの 方法は「帯域外」メッセージを使うことです。しかし、これはその内容 に制限があるし、他にも(順序が失われるといったような)影響がありま す(なお、ctrl-C に対しては帯域外メッセージもよく使われます)。
さらに Vic より:
というわけで全てをまとめると、何か問題があってソケットをフラッシュ する必要があるときは、通常 TCP_NODELAY
オプションを設定する ことで問題は解決するでしょう。これで解決しない場合には帯域外メッ セージを使用する必要があります。しかし Andrew は次のように述べて います:「帯域外データはそれ独自の問題があり、バッファリング遅延 を解決するのにあまりうまく働くとは思えません(まあ、試したことは ないけど)。これは他のプロトコルに存在するような意味での「速効デー タ」では ありません。これは通常の流れの中で転送されていて、 それがある場所をポインタによって示されているだけなんです。」
私は Andrew に「TCP はいつネットワークにデータを書き出すのか について、どんな約束があるのか? 」という趣旨の質問をしまし た。この質問に対する彼の返事を書きます。
約束事はたくさんはありませんが、でもいくつかあります。
これに関する章と一節を引用していきます。
参考文献:
私は離婚せずに再婚することができます
RFC 1122, "Requirements for Internet Hosts" (also STD 3)
RFC 793, "Transmission Control Protocol" (also STD 7)
- ソケットインターフェースは TCP PUSH フラグへのアクセスは提 供しません。
- RFC1122 (4.2.2.2)にはこうあります: TCP は SEND 呼び出し時の PUSH フラグを実装している「かもしれませ ん」。PUSH フラグが実装されていなければ、TCP の送信は (1) 無期限 にデータをバッファリングしてはならない。そして (2) 最後のバッファ リングされたセグメント中の PSH ビットを設定「しなければならない」 (すなわち、もうそれ以上送られるべきデータがキューにない場合)。
- RFC793 (2.8) にはこうあります: 受信側の TCP が PUSH フラグを見つけたならば、そのデータを受信プ ロセスに渡す前に、それ以上送信側 TCP からのデータを待ってはなら ない。 [RFC1122 ではこの文章を支持しています。]
- 従って、
write()
呼び出しに渡されたデータは、プロトコ ル上の理由で妨げられない限り、有限時間内に相手側に必ず配送されま す。 - データの送信時には、遅延を引き起こすことがある約 11 のテス トが行われます(FAQ で引用されている Stevens の投稿による [ この答えの前の方にあります - Vic])。しかし私が見たところ、 重要なものは二つだけです。それは再送時間の引き延ばしのようなもの は a) プログラマから制御できない b) 有限時間内に解決するかコネク ションが落ちるかのどちらかしかない、からです。
一つ目の興味深い場合は「ウインドウが一杯」の場合です(すなわち、 受信側にバッファスペースがない場合。これは無期限のデータ遅延が有 り得ます。しかしこれは、受信側のプロセスが到着したデータを本当に 読んでいない場合だけです)。
Vic の質問:
なるほど、クライアントが読み出ししていなければ、データがコネ クションを渡っていかないということはよくわかります。これは受信側 のキューが一杯になった後には送信側がブロックしてしまう、という意 味だと私は受け取りましたが?
送信側は、ソケットの送信バッファが一杯になった時にブロックします。 つまり両端においてバッファは一杯になります。
ウインドウが閉じている間、送信側の TCP はウインドウ探索パケット を送ります。これで、ウインドウがいつか再び開いた時に、送信側がそ の事実を検出することが保証されます。[RFC1122, ss 4.2.2.17]
二つ目の興味深い場合は「Nagle アルゴリズム」です(相手側からのACK が予想される時は、キー入力のような短いセグメントは遅延されてより 大きなセグメントを作ります。これが TCP_NODELAY
で無効にでき ることです)。
Vic が質問します:
それはつまり、私の tcpclient のサンプルプログラムでは、送信 時に改行コードを間違いなくネットワークに送り出すことを保証するた めに TCP_NODELAY を設定すべきだ、という意味でしょうか?
いいえ。tcpclient.c は今のままで正しいことを行なっています。つま り、できる限り少ない write()
の呼び出しで可能な限りたくさん のデータを書き込もうとしているということです。データの量はソケッ トの送信バッファに比べて小さくなりそうなのだから、(コネクション はこの時点ではアイドル中なのだから)全要求はただ一度の write()
の呼び出しだけしか必要としないでしょうし、TCP 層は その要求を一個のセグメントとして(PSH フラグによって。上記の 2.2 の点を参照)即座に発送するでしょう。
Nagle アルゴリズムはデータの到着確認がまだされていないうちに二番 目の write()
呼び出しが行われた時にのみ影響があります。通常 の場合このデータは、a) 到着未確認のデータがなくなった、あるいは b) 満タンの大きさのセグメントを発送するために十分なだけのデータ が用意された、のいずれかになるまでバッファに留められます。条件 (a) は再送タイムアウト内に必ず真になるか、そうでなければコネクショ ンが死ぬので、この遅延が無期限となることは有り得ません。
この遅延はある種のアプリケーション、一般には例えばマウス動作のよ うに、応答の無い短い要求が送られているストリームがあるアプリケー ションにとっては嬉しくない結果をもたらすので、規格ではこれを無効 にするオプションが存在しなければならないと規定しています。 [RFC1122, ss 4.2.3.4]
追記: RFC1122 ではこうも書いてあります:
- [議論]:
SEND 呼び出しにおける PUSH フラグが実装されていない時、すなわち アプリケーション/TCP インターフェースが純粋なストリーミングモデ ルを使っている時は、小さなデータの断片を適当に集めて手頃な大きさ のセグメントを作り上げる責任の一部は、アプリケーション層に負って もらいます。
というわけで、プログラムは、小さな長さのデータ(つまり、MSS に比 べて小さな、ということですが)で write()
を呼び出すのは避け るべきです。つまり、バッファの中で要求を組み立てて、そして sock_write()
かそれと同様のものを一回呼び出す方が良い、とい うことです。
他に有り得る TCP の遅延の源はプログラムによって制御することは全 くできず、一時的にデータを遅延させるしかありません。
Vic が質問します:
一時的に、とは、データはできる限り早く届けられるということで、 一方が応答を待っていて他方がその要求を受信していないという時点で 止まってしまうことは決してない、という意味でしょうか? (あるいは、 少なくとも永遠に止まってしまうことはない?)
もしあなたが何とかして両方の向きのバッファを全て一杯にすればデッ ドロックすることができますが... 簡単じゃないです。
もしそうすることが可能であるなら(いい例を思いつけないんだけど)、 解決法は、特に書き込みのために非ブロックモードを使うことです。そ うすれば必要に応じて超過データをプログラム中でバッファできます。
Charles E. Campbell, Jr. PhD. 氏と Terry McRoberts 氏による簡単 なソケットライブラリがあります。それは ssl.tar.gz というファイル名で、この FAQ のホームページか らダウンロードできます。C++ 用にはSocket++ というライブラリがあ り、 にありま す。また C++ Wrapper というのもあります。これは というファイルです。Bill McKinnon 氏へ、これを見つけてくれてあり がとう! からは、ACE ツールキットを 見つけることができるでしょう。PING Software Group はその他のもの の中にソケットインタフェースを含むライブラリを持っています。この Web サイトへの私のリンクは古くなっていて、新しいサイトがどこにあ るかわかりません。もし見つけたら私にメールを送ってください。
Philippe Jounin
私はこれらのライブラリのどれも使った経験はないので、どれかを推薦 することはできません。
select から戻ってくるのは、相手側がコネクションを閉じたことによっ る EOF というデータがあるからです。このとき read は 0 を返します。 さらなる情報は 2.1 相手側のソケットが閉じられたことをどうやって知ることが できますか? を参照してください。
Richard Stevens 氏 ( rsteven/p>
基本的な違いは、select()
の fd_set
はビットマスクであっ て、それゆえに固定サイズであるということです。カーネルのコンパイ ル時にこのサイズの制限を外し、アプリケーションに必要なだけ FD_SETSIZE
で定義できるようにすることは可能ですが、たくさん の作業が必要になります。4.4BSD のカーネルと Solaris のライブラリ 関数の両方にはこの制限があります。しかし、BSD/OS 2.1 にはこの制 限を避けるようにコードされているのを見つけました。ですからそれは できます。小さなプログラミング上の問題です :-)。誰か Solaris の バグレポートにこれを登録してみて、それが修正されるかどうかを見て みるといいですね。
しかし poll()
では、ユーザは pollfd
構造体の配列を割り 当ててなければなりません。そしてこの配列のエントリの数を渡すので、 根元的には上限はありません。Casper が言及しているように、 poll()
を持つシステムは select
よりも少ないので、後者 の方が移植性は高いです。また、オリジナルの実装(SVR3)では、ディス クリプタに -1 を設定することでカーネルに pollfd
構造体の中 のエントリを無視させることができませんでした。これは配列の中から エントリを削除するのが面倒になります。SVR4 ではこれは回避されま した。個人的には、私はいつも select()
を使い、poll()
は 滅多に使いません。それは私のコードを BSD 環境にも移植するからで す。誰かが select()
を使った poll()
の実装を書いている かもしれませんが、私は見たことがありません。select()
と poll()
は両方とも POSIX 1003.1g によって標準化されています。
単純バイト列のデータ以外はおそらく、あなたが面倒を見てあげない限 りめちゃくちゃにされてしまいます。整数値には htons()
かその 仲間を使うことができますし、文字列は実際正に単純バイト列の集まり なので、これらは OK のはずです。ですが文字列のポインタを送らない ように注意してください。それはポインタは別のマシンでは意味を持た ないからです。もし構造体を送る必要があるのなら、一方でその構造体 を分解し、他方でそれを元通りに戻すための全ての作業を行なう send何とかstruct()
と read何とかstruct()
関数を書くべ きです。浮動小数点数を送る必要があるのなら、さらにたくさんの仕事 が待ち構えています。一方のマシンから他方にデータを持っていくため の移植性のある方法について述べている RFC 1014 を読むべきでしょう (これを指摘してくれた Andrew Gabriel 氏に感謝します)。
まず第一に、そもそも本当にそれを使いたいのかを考えてください。こ れは Nagle アルゴリズム ( 2.11 ソケット内の バッファにあるデータを強制的に送るにはどうすればよいのですか? 参照) を無効にします。これは不必要に小さなパケットで帯域を消 費し、ネットワークトラフィックを増加させてしまいます。さらに、私 から言える限りでは、速度はごく僅かしか向上しないので、まず TCP_NODELAY
無しで行なって、それで問題があった時にのみオン にするべきでしょう。
以下がコードの例と、Andrew Gierth 氏による使用上の注意です:
int flag = 1; int result = setsockopt(sock, /* 影響するソケット */ IPPROTO_TCP, /* TCP レベルのオプション設定 */ TCP_NODELAY, /* オプションの名前 */ (char *) &flag, /* このキャストは歴史的な 汚点 */ sizeof(int)); /* オプション値の長さ */ if (result < 0) ... エラーの処理 ...
TCP_NODELAY
は、Nagle バッファリングアルゴリズムを無効にす るという、特定の 目的のためのものです。これは、タイムリーなデー タ配送が要求される場面で、頻繁に発生する小さな情報を即座の応答を 得ること無しに送信するアプリケーションにおいてのみ設定するべきで す(模範的な例はマウスの移動)。
これはコネクションの相手側からの ACK データをできる限りたくさん 一緒にまとめるものです。これは Andrew Gierth 氏 ( andreが以下に示す図を書いて説明 してくれるまでは、非常に混乱させられるものだと悟っていました:
この図は完全を目指したものではなく、より分かりやすく説明するため のものです...
場合 1: クライアントは一回の write()
呼び出しで 1 バイ ト書き込む場合。ホスト B 側のプログラムはこの FAQ の例にある tcpserver.c です。
CLIENT SERVER APP TCP TCP APP [コネクション設定は省略] "h" ---------> [1 byte] ------------------> -----------> "h" [ack の遅延] "e" ---------> [Nagle アルゴ . リズム実施中] . "l" ---------> [同上] . "l" ---------> [同上] . "o" ---------> [同上] . "\n"---------> [同上] . . . [ack 1 byte] <------------------ [キューのデータを 送信] [5 bytes] ------------------> ------------> "ello\n" <------------ "HELLO\n" [6 bytes, ack 5 bytes] <------------------ "HELLO\n" <---- [ack の遅延] . . . [ack 6 bytes] ------------------>
全セグメント数: 5 (もし TCP_NODELAY
が設定されていたら、最大 10 までになることがある)。 応答の時間: 2*RTT に加えて ack の遅れ分。
場合 2: クライアントは全部のデータを一回の write()
呼 び出しで書き込む場合。
CLIENT SERVER APP TCP TCP APP [コネクション設定は省略] "hello\n" ---> [6 bytes] ------------------> ------------> "hello\n" <------------ "HELLO\n" [6 bytes, ack 6 bytes] <------------------ "HELLO\n" <---- [ack の遅延] . . . [ack 6 bytes] ------------------>
全セグメント数: 3。
応答時間 = RTT (つまり最小限)。
これで多少分かりやすくなれば良いのですが...
場合 2 においては、実装が勝手にデータの送信を遅延させて欲しく ない ということに注意してください。それは応答時間にそのまま 追加されてしまうからです。
Andrew Gierth 氏 ( andre/p>
read()
は recv()
の flags
パラメータ に 0 を与え たものと同一です。flags
パラメータに他の値を与えると recv()
の振る舞いが変わります。同様に、write()
は flags
== 0 のときの send()
と同一です。
send()/recv() が消えて無くなることはないでしょう。どなたか、ソケッ ト呼び出しに関する POSIX のドラフトのコピーを持ってるならおそら く確認できると思いますが...
移植性上の注意: UNIX 以外のシステムでは、ソケットに対する read()/write()
は許されていないものがあるかもしれません が、recv()/send()
は普通 OK です。これは例えば、Windows と OS/2 において成り立ちます。
Andrew Gierth 氏 ( andreより:
一般的に、シグナルハンドラに渡されるパラメータは、それが呼び出さ れる原因となったシグナル番号だけです。システムによっては付加的な オプションパラメータを持っていますが、この場合においてはあなたの 役には立ちません。
私のアドバイスは、あなたがおっしゃるように SIGPIPE
を単に無 視することです。それが、私のほとんど全てのソケットのコードでやっ ていることです。errno の値はシグナルを取り扱うよりも簡単なのです (実際、この FAQ の最初の版においては、この文脈での SIGPIPE
について言及するのを忘れていました。私はそれを無視するのに慣れす ぎてしまってたので...)。
SIGPIPE
を無視するべき ではない 状況が一つあります。 stdout をソケットにリダイレクトして他のプログラムを exec()
しようとしている場合です。この場合はおそらく exec()
する前 に SIGPIPE
を SIG_DFL
に設定する方が賢いです。
Andrew Gierth 氏 ( andreより:
ソケットが STREAMS 上で実装されているシステム(例えば SysV ベース のシステム全て、たぶん Solaris を含む)では、socket()
関数は 実は /dev 内のある特殊ファイルをオープンします。擬似 root ディレ クトリの下に /dev を作成して、必要なデバイスノード(のみ)を移住さ せる必要があるでしょう。
あなたのシステムの文書は、どのデバイスノードが必要であるかを正確 に規定しているかも知れませんし、していないかも知れません。私はお 助けできません(ごめんね)。 (編集者注: Adrian Hall 氏( adriaは ftpd の man ページを確認することを提案しています。これには chroot された環境にコピーする必要のあるファイルと作成する必要の あるデバイスがリストされているはずです。)
chroot()
の目立たない問題は、多くのデーモンが行なうように syslog()
を呼び出す場合、syslog()
は (システムによって) UDP ソケットか FIFO か UNIX ドメインソケットのどれかをオープンし ます。ですから chroot()
呼び出しの後にそれを使うには、 chroot の *前に* openlog()
を呼び出すことを忘れないでくださ い。
これは終了状態のような本当のエラーではありません。これはつまり、 この呼び出しがシグナルによって割り込まれたという意味です。ブロッ クする可能性のある呼び出しは全て、コード例 ( 7. Sample Source Code参照)で行なっているように EINTR
をチェックするループで包み込むべきです。
Richard Stevens 氏 ( rstevenより:
とても簡単です: TCP では、コネクションのあなた側の端が、他端から の RST を受信した時に SIGPIPE
が発生します。これはまた、 write の代わりに select
を使っているのなら、select はそのソ ケットが読み出し可能であることを示す、ということも意味しています。 それは RST があなたに読み出されるためにそこにあるからです (read は errno
に ECONNRESET
を設定してエラーを返すでしょう)。
RST は基本的に、予期されていない、他に取り扱いようがないパケット に対する TCP の応答です。よくある場合は、相手が(あなたに FIN を 送って)コネクションをクローズしたのに、あなたが読み出し中ではな く書き込み中であったためにそれを無視してしまった時です。(あなた は select
を使っているべきです。) というわけで、あなたは相 手側の端によってクローズされてしまったコネクションに書き込んでし まい、相手側端の TCP は RST の応答を返すのです。
C++ の例外とは異なり、ソケット例外はエラーが起こったということを 示しているのではありません。ソケット例外は通常、帯域外データが到 着しているということの通知を意味しています。帯域外データ(TCP で は「緊急データ」"urgent data" と呼ばれる)はアプリケーションにとっ ては、メインのデータストリームとは別のストリームであるように見え ます。これは二つの違った種類のデータを分離するために便利なことが あります。ただそれが「緊急データ」と呼ばれていても、それが通常の 帯域のデータストリームのデータより早く、あるいは高い優先度で配送 されるわけではない、ということに注意してください。また、メインデー タストリームとは違って、帯域外データはアプリケーションが追いつか ないと失われる可能性があるということにも注意してください。
Richard Stevens 氏 ( rstevenより:
システムによってはホスト名は FQDN に設定されているし、別のシステ ムではただの未修飾のホスト名が設定されます。現在の BIND FAQ は FQDN を推奨しているということは知っているのですが、例えば多くの Solaris システムでは未修飾のホスト名のみを使う傾向があります。
とにかくこれを回避する方法は、まずホストの名前(FQDN かも知れない し、未修飾名かも知れない)を取得します。多くのシステムではこれを 行なうのに、 uname()
を使うという POSIX 流の方法をサポート していますが、古い BSD システムでは gethostname()
しか提供 していません。次に gethostbyname()
を呼び出してあなたの IP アドレスを見つけます。そしてその IP アドレスを取って gethostbyaddr()
を呼び出します。 すると hostent{}
の h_name
メンバーは FQDN であるはずです。
0 件のコメント:
コメントを投稿