第 8 回 TCP(2)

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

8-1. TCP のフロー制御

受信者のウィンドウ制御

送信者が通信量を制限しない場合、どのようなことが起こるでしょうか。 ここでは送信者、受信者とも 1Gbps のネットワークにつながっており、中間 に 10 Mbps のネットワークがあったとしましょう。 TCP の接続が行われた時、受信者は 1Gbit を同時に受け取れると送信者に報 告したとします。 このとき、送信者がその値を真に受けて、Ack の値の指すセグメントを含み 1Gbit まで同時に送るとどのようなことが起きるでしょうか?

前回のセルフクロックではルータに理想的なバッファがあることを仮定してま した。このような仮定では前回同様、セルフクロックによりもっとも遅いネッ トワークである 10Mbps に伝送速度が調整されます。 しかし、 IP ネットワークでは IP パケットの通信保証をしなくて良いことに なっており、バッファから溢れたパケットはすべて破棄されます。 ここではルータのバッファサイズを 0.1 秒分=100Mbit とします。

このような状況で、送信側の送信ウィンドウは受信者から送られてきたウィン ドウサイズに固定し、またタイムアウト時間を勝手に固定することを考えます。 ここでは仮にそれぞれ 1Gbps30秒 であるとします。 すると以下のように非常に効率の悪い状況が発生します。

タイムアウトの短さにより再送しているもののほとんどが無駄になっているこ ともわかりますが、一度に送る量が途中の通信速度を越えていることが、この ような現象を引き起こしていることに注目します。 つまり、送信側で送信量を絞らない限り、不要なパケットを送っては捨てられ ることを繰り返すことになります。 これは一対一通信の非効率性を招くだけでなく、不要なパケットを大量に送出 していることになるため、ネットワークを共有している他者にも迷惑をかける ことになります。 今回はこの送信者の送信ウィンドウとタイムアウト時間の制御について考えま す。

送信者の輻輳制御

送信側では一度に送るセグメント数は、途中のボトルネック、つまりもっとも 遅いネットワークの処理できる数程度に落す必要があります。 そうしないと、ボトルネックで輻輳が起こります。 送信側のこのような通信量の制限のことを輻輳ウィンドウサイズ と呼びます。 記号で cwndと書くことがありますのでここでもそれに従います。

輻輳ウィンドウサイズcwndの制御は主に受信者からの ACK 情報で 行います。 これは、有線ネットワークでのパケットロスの原因のほとんどは輻輳によるか らです。 ですから、パケットロスを検知したら、輻輳ウィンドウサイズを小さくすれば 良いことになります。

TCP Tahoe

1988 年に発表された TCP Tahoe は次のようなアルゴリズムで輻輳制御を行って います。

スロースタート

もっとも単純な発想として、輻輳を検出したら輻輳ウィンドウサイズを 1 に してしまう手があります。 但し、高速なネットワークに対して、1 から始めることになるので、急速に輻 輳ウィンドウサイズを増やす必要があります。 そこで、指数関数的に増やすことを考えます。 ある時点で cwnd 個のセグメントを送った時、輻輳が起きなければ次は 2cwnd 個のセグメントを送りたい場合、どのようなプロトコルになるでしょう か? 基本的に送信者のわかる情報は ACK の観測だけです。 輻輳が起きなければ ACK はすべて返ってきます。 cwnd 個のセグメントを送ると cwnd 個の ACK が返ってきますので、この情報 を元に cwnd を 2cwnd にするためには一つの ACK 毎に cwnd を 1 だけ増や せばいいことになります。 このように 1 から指数関数的に cwnd を増やすために、 ACK 毎に cwnd を 1 ずつ増やすアルゴリズムをスロースタートと言います。

輻輳回避モード

スロースタートにより cwnd は指数関数的に増えるため、ホストの性能が途中 の通信回線より高速な場合、いつかは必ず輻輳が発生します。 そのためスロースタートを途中で止め輻輳を回避する必要があります。 スロースタートである cwnd では輻輳が起きず、 2cwnd で輻輳を検知したと すると、次回も同様の状況になると予想できます。つまり、 cwnd 以上 2cwnd 以下のパケット同時送信で輻輳が起きると予想できます。 そこで、輻輳を検知した時の cwnd(上の 2cwnd) の半分の値を覚え ておき(変数名 ssthresh)、次回のスロースタートの時、 cwnd の値が ssthresh に達したら輻輳回避モードに移行するようにします。

輻輳回避モードに移行したら何をすれば良いでしょうか? cwnd を固定してしまう手もありますが、輻輳検知をミスし、ただたまたまパ ケットロスが起きただけの場合でも cwnd が固定されてしまうおそれがありま す。 そのため、cwnd は緩やかに増えるべきです。 そこで、cwnd 個のセグメントを送った後、次回は cwnd+1 個のセグメントを 送ることを考えます。 スロースタートで考えた時と同様、 ACK 一つ当たりで増やすべき値を考えま す。 ACK は全部で cwnd 個返ってきてこれで cwnd を 1 増やすことになるので、 ACK 一つ当たりの増やす量は単純に考えれば 1/cwnd になります(逐次的に増 やす場合は毎回 cwnd が書き変わるので、実際に増える量は少なくなる)。

トリプル ACK と高速再送

有線ネットワークではパケットロスの原因のほとんどは輻輳です。 また、ほとんどのパケットは順番に流れると仮定できます。 このような仮定を前提とすると、重複 ACK は何を示していることになるので しょうか? 重複 ACK とは複数回同じ ACK が返ってくるものです。 例えばセグメント 101 の ACK が返ってくると言うことは、セグメント 100 までは連続して受信できたことを意味するので、もう一度同じ ACK が来たと 言うことは、101 セグメントを受信せず、 102 以上のセグメントを受信した ことを意味します。 セグメントが連続して送られる確率が高いなら、重複 ACK はセグメントを失っ た確率が高いことを意味します。 また、セグメントを失ったと推測できるなら、それは輻輳が起きていることが 原因だと推測されます。

このような考察から、より慎重を期するため三回おなじ ACK が来る時( トリプルACK)、輻輳とみなし、そのセグメントを再送します。 これを高速再送(Fast Retransmit)と言います。

TCP Tahoe のまとめ

以上のようにスロースタート、輻輳回避モード、 Fast Retrasmit が組み込ま れた TCP が TCP Tahoe と言います。

TCP Tahoe の特性

TCP Reno

TCP Tahoe では輻輳を検知した時、スロースタートに移行してしまいます。 これは通信速度を落し過ぎなのではないでしょうか? TCP Reno はこれを改善しています。

高速リカバリ

タイムアウトによりパケットロスを検知した時は受信者と通信不能になってい る可能性もあるため、本当に cwnd を 1 にすべきです。 しかし、トリプル ACK による輻輳検知では、受信者と通信できているわけで すし、直前の cwnd では輻輳が起きなかったことがわかっています。 そこで、トリプル ACK により輻輳を検知した時、cwnd に 1 を代入せずに半 分にし、ssthresh も同じ値にします。 そしてスロースタートではなく輻輳回避モードで通信を行います。 このようにすると、実際の回線容量の半分以下にならずに通信を続けることが できます。 これを高速リカバリ(Fast Recovery)と言います。

TCP Reno の特性

タイマー

パケットが何らかの原因で失った時、それはある程度の時間受け取れなかった と言うことでしか判断できません。 そのため、タイムアウト時間を設定し、その時間内に ACK が来なければパケッ トを失ったと判断することにします。 このタイムアウト時間が短過ぎると重複してパケットを送ることになるため、 ネットワークの輻輳を引き起こすかも知れません。 一方、長過ぎるとパケットを失ったと判断するまでの時間が長くなり、伝送速 度を大きく落してしまいます。 そのため、 TCP では動的にこのタイムアウト時間を計算します。

データリンク層ではネットワークが直接つながっているため、パケットの伝送 はある意味物理現象に支配されています。従って、受信者にパケットが届き、 ACK が返ってくるまでの平均時間(Round Trip Time RTT)の理論値 は割りあい正確に求められ、誤差も少ないです。 しかし、複数のネットワークをルータにより経由するネットワーク層ではそう は行きません。 輻輳が起こったり、ルータでバッファリングされたりするとパケットの伝送時 間が引き延ばされるため、受信者までの到着時間の平均値の誤差は非常に大き くなります。 ネットワークがすいている時は短い時間でパケットが往復しても、混んでいる 時は時間がかかることがあり、しかも時間のかかり方は予想しづらいものです。

このように一般に RTT の予想は難しく、また時間とともに変化します。 そこで、理論値から決定する方法より、実測値を元に決定します。 但し、RTT のモデルは平均値と偏差を持つような確率変数になるはずなので、単純に 実データそのものをタイムアウト時間とすることはできません。 Jacobison は以下のように、経験的な値と実測値の重み付き平均を求めること を推奨しました。

M を実測の RTT とし、時刻 t の予測 RTT を RTTt とし、予測偏差を Dt とします。

RTT の予測値
RTTt = α RTTt-1 + 1-α M
偏差の予測値
Dt = α Dt-1 + 1-α RTTt-1 - M
タイムアウト時間
タイムアウト時間 = RTTt + 4 Dt

ただしここでαは 7/8 とします。

ここで、再送されたパケットの取扱について考える必要があります。 再送パケットに関してタイムアウト時間を更新すべきか否かと言う問題があり ます。 Karn は次のアルゴリズムを提唱しました。

  1. 再送パケットに関してはタイムアウト時間を更新しない。
  2. 但しセグメントが消失している間は消失を検知する度にタイムアウト時間 を倍に増やしていく(最大値 64 秒)。

8-2. ソケット

TCP をプログラムから扱うにはどうすれば良いのでしょうか? これは、別々のプロセスが通信を行う必要がありますが、他のプロセスとの通 信の窓口をファイルハンドルのように扱えると楽です。 これをソケットと言います。 クライアントはサーバを指定することでソケットを得て、そのソケットに対し て文字列を書いたり読んだりすることで通信を行います。

サーバーはもう少し複雑です。 サーバはクライアントを受け付けると言う仕事と、実際にクライアントにサー ビスを提供すると言う二つの仕事をします。 クライアントへサービスをしている間でも、他のクライアントを受け付けられ るように、通常、受け付け用のポート、プロセスと、サービス用のポートやプ ロセスは別になります。 この考え方をソケットに当てはめると次のようになります。

  1. サーバはまず受け付け用のソケットを作ります。
  2. クライアントの要求が来たら、サービス用のソケットを得ます。
  3. サービス用のソケットを使うサービスプロセスを起動します。
  4. 1 へ戻る。

このような手順を踏むことになりますが、記述の仕方はプログラミング言語に 依存します。 但し、 OS レベルとしてはプリミティブ(基本操作)として次の動作が提供され ます。

サーバの動作

  1. 特定のポートを LISTEN する(接続待ち状態)
  2. 接続要求を受けたら、サービスを開始するため、サービス用のポートを用 意する
  3. 別ポートを使用するサービスプロセスを起動する
  4. 1 へ戻る

Java の例


import java.net.*;
...
try{
   ServerSocket serv = new ServerSocket(ポート番号); // LISTEN
   for(;;){ // 無限ループ
     Socket sock = serv.accept(); // 要求を受付け、新たなポートへのソ
                                  // ケットを用意する
     サービススレッド sth = new サービススレッド(sock);
     sth.start();                // 実際のサービス開始
   }
}
class サービススレッド extends java.lang.Thread {
  Socket sock;
  サービススレッド(Socket _sock){ //コンストラクタ
    sock = _sock;
  }
  public void run(){
  // 実際のサービス
    sock.close();
  }
}  

クライアントの動作

  1. 特定のホスト、特定のポートに CONNECT
  2. ソケットを得る
  3. ソケットに対して SEND, RECEIVE でデータのやりとりをする
  4. DISCONNECT

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科