Skip to content
Published on

[コンピュータネットワーク] 10. TCP完全攻略:接続、フロー制御、輻輳制御

Authors

本記事は James Kurose, Keith Ross 著 Computer Networking: A Top-Down Approach (6th Edition) の教科書を基にまとめた内容です。


1. TCP接続

1.1 TCP接続の特徴

TCP接続の核心属性:
  ├── コネクション指向(Connection-Oriented)
  │   データ転送前に必ず接続を確立
  ├── 全二重(Full-Duplex)
  │   双方向同時データ転送
  ├── ポイントツーポイント(Point-to-Point)
  │   正確に1つの送信者と1つの受信者
  └── バイトストリームサービス
      メッセージ境界なしの連続したバイトの流れ

1.2 3ウェイハンドシェイク

TCP接続確立プロセスである。

クライアント                      サーバー
   |                               |
   |── SYN (seq=client_isn) ──────>|  ステップ1:SYN
   |                               |
   |<── SYN+ACK ──────────────────|  ステップ2:SYN+ACK
   |   (seq=server_isn,            |
   |    ack=client_isn+1)          |
   |                               |
   |── ACK (ack=server_isn+1) ───>|  ステップ3:ACK
   |   (データ含有可能)           |
   |                               |

各ステップの説明

ステップ1 - SYN:
  - クライアントがSYNセグメントを送信
  - SYNビット = 1
  - 初期シーケンス番号(client_isn)を設定
  - データなし

ステップ2 - SYN+ACK:
  - サーバーがSYN+ACKセグメントで応答
  - SYNビット = 1、ACKビット = 1
  - サーバーの初期シーケンス番号(server_isn)を設定
  - 確認番号 = client_isn + 1

ステップ3 - ACK:
  - クライアントがACKセグメントを送信
  - SYNビット = 0(接続確立完了)
  - このセグメントからデータ含有可能

1.3 TCP接続終了(4ウェイハンドシェイク)

クライアント                      サーバー
   |                               |
   |── FIN ───────────────────────>|  ステップ1
   |                               |
   |<── ACK ──────────────────────|  ステップ2
   |                               |
   |<── FIN ──────────────────────|  ステップ3
   |                               |
   |── ACK ───────────────────────>|  ステップ4
   |                               |
   | (TIME_WAIT: 30秒待機)         |
   |   → 接続完全終了              |

TIME_WAIT状態:最後のACKが損失した場合のFIN再送に対応するため、一定時間待機する。


2. TCPセグメント構造

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
┌───────────────────────┬───────────────────────┐
│   送信元ポート (16)     │   宛先ポート (16)      │
├───────────────────────┴───────────────────────┤
│              シーケンス番号 (32)                  │
├───────────────────────────────────────────────┤
│              確認番号 (32)                       │
├────────┬──────┬───────┬───────────────────────┤
│ヘッダ長 │未使用│フラグ  │   受信ウィンドウ (16)    │
│  (4)   │ (6) │(6bits)│                        │
├────────┴──────┴───────┼───────────────────────┤
│   チェックサム (16)     │   緊急ポインタ (16)     │
├───────────────────────┴───────────────────────┤
│            オプション(可変長)                    │
├───────────────────────────────────────────────┤
│                                                │
│            データ(可変長)                       │
│                                                │
└───────────────────────────────────────────────┘

主要フィールド

フィールドサイズ説明
シーケンス番号32ビットセグメントデータの最初のバイト番号
確認番号32ビット期待する次のバイト番号
受信ウィンドウ16ビット受信可能なバイト数(フロー制御)
ヘッダ長4ビットTCPヘッダの長さ(4バイト単位)
フラグビット6ビットSYN、ACK、FIN、RST、PSH、URG

3. シーケンス番号と確認番号

3.1 シーケンス番号(Sequence Number)

セグメントに含まれるデータの最初のバイトのバイトストリーム番号である。

例:500,000バイトのファイル、MSS = 1,000バイト

セグメント1:seq = 0、     データ = バイト0〜999
セグメント2:seq = 1000、  データ = バイト1000〜1999
セグメント3:seq = 2000、  データ = バイト2000〜2999
...
セグメント500:seq = 499000、データ = バイト499000〜499999

3.2 確認番号(Acknowledgment Number)

受信者が次に期待するバイト番号である。累積確認(cumulative acknowledgment)方式を使用する。

例:telnetセッションで'C'を入力

ホストA                          ホストB
   |── seq=42, ack=79, 'C' ───>|
   |                             |  'C'受信、エコー応答
   |<── seq=79, ack=43, 'C' ───|
   |                             |
   |── seq=43, ack=80 ──────── >|  (ACK)

  seq=42:Aが送信するデータの42番目のバイト
  ack=79:AがBから79番目のバイトを期待
  ack=43:BがAから43番目のバイトを期待(42番受信完了)

4. RTT推定とタイムアウト

4.1 SampleRTT

実際に測定したRTT値である。セグメント送信時点からACK受信時点までの時間である。

  • 再送されたセグメントは測定から除外
  • SampleRTTは変動が大きい

4.2 EstimatedRTT(指数加重移動平均)

EstimatedRTT = (1 - alpha) * EstimatedRTT + alpha * SampleRTT

  alpha = 0.125(推奨値)

  → 最近の測定値により大きな重みを付与
  → SampleRTTの急激な変動を緩和

4.3 DevRTT(RTT偏差)

DevRTT = (1 - beta) * DevRTT + beta * |SampleRTT - EstimatedRTT|

  beta = 0.25(推奨値)

4.4 タイムアウト間隔

TimeoutInterval = EstimatedRTT + 4 * DevRTT

  EstimatedRTT:平均RTT推定値
  4 * DevRTT:安全マージン(変動性を考慮)
RTT推定の例(時間経過に伴う変化):

RTT
(ms)
 ^
 │  *    *
 │   *  * *  *         SampleRTT(変動大)
 │    **    * *  *
 │  ___________*____   EstimatedRTT(安定的)
 │                     TimeoutInterval(上限)
 └──────────────────> 時間

5. TCPの信頼性のあるデータ転送

TCPはIPの信頼性のないサービスの上に信頼性のある転送を実装する。

5.1 TCP送信者の核心イベント

イベント1:上位層からデータを受信
  → セグメント生成、シーケンス番号付与
  → IPに渡す
  → タイマーが実行中でなければ開始

イベント2:タイマー満了
  → 最も古い未確認セグメントを再送
  → タイマー再開始

イベント3:ACK受信
  → SendBase更新(累積確認)
  → まだ未確認セグメントがあればタイマー再開始

5.2 TCP再送シナリオ

シナリオ1:ACK損失

ホストA                    ホストB
  |── seq=92, 8バイト ───>|
  |                        |── ACK=100(損失!)
  | タイマー満了            |
  |── seq=92, 8バイト ───>|(再送)
  |<── ACK=100 ────────── |

シナリオ2:早期タイムアウト

ホストA                    ホストB
  |── seq=92, 8バイト ───>|
  |── seq=100, 20バイト ─>|
  |                        |── ACK=100
  | タイマー満了!          |── ACK=120
  |(ACK=100がまだ未到着)
  |── seq=92, 8バイト ───>|(不要な再送)
  |<── ACK=100 ────────── |
  |<── ACK=120 ────────── |(累積ACKで自然に解決)

シナリオ3:累積ACK

ホストA                    ホストB
  |── seq=92, 8バイト ───>|
  |── seq=100, 20バイト ─>|
  |                        |── ACK=100(損失!)
  |                        |── ACK=120(到着)
  |<── ACK=120 ────────── |

  ACK=120は120以前のすべてのバイトを確認
  → seq=92パケットも確認済み!
  → 再送不要

5.3 高速再送(Fast Retransmit)

タイムアウトを待たず、3つの重複ACKを受け取ったら即座に再送する。

ホストA                    ホストB
  |── seq=92 ────────────>|  ACK=100
  |── seq=100 ───────────>|(損失!)
  |── seq=120 ───────────>|  ACK=100(重複1)
  |── seq=140 ───────────>|  ACK=100(重複2)
  |── seq=160 ───────────>|  ACK=100(重複3)
  |                        |
  | 3つの重複ACK受信!      |
  | タイマー満了前に即座に再送
  |── seq=100 ───────────>|  ACK=180(累積確認)

3つの重複ACK = 元のACK + 追加3つ = 合計4回の同一ACK


6. フロー制御(Flow Control)

6.1 問題

送信者がデータを速く送りすぎると受信者のバッファが溢れる可能性がある。

受信側バッファ:
  ┌──────────────────────────────┐
  │[データ][データ][  空き空間     ]│
  └──────────────────────────────┘
  │←── 使用中 ──→│←── rwnd ────→│
                    受信ウィンドウ

6.2 受信ウィンドウ(Receive Window)

TCPは**受信ウィンドウ(rwnd)**を通じてフロー制御を行う。

rwnd = RcvBuffer - (LastByteRcvd - LastByteRead)

  RcvBuffer:受信バッファの全体サイズ
  LastByteRcvd:最後に受信したバイト番号
  LastByteRead:上位層が読み取った最後のバイト番号

6.3 動作方式

受信者:ACKにrwnd値を含めて送信

  ACKセグメント:
  ┌────────┬────────┐
  │ ACK=N  │ rwnd=X │  「Nバイトまで受け取った、
  └────────┴────────┘   Xバイト分さらに受け取れる」

送信者:rwndを超えないように送信量を制限

  LastByteSent - LastByteAcked <= rwnd

  rwndが減少 → 送信速度低下
  rwndが0 → 送信停止(ただし1バイトのプローブパケットを送信)

rwnd = 0の場合の問題

問題:受信者がrwnd=0を送った後にバッファを空けても、
     送信者に通知する方法がない(送るデータがないため)

解決:送信者が定期的に1バイトのプローブ(probe)セグメントを送信
  → 受信者が現在のrwnd値を含むACKで応答
  → 空き空間ができたら送信再開

7. TCP輻輳制御(Congestion Control)

7.1 輻輳とは

輻輳(Congestion):
  ネットワーク内のデータ量がネットワーク容量を超える状態

症状:
  ├── パケット損失(ルーターバッファオーバーフロー)
  ├── 長い遅延(ルーターキューでの待機)
  └── 不要な再送(タイムアウトによる)

7.2 輻輳制御 vs フロー制御

フロー制御:  受信者保護(受信バッファオーバーフロー防止)
              → rwndで制御

輻輳制御:    ネットワーク保護(ネットワーク過負荷防止)
              → cwnd(congestion window)で制御

実際の送信量:
  LastByteSent - LastByteAcked <= min(cwnd, rwnd)

7.3 AIMD(Additive Increase Multiplicative Decrease)

TCP輻輳制御の基本哲学である。

cwnd調整原則:

  パケット損失なし(ACK正常受信):
    → cwnd増加(帯域幅探索)

  パケット損失発生(タイムアウトまたは3重複ACK):
    → cwnd減少(輻輳緩和)

  「のこぎり歯」パターン:

  cwnd
    ^
    │    /\      /\      /\
    │   /  \    /  \    /  \
    │  /    \  /    \  /    \
    │ /      \/      \/      \
    └──────────────────────────> 時間

7.4 スロースタート(Slow Start)

接続初期にcwndを指数的に増加させる。

初期cwnd = 1 MSS

各ACK受信時:cwnd += 1 MSS
(1 RTTですべてのACKを受け取るとcwndが2倍)

cwndの変化:
  RTT 1:cwnd = 1 MSS  → 1セグメント送信
  RTT 2:cwnd = 2 MSS  → 2セグメント送信
  RTT 3:cwnd = 4 MSS  → 4セグメント送信
  RTT 4:cwnd = 8 MSS  → 8セグメント送信
  ...

  指数的増加:1, 2, 4, 8, 16, 32, ...

スロースタートの終了条件

1. cwnd >= ssthresh(スロースタート閾値)→ 輻輳回避に移行
2. タイムアウト発生 → ssthresh = cwnd/2、cwnd = 1 MSS、スロースタート再開
3. 3つの重複ACK → ssthresh = cwnd/2、cwnd = ssthresh + 3、高速回復

7.5 輻輳回避(Congestion Avoidance)

cwndがssthreshに到達した後、線形的に増加させる。

各RTTごと:cwnd += 1 MSS
(ACK1つにつき:cwnd += MSS * MSS / cwnd)

cwndの変化(MSS単位):
  RTT n:  cwnd = 10
  RTT n+1:cwnd = 11
  RTT n+2:cwnd = 12
  ...

  線形増加:10, 11, 12, 13, ...

輻輳イベント時の動作

タイムアウト発生:
  ssthresh = cwnd / 2
  cwnd = 1 MSS
  → スロースタートに復帰

3つの重複ACK:
  ssthresh = cwnd / 2
  cwnd = ssthresh + 3 MSS
  → 高速回復に移行

7.6 高速回復(Fast Recovery)

3つの重複ACKを受け取った時の状態である。TCP Renoで導入された。

高速回復の動作:
  1. 重複ACK受信ごとに:cwnd += 1 MSS
  2. 新しいACK受信(損失回復完了):
     cwnd = ssthresh
     → 輻輳回避に移行
  3. タイムアウト発生:
     ssthresh = cwnd / 2
     cwnd = 1 MSS
     → スロースタートに復帰

7.7 TCP輻輳制御の全体状態ダイアグラム

                    ┌─────────────┐
       開始 ───────>│ スロースタート │
                    │ cwnd指数増加  │
                    └──────┬──────┘
                cwnd >= ssthresh
                    ┌──────▼──────┐
              ┌────>│  輻輳回避    │<────┐
              │     │ cwnd線形増加  │     │
              │     └──────┬──────┘     │
              │            │            │
         新ACK       3重複ACK      タイムアウト
         (回復完了)       │            │
              │     ┌──────▼──────┐     │
              │     │  高速回復    │     │
              └─────│ cwnd調整    │     │
                    └─────────────┘     │
                           │            │
                      タイムアウト        │
                           └────────────┘
                           ssthresh=cwnd/2
                           cwnd=1 MSS
                           → スロースタート

7.8 TCP Tahoe vs TCP Reno

cwnd
  ^
  │           *
  │          * *
  │         *   *        TCP Reno
  │        *     *      /
  │       *       *    *
  │      *         *  * *
  │     *           **   *
  │    *                  *
  │   *
  │  * TCP Tahoe:常にcwnd=1に復帰
  │ *
  └─────────────────────────────> 時間
       ↑         ↑
     損失1     損失2
イベントTCP TahoeTCP Reno
タイムアウトcwnd = 1 MSScwnd = 1 MSS
3重複ACKcwnd = 1 MSScwnd = cwnd/2(高速回復)

8. TCP公平性(Fairness)

8.1 公平性の定義

容量 R のボトルネックリンクを K 個のTCP接続が共有する場合、各接続が R/K のスループットを得ることが理想的である。

8.2 TCPが公平な理由

2つの接続が帯域幅Rを共有する場合:

スループット2
    ^
    │  ╲  公平ポイント
    │   ╲ (R/2, R/2)
    │    ╲   *
R   │     ╲ / \
    │      *   *
    │     / ╲   \
    │    /   *   *
    │   /   / ╲   \
    │  /   /   *   *  → 公平ポイントに収束
    └──────────────────> スループット1
    0                 R

  AIMDが公平ポイントに収束する過程:
  1. 両接続が均等にcwndを増加(Additive Increase)
     → 45度方向に移動(合計=Rの線に向かって)
  2. 損失発生時にそれぞれcwndを半分に(Multiplicative Decrease)
     → 原点方向に移動
  3. 繰り返すと公平ポイント(R/2, R/2)に収束

8.3 公平性の現実的な限界

UDPは輻輳制御を行わない:
  → TCPが譲った帯域幅をUDPが占有
  → TCPに不公平

並列TCP接続:
  → 1つのアプリケーションが複数のTCP接続を開くと
  → 単一接続アプリケーションより多くの帯域幅を占有
  → 例:ブラウザが複数の接続でWebオブジェクトを同時ダウンロード

9. まとめ

TCPの核心メカニズム要約:

  接続管理:
  ├── 3ウェイハンドシェイク(SYN → SYN+ACK → ACK)
  └── 4ウェイハンドシェイク終了(FIN → ACK → FIN → ACK)

  信頼性のある転送:
  ├── シーケンス番号 + 確認番号(累積ACK)
  ├── タイマーベースの再送
  ├── 高速再送(3重複ACK)
  └── RTT推定(EWMA)

  フロー制御:
  └── rwnd(受信ウィンドウ)で受信者を保護

  輻輳制御:
  ├── スロースタート:指数的増加
  ├── 輻輳回避:線形的増加
  ├── 高速回復:3重複ACK時にcwndを半分
  └── AIMD → 公平な帯域幅共有

10. 確認問題

Q1. 3ウェイハンドシェイクでなぜ3回のメッセージ交換が必要か?

1回目(SYN):クライアントが接続を要求し、自分の初期シーケンス番号を通知する。 2回目(SYN+ACK):サーバーが接続を受諾し、自分の初期シーケンス番号を通知し、クライアントのシーケンス番号を確認する。 3回目(ACK):クライアントがサーバーのシーケンス番号を確認する。

この過程を通じて双方が相手の初期シーケンス番号を確認し、接続の意思を確認する。2回だけではサーバーが送ったSYN+ACKをクライアントが受信したかどうかサーバーが知ることができない。

Q2. フロー制御と輻輳制御の違いは?
  • フロー制御受信者を保護する。受信バッファが溢れないように rwnd(受信ウィンドウ)を使用して送信者の送信量を制限する。
  • 輻輳制御ネットワークを保護する。ネットワークが過負荷にならないように cwnd(輻輳ウィンドウ)を使用して送信者の送信量を制限する。

実際の送信量は min(cwnd, rwnd) によって決定される。

Q3. スロースタートでcwndが指数的に増加するのに、なぜ「遅い(slow)」スタートなのか?

「スロースタート」という名前は初期の cwnd1 MSS と非常に小さく始まることに由来する。TCP以前のプロトコルは接続開始時に許可された最大ウィンドウサイズで一気にデータを送り込んでいたが、これはネットワーク輻輳を引き起こした。スロースタートはそれに比べて「遅く」1 MSSから始めるという意味である。もちろん指数的増加なので実際には急速に増加する。

Q4. TCP Renoでタイムアウトと3重複ACKを異なる方法で処理する理由は?

3重複ACKは特定のセグメントのみが損失したが、その後のセグメントは到着していることを意味する。ネットワークがまだ一部のパケットを配信しているため、輻輳が深刻ではないと判断し、cwnd を半分にのみ減少させる(高速回復)。

一方、タイムアウトはACKが全く来ない状況であり、ネットワーク輻輳が深刻であると判断して cwnd を1 MSSに初期化し、スロースタートから再開する。