# TCP の仕組み完全解説：3ウェイハンドシェイク・状態遷移・再送・輻輳制御を RFC 9293 で理解する

> TCP がどうやって信頼性を作るのかを、IETF の一次情報（RFC 9293・5681・6298）に忠実に解説する実装ガイド。3ウェイハンドシェイク、11状態の状態機械、シーケンス番号とACK・再送（RTO・高速再送）、フロー制御（ウィンドウ）、輻輳制御（スロースタート・CUBIC・BBR）を、図とコード・観測コマンドで本番のデバッグに使える形にまとめます。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: TCP/IP, TCP, ネットワーク, パフォーマンス, 可観測性, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/tcp-three-way-handshake-state-transition-retransmission-congestion-control-guide
- カテゴリ: TCP/IP・ネットワーク
- 総合ガイド: https://tomodahinata.com/blog/tcp-ip-protocol-suite-fundamentals-complete-guide

## 要点

- TCP の信頼性は『シーケンス番号・累積ACK・再送』で作られる。接続は SYN→SYN/ACK→ACK の3ウェイハンドシェイクで、初期シーケンス番号(ISN)を双方向に同期して確立する（RFC 9293）
- TCP には11の状態がある。SYN-SENT/SYN-RECEIVED で確立、FIN-WAIT/CLOSE-WAIT/LAST-ACK で解放、TIME-WAIT は能動クローズ側が 2×MSL 待つ。状態を読めれば障害切り分けは推測から観測になる
- 再送は2系統。タイマー満了の RTO（RFC 6298・RTT実測の srtt/rttvar から算出）と、重複ACK3回で待たずに送る高速再送。SACK（RFC 2018）で部分欠落を効率回復する
- フロー制御（受信ウィンドウ）と輻輳制御（ネットワーク混雑）は別物。RFC 5681 のスロースタートと輻輳回避(AIMD)が土台で、Linux 既定は CUBIC、帯域遅延積を攻める BBR も実用域
- TCP の弱点はヘッドオブラインブロッキングと接続確立RTT。これを克服するために生まれたのが QUIC/HTTP3。仕組みを知ると『なぜ速くならないか』を構造で説明できる

---

「TCP は信頼性がある」——よく聞く説明ですが、**「どうやって」信頼性を作っているのか**を説明できる人は多くありません。そして本番障害の現場では、まさにその「どうやって」を知っているかどうかが切り分けの速度を決めます。なぜ確立に時間がかかるのか。なぜパケットロスが起きるとスループットが急落するのか。`TIME-WAIT` は何のためにあるのか。`CLOSE-WAIT` が溜まるのはなぜ「自分のバグ」なのか。

この記事は、TCP の心臓部——**3ウェイハンドシェイク・11状態の状態機械・再送・フロー制御・輻輳制御**——を、IETF の一次情報に忠実に、かつ図と観測コマンドで「本番で使える」形に解説します。[TCP/IP 全体像の記事](/blog/tcp-ip-protocol-suite-fundamentals-complete-guide)で「TCP は信頼性・順序・フロー制御・輻輳制御を提供する」と述べた、その内部です。

> **この記事のルール**：規定は **TCP = [RFC 9293](https://www.rfc-editor.org/rfc/rfc9293.html)（2022年8月、RFC 793 ほかを廃止し RFC 1122 を更新）**、**輻輳制御 = [RFC 5681](https://www.rfc-editor.org/rfc/rfc5681)**、**再送タイマー(RTO) = [RFC 6298](https://www.rfc-editor.org/rfc/rfc6298)**、**SACK = [RFC 2018](https://www.rfc-editor.org/rfc/rfc2018)**、**ウィンドウスケール/タイムスタンプ = [RFC 7323](https://www.rfc-editor.org/rfc/rfc7323)** に基づきます。CUBIC・BBR など具体的な輻輳制御アルゴリズムは OS 実装依存です。RFC は改訂されるため、最新版を [rfc-editor.org](https://www.rfc-editor.org/) で確認してください。

---

## 1. TCPセグメントのヘッダ——信頼性の「部品」を先に押さえる

仕組みの前に、TCP が信頼性を作るための「道具」がヘッダに揃っていることを確認します。RFC 9293 が定める主要フィールドはこれです（最小20バイト）。

| フィールド | サイズ | 役割 |
| --- | --- | --- |
| 送信元ポート / 宛先ポート | 各16ビット | プロセスの多重化（5タプルの一部） |
| **シーケンス番号** | 32ビット | このセグメントの先頭バイトの通し番号。順序と欠落検知の核 |
| **確認応答番号(ACK番号)** | 32ビット | 「次に欲しいバイト番号」＝ここまでは受け取った（累積ACK） |
| データオフセット | 4ビット | ヘッダ長（オプションで可変なため必要） |
| **制御フラグ** | 各1ビット | **CWR, ECE, URG, ACK, PSH, RST, SYN, FIN** |
| **ウィンドウ** | 16ビット | 受信側が受け取れるバイト数＝フロー制御 |
| チェックサム | 16ビット | ヘッダ＋データ＋擬似ヘッダの完全性検査 |
| 緊急ポインタ | 16ビット | URG 有効時のみ |

制御フラグの意味を押さえると、以降の状態遷移がすべて読めます。

- **SYN**：接続を開く。シーケンス番号を同期（Synchronize）する。
- **ACK**：確認応答番号が有効。確立後の全セグメントで立つ。
- **FIN**：送信完了。「もう送るデータはない」を通知し、正常クローズを始める。
- **RST**：接続を即座に異常リセット。`ECONNRESET` の正体。
- **PSH**：バッファリングせず即アプリへ渡せ、の指示。
- **URG / 緊急ポインタ**：緊急データ。現代ではほぼ使われない。
- **ECE / CWR**：明示的輻輳通知（ECN, [RFC 3168](https://www.rfc-editor.org/rfc/rfc3168)）でパケットを落とさずに混雑を伝えるための拡張。

---

## 2. 3ウェイハンドシェイク——なぜ「3回」必要なのか

TCP は通信前に**コネクションを確立**します。RFC 9293 はこれを「初期シーケンス番号(ISN)を双方向で同期し、互いに確認し合う手続き」と定義します。

```text
クライアント                                サーバー
   │                                          │
   │   ① SYN  seq=x                           │   "開きたい。私の開始番号は x"
   │ ───────────────────────────────────────► │
   │                                          │
   │   ② SYN, ACK  seq=y, ack=x+1             │   "了解(x+1まで受領)。私の開始番号は y"
   │ ◄─────────────────────────────────────── │
   │                                          │
   │   ③ ACK  ack=y+1                          │   "了解(y+1まで受領)。確立完了"
   │ ───────────────────────────────────────► │
   │                                          │
  ESTABLISHED                             ESTABLISHED
```

**なぜ2回ではダメか**：信頼通信には**双方向**の合意が要ります。①でクライアント→サーバー方向のISN(x)を、②でサーバー→クライアント方向のISN(y)を伝え、それぞれに相手のACKが必要です。②はサーバーのSYNとクライアントSYNへのACKを兼ねるため、合計3回に圧縮されます。これが「3ウェイ」の理由です。

**ISN はなぜランダムか**：ISN を予測可能にすると、第三者が偽のセグメントを差し込む「シーケンス番号予測攻撃」が成立します。[RFC 6528](https://www.rfc-editor.org/rfc/rfc6528) は ISN を暗号ハッシュベースで生成し予測困難にすることを規定します。**セキュリティが手続きそのものに織り込まれている**好例です。

> **設計への示唆**：ハンドシェイクは**最低1往復(1 RTT)を必ず消費**します。さらに HTTPS なら TLS ハンドシェイクが上乗せされる。**遠距離・高RTT環境で新規接続を毎回張るのが致命的に遅い**のはこのためで、Keep-Alive とコネクションプールが効く根拠です。TCP Fast Open（[RFC 7413](https://www.rfc-editor.org/rfc/rfc7413)）はこの初回RTTを削る拡張ですが、適用範囲は限定的です。

---

## 3. 11状態の状態機械——TCP接続のライフサイクル

RFC 9293 は TCP 接続を **11の状態**で定義します：`LISTEN, SYN-SENT, SYN-RECEIVED, ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT` と、観念上の `CLOSED`。`ss` や `netstat` で見えるあの状態名の出典がこれです。

### 3.1 確立フェーズ

```text
            ┌──────────┐
            │  CLOSED  │
            └────┬─────┘
       passive   │   active open (connect)
       open      │   send SYN
   (listen)      ▼
   ┌────────┐  ┌──────────┐
   │ LISTEN │  │ SYN-SENT │
   └───┬────┘  └────┬─────┘
  recv SYN        recv SYN/ACK
  send SYN/ACK    send ACK
       ▼               │
 ┌───────────────┐     │
 │ SYN-RECEIVED  │     │
 └──────┬────────┘     │
   recv ACK            │
        └──────┬───────┘
               ▼
        ┌─────────────┐
        │ ESTABLISHED │  ← データ送受信はここ
        └─────────────┘
```

### 3.2 解放フェーズ（4ウェイ、そして TIME-WAIT）

クローズは送受信が独立に閉じるため、原則**4回**のやり取り（双方向のFIN/ACK）になります。

```text
能動クローズ側(close を呼んだ方)          受動クローズ側
   ESTABLISHED                              ESTABLISHED
       │  ① FIN ─────────────────────────────►│
   FIN-WAIT-1                                  │ recv FIN
       │◄───────────────────── ② ACK ─────────│ CLOSE-WAIT
   FIN-WAIT-2                                  │ (アプリが close するまで滞在)
       │◄───────────────────── ③ FIN ─────────│ LAST-ACK
       │  ④ ACK ─────────────────────────────►│
   TIME-WAIT                                CLOSED
   (2×MSL 待機)
       ▼
   CLOSED
```

ここから実務的な3つの教訓が出ます。

- **TIME-WAIT（能動クローズ側が 2×MSL 待つ）**：遅延して届いた古いセグメントが、同じ5タプルの新接続を汚染しないための安全装置。**正しい挙動**であって異常ではありません。ただし短命接続を大量に張ると溜まってポート枯渇を招く（→[全体像の記事](/blog/tcp-ip-protocol-suite-fundamentals-complete-guide)§5.1）。
- **CLOSE-WAIT の滞留＝ほぼ自分のバグ**：相手のFINを受けてACKは自動で返るが、`LAST-ACK` へ進むには**アプリが明示的に `close()` する**必要があります。これを呼び忘れると `CLOSE-WAIT` に居座り続け、FD（ファイルディスクリプタ）をリークします。
- **RST による中断**：4ウェイの正常クローズに対し、`RST` は問答無用で接続を破棄します。受信側は `ECONNRESET`。LBのアイドルタイムアウト切断、クラッシュ、キューあふれなどが原因。

---

## 4. 信頼性の作り方①：シーケンス番号・累積ACK・再送

### 4.1 累積ACK と再送の基本

TCP は送ったバイトに**シーケンス番号**を振り、受信側は「次に欲しいバイト番号」を**ACK番号**で返します（**累積ACK**＝それ未満は全部受け取った、の意味）。送信側は ACK が返らないデータを**再送**します。これが信頼性の根です。

再送の引き金は2系統あります。

### 4.2 RTO（再送タイムアウト）——時間で諦める

ACK が一定時間返らなければ再送します。その「一定時間」が **RTO** で、[RFC 6298](https://www.rfc-editor.org/rfc/rfc6298) は**実測 RTT から動的に算出**することを規定します。固定値ではありません。

```text
SRTT     = 平滑化した往復時間（RTT の移動平均）
RTTVAR   = RTT のばらつき（分散）
RTO      = SRTT + max(G, 4 × RTTVAR)      （G はクロック粒度）
```

ポイントは**RTOは保守的（やや長め）**で、かつ再送のたびに**指数的に伸びる（バックオフ）**こと。だから「パケットが1個落ちただけ」でも、RTO 起因の再送は体感で大きな遅延になりえます。これを避けるのが次の高速再送です。

### 4.3 高速再送（Fast Retransmit）——重複ACK 3回で待たずに送る

受信側は、順番が飛んだセグメント（例：1,2,_,4,5）を受け取ると、**「まだ3が欲しい」という同じACKを繰り返し**返します。送信側は**重複ACKを3回**受け取った時点で、RTO 満了を待たず**ただちに**該当セグメントを再送します。タイマー満了を待たない分、回復が速い。

### 4.4 SACK（選択的確認応答）——「どこが抜けたか」を正確に伝える

累積ACKだけだと「3が抜けた」ことは分かっても「4,5は届いている」ことを伝えられず、4,5まで無駄に再送しかねません。**SACK**（[RFC 2018](https://www.rfc-editor.org/rfc/rfc2018)）は「受信済みの飛び地」をACKに添えて伝え、**抜けた穴だけ**をピンポイント再送できるようにします。現代のスタックでは既定で有効です。

```bash
# SACK が有効か（Linux）
sysctl net.ipv4.tcp_sack          # = 1 なら有効
# 接続ごとの再送・RTT を覗く
ss -tin                            # rtt:, retrans:, cwnd: などが見える
```

---

## 5. 信頼性の作り方②：フロー制御と輻輳制御は「別物」

ここを混同すると性能問題の原因を見誤ります。**フロー制御は受信側を守り、輻輳制御はネットワークを守る**——目的が違います。

### 5.1 フロー制御（受信ウィンドウ）——「速い送信側 vs 遅い受信側」

受信側はヘッダの**ウィンドウ**フィールドで「いま受け取れるバイト数」を毎回通知します。送信側はこれを超えて送れません。受信アプリが読み出さずバッファが満ちると**ウィンドウが0**になり、送信は停止（ゼロウィンドウ）。受信が回復するとウィンドウ更新で再開します。

16ビットのウィンドウは最大64KBしか表せず、高速・高遅延の回線では足りません。これを拡張するのが**ウィンドウスケール**オプション（[RFC 7323](https://www.rfc-editor.org/rfc/rfc7323)）で、ハンドシェイク時に倍率を交換します。

### 5.2 輻輳制御——「ネットワークの混雑」から全体を守る

送信側は受信ウィンドウとは別に、**輻輳ウィンドウ(cwnd)**という「ネットワークがいま耐えられそうな量」の内部見積もりを持ちます。**実際に送れるのは min(受信ウィンドウ, cwnd)** です。RFC 5681 が定める土台はこうです。

```text
① スロースタート：cwnd を 1RTT ごとに倍増（指数的）。ssthresh に達するまで。
② 輻輳回避     ：ssthresh 超過後は 1RTT ごとに +1MSS（線形）。慎重に増やす。
③ ロス検知時   ：
   - 重複ACK(高速再送)→ ssthresh を半減し cwnd を絞る（高速回復）。
   - RTO 満了       → cwnd を 1 に戻しスロースタートからやり直す（重いペナルティ）。
```

この **AIMD（加算増加・乗算減少）** が「パケットロスでスループットが急落する」現象の正体です。**だからロスは性能の天敵**で、わずかなロス率でも遠距離の太い回線では帯域を使い切れません。

### 5.3 具体アルゴリズムは OS 実装依存——CUBIC と BBR

RFC 5681 は枠組みで、実際の増減カーブはアルゴリズムが決めます。本番で出会う2つ：

- **CUBIC**：Linux の既定。cwnd を3次関数で増やし、高帯域・高遅延でも素早く回復。**ロスを混雑のシグナルとする**従来型の改良版。
- **BBR**（Google）：ロスではなく**実測の帯域とRTT**から最適送信レートを推定する。バッファブロート（過大なバッファによる遅延）に強く、ロスの多い無線・長距離で有利なことがある。

```bash
# 利用可能/現在の輻輳制御アルゴリズム（Linux）
sysctl net.ipv4.tcp_available_congestion_control
sysctl net.ipv4.tcp_congestion_control     # 既定はたいてい cubic
```

> **設計への示唆**：「サーバーを増やしたのにスループットが伸びない」とき、原因がアプリではなく**輻輳制御 × ロス × RTT**ということは珍しくありません。`ss -tin` の `retrans` と `rtt` を見て、ロス率が高ければ経路（無線/トンネル/過負荷なミドルボックス）を、RTTが大きければ接続の張り直し（プール化）やリージョン配置を疑います。

---

## 6. コードで観る：半開（ハーフオープン）接続と Keep-Alive

状態機械の知識が**コードのバグ**に直結する例を示します。一方の端が**クラッシュやNAT/LBのアイドル切断**で消えると、もう一方は接続が死んだことを**何も送らない限り気づけません**（TCPはアイドル時に無通信だから）。これが**ハーフオープン**です。気づかないと、死んだ接続をプールから掴んで `ECONNRESET` を踏みます。

```ts
import net from "node:net";

/** 死活を能動検知し、アイドルも区別して安全に畳む TCP クライアント設定 */
export function createResilientSocket(host: string, port: number): net.Socket {
  const socket = net.createConnection({ host, port });

  // ① TCP Keep-Alive：アイドル接続にプローブを送り、相手が消えていれば検知する
  //    （ハーフオープンの主防御。OS が定期的に空セグメントで生存確認）
  socket.setKeepAlive(true, 30_000); // 30秒アイドルでプローブ開始

  // ② アプリ層のアイドルタイムアウト：一定時間データが無ければ自分から畳む
  socket.setTimeout(60_000, () => {
    socket.destroy(new Error("idle timeout: no data for 60s"));
  });

  // ③ RST/異常を握りつぶさず観測（ECONNRESET の可視化）
  socket.on("error", (err) => {
    console.error(`[tcp] ${host}:${port} ${(err as NodeJS.ErrnoException).code ?? ""} ${err.message}`);
  });

  return socket;
}
```

**Keep-Alive(OSのTCPプローブ)とアプリのタイムアウトは別物**です。前者は「接続が物理的に生きているか」、後者は「論理的に応答があるか」を見ます。本番では両方を、上流のLBアイドルタイムアウトより**短く**設定するのが安全です（LBが先に切ると、こちらが死んだ接続を掴む）。

---

## 7. TCP の限界——だから QUIC/HTTP3 が生まれた

最後に、TCP の構造的な弱点を2つ挙げます。これを知ると次の技術選定が見通せます。

1. **ヘッドオブラインブロッキング（HoLB）**：TCP は「順序通りの単一バイトストリーム」なので、1つのセグメントが失われると、**その後ろの（無関係な）データもアプリへ渡せず待たされる**。HTTP/2 が1本のTCP上で多重化しても、TCP層のロスで全ストリームが止まるのはこのため。
2. **接続確立RTTの固定費**：3ウェイ＋TLSで毎回複数RTT。高遅延ほど効く。

**QUIC**（[RFC 9000](https://www.rfc-editor.org/rfc/rfc9000)、2021年）は、UDP の上に「ストリーム独立の信頼性＋暗号化＋輻輳制御」を再実装し、ストリームごとに独立して回復することで HoLB を解消し、接続確立も0〜1RTTに短縮しました。**HTTP/3**（[RFC 9114](https://www.rfc-editor.org/rfc/rfc9114)、2022年）はこの QUIC を土台にします。**TCP を捨てたのではなく、TCP の制約を回避するために Transport 層ごと作り直した**——これが [TCP/IP 全体像](/blog/tcp-ip-protocol-suite-fundamentals-complete-guide)で触れた「層の差し替え」の最大の実例です。

TCP と UDP（そして QUIC）の**使い分けの判断軸**は、本クラスタの[『TCP と UDP の違いと使い分け』](/blog/tcp-vs-udp-quic-http3-difference-when-to-use-guide)で具体化します。

---

## 8. まとめ

- **確立**：SYN→SYN/ACK→ACK の3ウェイで ISN を双方向同期。ISN はセキュリティのためランダム（RFC 6528）。1RTTの固定費が必ずかかる。
- **状態機械**：11状態。`ss` で読めば障害切り分けは観測になる。`CLOSE-WAIT` 滞留＝自分の `close()` 漏れ、`TIME-WAIT` は能動クローズ側の正常な安全装置。
- **再送**：RTO（RTT実測から算出・指数バックオフ）と高速再送（重複ACK3回）。SACK で穴だけ回復。
- **2つのウィンドウ**：フロー制御（受信ウィンドウ＝受信側を守る）と輻輳制御（cwnd＝ネットワークを守る）は別物。AIMD ゆえロスでスループットが急落する。アルゴリズムは CUBIC（既定）/BBR。
- **限界**：HoLB と確立RTT。これを克服したのが QUIC/HTTP3。

TCP の仕組みは、`ss -tin` の数字を「読める」ようになって初めて武器になります。次は、では実際の案件で TCP と UDP のどちらを選ぶのか——その判断軸を扱います。

---

私（友田 陽大）は、決済・リアルタイム系のバックエンドで、TCP の状態遷移・再送・タイムアウト設計に踏み込んだ信頼性設計を手がけています。「スループットが頭打ち」「`ECONNRESET`/`CLOSE-WAIT` が増える」「再送が多くレイテンシが不安定」——こうした症状を、`ss`/`tcpdump` での観測から原因を特定し、プール化・タイムアウトバジェット・冪等性で根治します。お気軽にご相談ください。
