NET // communication networks
LESSON 18 / 標準編

TCP の信頼性 ── 再送・ウィンドウ・輻輳制御

「TCP は確実に届けるプロトコル」の一歩先へ。コネクション確立(3-way handshake)シーケンス番号と累積 ACK再送タイムアウト・高速再送受信ウィンドウによるフロー制御輻輳制御の 4 フェーズ(slow start / congestion avoidance / fast retransmit / fast recovery)、そして AIMD の考え方まで踏み込みます。基本情報技術者・学部 1〜2 年向け。

学習目標

本講を終えると、以下の問いに答えられるようになります。

本講は 標準 ネットワーク全体像とレイヤー観 と「標準 UDP と多重化」(準備中) の続きにあたります。基礎の 第 3 回 プロトコルと TCP/IP 階層モデル で「TCP は確実に届ける係」と覚えた内容を、ヘッダ図・状態遷移・制御アルゴリズムに踏み込んで再構成します。発展的な輻輳制御アルゴリズム(BBR / CUBIC など) は 発展編 で扱います。

このレッスンの目次

01 TCP の特徴 TCP(Transmission Control Protocol) は IP の上… 02 セグメント構造 TCP の各機能は、すべて セグメントヘッダ のフィールドに支えられています。20 … 03 コネクション TCP は 会話の前に「もしもし」 のやり取り( 3-way handshake )… 04 Seq と ACK TCP は 「バイトストリーム」 を運ぶプロトコルです。アプリから渡されたデータを「… 05 再送制御 インターネットでは、ルータの バッファ溢れ や物理エラーでパケットが 消えること が… 06 フロー / 輻輳 どちらも「一度に送る量を制限する」しくみですが、 誰のために何を守るか が違います。… 07 輻輳制御 4 状態 輻輳制御の本体は「 cwnd をいつ・どれだけ・どう動かすか 」のアルゴリズムです。… 08 RTT / Nagle 最後に、TCP の挙動を支えるいくつかの重要な補助メカニズムを押さえておきましょう。… 09 まとめと用語 本講の重要語句を整理 10 確認問題 理解度を問題でチェック

TCP の 5 つの特徴 ── 何を保証してくれるのか

TCP(Transmission Control Protocol) は IP の上に乗って動く トランスポート層プロトコル です。IP は「ベストエフォート」── 届くかどうかも順序も保証しない ── ですが、TCP はそこに 5 つの保証 を被せて、アプリから見ればまるで「電話線がつながっているような」信頼できる通信路を作り出します。
POINT TCP のすべての仕組みは「IP の弱点を埋めるため」にあります。失われる・順序が乱れる・速すぎて受け切れない・混雑で詰まる という IP 層・経路上の問題を、エンドツーエンドで解決します。

5 つの特徴を 1 行で

特徴意味実現するしくみ
コネクション指向送信前に「接続」を確立してから話す3-way handshake / 4-way close
信頼性パケットロスがあっても確実に届けるシーケンス番号・ACK・再送制御
順序保証送った順にアプリへ渡すシーケンス番号と受信側の並べ直し
フロー制御受信側のバッファ溢れを防ぐ受信ウィンドウ(rwnd)
輻輳制御ネットワーク全体の混雑を抑える輻輳ウィンドウ(cwnd)・AIMD

補足:「コネクション指向」の「指向」とは

指向は「その考え方を中心に設計されている」という意味です。TCP の コネクション指向 は、通信路が物理的に 1 本の専用線になるという意味ではありません。実際には、IP パケットは毎回独立に転送されます。ただし TCP は、送信側と受信側が コネクションの状態 を持ち、シーケンス番号・ACK・ウィンドウなどを使って、アプリから見ると 1 本の会話が続いている ように扱います。

「コネクション型」より「コネクション指向」: 「型」は固定された形に聞こえますが、「指向」は設計の向きです。TCP は 接続を確立してから通信することを前提に、確認・順序・再送・終了処理を組み立てる プロトコル、と読むと自然です。

もう一つ:全二重(full-duplex)

TCP は 全二重通信 です。1 つのコネクションの中で、両端が 同時に データを送り合えます。ヘッダの ACK 番号フィールド を使って、受信したことの通知(ACK) は 反対方向のデータに相乗り させて運べます ── これを ピギーバック(piggyback) と呼び、ACK のためだけにわざわざ別パケットを出す無駄を省きます。

UDP との対比で覚えるとスッキリします:UDP は「投げっぱなし・コネクションなし・ヘッダ 8 バイト」、TCP は「順序・到達・速度を全部面倒見る・ヘッダ最低 20 バイト」。確実さの代償としてオーバーヘッドが大きい のが TCP、と理解しましょう。

コネクションの確立と解放 ── 状態遷移を歩く

TCP は 会話の前に「もしもし」 のやり取り(3-way handshake)、会話の後に「じゃあ切りますね」 のやり取り(4-way close) を必ず行います。これは単なる挨拶ではなく、両端でシーケンス番号の初期値を合わせる という具体的な目的があります。

大きな図 ② コネクションを 8 ステップで体験(walkthrough)

クライアント A サーバ B 時間 → ① SYN=1, seq=client_isn A: SYN_SENT 状態に遷移 ② SYN=1, ACK=1, seq=server_isn, ack=client_isn+1 B: SYN_RCVD 状態(SYNACK セグメント) ③ ACK=1, seq=client_isn+1, ack=server_isn+1 両端 ESTABLISHED ── 接続確立 ④ データ送信(seq=X, length=L) アプリのデータが流れ始める ⑤ ACK=X+L(累積 ACK) 必要なら応答データに ACK をピギーバック ⑥ FIN(A → B):もう送るデータがない A: FIN_WAIT_1 / B: CLOSE_WAIT ⑦ B → A:ACK + 続いて FIN B はまだ送りたければ送り続けてから FIN を送る ⑧ 最後の ACK ── A は TIME_WAIT、B は CLOSED
ステップ 1. クライアント A が SYN セグメント を送ります。SYN ビット = 1、シーケンス番号は乱数で選んだ client_isn(initial sequence number)。なぜ 0 ではなく乱数なのか:過去のコネクションの遅延セグメントが新しいコネクションに紛れ込むのを防ぐためです。
ステップ 2. サーバ B は SYNACK セグメント で応答します。SYN=1 と ACK=1 が同時に立ち、自分の初期番号 server_isn をセット、ACK 番号には client_isn + 1(=「あなたが送ってきた SYN を 1 バイト分受け取りました」)を入れます。SYN は 1 バイトを消費する 仮想的な制御セグメント、と覚えてください。
ステップ 3. A は最後に ACK セグメント を返します。これで両者が「自分の初期番号は相手に届いた」と確認できました。ESTABLISHED 状態に遷移し、ここから 双方向にデータ送信可能 です。3 つ送り合うのは「初期番号の合意を双方向で確認」するため です。
ステップ 4. 確立済みコネクションでは、好きなタイミングで データセグメント を送れます。Seq には 送る最初のバイトの番号、ACK には 相手から最後に受け取った位置の次(= ピギーバック ACK) を必ず入れます。
ステップ 5. 受信側は 累積 ACK で「次に欲しいバイト番号」を返します。応答用のデータ(例えば HTTP の応答ボディ) があれば、そこに ACK を ピギーバック させて 1 セグメントで済ませます。なければ ACK のみのセグメント(40 バイトほど) が飛びます。
ステップ 6. 通信を終わらせたい側(ここでは A) が FIN セグメント を送ります。「もう送りたいデータはないですよ」という宣言です。重要な点:FIN は片方向の終了宣告 であり、反対方向(B → A) はまだ生きています
ステップ 7. B は ACK を返します(ここで A の送信側はクローズ完了)。B は残りの送信を片付けてから、自分も FIN を送ります。これが 「4-way close」と呼ばれる理由:両方向それぞれを別々に閉じる必要があるため、最低 4 つのセグメントが要るのです。
ステップ 8. A は B の FIN に ACK を返して TIME_WAIT 状態に入ります。2×MSL(Maximum Segment Lifetime, 通常 60 〜 240 秒) 待ってからコネクション情報を完全に破棄します。これは「最後の ACK が途中で消失したときの再送に応じるため」と「過去のセグメントの誤再来を吸収するため」です [要確認:OS 既定値]。
1 / 8

図の見方:左が時間軸の早い順に、上から下へセグメントが流れます。① 〜 ③ が 3-way handshake、④ 〜 ⑤ が確立中のデータ送受信(双方向・累積 ACK + ピギーバック)、⑥ 〜 ⑧ が 4-way close。「次へ」ボタンで矢印が進みます。

シーケンス番号と確認応答 ── ストリームをバイトで数える

TCP は 「バイトストリーム」 を運ぶプロトコルです。アプリから渡されたデータを「セグメント」に切り分けつつ、1 バイトずつ通し番号 を振って管理します。これがシーケンス番号(Seq) と ACK 番号の正体です。

例:500,000 バイトのファイルを 1,000 バイトずつ送る

1 つ目のセグメントは seq = 0(ファイルの 0 バイト目から開始)、2 つ目は seq = 1000、3 つ目は seq = 2000 ……となります。受信側が「3000 バイト目まで受け取った」なら、ACK には 3000(= 次に欲しいバイト番号) を入れて返します。

ストリームを 1,000 バイトずつ切ってシーケンス番号を振る seq=0 [0, 999] seq=1000 [1000, 1999] seq=2000 [2000, 2999] seq=3000 [3000, 3999] 最後 [499000, 499999]

図の見方:Seq 番号は「このセグメントの先頭バイトのストリーム上の位置」を表します。1 つ前のセグメントの長さを足したものが次の Seq になります。受信側はこの番号を見て 並べ直し ます ── ネットワーク経路の違いで順序が乱れて到着しても、アプリには順番通りに渡せます。

累積 ACK(cumulative ACK) のからくり

TCP の ACK は 「ここまで連続して受け取りましたよ」 という意味です。仮に受信側が seq=1000 と seq=2000 を受け取ったら、ACK = 3000(次の期待バイト) を返します。途中の ACK が 1 つロスしても、後の ACK が届けば前の ACK の役割も兼ねるので問題ありません。

累積 ACK は「次に欲しい先頭番号」を返す ACK は最後に届いた番号ではなく、連続して受け取れた範囲の次を示す すべて順番に届く場合 seq=0 seq=1000 seq=2000 seq=3000 ACK=1000 ACK=2000 ACK=3000 ACK=4000 途中が欠ける場合: seq=1000 が消えた seq=0 seq=1000 ロス seq=2000 seq=3000 ACK=1000 ACK=1000 ACK=1000 穴が埋まるまで「次は1000が欲しい」と言い続ける seq=1000 を再送して受信できると、ACK は一気に 4000 へ進む

図の見方:上段では 0, 1000, 2000, 3000 が順に届くので ACK も 1000, 2000, 3000, 4000 と進む。下段では seq=1000 が欠けているため、後ろの 2000 や 3000 が届いても ACK は 1000 のまま止まる。この同じ ACK の繰り返しが重複 ACK。

POINT 累積 ACK の値 = 連続して受信した最後のバイト + 1。途中欠けがあると、ACK の値はその 欠けの直前で止まり続ける ── これが 重複 ACK となり、後で見る 高速再送のトリガ になります。
もっと詳しく:SACK(選択的 ACK) の実例

累積 ACK だけでは、たとえば seq=1000 / 2000 / 3000 を送って 2000 だけがロス した場合に「ACK=2000 が連続で返ってくる」だけで、3000 が届いたのか不明になります。送信側は安全のため 2000 以降を全部再送 …… すると無駄が多いですね。

SACK(Selective Acknowledgment, RFC 2018) は TCP オプションで「3000 は受け取り済みです」を別途明示できる仕組みです。具体的には ACK 番号 = 2000 に加え、SACK オプション領域に 受信済みブロック [3000, 4000) を列挙します。送信側は欠けた 2000 だけを再送すればよくなり、効率が上がります。

ハンドシェイク時に SYN セグメントへ SACK-Permitted オプション を付けて両端が合意します。現代の OS はほぼ既定で有効化しており、Wireshark で TCP セグメントを見ると SACK PermSLE/SRE といった表記が観察できます。

再送制御 ── 失われたセグメントを取り戻す 2 つの道

インターネットでは、ルータの バッファ溢れ や物理エラーでパケットが 消えること があります。TCP の信頼性は、消えたセグメントを 送信側が能動的に再送 することで担保されています。再送のトリガは大きく 2 つあります。

(A) 再送タイムアウト(RTO)による再送

セグメントを送ったら、送信側は 再送タイマ を起動します。一定時間(RTO, Retransmission Time Out) 経っても ACK が返ってこなければ 「ロスしたとみなして再送」 します。安全策ですが、タイマ満了を待つ分 遅延が大きい のが弱点です。

(B) 高速再送(Fast Retransmit)── 重複 ACK を見て早めに再送

累積 ACK のしくみ上、あるセグメントだけが欠けて後続が届くと、受信側は同じ ACK を何度も返します(=重複 ACK)。送信側は 同じ ACK を 4 つ受け取った時点(= 重複 ACK が 3 つ来た時点) で、タイマを待たずに 即座にそのセグメントを再送 します。これが高速再送です。

POINT 3 重 ACK = 高速再送のトリガ。なぜ 3 つも待つのか? それは「経路上の 順序逆転(reordering) による誤判定」を避けるためです。1 〜 2 つの重複なら「ちょっと順番が乱れただけ」かもしれず、再送しないほうが効率的なのです。

大きな図 ③ RTO 再送と高速再送の違い

再送の判断材料は 2 種類 時間で判断する RTO / ACK の繰り返しで判断する高速再送 A RTO再送 ACKが返らないので タイマ満了まで待つ 送信側 受信側 seq=1000 途中で消失 RTOまで待つ 判断材料: 時間 復旧は遅め タイマ満了後に seq=1000 を再送 B 高速再送 重複 ACK が 3 つ来たら 欠けたセグメントをすぐ再送 送信側 受信側 1000欠け 2000 3000 4000 後続は届く ACK=1000 ACK=1000 ACK=1000 重複 ACK が 3 つ → 1000 が欠けたと判断 RTOを待たず seq=1000 を再送

図の見方:上段の RTO 再送 は ACK が戻らないため、送信側はタイマが切れるまで待ってから再送する。下段の 高速再送 は、後続セグメントが届くたびに受信側が同じ ACK を返すので、送信側が 「同じ ACK が続く = その手前が欠けている」 と判断して、RTO を待たずに再送する。

RTO 再送と高速再送の違い

項目RTO による再送高速再送
トリガ再送タイマ満了同じ ACK が 4 回(重複 3 つ)
速さ遅い(RTO ≧ RTT)速い(およそ 1 RTT)
再送するものロスしたもの以降を全部(伝統的な実装の挙動)ロスしたものだけ
輻輳判定輻輳が 重い と解釈 → slow start からやり直し輻輳が 軽い と解釈 → fast recovery で復帰

フロー制御 と 輻輳制御 ── 似て非なる 2 つのウィンドウ

どちらも「一度に送る量を制限する」しくみですが、誰のために何を守るか が違います。輻輳(ふくそう) は難しい漢字ですが、英語では congestion、つまり 混雑 のこと。輻輳制御は、ネットワークの混雑をひどくしないための制御です。

フロー制御 ── 相手の PC の負荷を大きくしすぎない

受信ウィンドウ(rwnd) は、相手の PC(受信側)が「今あと何バイトまでなら受け取れますよ」を ACK のヘッダで通知する仕組みです。相手のアプリ処理が進まず受信バッファが詰まっているときに、こちらがさらにパケットを送り続けると、相手 PC の負荷が大きくなりすぎます。そこで送信側は rwnd を超えてデータを送りません。

相手 PC が詰まっているときは送信量を抑える rwnd は「相手が今受け取れる余裕」を知らせる数字 自分のPC 送信側 相手のPC 受信側 受信バッファ 未処理データが多い 空き少し 処理が遅い データ データ ACKで rwnd=小さい と通知 送信側は量を絞る 相手の負荷を増やしすぎない

図の見方:相手 PC のアプリ処理が遅いと、届いたデータが受信バッファに残り続ける。空きが少ないと相手は ACK の中で rwnd を小さく通知 し、送信側は送る量を抑える。これにより、相手 PC にさらに大量のパケットを押し込んで負荷を大きくしすぎることを防ぐ。

もし rwnd を無視して送り続けると、相手 PC の受信バッファが溢れてセグメントを 捨てる しかなくなり、結局再送が増えてしまいます。フロー制御は 「速い送信側が、処理の追いつかない受信側を踏み潰さない」 ためにあるのです。

ゼロウィンドウ問題: rwnd=0 になると送信側は止まりますが、その後 rwnd が回復したことを誰が伝えるのでしょうか? 受信側からの「窓が開きましたよ」通知が消失すると永遠に止まってしまうため、送信側は定期的に 1 バイトの「ウィンドウプローブ」 を投げて確認します。

輻輳制御 ── ネットワーク全体の混雑を抑える

輻輳ウィンドウ(cwnd)送信側が自分で決める 値で、「今ネットワークがどれくらい受け入れられそうか」の見積もりです。ヘッダのフィールドではなく、送信側ホストの内部状態として保持されます。

送信側が一度に投げ入れる量は、実は次の 最小値 で決まります。

未確認バイト数 ≦ min(rwnd, cwnd)
rwnd は 受信側 の制限、cwnd は ネットワーク への配慮。厳しい方 が実効的に効きます。

みんなが好き勝手に大量のデータを送ると、ルータのバッファが溢れて 輻輳(ふくそう) が起きます。すると大量のロスが起き、再送がさらに混雑を加速させ、最悪 輻輳崩壊(congestion collapse) に至ります。これを防ぐため、TCP は「ロスを観測したら自分で送信レートを落とす」協調動作をします。

コラム:なぜトランスポート層なのにネットワークの混雑を考えるの?
トランスポート層は本来、送信ホストと受信ホストの間で動き、ホスト側の通信を担当する層です。だから「ネットワークの中の混雑まで見るのは変では?」と感じるのは自然です。ポイントは、TCP がルータを直接操作しているわけではないことです。実際に調整しているのは 送信ホストがネットワークへ流し込む量 です。ルータの中が混雑するとロスや重複 ACK、RTO といった形で端のホストに現れるため、送信側 TCP はそれを手がかりに「今は出しすぎかも」と判断して送信量を下げます。つまり輻輳制御は、ネットワーク内部を直接制御する機能 というより、ホストが入口で送信量を絞ってネットワークを詰まらせないための協調ルール です。

横並びで比較

項目フロー制御輻輳制御
守りたい相手受信ホスト のバッファネットワーク 全体(ルータのバッファ)
使う変数rwnd(受信ウィンドウ)cwnd(輻輳ウィンドウ)
誰が決める受信側(ヘッダで通知)送信側(自分で増減)
増減の根拠受信バッファの空き状況ロス(RTO や 3 重 ACK) の有無
仕様の場所セグメントヘッダ(rwnd フィールド)OS 実装内のアルゴリズム
典型的な値数十 KB 〜 数 MB(自動チューニング)数 KB から始まり指数 / 線形に変動

送信側はこの 2 つの両方に従って送出量を決めます。「rwnd と cwnd の小さい方」 ── これが鉄則です。

輻輳制御の 4 フェーズ ── slow start / avoidance / fast retx / fast recovery

輻輳制御の本体は「cwnd をいつ・どれだけ・どう動かすか」のアルゴリズムです。古典的な TCP Reno を例に、4 つの状態(フェーズ) を順に見ていきましょう。これは AIMD(Additive Increase / Multiplicative Decrease) ── 加算で増やし、乗算で減らす ── の考え方を具体化したものです。

① slow start(スロースタート) ── 小さく始めて指数関数的に立ち上げる

コネクション開始直後は cwnd を 非常に小さい値(初期は 1 〜 10 MSS [要確認:現代の初期値は IW=10]) から始め、ACK が 1 つ返るごとに cwnd を 1 MSS ずつ増やす ── これにより 1 RTT ごとに cwnd は 2 倍 になっていきます(1 → 2 → 4 → 8 → …)。指数増加です。

「スロー」と呼ぶのは、初期 TCP は要求した分すべてを一気に投げていた のに対して、最初は控えめに立ち上げる、という意味です。実際の伸び方は決して遅くありません。

名前に注意:slow は「増え方が遅い」ではない
slow start は、いきなり大量送信しないで 小さい cwnd から始める という意味です。いったん ACK が順調に返ってくると、cwnd は 1 RTT ごとにほぼ 2 倍 になるので、増え方そのものはむしろ速いです。つまり slow start = ゆっくり増やす ではなく、小さく始めて、安全そうなら一気に増やす 方式です。

② congestion avoidance(輻輳回避) ── 線形に増やす

cwnd が 閾値(ssthresh, slow start threshold) に達したら、増加を 1 RTT ごとに +1 MSS線形増加 に切り替えます。これが AIMD の 「加算増加」 部分です。容量近くまで来ているはずなので、急加速で詰まらないよう慎重に伸ばします。

③ fast retransmit(高速再送) ── 軽い輻輳の検知

3 重 ACK を受け取ったら、上で見たように 失われたセグメントを即再送。同時に 「軽い輻輳」 と判断し、ssthresh を現在の cwnd の半分にして、cwnd 自体も小さくします。これが AIMD の 「乗算減少」 部分(× 1/2)です。

④ fast recovery(高速回復) ── スロースタートに戻らず復帰

Reno の特徴で、3 重 ACK 後は slow start に戻らず、ssthresh の位置から再び 輻輳回避(線形増加)で再開 します。「ちょっと欠けただけならパイプはまだ機能している」という前向きな判断です。一方 RTO 再送 は重い輻輳とみなし、cwnd=1 MSS から slow start でやり直し です。

大きな図 ④ 輻輳ウィンドウの増減アニメーション

cwnd の時間変化(TCP Reno のイメージ) cwnd(MSS 単位) 時間 (RTT) ssthresh(初期) slow start(指数) 輻輳回避(線形) 3 重 ACK cwnd を半減 新 ssthresh 輻輳回避で再開(高速回復) RTO ! cwnd=1 から slow start

図の見方:横軸は時間(RTT 数)、縦軸は cwnd。青=slow start で指数増加 → ssthresh で 緑=輻輳回避 に切替えて線形増加 → 赤の 3 重 ACK で半減し fast recovery で輻輳回避に戻る → 紫の RTO が起きると cwnd=1 まで落として slow start からやり直し。これが TCP Reno の「のこぎり波」パターンです。

小問: Slow start フェーズで、ある RTT 開始時に cwnd = 4 MSS だったとき、その RTT が終わった瞬間の cwnd はおよそいくつになるか(すべての ACK が遅延なく返ってくると仮定)?
正解:B。slow start では ACK 1 つにつき cwnd を +1 MSS 増やします。1 RTT 内に 4 つのセグメントを送って 4 つの ACK が返ってくるので、cwnd は 4 + 4 = 8 になります。RTT ごとに「2 倍」が指数増加の本質です。

AIMD ── 公平性の数学

AIMD(Additive Increase, Multiplicative Decrease) は、複数の TCP コネクションが同じボトルネック回線を共有する際に 長期的に帯域を公平に分け合う という重要な性質を持ちます。直感的には:

この組み合わせを繰り返すと、各コネクションのスループットは 「公平な帯域共有」点 に収束していきます ── これが TCP の公平性 と呼ばれる古典的な結果です。

もっと詳しく:Reno / NewReno / CUBIC ── アルゴリズムの世代交代

本講義は古典的な Reno を土台に解説しています。最新動向は 発展編 を参照してください。

RTT 推定 / Nagle / Delayed ACK ── 周辺の最適化

最後に、TCP の挙動を支えるいくつかの重要な補助メカニズムを押さえておきましょう。試験にも出やすい古典的トピックです。

RTT(Round Trip Time) と RTO の決め方

RTT は「セグメントを送ってから、その ACK が返るまでの往復時間」。RTO はこの RTT より少し長く取らないと「まだ来ていないのに再送」という誤判定になります。だが長すぎると、ロス時の復旧が遅れます。

Linux などの実装は EWMA(指数加重移動平均)SRTT(平滑化 RTT)RTTVAR(RTT のばらつき) を更新し、おおむね次の式で決めます(Jacobson / Karels アルゴリズム, RFC 6298):

SRTT ← (1 − α) · SRTT + α · 新しい RTT サンプル
RTO ← SRTT + 4 × RTTVAR
α ≒ 1/8(新しい観測を 12.5% 反映)。安定した平均と、ばらつきへのマージン(× 4) が両立する。

Nagle アルゴリズム ── 小さなパケットをまとめて投げる

たとえばエディタで 1 文字打つたびに 1 バイトの TCP セグメントが飛ぶと、ヘッダ(IP+TCP で最低 40 バイト) のオーバーヘッドが 40 倍になり、ネットワークが「小パケットだらけ」で詰まります。Nagle アルゴリズム(RFC 896, 1984) は 「ACK が返るまで、新しい小データはバッファに溜めて送らない」 ルールで、小さな送信を自動的に束ねます。

Delayed ACK ── ACK 専用パケットを減らす

ACK だけを送るセグメントもオーバーヘッドが大きいので、受信側は ACK を少し遅らせて、(a) 反対方向のデータにピギーバックできれば乗せる、(b) できなくても 200 ms 程度経つか、もう 1 セグメント受け取ったタイミングで返す、という最適化をします。これが Delayed ACK(RFC 1122) です。

もっと詳しく:Nagle と Delayed ACK の競合

2 つの最適化はそれぞれ単独では正しいのですが、同時に有効だと相性が悪い 場面があります。

典型的な例:クライアントが 「短い要求 → サーバの応答待ち」 という対話を繰り返すケース。クライアントの Nagle は「未応答の小パケットがある間は次を送らない」、サーバの Delayed ACK は「200 ms 待ってからまとめて ACK」── 結果、クライアントが 200 ms じっと待ってからやっと次の小データを送る という奇妙な遅延が発生します。これが 「Nagle と Delayed ACK のデッドロック」 として知られる古典的な落とし穴です。

対話的に小データをやり取りするアプリ(Telnet・SSH 入力・REST API のキープアライブ等) では、ソケットオプション TCP_NODELAY で Nagle を無効にしておくのが定番の対処です。逆に大量バルク転送(ファイル送信など) では Nagle のままで OK です。

現代では 大規模送信 寄りに最適化された設定が多く、Nagle/Delayed ACK の影響は減りましたが、低レイテンシ要件のリアルタイム通信 では今でも要注意のポイントです。リアルタイム性を最優先するなら UDP / QUIC を選ぶ、というのも合理的な判断です。

まとめと用語チェック

SUMMARY 1. TCP は コネクション指向・信頼性・順序保証・フロー制御・輻輳制御 + 全二重 を提供するトランスポート層プロトコル
2. ヘッダ最小 20 バイト。中核は Seq / ACK 番号(順序と到達確認)、フラグ(SYN / ACK / FIN / RST / PSH / URG)受信ウィンドウチェックサム
3. 確立は 3-way handshake(SYN → SYN+ACK → ACK)、解放は 4-way close(FIN → ACK → FIN → ACK)。理由は「双方向に初期番号と終了を合意するため」
4. 累積 ACK = 「次に欲しいバイト番号」。途中欠けで 重複 ACK が発生
5. 再送は 2 系統:RTO 再送(タイマ満了・遅い・以降を全部再送) と 高速再送(3 重 ACK・速い・該当だけ再送)
6. SACK オプションで「飛び石で受信したブロック」を明示できる
7. フロー制御 = rwnd(受信側のバッファ保護) と 輻輳制御 = cwnd(ネットワーク保護) は別物。実効送信量は min(rwnd, cwnd)
8. 輻輳制御 4 フェーズ:slow start(指数)congestion avoidance(線形 +1/RTT)fast retransmit(3 重 ACK で再送)fast recovery(× 1/2 で輻輳回避に復帰)。RTO は重い輻輳と判断し slow start からやり直し
9. AIMD(加算増加・乗算減少):長期的に帯域を 公平に 分け合う性質をもたらす
10. RTO は SRTT + 4×RTTVAR(EWMA + 分散)。Nagle(小データを束ねる) と Delayed ACK(ACK を少し遅らせる) は相性問題があり、対話的アプリでは TCP_NODELAY の検討を
11. アルゴリズム名:Tahoe / Reno / NewReno → 現代主流は CUBIC(Linux 既定)。新興 BBR。詳細は発展

用語チェック

用語1 行説明
セグメントTCP の通信単位。ヘッダ + データ
シーケンス番号(Seq)セグメント先頭バイトのストリーム上の位置
ACK 番号次に受け取りたいバイト番号(= 累積)
SYN / FIN / RST接続開始 / 終了 / 強制切断のフラグ
3-way handshake双方向の初期 Seq 合意。SYN → SYN+ACK → ACK
4-way close両方向を別々に閉じる。FIN → ACK → FIN → ACK
累積 ACK「ここまで連続受信」を 1 つの番号で示す方式
重複 ACK同じ ACK 番号が繰り返されること。途中欠けの兆候
SACK選択的 ACK。飛び石受信を明示する TCP オプション
RTO再送タイムアウト。SRTT + 4 × RTTVAR が目安
高速再送3 重 ACK で即再送。RTO を待たない
rwnd(受信ウィンドウ)受信側のバッファ空きをヘッダで通知
cwnd(輻輳ウィンドウ)送信側内部の混雑見積もり
slow startcwnd を ACK ごとに +1(RTT で 2 倍)。指数増加
congestion avoidanceRTT ごとに +1。線形増加(AIMD の AI)
fast recovery3 重 ACK 後 cwnd を半減し、輻輳回避から再開
AIMD加算増加・乗算減少。長期で公平な帯域共有
Nagle / Delayed ACK小データの集約 / ACK の遅延送信。相性問題あり
TCP_NODELAYNagle を無効にするソケットオプション
Reno / CUBIC / BBR古典 / 現代主流 / 新興 の輻輳制御アルゴリズム
NEXT: 次回(第19回)は対極の UDP ── 多重化と簡易な誤り検出だけを提供する最小機能トランスポート。TCP の「あれもこれも」と UDP の「あえて何もしない」を対比すると、両者の役割分担が立体的に見えてきます。続く第20回でネットワーク層 IP・サブネット計算に降りていきます。

確認問題

問1. TCP の特徴に 当てはまらない ものを 1 つ選べ。

次の選択肢から最も適切なものを選択してください。
A. コネクション指向で、データ送信前に接続を確立する
B. 受信側の処理能力に応じて送信レートを抑えるフロー制御を持つ
C. データを必ず暗号化して送信する
D. 失われたセグメントを再送し、順序通りにアプリへ渡す
正解:C
TCP 自体に 暗号化機能はありません。暗号化が必要な場合は TCP の上に TLS を被せて使います(HTTPS = HTTP over TLS over TCP)。A・B・D はそれぞれコネクション指向・フロー制御・信頼性 + 順序保証 で、いずれも TCP の特徴です。

問2. 3-way handshake の手順として正しいものを 1 つ選べ。

次の選択肢から最も適切なものを選択してください。
A. SYN → ACK → FIN(クライアント送 → サーバ送 → クライアント送)
B. SYN → SYN+ACK → ACK(クライアント送 → サーバ送 → クライアント送)
C. SYN+ACK → ACK → SYN(サーバ送 → クライアント送 → クライアント送)
D. ACK → SYN → ACK(クライアント送 → サーバ送 → クライアント送)
正解:B
3-way handshake は SYN → SYN+ACK → ACK の 3 セグメント。これにより両端が 初期シーケンス番号を双方向に合意 します。FIN は接続解放(4-way close) のフラグなので、確立手順には登場しません。

問3. 高速再送(Fast Retransmit) のトリガとして正しいものを 1 つ選べ。

次の選択肢から最も適切なものを選択してください。
A. 再送タイマが満了したとき
B. 受信ウィンドウが 0 になったとき
C. RTT が一定の閾値を超えたとき
D. 同じ ACK 番号を 4 つ(重複 ACK が 3 つ) 受信したとき
正解:D
高速再送は 3 重 ACK(同じ ACK 番号を 4 つ続けて受信) をトリガに、タイマを待たずに該当セグメントだけを再送します。A は別系統の RTO 再送 のトリガで、こちらは(伝統的に) 以降全部を再送する重い動作です。B はゼロウィンドウでこちらは送信側が止まる別現象。C は再送には直結しません(ただし RTO の長さ計算には RTT を使います)。

問4. フロー制御と輻輳制御の関係として、最も適切なものを 1 つ選べ。

次の選択肢から最も適切なものを選択してください。
A. フロー制御は受信側のバッファを守るための rwnd、輻輳制御はネットワークを守るための cwnd を使い、実効送信量は min(rwnd, cwnd) で決まる
B. フロー制御と輻輳制御はどちらも cwnd を共用し、状況によって意味だけが切り替わる
C. フロー制御は IP 層の機能、輻輳制御は TCP 層の機能で、別の層で別々に動く
D. 受信側が rwnd と cwnd の両方をヘッダで通知する
正解:A
rwnd は受信側が通知する受信バッファの空きcwnd は送信側が内部で計算するネットワーク状況の見積もり。実際に送出を許される未確認バイト数は min(rwnd, cwnd) です。B は誤り(別変数)、C は誤り(両方とも TCP 層)、D は誤り(cwnd は送信側だけが持つ内部状態)。

問5. TCP Reno の輻輳制御に関する記述として、最も適切なものを 1 つ選べ。

次の選択肢から最も適切なものを選択してください。
A. slow start では cwnd を 1 RTT ごとに +1 MSS の線形増加で増やす
B. RTO 再送が起きても cwnd は変えず、すぐ送信を再開する
C. 3 重 ACK で高速再送した後、cwnd と ssthresh を半減させ、slow start には戻らず輻輳回避から再開する(高速回復)
D. AIMD は加算減少・乗算増加の略で、ロス時に cwnd を倍増させる
正解:C
Reno の特徴は fast recovery:3 重 ACK 後は slow start に戻さず、ssthresh と cwnd を半減して 輻輳回避から再開 します。A は逆(slow start は指数増加、輻輳回避が線形)。B は誤り(RTO は重い輻輳とみなし、cwnd=1 から slow start でやり直し)。D は 正しくは「加算増加・乗算減少」(AIMD = Additive Increase, Multiplicative Decrease)。
← PREV
第17回 ポート番号とソケット
NEXT →
第19回 UDP