Previous Next Table of Contents

2. クライアントとサーバ(TCP/SOCK_STREAM)両方に関する質問

2.1 相手側のソケットが閉じられたことをどうやって知ることができますか?

Andrew Gierth 氏 ( andrew@erlenstar.demon.co.uk) より:

私の知る限り…

相手側が (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 */
{
    /* ファイルを閉じて削除する(完全なデータではないので)
       エラーを報告する、など */
}

2.2 bind() の二番目の引数には何を与えるのですか?

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 です。

2.3 あるサービス用のポート番号を得るにはどうするのですか?

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;
}

2.4 もし bind() に失敗したら、そのソケットディスクリプタはどうすれば良いのでしょうか?

もし終了しようとしているのであれば、全ての UNIX では開いたファイ ルディスクリプタを終了時に閉じてくれる、ということを Andrew が保 証してくれました。終了するのでなければ、通常のclose() 呼び 出しによって閉じることができます。

2.5 ソケットを正しく閉じるにはどうすればよいのですか?

この質問はよく、close() しようとしている人達から尋ねられます。 なぜなら、その人達はそれがするべきことだと思っていて、そして netstat を実行してそのソケットがまだ生きていることを見つけるから です。そう、close() は正しい方法です。TIME_WAIT 状態につい て、そしてなぜそれが重要であるかを読みたければ、 2.7 TIME_WAIT 状態について説明してください。 を参照して ください。

2.6 shutdown() はどういうときに使うべきなのですか?

Michael Hunter ( mphunter@qnx.com) 氏より:

shutdown() は、TCP を使ってサーバへ要求を送ることをいつ終了 したのか、の線引きをするのに便利です。典型的な使い方は、サーバに 要求を送り、続けて shutdown() を行なうことです。サーバはあ なたの要求を受け取り、続けて EOF (ほとんどの UNIX の実装で は 0 バイトの read)を受け取るでしょう。これは、それであなたの要 求が全てである、ということをサーバに伝えます。そしてあなたはその ソケットの読み出しでブロックします。サーバはあなたの要求を処理し、 必要なデータをあなたに送り返し、続けて close します。あなたがそ の要求に対する応答を全て読み出した後は、あなたは全ての応答を受け 取ったということを示す EOF を読み出すことになるでしょう。 TTCP (TCP for Transactions -- R.Stevens 氏のホームページを参照) は TCP トランザクションの管理のより良い方法を提供しているという ことは憶えておくべきです。

S.Degtyarev ( deg@sunsr.inp.nsk.su) 氏はこれについて、とても良い徹底的 なメッセージを書いてきてくれました。彼は、一方が「読み出し」プロ セス、もう一方が「書き込み」プロセスであるときのクライアントプロ セスの同期を手助けする 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() を使わなければなりません。

2.7 TIME_WAIT 状態について説明してください

TCP は、全ての転送データを可能な限り配送することが保証されている、 ということを思い出してください。ソケットを閉じたとき、サーバは、 全てのデータが届いたことを本当に本当に間違いなく確認するために、 TIME_WAIT 状態に入ります。ソケットが閉じられると、両側からお互い にメッセージを送りあうことによって、これ以上はもう送るデータが無 いということを合意します。私にはそれで十分だと思えたのですが、こ のハンドシェイクが終わった後にソケットが閉じられなければならない のです。問題は二つにまとめられます。第一に、最後の ACK が通信に 成功したことを確認する方法がないこと、第二に、「漂流中の重複パケッ ト」がネットワーク上に残っているかも知れないことで、これが配送さ れたときに対処しなければならないのです。

Andrew Gierth 氏 ( andrew@erlenstar.demon.co.uk) から以下に示す usenet 投稿で、 クローズ手順の説明を補足していただきました:

コネクションが ESTABLISHED 状態で、クライアントが通常の開放を行 なおうとしていると仮定しましょう。クライアントのシーケンス番号は Sc で、サーバのシーケンス番号は Ss です。パイプはどちらの向きも 空です。

   Client                                                   Server
   ======                                                   ======
   ESTABLISHED                                              ESTABLISHED
   (client が close する)
   ESTABLISHED                                              ESTABLISHED
                <CTL=FIN+ACK><SEQ=Sc><ACK=Ss> ------->>
   FIN_WAIT_1
                <<-------- <CTL=ACK><SEQ=Ss><ACK=Sc+1>
   FIN_WAIT_2                                               CLOSE_WAIT
                <<-------- <CTL=FIN+ACK><SEQ=Ss><ACK=Sc+1>  (server が close する)
                                                            LAST_ACK
                <CTL=ACK>,<SEQ=Sc+1><ACK=Ss+1> ------->>
   TIME_WAIT                                                CLOSED
   (2*msl 時間経過...)
   CLOSED

注: シーケンス番号の +1 は FIN が 1 バイトのデータであると数えら れるため(上記の図は RFC 793 の図13 と同一のものです)。

さてここで、この中の最後のパケットがネットワーク中で落ちてしまっ たときに何が起こるかを考えてみましょう。クライアントはコネクショ ンを終了しています。すなわちこれ以上送るべきデータも制御情報もな く、受け取ることもありません。しかしサーバは、クライアントが全て のデータを正しく受け取ったかどうかを知らないのです。最後の ACKセ グメントはそのためのものです。さて、サーバにとっては、クライアン トがデータを受け取ったかどうかは 気にしない かも知れません が、それは TCP の関知するところではありません。TCP は信頼性のあ るプロトコルですから、全てのデータが転送された通常のコネクション クローズ と、データが失われたかもしれないコネクション中 断 は区別 しなければなりません

ですから、最後のパケットが落ちてしまうと、サーバはそれを再送し (つまりそれが確認されていないセグメントだから)、正しい ACKセグメ ントの応答がくることを期待します。もしクライアントがCLOSED 状態 に直行していたとすると、その再送セグメントに対するただ一つの可能 な応答は RST であり、これは実際にはデータが失われていないのに、 データが失われたとサーバに示してしまうのです。

(サーバからの FIN セグメントには、データが付加されているかも知れ ないということを思い出してください。)

免責: これは RFC (私が見つけた TCP 関連のもの全て) に対する私の 解釈で、これを確認するために実装のソースコードを調べたり、実際の コネクションをトレースしたりしたことはありません。ですが、私はこ のロジックが正しいことで満足しています。

さらに Vic からの注釈:

二番目の問題について Richard Stevens 氏( rstevens@noao.edu "Unix Network Programming" の著者。 1.6 [あ る本の題名] という本のソースコードはどこから取得できますか? を参照) からお話を頂いています。これについて説明している彼の 投稿とメールからの引用を一緒に掲載しておきます。別々の投稿記事か らの段落を一緒にしてますが、できる限り原文のままにしてあります。

Richard Stevens 氏 ( rstevens@noao.edu) より:

TIME_WAIT 状態の期間が単に TCP の全二重クローズを扱うためだけで あったのなら、その時間はもっと短くなるでしょう。それは MSL(パケッ ト生存時間) ではなく、現在の RTO (再送タイムアウト) の機能になり ます。

TIME_WAIT 状態についていくつか論点を述べます。

漂流中の重複パケットは、喪失したと思われて再送されたパケットです。 しかしそれは本当に喪失したのではなく…どこかのルータに問題が起き て、そのパケットをしばらくの間(秒のオーダ、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 のどちらかを使用する必要がある ようです。

2.8 相手側が死んだことを検出するのにどうしてこんなに時間がかかるのですか?

Andrew Gierth 氏 ( andrew@erlenstar.demon.co.uk) より:

それはデフォルトでは、送るべきデータや確認がない限り、TCP コネク ションに送られるパケットはないからです。

つまり、もしあなたが相手からのデータを単純に待っているとすると、 相手側が黙ってどこかに行ってしまったのか、単に次のデータを送る準 備がまだできていないのかを知る方法はないのです(特に相手側が PC で、ユーザがあの「でっかいスイッチ」にぶつかってしまったりする と…)。

一つの解決法は SO_KEEPALIVE オプションを使うことです。この オプションはコネクションの定期的な診断を有効にし、相手側が存在す ることを保証します。警告: このオプションのデフォルトのタイ ムアウトは 最低 2 時間 です。このタイムアウトは(システム依 存の方法で)変更できることもよくありますが、しかしそれは通常コネ クション毎ではありません(私の知る限り)。

RFC1122 は、このタイムアウトを(もし存在すれば)変更可能とすべきだ と規定しています。メジャーな UNIX の種類においては、この変更は大 域的にしか行なうことできず、keepalive が有効になっている全ての TCP コネクションに影響があります。さらに、この値を変更する方法は、 たいてい難しく、文書化もあまりされておらず、とにかく存在するバー ジョン毎に対してさえも異なっているのです。

もしあなたがこの値を変更しなければならないのなら、カーネルコンフィ グレーションかネットワークオプションコンフィグレーションの中から、 tcp_keepidle かそれに似たようなものを探してみてください。

しかし、もしあなたが相手側に 送信して いるのであれば、もっ とよい保証があります。それは、データを送るということは相手側から の ACK を受け取るということを意味しているので、相手側がまだ生き ているかどうかは、再送タイムアウトが過ぎた後に知ることができるで しょう。しかし再送タイムアウトはさまざまな不測の事態があることを 想定して設計されているので、ちょっとしたネットワークの混乱だけで は単純に TCP コネクションが落ちないようになっています。ですから 送信失敗の通知を受け取るには、それでも数分の遅延があると思ってく ださい。

現在 Internet 上で使用されている多くのアプリケーションプロトコル (例えば FTPや SMTP など)で取られている方法は、サーバ側で読み出し タイムアウトを実装することです。つまりサーバは、ある与えられた時 間(しばしば 15分の単位で)クライアントからの要求を受け取らなかっ た場合には、単純にそのクライアントに見切りをつけます。長い期間の アイドル時間を維持するコネクションをもつプロトコルであっても、二 つの選択があります:

  1. SO_KEEPALIVE を使う
  2. 上位レベルでの keepalive 機構を使う(時折サーバに空要求を送 るなど)

2.9 select()、非ブロック I/O、SIGIO の利点、欠点は何ですか?

非ブロック I/O を使うということは、ソケットから読み出すべきデー タがあるかどうかを見るために、ソケットをポーリングしなければなら ないということです。

SIGIO を使うことで、あなたのアプリケーションにするべきこと をさせ、オペレーティングシステムにソケットに待っているデータがあ るということを(シグナルによって)教えさせることができます。この解 決法の唯一の欠点は、混乱するするかもしれないことと、複数のソケッ トを扱っている場合は、どのソケットからの読み出し準備ができている かを探すためにいずれにせよ select() を使わなければならない、 ということです。

select() を使う方法は、アプリケーションが複数のソケットから のデータを同時に受付けなければならない場合には最良の方法です。こ れは複数ソケットのうちどれか一つのデータの準備ができるまでブロッ クするからです。select() を使うもう一つの利点は、いずれかの ソケットにデータがあるかどうかに関わらず、あなたに制御が戻ってく るまでのタイムアウト値を設定することができる、という点です。

2.10 どうして read() から EPROTO が返ってくるのでしょうか?

Steve Rago 氏 ( sar@plc.com) より:

EPROTO はその終端において、そのプロトコルが回復不可能なエラー に出会ったことを意味しています。EPROTO は、STREAMS ベースの ドライバにおいて、他に良いコードがない場合に返される「何でもあり」 のエラーコードの一つです。

Andrew ( andrew@erlenstar.demon.co.uk) からの追記:

read() からの EPROTO に対してできることはほとんどあり ませんが、ある STREAMS ベースの実装において、accept 処理完了前に コネクションがリセットされたときに、 accept() から EPROTO が返されるというものを見つけたことがあります。

別のある実装においては、もしそれが起こった場合には accept がブロッ クする可能性があるようでした。これは重要なことです。なぜなら、も し select() が listen 中のソケットが読み出し可能であると返 してきたのなら、あなたは普通 accept() 呼び出しの中でブロッ クするとは予期 しない でしょうから。この修正はもちろん、 listen 中のソケットに対して select() を使おうとしている場合 には、そのソケットを非ブロックモードに設定することです。

2.11 ソケット内のバッファにあるデータを強制的に送るにはどうすればよいのですか?

Richard Stevens ( rstevens@noao.edu) 氏より:

強制的に送ることはできません。以上。TCP がいつデータを送ることが できるかは、TCP 自身が決めることです。さて、通常 TCP ソケッ トに対して write() を呼び出したときは、TCP は実際にそのセグ メントを送信するでしょう。しかし、それは保証されてはいませんし、 強制する方法もありません。なぜ TCP がセグメントを送信しないかは たくさんの 理由があります。すぐ思い付くことは、ウインドウが 一杯のときと Nagle アルゴリズムの二つです。

(TCP_NODELAY を使用するという Andrew Gierth の提案から一部引用)

これを設定することは、多くのテストのうちの Nagle アルゴリズムを 無効にするだけです。ですが、元記事を書いた人の問題がこれであれば、 このソケットオプションを設定することが役に立ちます。

tcp_output() をちょっと眺めたところでは、セグメントを送るか否か を決めるために TCP が行なわなければならないテストは 11 くらいあ りました。

次は Dr. Charles E. Campbell Jr. ( cec@gryphon.gsfc.nasa.gov) 氏より:

ご推察の通り、私は 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)

  1. ソケットインターフェースは TCP PUSH フラグへのアクセスは提 供しません。
  2. RFC1122 (4.2.2.2)にはこうあります: TCP は SEND 呼び出し時の PUSH フラグを実装している「かもしれませ ん」。PUSH フラグが実装されていなければ、TCP の送信は (1) 無期限 にデータをバッファリングしてはならない。そして (2) 最後のバッファ リングされたセグメント中の PSH ビットを設定「しなければならない」 (すなわち、もうそれ以上送られるべきデータがキューにない場合)。
  3. RFC793 (2.8) にはこうあります: 受信側の TCP が PUSH フラグを見つけたならば、そのデータを受信プ ロセスに渡す前に、それ以上送信側 TCP からのデータを待ってはなら ない。 [RFC1122 ではこの文章を支持しています。]
  4. 従って、write() 呼び出しに渡されたデータは、プロトコ ル上の理由で妨げられない限り、有限時間内に相手側に必ず配送されま す。
  5. データの送信時には、遅延を引き起こすことがある約 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 が質問します:

一時的に、とは、データはできる限り早く届けられるということで、 一方が応答を待っていて他方がその要求を受信していないという時点で 止まってしまうことは決してない、という意味でしょうか? (あるいは、 少なくとも永遠に止まってしまうことはない?)

もしあなたが何とかして両方の向きのバッファを全て一杯にすればデッ ドロックすることができますが... 簡単じゃないです。

もしそうすることが可能であるなら(いい例を思いつけないんだけど)、 解決法は、特に書き込みのために非ブロックモードを使うことです。そ うすれば必要に応じて超過データをプログラム中でバッファできます。

2.12 ソケットプログラミングのためのライブラリはどこから入手できますか?

Charles E. Campbell, Jr. PhD. 氏と Terry McRoberts 氏による簡単 なソケットライブラリがあります。それは ssl.tar.gz というファイル名で、この FAQ のホームページか らダウンロードできます。C++ 用にはSocket++ というライブラリがあ り、 ftp://ftp.virginia.edu/pub/socket++-1.10.tar.gz にありま す。また C++ Wrapper というのもあります。これは ftp://ftp.huji.ac.il/pub/languages/C++/C++_wrappers.tar.gz というファイルです。Bill McKinnon 氏へ、これを見つけてくれてあり がとう! http://www.cs.wustl.edu/~schmidt からは、ACE ツールキットを 見つけることができるでしょう。PING Software Group はその他のもの の中にソケットインタフェースを含むライブラリを持っています。この Web サイトへの私のリンクは古くなっていて、新しいサイトがどこにあ るかわかりません。もし見つけたら私にメールを送ってください。

Philippe Jounin 氏は、http とftp その他のプロ トコルのための上位レベルサポートを含んでいるクロスプラットフォー ムライブラリを開発しています。これは http://perso.magic.fr/jounin-ph/P_tcp4u.htm で見つけるこ とができます。これの評価は http://www6.zdnet.com/cgi-bin/texis/swlib/hotfiles/info.html?fcode=000H4F で見ることができます。

私はこれらのライブラリのどれも使った経験はないので、どれかを推薦 することはできません。

2.13 select はデータがあると言っているのに read が 0 を返すのはどうしてでしょうか?

select から戻ってくるのは、相手側がコネクションを閉じたことによっ る EOF というデータがあるからです。このとき read は 0 を返します。 さらなる情報は 2.1 相手側のソケットが閉じられたことをどうやって知ることが できますか? を参照してください。

2.14 select() と poll() の違いは何ですか?

Richard Stevens 氏 ( rstevens@noao.edu)より:

基本的な違いは、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 によって標準化されています。

2.15 ソケットを通して [あるデータ型] を送るにはどうするのですか?

単純バイト列のデータ以外はおそらく、あなたが面倒を見てあげない限 りめちゃくちゃにされてしまいます。整数値には htons() かその 仲間を使うことができますし、文字列は実際正に単純バイト列の集まり なので、これらは OK のはずです。ですが文字列のポインタを送らない ように注意してください。それはポインタは別のマシンでは意味を持た ないからです。もし構造体を送る必要があるのなら、一方でその構造体 を分解し、他方でそれを元通りに戻すための全ての作業を行なう send何とかstruct()read何とかstruct() 関数を書くべ きです。浮動小数点数を送る必要があるのなら、さらにたくさんの仕事 が待ち構えています。一方のマシンから他方にデータを持っていくため の移植性のある方法について述べている RFC 1014 を読むべきでしょう (これを指摘してくれた Andrew Gabriel 氏に感謝します)。

2.16 TCP_NODELAY はどうやって使うのですか?

まず第一に、そもそも本当にそれを使いたいのかを考えてください。こ れは 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 バッファリングアルゴリズムを無効にす るという、特定の 目的のためのものです。これは、タイムリーなデー タ配送が要求される場面で、頻繁に発生する小さな情報を即座の応答を 得ること無しに送信するアプリケーションにおいてのみ設定するべきで す(模範的な例はマウスの移動)。

2.17 Nagle アルゴリズムとは正確には何をやっているのですか?

これはコネクションの相手側からの ACK データをできる限りたくさん 一緒にまとめるものです。これは Andrew Gierth 氏 ( andrew@erlenstar.demon.co.uk) が以下に示す図を書いて説明 してくれるまでは、非常に混乱させられるものだと悟っていました:

この図は完全を目指したものではなく、より分かりやすく説明するため のものです...

場合 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 においては、実装が勝手にデータの送信を遅延させて欲しく ない ということに注意してください。それは応答時間にそのまま 追加されてしまうからです。

2.18 read() と recv() の違いは何ですか?

Andrew Gierth 氏 ( andrew@erlenstar.demon.co.uk)より:

read()recv()flags パラメータ に 0 を与え たものと同一です。flags パラメータに他の値を与えると recv() の振る舞いが変わります。同様に、write()flags == 0 のときの send() と同一です。

send()/recv() が消えて無くなることはないでしょう。どなたか、ソケッ ト呼び出しに関する POSIX のドラフトのコピーを持ってるならおそら く確認できると思いますが...

移植性上の注意: UNIX 以外のシステムでは、ソケットに対する read()/write() は許されていないものがあるかもしれません が、recv()/send() は普通 OK です。これは例えば、Windows と OS/2 において成り立ちます。

2.19 send()/write() が SIGPIPE を発生することがあるのは分かりました。これを無視して EPIPE エラーをチェックする方法ではなく、シグナルによって取り扱うのには何か利点があるのですか? シグナルを捕捉する関数には何か便利なパラメータが渡されるのですか?

Andrew Gierth 氏 ( andrew@erlenstar.demon.co.uk) より:

一般的に、シグナルハンドラに渡されるパラメータは、それが呼び出さ れる原因となったシグナル番号だけです。システムによっては付加的な オプションパラメータを持っていますが、この場合においてはあなたの 役には立ちません。

私のアドバイスは、あなたがおっしゃるように SIGPIPE を単に無 視することです。それが、私のほとんど全てのソケットのコードでやっ ていることです。errno の値はシグナルを取り扱うよりも簡単なのです (実際、この FAQ の最初の版においては、この文脈での SIGPIPE について言及するのを忘れていました。私はそれを無視するのに慣れす ぎてしまってたので...)。

SIGPIPE を無視するべき ではない 状況が一つあります。 stdout をソケットにリダイレクトして他のプログラムを exec() しようとしている場合です。この場合はおそらく exec() する前 に SIGPIPESIG_DFL に設定する方が賢いです。

2.20 chroot() した後の socket() の呼び出しが失敗します。どうして?

Andrew Gierth 氏 ( andrew@erlenstar.demon.co.uk) より:

ソケットが STREAMS 上で実装されているシステム(例えば SysV ベース のシステム全て、たぶん Solaris を含む)では、socket() 関数は 実は /dev 内のある特殊ファイルをオープンします。擬似 root ディレ クトリの下に /dev を作成して、必要なデバイスノード(のみ)を移住さ せる必要があるでしょう。

あなたのシステムの文書は、どのデバイスノードが必要であるかを正確 に規定しているかも知れませんし、していないかも知れません。私はお 助けできません(ごめんね)。 (編集者注: Adrian Hall 氏( adrian@hottub.org)) は ftpd の man ページを確認することを提案しています。これには chroot された環境にコピーする必要のあるファイルと作成する必要の あるデバイスがリストされているはずです。)

chroot() の目立たない問題は、多くのデーモンが行なうように syslog() を呼び出す場合、syslog() は (システムによって) UDP ソケットか FIFO か UNIX ドメインソケットのどれかをオープンし ます。ですから chroot() 呼び出しの後にそれを使うには、 chroot の *前に* openlog() を呼び出すことを忘れないでくださ い。

2.21 どうしてソケット呼び出しから EINTR が返されてしまうのでしょうか?

これは終了状態のような本当のエラーではありません。これはつまり、 この呼び出しがシグナルによって割り込まれたという意味です。ブロッ クする可能性のある呼び出しは全て、コード例 ( 7. Sample Source Code参照)で行なっているように EINTR をチェックするループで包み込むべきです。

2.22 アプリケーションには、いつ SIGPIPE が発生するんですか?

Richard Stevens 氏 ( rstevens@noao.edu) より:

とても簡単です: TCP では、コネクションのあなた側の端が、他端から の RST を受信した時に SIGPIPE が発生します。これはまた、 write の代わりに select を使っているのなら、select はそのソ ケットが読み出し可能であることを示す、ということも意味しています。 それは RST があなたに読み出されるためにそこにあるからです (read は errnoECONNRESET を設定してエラーを返すでしょう)。

RST は基本的に、予期されていない、他に取り扱いようがないパケット に対する TCP の応答です。よくある場合は、相手が(あなたに FIN を 送って)コネクションをクローズしたのに、あなたが読み出し中ではな く書き込み中であったためにそれを無視してしまった時です。(あなた は select を使っているべきです。) というわけで、あなたは相 手側の端によってクローズされてしまったコネクションに書き込んでし まい、相手側端の TCP は RST の応答を返すのです。

2.23 ソケット例外とは何ですか? 帯域外データとは何でしょうか?

C++ の例外とは異なり、ソケット例外はエラーが起こったということを 示しているのではありません。ソケット例外は通常、帯域外データが到 着しているということの通知を意味しています。帯域外データ(TCP で は「緊急データ」"urgent data" と呼ばれる)はアプリケーションにとっ ては、メインのデータストリームとは別のストリームであるように見え ます。これは二つの違った種類のデータを分離するために便利なことが あります。ただそれが「緊急データ」と呼ばれていても、それが通常の 帯域のデータストリームのデータより早く、あるいは高い優先度で配送 されるわけではない、ということに注意してください。また、メインデー タストリームとは違って、帯域外データはアプリケーションが追いつか ないと失われる可能性があるということにも注意してください。

2.24 実行されているシステムのホストのフルネーム (FQDN) はどうすればわかるのでしょうか?

Richard Stevens 氏 ( rstevens@noao.edu) より:

システムによってはホスト名は FQDN に設定されているし、別のシステ ムではただの未修飾のホスト名が設定されます。現在の BIND FAQ は FQDN を推奨しているということは知っているのですが、例えば多くの Solaris システムでは未修飾のホスト名のみを使う傾向があります。

とにかくこれを回避する方法は、まずホストの名前(FQDN かも知れない し、未修飾名かも知れない)を取得します。多くのシステムではこれを 行なうのに、 uname() を使うという POSIX 流の方法をサポート していますが、古い BSD システムでは gethostname() しか提供 していません。次に gethostbyname() を呼び出してあなたの IP アドレスを見つけます。そしてその IP アドレスを取って gethostbyaddr() を呼び出します。 すると hostent{}h_name メンバーは FQDN であるはずです。


Previous Next Table of Contents