# TCP/IP 完全ガイド：4階層モデル・IP・TCP・UDP の仕組みを RFC と実コードで本番設計に変える

> TCP/IP を本番の設計に使える形で解説する実装ガイド。IETF の一次情報（RFC 1122・791・8200・9293・768）に忠実に、4階層モデルとカプセル化、IPアドレッシングとCIDR、TCPとUDPの違い、Node.js/TypeScript の実コード、TIME_WAIT・Keep-Alive・MTU など本番で効く挙動、観測・デバッグまでを、公式ドキュメントより分かりやすく体系化します。

- 公開日: 2026-06-28
- 著者: 友田 陽大
- タグ: TCP/IP, ネットワーク, TCP, UDP, アーキテクチャ設計, 可観測性
- URL: https://tomodahinata.com/blog/tcp-ip-protocol-suite-fundamentals-complete-guide
- カテゴリ: TCP/IP・ネットワーク

## 要点

- 「TCP/IP」とは単一の規格ではなく、インターネットを動かすプロトコル群の総称。IETF は RFC 1122 で Link/Internet/Transport/Application の4階層として定義する
- IP（RFC 791／IPv6 は RFC 8200）はベストエフォート——到達も順序も重複排除も保証しない。その上で TCP（RFC 9293）が信頼性・順序・フロー制御・輻輳制御を、UDP（RFC 768）が最小限の多重化だけを足す
- TCP は再送（at-least-once）で信頼性を作る。つまりアプリには重複が届きうる。決済のような領域では、TCPの信頼性に頼り切らず冪等性で exactly-once を作るのがプロの設計
- 本番の体感品質は TIME_WAIT・Keep-Alive・Nagle/遅延ACK・コネクションプール・MTU/MSS・backlog といった『RFCの先の運用知識』で決まる。コード例で具体化する
- 障害解析は ss / tcpdump / ip で TCP 状態遷移を読む。Three-way handshake と11状態の状態機械を知っていれば、『繋がらない』を勘ではなく事実で切り分けられる

---

「ネットワークは詳しい人に任せればいい」——そう思っていられるのは、本番が順調なうちだけです。`ECONNRESET` がログに溢れる。デプロイ直後だけ接続が詰まる。L7ロードバランサ越しのAPIが30秒でタイムアウトする。決済リクエストが「成功したのか失敗したのか分からない」状態で返ってくる。**これらはすべて、アプリのバグではなく TCP/IP の挙動そのものです。** 仕組みを知らないと、ログは「呪文」のままで、対症療法のリトライを足すたびに状況が悪化します。

この記事は、TCP/IP を「資格試験の暗記」ではなく **本番の設計判断に使える知識**へ変えるための実装ガイドです。各レイヤーが何を保証し、何を保証しないのかを IETF の一次情報（RFC）で押さえ、Node.js/TypeScript の実コードで動かし、そして「公式ドキュメントには書かれていないが本番で必ず効く運用知識」（TIME_WAIT・Keep-Alive・MTU・コネクションプール）までを地続きでつなぎます。題材には、私が決済信頼性レイヤーを設計・主導した[サーバーレス決済プラットフォーム](/case-studies/payment-platform-reliability)（モバイル回線のタイムアウトと再送を前提に、冪等性で**本番二重課金0件**を達成）での判断も交えます。

> **この記事のルール**：プロトコルの規定は **IETF の一次情報（RFC）** に基づきます。中核は **TCP = [RFC 9293](https://www.rfc-editor.org/rfc/rfc9293.html)（2022年8月・RFC 793 ほかを廃止）**、**UDP = [RFC 768](https://www.rfc-editor.org/rfc/rfc768.txt)（1980年）**、**IPv4 = [RFC 791](https://www.rfc-editor.org/rfc/rfc791)**、**IPv6 = [RFC 8200](https://www.rfc-editor.org/rfc/rfc8200)**、**ホスト要件と階層 = [RFC 1122](https://www.rfc-editor.org/rfc/rfc1122)** です。RFC は改訂・廃止されるため、本番設計の前に必ず最新版を [rfc-editor.org](https://www.rfc-editor.org/) で確認してください。コードは Node.js の標準ライブラリ（`net`・`dgram`）で動く形に整えていますが、**ポート・ホスト・タイムアウト値は環境変数前提**で、本番では必ず観測値に基づき調整してください。

---

## 0. まず立ち位置：「TCP/IP」は1つの規格ではない

設計の前に、用語の正体を3行で押さえます。

- **TCP/IP は「プロトコル群（suite）」の総称**です。2つの代表プロトコル（TCP と IP）の名を借りた呼び名で、実体は IP・TCP・UDP・ICMP・ARP・DNS など多数のプロトコルの集合です。
- **IETF はこれを「階層」として定義します。** [RFC 1122](https://www.rfc-editor.org/rfc/rfc1122)（Host Requirements）は、インターネット通信を **Application / Transport / Internet / Link** の4層に分けて規定しています。学校で習う OSI 参照モデル（7層）は概念整理用で、実装の正典はこちらです。
- **各層は「下の層を信頼しない」前提で積まれています。** IP は届くことを保証しない。だから TCP がその上で信頼性を作る。この「保証の境界」を理解することが、TCP/IP を学ぶ唯一にして最大の勘所です。

この記事は下から上へ——Internet層（IP）→ Transport層（TCP/UDP）→ アプリでの実装と運用——の順で、各層が「何を保証し、何を保証しないか」を軸に進めます。

---

## 1. 4階層モデルとカプセル化——データはどう包まれて飛ぶか

### 1.1 4層モデルと OSI の対応

| RFC 1122 の階層 | 役割（何を保証するか） | 代表プロトコル | アドレス単位 | データの呼称 |
| --- | --- | --- | --- | --- |
| Application | アプリ間の意味（HTTP, gRPC など） | HTTP, DNS, TLS, SMTP | — | メッセージ |
| Transport | プロセス間の多重化・（TCPは）信頼性 | **TCP, UDP** | ポート番号 | セグメント / データグラム |
| Internet | ホスト間のエンドツーエンド配送 | **IP, ICMP** | IPアドレス | パケット |
| Link | 同一リンク内のノード間転送 | Ethernet, Wi-Fi, ARP | MACアドレス | フレーム |

OSI 7層との対応はおおよそ次の通りです。OSIの「セッション層・プレゼンテーション層」に相当する関心事（TLS・文字コード・圧縮）は、TCP/IPでは Application 層がまとめて担います。

```text
OSI 7層            TCP/IP 4層 (RFC 1122)
Application  ┐
Presentation ├──►  Application   (HTTP, TLS, DNS, gRPC)
Session      ┘
Transport    ───►  Transport     (TCP, UDP)
Network      ───►  Internet      (IP, ICMP)
Data Link    ┐
Physical     ┴──►  Link          (Ethernet, Wi-Fi)
```

### 1.2 カプセル化（encapsulation）——「封筒の入れ子」

送信時、上位層のデータは下位層のヘッダで順に包まれます。これが**カプセル化**です。`GET /` という HTTP リクエストが物理的に飛ぶまでに、こう包まれます。

```text
[ Ethernet ヘッダ [ IP ヘッダ [ TCP ヘッダ [ HTTP データ ] ] ] FCS ]
   └ Link 層       └ Internet 層 └ Transport 層 └ Application 層
```

受信側は逆順に開封（**逆カプセル化**）し、各層のヘッダを剥がして上位へ渡します。**重要なのは、各層が「自分のヘッダ」しか見ない**こと。IP層はTCPの中身を知らないし、TCP層はHTTPの意味を知りません。この**関心の分離**が、TCP/IPが半世紀スケールしてきた設計の核です（SRP がプロトコル設計に効いている好例です）。

> **設計への示唆**：この入れ子は、各層が独立に進化できることを意味します。HTTP/1.1 → HTTP/2 はTCPを変えずに実現でき、HTTP/3 は逆にTransport層をTCPからQUIC（UDPベース）へ差し替えました。「どの層を差し替えると何が変わるか」を意識すると、技術選定の解像度が上がります。

---

## 2. Internet 層：IP は「ベストエフォート」——届くことを保証しない

### 2.1 IP が保証すること・しないこと

[RFC 791](https://www.rfc-editor.org/rfc/rfc791)（IPv4）と [RFC 8200](https://www.rfc-editor.org/rfc/rfc8200)（IPv6）が定める IP の役割は、**「宛先IPアドレスへパケットを届けようと最善を尽くす」**こと、ただそれだけです。具体的には次を **保証しません**。

- **到達保証なし**：経路の途中で破棄されうる（輻輳・TTL切れ・ルーティング障害）。
- **順序保証なし**：別経路を通って順番が入れ替わりうる。
- **重複排除なし**：同じパケットが複数届きうる。
- **完全性の保証は限定的**：IPv4ヘッダにはチェックサムがあるが**ヘッダのみ**対象。ペイロードの完全性はTrasport層（TCP/UDPのチェックサム）に委ねる。IPv6はヘッダチェックサム自体を廃止した。

つまり IP は「投函はするが配達は保証しない郵便」です。この割り切りこそが、上位の TCP に「信頼性をどこで作るか」という設計責務を生みます。

### 2.2 IPアドレスとCIDR——本番で必ず使う知識に絞る

IPv4 は32ビット（例 `192.0.2.10`）、IPv6 は128ビット（例 `2001:db8::1`）。本番のインフラ設計で必須なのは次の3点です。

**(1) CIDR 表記**（[RFC 4632](https://www.rfc-editor.org/rfc/rfc4632)）：`10.0.0.0/16` の `/16` は「上位16ビットがネットワーク部」を意味します。VPCのサブネット設計はこれを読めないと始まりません。

```text
10.0.0.0/16   → 10.0.0.0 〜 10.0.255.255   （65,536 アドレス、ホスト 65,534）
10.0.1.0/24   → 10.0.1.0 〜 10.0.1.255     （256 アドレス、ホスト 254）
              ※ 各サブネットでネットワークアドレスとブロードキャストの2つは予約
```

**(2) プライベートアドレス**（[RFC 1918](https://www.rfc-editor.org/rfc/rfc1918)）：インターネットに出ない閉域用の予約レンジ。VPC・社内網はここから切ります。

```text
10.0.0.0/8        （10.x.x.x）            最大規模
172.16.0.0/12     （172.16〜172.31.x.x）  中規模・Docker のデフォルト
192.168.0.0/16    （192.168.x.x）         家庭・小規模
```

**(3) MTU と MSS**：1つのリンクで運べる最大ペイロード（MTU）はEthernetで通常 **1500バイト**。ここからIP/TCPヘッダを引いた **MSS（Maximum Segment Size）**が、TCPが1セグメントで送れる上限です（標準的に 1460 バイト）。**VPN・トンネル・クラウド間でMTUが縮むと、大きなパケットが黙って落ちて「特定サイズの通信だけ刺さる」という悪夢の障害**になります。Path MTU Discovery（[RFC 8201](https://www.rfc-editor.org/rfc/rfc8201)）が効かない経路では、ここを疑うと当たります。

### 2.3 IPv4 ヘッダの読みどころ

全フィールドの暗記は不要です。障害解析で実際に見るのは次だけです。

- **TTL（Time To Live）**：ルータを1つ越えるごとに1減り、0で破棄。ループ防止であり、`traceroute` の原理でもある。
- **Protocol**：上位プロトコルの種別。**6 = TCP**、**17 = UDP**、**1 = ICMP**。`tcpdump` でこの値を見れば中身の見当がつく。
- **Source / Destination Address**：送信元・宛先のIP。NAT越えで送信元が書き換わる点に注意。

---

## 3. Transport 層：TCP と UDP——「信頼性」をどこで作るか

Internet層が「ベストエフォート」だと決めた以上、**信頼性が必要なら誰かが作らねばなりません**。その責務をフルセットで引き受けるのが TCP、あえて引き受けず最小限に留めるのが UDP です。

### 3.1 ポート番号——1台のホストで複数プロセスを多重化する

IPアドレスが「どのホストか」を、**ポート番号（16ビット, 0〜65535）**が「そのホストのどのプロセスか」を指します。`(送信元IP, 送信元ポート, 宛先IP, 宛先ポート, プロトコル)` の**5タプル**で1本の通信が一意に識別されます。

- **Well-known ports（0〜1023）**：HTTP=80, HTTPS=443, SSH=22, DNS=53 など。
- **Ephemeral ports（49152〜65535、IANA推奨）**：クライアントが接続のたびに動的に使う一時ポート。**この枯渇が、後述する TIME_WAIT 問題の本質**です。

### 3.2 TCP（RFC 9293）が提供する4つの保証

[RFC 9293](https://www.rfc-editor.org/rfc/rfc9293.html) によれば、TCP は **コネクション指向の信頼ストリーム**を提供します。具体的には次の4つです。

1. **信頼性（reliability）**：シーケンス番号とACK・**再送**で、失われたデータを回復する。
2. **順序保証（ordering）**：受信側はシーケンス番号で並べ直し、アプリには順序通りに渡す。
3. **フロー制御（flow control）**：受信側の **Window フィールド**（受け取れるバイト数）で、速い送信側が遅い受信側を溢れさせないよう制御する。
4. **輻輳制御（congestion control）**：ネットワークの混雑を検知して送信速度を自律的に絞る（[RFC 5681](https://www.rfc-editor.org/rfc/rfc5681) のスロースタート・輻輳回避）。RFC 9293 は輻輳制御の実装を**必須**としています。

これらの代償は「接続確立のための往復（3-way handshake）」「状態の保持」「ヘッドオブラインブロッキング」です。**仕組みの詳細（ハンドシェイク・11状態の状態機械・再送・輻輳制御）は、専用記事で深掘りします**（本クラスタの『TCP の仕組み完全解説』）。

### 3.3 UDP（RFC 768）——あえて何もしない潔さ

[RFC 768](https://www.rfc-editor.org/rfc/rfc768.txt) の UDP ヘッダはわずか **8バイト**（送信元ポート・宛先ポート・長さ・チェックサム）。仕様は明確に「**delivery and duplicate protection are not guaranteed（配送と重複排除は保証しない）**」と述べます。接続確立もなく、いきなりデータグラムを投げます。

この「潔さ」が武器になる領域があります——リアルタイム音声・映像、DNS、ゲーム、そして QUIC（HTTP/3 の土台）。**TCP と UDP のどちらを選ぶかは、本クラスタの『TCP と UDP の違いと使い分け』で、QUIC/HTTP/3 まで含めて判断軸を示します。**

---

## 4. 実コード：Node.js で TCP / UDP を「触って」理解する

理屈は手を動かすと定着します。Node.js 標準ライブラリだけで、TCP と UDP の差を体感します。型は TypeScript で固めます。

### 4.1 TCP エコーサーバー（`net` モジュール）

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

const PORT = Number(process.env.TCP_PORT ?? 9000);

const server = net.createServer((socket: net.Socket) => {
  // 5タプルでこの接続を識別できる
  const peer = `${socket.remoteAddress}:${socket.remotePort}`;
  console.log(`[open] ${peer}`);

  // TCP はバイトストリーム。データは「メッセージ単位」では届かない点に注意（後述）
  socket.on("data", (chunk: Buffer) => {
    console.log(`[recv] ${peer} ${chunk.length} bytes`);
    socket.write(chunk); // そのまま返す（echo）
  });

  socket.on("end", () => console.log(`[end]  ${peer}`)); // 相手が FIN を送った
  socket.on("error", (err) => console.error(`[err]  ${peer} ${err.message}`)); // RST 等
});

server.listen(PORT, () => console.log(`TCP echo listening on :${PORT}`));
```

```ts
// クライアント
import net from "node:net";

const socket = net.createConnection(
  { host: "127.0.0.1", port: Number(process.env.TCP_PORT ?? 9000) },
  () => socket.write("hello tcp"), // 接続確立(3-way handshake 完了)後に送信
);

socket.on("data", (data: Buffer) => {
  console.log("echo:", data.toString());
  socket.end(); // FIN を送って正常クローズ
});
socket.setTimeout(5_000, () => socket.destroy(new Error("idle timeout")));
```

### 4.2 致命的な落とし穴：「TCP はメッセージの境界を保たない」

上のサーバーで、クライアントが `socket.write("AB")` と `socket.write("CD")` を連続で呼んでも、サーバーの `data` イベントは **`"ABCD"` が1回**で来るかもしれないし、**`"A"` と `"BCD"` の2回**かもしれません。**TCP はバイトストリームであり、`write` 1回が `data` 1回に対応する保証はない**——これは初学者が必ず踏むバグです。

だから**アプリ層で「フレーミング（区切り）」を自分で実装する**必要があります。代表は「長さプレフィックス」方式です。

```ts
// 4バイトのビッグエンディアン長 + 本体、というフレームを復元する
class LengthPrefixedDecoder {
  private buf = Buffer.alloc(0);

  /** チャンクを push し、完成したメッセージだけを配列で返す（総関数：未完成なら []） */
  push(chunk: Buffer): Buffer[] {
    this.buf = Buffer.concat([this.buf, chunk]);
    const out: Buffer[] = [];
    while (this.buf.length >= 4) {
      const len = this.buf.readUInt32BE(0);
      if (this.buf.length < 4 + len) break; // まだ本体が揃っていない
      out.push(this.buf.subarray(4, 4 + len));
      this.buf = this.buf.subarray(4 + len); // 消費した分を捨てる
    }
    return out;
  }
}
```

> HTTPやgRPCが「メッセージ指向」に見えるのは、こうしたフレーミング（HTTPなら`Content-Length`やchunked、gRPCなら5バイトのプレフィックス）を**上位で実装しているから**です。生のTCPを使うときは、この層を自分で用意する必要があると肝に銘じてください。

### 4.3 UDP（`dgram` モジュール）——境界は保たれるが、届く保証はない

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

const PORT = Number(process.env.UDP_PORT ?? 9001);
const server = dgram.createSocket("udp4");

// UDP は「1 send = 1 メッセージ」。境界は保たれる。ただし順序も到達も保証されない
server.on("message", (msg: Buffer, rinfo) => {
  console.log(`[recv] ${rinfo.address}:${rinfo.port} "${msg}"`);
  server.send(msg, rinfo.port, rinfo.address); // エコー（届かなくても誰も気づかない）
});
server.bind(PORT, () => console.log(`UDP listening on :${PORT}`));
```

**TCPとUDPの本質的な差がコードに出ています**：TCPは「接続」を張ってバイトを流すが境界を保たない。UDPは「接続」なしでメッセージ単位に投げるが届く保証がない。**この非対称性が、すべての設計判断の根です。**

---

## 5. 本番で効くTCPの挙動——RFCの先にある「運用知識」

ここからが、検定試験には出ないが**本番で必ず効く**領域です。私が決済基盤で実際に踏み、設計で対処した論点を中心に挙げます。

### 5.1 TIME_WAIT——「接続を閉じたのに繋がらない」の正体

TCP接続を**能動的に閉じた側**（多くはクライアント、あるいはL7プロキシ）は、接続を閉じた後 **TIME_WAIT 状態**に **2×MSL（Maximum Segment Lifetime、典型的に合計60秒前後）**留まります。これは「遅延して届いた古いパケットが、同じ5タプルの新しい接続を汚染しないため」の安全装置で、RFC 9293 が規定する正しい挙動です。

問題は**高頻度で短命な接続を大量に張る**ケース（例：プロキシが上流APIへ毎回新規接続）。TIME_WAIT が溜まると **Ephemeral ポートが枯渇**し、`EADDRNOTAVAIL` で新規接続が張れなくなります。対策は次の優先順位です。

1. **接続を使い回す（最重要）**：Keep-Alive とコネクションプール。そもそも閉じなければ TIME_WAIT は生まれない。
2. カーネルパラメータの調整（`net.ipv4.tcp_tw_reuse` 等）は**症状緩和の最終手段**。アプリ設計で接続を減らすのが先。

### 5.2 Keep-Alive とコネクションプール——「閉じない」が正義

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

const socket = net.createConnection({ host, port });

// TCP Keep-Alive：アイドル接続が生きているかを定期的に確認（死活監視）
socket.setKeepAlive(true, 30_000); // 30秒アイドルで keepalive プローブ開始

// Nagle アルゴリズムを無効化：小さなパケットを即送る（後述）
socket.setNoDelay(true);
```

HTTPクライアントなら、**コネクションプールを必ず使う**こと。Node.js の `undici`（`fetch` の実体）や AWS SDK v3 は既定でプールします。プールサイズと**アイドルタイムアウトを上流のタイムアウトより短く**設定するのが鉄則です（上流が先に切ると、こちらが死んだ接続を掴んで `ECONNRESET` を踏む）。

### 5.3 Nagle アルゴリズムと遅延ACK——相性最悪の組み合わせ

- **Nagle アルゴリズム**：小さなデータを溜めてまとめて送る（帯域の節約）。
- **遅延ACK**：ACKを少し遅らせてまとめる（同上）。

この2つが噛み合うと、**「送信側はACK待ち、受信側はデータ待ち」**で最大数百msのデッドロック的遅延が発生します。**対話的な小さなRPCを低レイテンシで流したいなら `setNoDelay(true)`（`TCP_NODELAY`）でNagleを切る**のが定石です。一方、スループット重視のバルク転送では切らない方が良いこともある——「常に切る」ではなく**ワークロードで判断**します。

### 5.4 backlog と SYN——「デプロイ直後だけ詰まる」

`listen(backlog)` の `backlog` は、**まだ accept されていない確立済み接続を保持するキュー長**です。これが小さいと、起動直後やスパイク時に接続が**黙って落ちる/遅延する**。Node.js の既定（511）で足りないことは稀ですが、**フロントにいるリバースプロキシやLBの backlog/接続数上限**と合わせて設計する必要があります。

### 5.5 タイムアウトは「3種類」を区別する

「タイムアウト」を一括りにすると設計を誤ります。最低限この3つを分けます。

| 種別 | 何を待つ時間か | 短すぎると | 長すぎると |
| --- | --- | --- | --- |
| 接続タイムアウト | 3-way handshake の完了 | 健全な接続も切る | 死んだ宛先を長く掴む |
| アイドル/受信タイムアウト | データが来ない時間 | 正常な低速応答を切る | ハングを検知できない |
| 全体（リクエスト）タイムアウト | 1リクエストの総時間 | リトライ嵐を誘発 | リソースを長く占有 |

決済基盤では、これらを上流から末端まで**「外側ほど長く、内側ほど短く」**の階段状に設計しました（タイムアウトバジェット）。内側が外側より長いと、外側が諦めた後も内側が走り続け、**リソースを食い潰しながら誰も待っていない仕事をする**最悪の状態になります。

---

## 6. 観測とデバッグ——「繋がらない」を事実で切り分ける

TCPの状態遷移を読めれば、障害の切り分けは推測から観測に変わります。Linux での実務ツールを挙げます。

```bash
# 1) いまの TCP 接続と状態を一覧（ss は netstat の後継・高速）
ss -tan
#   State      Recv-Q Send-Q   Local Address:Port   Peer Address:Port
#   ESTAB      0      0        10.0.1.5:443         10.0.2.9:51324
#   TIME-WAIT  0      0        10.0.1.5:51200       10.0.3.4:5432   ← 大量なら 5.1 を疑う
#   SYN-SENT   0      1        10.0.1.5:40012       10.0.9.9:80     ← 相手が SYN-ACK を返していない

# 2) 状態ごとに件数を集計（TIME-WAIT 肥大の検知に有効）
ss -tan | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn

# 3) パケットを直接見る（handshake が成立しているか）
sudo tcpdump -ni eth0 'tcp port 443 and (tcp[tcpflags] & (tcp-syn|tcp-rst) != 0)'

# 4) 経路途中のどこで詰まるか（TTL を 1 ずつ増やして応答元を見る）
traceroute -T -p 443 api.example.com   # -T で TCP SYN を使う
```

**状態から原因を引く早見表**：

- `SYN-SENT` が滞留 → 宛先までSYNは出たが**SYN-ACKが返らない**。FW/SG/ルーティング/宛先プロセス停止を疑う。
- `SYN-RECV` が大量 → **SYN フラッド**か backlog 枯渇。SYN Cookies の確認。
- `TIME-WAIT` が大量 → 短命接続の作りすぎ（§5.1）。Keep-Alive 化。
- `CLOSE-WAIT` が滞留 → **アプリが `close()` を呼んでいない**（こちらのコードのバグ）。リソースリークの典型。
- `ESTAB` のまま無反応・`Recv-Q` 増大 → アプリがデータを読んでいない（処理が詰まっている）。

> `CLOSE-WAIT` の滞留だけは**ほぼ確実に自分のコードのバグ**（FIN を受けたのにソケットを閉じていない）です。他は相手やネットワークの可能性がありますが、ここはアプリを疑ってください。

---

## 7. 信頼性の設計：TCP の「信頼性」に頼り切らない

最後に、最も実務的な教訓です。**TCP は信頼性を「再送」で作ります。** これは裏を返せば、**ネットワークから見れば配送は at-least-once（少なくとも1回）**だということです。さらにその上の世界——ロードバランサ、APIゲートウェイ、モバイル回線、Lambda のリトライ——では、**同じリクエストが複数回到達するのは異常ではなく日常**です。

[決済プラットフォーム](/case-studies/payment-platform-reliability)で私が設計した中核がここでした。課題はまさに「モバイル回線のタイムアウトや API Gateway／Lambda の再試行により、同一の決済リクエストが複数回到達する」こと。これに対し、

- **リトライ自体は正常系として受け入れる**（ネットワークは必ず再送する、と認める）。
- そのうえで**課金は1回だけに収束させる**——クライアント発行の**冪等性キー**を条件付き書き込み（`attribute_not_exists`）で1度きりに制約し、DynamoDB の原子的トランザクションで残高更新をアトミックにする。

結果として、TCP より上のすべての層で再送が起きても、**アプリの意味としては exactly-once**になり、**本番二重課金0件**を達成しました。

教訓を一般化すると：

> **TCP の信頼性は「同一接続内のバイトの欠落」を直すだけ。接続が切れて張り直す・上位がリトライする、という現実の重複は直してくれない。だから決済・在庫・メッセージングのような領域では、信頼性をネットワークに丸投げせず、アプリ層で冪等性として設計する。**

これは TCP/IP の知識が、低レイヤの教養で終わらず、**本番の金銭的正しさに直結する**好例です。

---

## 8. まとめ：TCP/IP を「設計判断」に変えるチェックリスト

- [ ] **保証の境界を言える**：IP はベストエフォート（到達・順序・重複排除を保証しない）。信頼性は TCP が、最小限の多重化だけは UDP が足す。
- [ ] **カプセル化の入れ子**を説明できる（各層は自分のヘッダしか見ない＝関心の分離）。
- [ ] **CIDR / RFC 1918 / MTU・MSS** を読める（VPC設計とMTU起因障害の切り分け）。
- [ ] **TCP はバイトストリーム**——`write` と `data` は1対1でない。生TCPではフレーミングを自前で実装する。
- [ ] **TIME_WAIT・Keep-Alive・Nagle・backlog・タイムアウト3種**を運用観点で設計している。
- [ ] **`ss` で TCP 状態を読み**、`SYN-SENT`/`CLOSE-WAIT`/`TIME-WAIT` から原因を引ける。
- [ ] **信頼性をネットワークに丸投げせず**、重複前提でアプリを冪等に設計している。

TCP/IP は「暗記する古典」ではなく、**本番のレイテンシ・コスト・正しさを左右する現役の設計知識**です。次は、その心臓部である TCP の状態機械・再送・輻輳制御を、RFC 9293 に沿って深掘りします。

---

私（友田 陽大）は、一人 × 生成AI（Claude Code）で、低レイヤの挙動まで踏まえた**落ちない・追える・正しい**バックエンドを設計・実装しています。「`ECONNRESET` が止まらない」「TIME_WAIT でポートが枯れる」「タイムアウトとリトライの設計が分からない」「決済・在庫で二重処理が怖い」——こうしたネットワーク／信頼性起因の課題は、現象の再現と観測から原因を特定し、冪等性と適切なタイムアウト設計で根治します。お気軽にご相談ください。
