# 第 16 章 TCP 连接管理


> 连接是 TCP 造出来的抽象：握手造它，挥手拆它，而线上绝大多数"连接建立失败/端口耗尽"故障，都落在这两个过程的状态与队列里。

## 学习目标

读完本章，你应当能够：

1. 逐字段说明 TCP（Transmission Control Protocol，传输控制协议）报文头每个字段被哪个机制使用；
2. 从零推导"为什么三次握手不能压缩成两次"，说清历史 SYN 造成"幽灵连接"的完整场景；
3. 区分 SYN 队列（半连接队列）与 accept 队列（全连接队列），用 `ss -lnt` 定位连接建立失败发生在哪一步；
4. 说明 `tcp_syncookies` 的原理与代价；给出 `TIME_WAIT` 必须存在且必须为 `2MSL` 的两条理由，并评估各种"减少 TIME_WAIT"手段的边界；
5. 画出 TCP 11 状态机全图，填出"某状态收到某标志位"的反应表。

### 前置知识检查

| 你需要知道 | 在哪一节补齐 |
|---|---|
| MTU/MSS 的层次归属与分片 | 见第 15 章对应小节 |
| 端口与四元组、NAT（Network Address Translation）对四元组的影响 | 见第 15 章对应小节 |
| fd → 打开文件表 → inode 三层结构、`dup` 的共享语义 | 见第 7 章对应小节 |
| RTO（Retransmission Timeout）与重传 | 见第 17 章对应小节 |

---

## 16.1 TCP 报文头全字段解读

教材讲过报头"有什么字段"，面试真正问的是**每个字段在哪个机制里被消费**。

| 字段 | 宽度 | 被哪个机制使用 | 不设/设错的后果 |
|---|---|---|---|
| 源端口 / 目的端口 | 各 16 bit | 四元组定位与多路分解（demultiplexing） | 无法区分同主机并发连接；端口耗尽故障的直接来源 |
| 序号（Sequence Number） | 32 bit | 字节流编号、确认、重排、去重、SACK（Selective ACK） | 无法检测乱序与重复；重传判断不出对端收到多少 |
| 确认号（Acknowledgment Number） | 32 bit | 累积确认；仅 `ACK` 置位时有效 | 发送方只能靠超时判丢包，吞吐塌陷 |
| 数据偏移（Data Offset） | 4 bit | 报文头长度，单位 4 字节，最小 5（20 字节） | 无法定位选项区与载荷边界，选项机制整体失效 |
| 保留位 + 标志位 | 3 + 9 bit | `URG/ACK/PSH/RST/SYN/FIN` 六大标志 + `CWR/ECE/NS` | 建连、拆除、复位全靠它们；`ECE/CWR` 是 ECN（Explicit Congestion Notification）的信道 |
| 窗口（Window） | 16 bit | 接收窗口 `rwnd`，流量控制；配合窗口扩大因子可左移 | 只能表达 65535 字节窗口，长肥管道跑不满 |
| 校验和（Checksum） | 16 bit | 覆盖伪首部 + TCP 头 + 载荷 | 无法发现链路层未检出的比特错误 |
| 紧急指针（Urgent Pointer） | 16 bit | 仅 `URG=1` 时指向紧急数据末尾 | `URG` 在实践中几乎无人实现，`select` 异常集合已废弃 |
| 选项（Options） | 变长，补齐到 4 字节 | MSS、窗口扩大（Window Scale）、SACK Permitted、时间戳 | 只在握手期协商，协商失败即退化为默认行为 |

```text
 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)                             |
+--------+-------+--------------+-------------------------------+
|数据偏移|保留(3)|  标志位 (9)  |           窗口 (16)           |
|  (4)   |       |              |                               |
+-------------------------------+-------------------------------+
|          校验和 (16)          |        紧急指针 (16)          |
+-------------------------------+-------------------------------+
|                 选项 (变长, 补齐到 4 字节)                     |
+---------------------------------------------------------------+
|                          数据 ...                             |
+---------------------------------------------------------------+
```

窗口字段的语义要记牢：它通告的是**本端还能接收多少字节**（接收缓冲区空位），不是"我打算发多少"。应用读走多少，窗口就回涨多少；回涨通知靠**不带数据的纯 ACK**——这就是延迟确认与零窗口探测的舞台（见第 17 章）。

> [!NOTE]
> `SYN` 与 `FIN` 各占 1 个序号。由此推出两个高频细节：第三次握手的 `ACK` 号是"服务端 ISN + 1"；挥手时 FIN 的确认号也要 `+1`。报头固定 20 字节 + 选项最多 40 字节，正是"为什么不能把时间戳、SACK、窗口扩大全部塞进去"的根因，也是 `MSS = MTU − 20 − 20 = 1460`（以太网）的来源。

---

## 16.2 三次握手：每一步究竟在做什么

"SYN、SYN+ACK、ACK"是结论，面试要的是**每步交换的信息与它解决的问题**。

```text
   客户端                                     服务端
     |                                          |  CLOSED -> LISTEN
     |  ① SYN  seq = x (客户端 ISN)             |  建半连接表项
     |----------------------------------------->|  LISTEN -> SYN_RCVD
     |     SYN_SENT                             |
     |                                          |
     |  ② SYN+ACK  seq = y (服务端 ISN)         |  表项移入 accept 队列
     |             ack = x + 1                  |  (服务端已单方面认定建连)
     |<-----------------------------------------|
     |                                          |
     |  ③ ACK  seq = x + 1, ack = y + 1         |  SYN_RCVD -> ESTABLISHED
     |----------------------------------------->|  (服务端确认客户端接受了 y)
     |     ESTABLISHED                          |
```

**第一步（`SYN, seq = x`）**：客户端宣告自己的初始序号（Initial Sequence Number, ISN），请求序号同步。它不确认任何东西——服务端还没开口。

**第二步（`SYN+ACK, seq = y, ack = x + 1`）**：两件事压进一个报文——宣告服务端 ISN `y`，并确认客户端 ISN `x`。此刻**服务端已单方面认为连接建立**，进入 `SYN_RCVD` 并分配接收缓冲区等资源。

**第三步（`ACK, seq = x + 1, ack = y + 1`）**：客户端确认服务端 ISN，把连接推到 `ESTABLISHED`，并**让服务端知道客户端接受了 `y`**。

**为什么双方各自独立编号、必须交换 ISN。** 全双工连接有两条独立的字节流，各自需要起点，才能计算确认号、判断乱序与重排。

**为什么 ISN 必须随机化、不能从 0 开始。** 两条独立理由，面试要都答：

1. **防止旧连接的报文被新连接误收。** 四元组会被复用（客户端端口释放后再用、NAT 复用映射、服务端重建同名连接）。若 ISN 从 0 每轮递增，上一轮某个迟到报文的序号恰好落进新连接的接收窗口，就会被当作合法数据交付应用层。随机化把这一概率压到约 `1/2³²`。
2. **防止序列号猜测攻击。** ISN 若可预测，攻击者无需看到握手报文即可盲猜有效序号，从而伪造数据注入或伪造 `RST` 打断连接——在没有认证机制的 TCP 上这是致命的。

**第三次握手能否携带数据？** 协议允许，服务端也会正常缓存。但默认不发：服务端 `cwnd` 初值只有 1 个 MSS 量级（见第 17 章），第二次报文一旦因 SYN 队列溢出被丢，客户端的数据会随重传的 `ACK` 一起重发，白费带宽；更根本的是客户端此刻还不确定服务端是否接受连接。

**为什么不是四次。** 把第二步拆成"单独回 `ACK`"+"单独发 `SYN`"语义完全等价，但白白多一个 RTT。第二步能合并的依据：`SYN` 与 `ACK` 是同一报头里的两个独立标志位，同时置位不违反任何约束，接收方也无歧义（`ack` 号永远指向上次收到的对端序号，`seq` 号是本端新序列的起点）。**凡两件事能无损放进一个报文，协议设计都会合并**——这是读 TCP 的通用规律。挥手侧同理会出现"少于四次"的情况，见 16.6.1。

> [!IMPORTANT]
> 正确表述："两次握手让服务端**单方面**完成状态迁移，而连接是双方的共同状态；第三次握手是客户端把自己的迁移结果通报给服务端，使双方对'连接已存在'达成一致。"——这个句式可以直接迁移到挥手题上。

---

## 16.3 为什么不能是两次握手

题目假设：客户端发 `SYN`，服务端回 `SYN+ACK` 后即进入 `ESTABLISHED`，不等第三次确认。

```text
   客户端 C                          服务端 S
      |                                 |
      | ① SYN(seq=x1) → 网络拥塞/路由抖动，被滞留
      |    (C 超时后放弃, 该报文仍漂在网络中)
      |                                 |
      | ② C 重发 SYN(seq=x2) ---------->|  S 认为连接建立
      |<---------------- SYN+ACK        |  S 分配缓冲区, 进入 ESTABLISHED
      | ③ ACK                           |
      |    C 用完连接, 正常四次挥手关闭  |
      |    S 释放连接, 回到 LISTEN       |
      |                                 |
      | ④ 迟到的 ① 终于送达 ----------->|  S 收到一个"格式完全合法"的 SYN
      |    (对一个早已关闭的旧连接的请求) |  两次模型下: 立即 ESTABLISHED,
      |                                 |  并分配缓冲区/表项 ← "幽灵连接"
      |    C 对此一无所知                |  S 发 SYN+ACK
      |<---------------- SYN+ACK        |
      |    C 无对应连接 → 回 RST 或不响应 |
      |                                 |  S 侧资源被占, 直到重传耗尽
```
关键点：**两次握手模型下，服务端无法区分"新连接请求"与"早已作废的历史连接请求"**——它对两者做出同样的承诺、分配同样的资源。而客户端本地没有任何相关状态，会忽略或 `RST` 掉那个 `SYN+ACK`，结果是服务端持有一批客户端根本不承认的连接。第三次 `ACK` 才是客户端对"我确实想建这条连接"的承诺：历史 `SYN` 送到时，客户端内核因找不到对应 socket 而回 `RST`，服务端收到即清理。**只有"客户端确认了服务端 ISN"这一事实能筛掉幽灵连接**。

> [!CAUTION]
> 两个会立刻被追问的错答：说成"两次握手不能保证可靠传输"——可靠性归第 17 章的重传机制，与轮次无关，会被反问"那三次握手就不丢包了吗"；说成"为了防 SYN Flood"——因果反了，防历史连接误建是设计动机，SYN Flood 是后来针对该设计的攻击。

---

## 16.4 SYN 队列与 accept 队列

内核为 `listen` socket 维护**两个独立队列**，这是排查"连接建立失败"的核心。

```text
   第一次 SYN 到达 ──►┌────────────────────────┐
                      │ SYN 队列 (半连接)       │ 上限:
                      │ 表项状态 SYN_RCVD       │ min(tcp_max_syn_backlog,
                      └───────────┬────────────┘     somaxconn, backlog)
                                  │ 第三次 ACK 到达
                                  ▼
                      ┌────────────────────────┐
   应用程序 accept() ◄─│ accept 队列 (全连接)    │ 上限:
                      │ 表项状态 ESTABLISHED    │ min(backlog, somaxconn)
                      └────────────────────────┘
```

| 项目 | SYN 队列（半连接） | accept 队列（全连接） |
|---|---|---|
| 存放对象 | 收到 SYN、未完成第三次握手的连接 | 握手已完成、等应用 `accept()` 的连接 |
| 长度上限 | `min(tcp_max_syn_backlog, somaxconn, backlog)` | `min(backlog, net.core.somaxconn)` |
| 溢出行为 | 默认**丢弃新 SYN**，不回 `RST`，客户端自行重传 | 看 `net.ipv4.tcp_abort_on_overflow`：0 = 忽略第三次 ACK（服务端悄悄丢弃）；1 = 回 `RST` 让客户端立刻失败重试 |
| 观测量 | `netstat -s` 的 `SYNs to LISTEN sockets dropped` | `netstat -s` 的 `listen overflows` |
| `ss -lnt` 列 | 不直接显示 | `Recv-Q` = 当前队列长度，`Send-Q` = 上限 |

`ss -lnt` 在 `LISTEN` 下的 `Recv-Q`/`Send-Q` 语义**与其他状态完全不同**，是最容易读错的地方：

```bash
# LISTEN 下这两列不是收发字节数，而是 accept 队列的"当前长度/上限"
# 之所以先看这一列：Recv-Q 持续贴近 Send-Q，就说明应用 accept 不够快，是溢出根因
ss -lnt 'sport = :8080'
# State  Recv-Q Send-Q Local Address:Port
# LISTEN 511    511    0.0.0.0:8080      <- 队列已满，新连接正在被丢弃

# 对比 ESTABLISHED：Recv-Q 是"已收到但应用还没 read 的字节数"
# 长期非 0 说明应用读取慢，与网络无关 —— 两列含义不同，切忌混读
ss -nt state established '( sport = :8080 )'
```

> [!WARNING]
> `tcp_abort_on_overflow = 0`（默认）的表现极具迷惑性：客户端 `connect()` **成功返回**，`ss` 里也能看到 `ESTABLISHED`，但服务端内核在 accept 队列满时把这个已握完手的连接**悄悄丢掉**，随后它因服务端不认账而被 `RST`。现象是"偶发连接被重置，重试即成功"。排查第一步是 `netstat -s | grep -i listen` 加 `ss -lnt` 的 `Recv-Q`。

---

## 16.5 SYN Flood 与防御

攻击机理直接利用 16.4 的队列：用伪造源 IP 的海量 `SYN` 填满 SYN 队列，服务端为每个表项分配资源并按 `tcp_synack_retries` 重传 `SYN+ACK`，而真实客户端的 `SYN` 因队列满被丢弃。**`tcp_syncookies` 的原理是：不分配状态，把状态编码进 ISN。**

```text
 收到 SYN 且半连接队列已满、tcp_syncookies=1 时:
   不创建 SYN_RCVD 表项, 而是构造 cookie 当作服务端 ISN 回给对方

   cookie = MAC( 客户端IP, 客户端端口, 服务端IP, 服务端端口,
                 时间戳计数器, 本机秘密值 )
            |<-------- 24 bit ------>|<--- 8 bit --->|

 第三次 ACK 回来时:
   用 ack 号减 1 反推时间戳与校验值, 重算 MAC 比对
   一致则直接建连 —— 全程不依赖任何已存表项
```

- **无状态是它的全部价值**：SYN 队列被"绕过"而非"扩容"，因此能抗住任意大的洪流。
- **代价一：选项协商退化。** 服务端 ISN 被占用来编码 cookie，MSS、窗口扩大因子、SACK、时间戳都无法随 cookie 编码，只能放弃。这就是"syncookies 不能与某些 TCP 选项共存"的确切含义——它用长肥管道与 SACK 的性能换抗洪能力。
- **代价二：cookie 编码空间有限**，时间戳计数器粒度决定了 cookie 有效期（约 64 秒量级），超时的第三次 `ACK` 会被拒。

| 参数 | 作用 | 取值建议与理由 |
|---|---|---|
| `tcp_syncookies` | 队列溢出时改用无状态 cookie 应答 | `1`（按需启用），避免常态下损失选项协商；`2` 为无条件启用 |
| `tcp_syn_retries` | **客户端** `SYN` 重传次数，决定 `connect()` 多久失败 | 默认 6，总退避约 127 秒，业务上通常过久，需配合应用层连接超时 |
| `tcp_synack_retries` | **服务端** `SYN+ACK` 重传次数 | 默认 5，退避约 1 + 2 + 4 + 8 + 16 + 32 秒，决定半连接表项存活时长，调小可加快回收 |
| `tcp_max_syn_backlog` | SYN 队列上限 | 默认 256~1024，高并发服务端须上调，否则先于 accept 队列溢出 |
| `net.core.somaxconn` / `tcp_abort_on_overflow` | accept 队列的全局上限 / 队列满时是否回 `RST` | `somaxconn` 5.4+ 默认 4096；`tcp_abort_on_overflow=0` 会造成"客户端成功、服务端丢弃"的幽灵连接 |

> [!NOTE]
> 更彻底的方向是把握手负担外移：负载均衡设备做 SYN 代理（客户端先与 LB 握手，LB 再与后端建连），或改用 QUIC（见第 18 章）——QUIC 把传输握手与 TLS 握手合并，并用地址验证令牌/无状态重试包达到类似 syncookies 的效果，且不必牺牲选项协商。

---

## 16.6 四次挥手与 TIME_WAIT

### 16.6.1 每一步的语义与状态迁移

```text
   主动关闭方 A                            被动关闭方 B
      | ① FIN  seq = u  (A 无数据要发了)        |  ESTABLISHED -> CLOSE_WAIT
      |--------------------------------------->|  (B 仍可发数据!)
      |     A: FIN_WAIT_1                       |
      |<---------------------------------------| ② ACK  ack = u + 1
      |     A: FIN_WAIT_2                       |
      |                                         |  B 的应用调用 close()
      |<---------------------------------------| ③ FIN  seq = w
      |     A: TIME_WAIT                        |  B: LAST_ACK
      |--------------------------------------->| ④ ACK  ack = w + 1
      |     等 2MSL -> CLOSED                   |  B: CLOSED
```
`FIN` 的语义**仅是"我没有数据要发了"**，它占一个序号，但**不关闭接收方向**。B 进 `CLOSE_WAIT` 后仍可继续发数据，A 必须继续 ACK——这就是半关闭（half-close），也是 `CLOSE_WAIT` 堆积的物理基础：B 的应用不调 `close()`，该状态就永远停留。

**为什么不一定四次。** ① `ACK` 与 `FIN` 若同时就绪，B 会合并成 `FIN+ACK`，四次退化为三次（延迟确认加剧这种合并）；② 同时关闭时双方都发 `FIN`，各自经 `CLOSING` 进 `TIME_WAIT`；③ 一方设了 `SO_LINGER`（见 16.7）或进程崩溃时直接回 `RST`，四次挥手被完全跳过——这是抓包里"突然 `RST`"的常见来源。

### 16.6.2 四个"堆积状态"的辨析与排查

| 状态 | 出现在哪一侧 | 含义 | 排查方法 |
|---|---|---|---|
| `CLOSE_WAIT` | 被动关闭方（已收 `FIN` 并回 `ACK`，等本端应用 `close()`） | 应用层没关闭 fd | `ss -tan state close-wait` 找 fd → `lsof -p <pid>` 定位 → 查连接池 `release` 是否被 `finally`/`try-with-resources` 覆盖。**必定是代码 bug，改内核参数无效** |
| `LAST_ACK` | 被动关闭方（已发 `FIN`，等对端最后 `ACK`） | 等对端确认本端 `FIN` | 通常瞬时。大量停留说明对端不回 `ACK`（对端崩溃或被防火墙丢弃），属网络/对端问题 |
| `TIME_WAIT` | **主动关闭方** | 正常关闭，等 `2MSL` 后释放 | 堆积说明**本端是主动关闭方**，见 16.6.3、16.6.4 |
| `FIN_WAIT_2` | 主动关闭方（已收 ACK，等对端 `FIN`） | 对端迟迟不发 `FIN` | 对端 `CLOSE_WAIT` 侧应用没关连接；`FIN_WAIT_2` 有超时，由 `tcp_fin_timeout`（默认 60 s）控制 |

一句话记住分辨线：**`CLOSE_WAIT` 是应用没关（bug），`TIME_WAIT` 是内核在等（正常或端口压力）**。

### 16.6.3 TIME_WAIT 为什么存在，且必须是 2MSL
先澄清 MSL（Maximum Segment Lifetime）：**报文能在网络中存活的最长时间**。RFC 793 建议 2 分钟；Linux 实现中 `TCP_TIMEWAIT_LEN` 固定为 60 秒，即 `2MSL = 60 s`，对应实现假设的 `MSL = 30 s`。`tcp_fin_timeout` **不控制 `TIME_WAIT` 时长**（它控制 `FIN_WAIT_2`），这是最常见的读错参数。TIME_WAIT 有两条**互相独立**的理由：

**理由一：保证最后一个 ACK 能到达对端。**

```text
   A(主动关闭)                        B(被动关闭)
      | ④ ACK 丢失 ✗                  |  B 收不到 ACK, 重传 FIN
      |<------------------------------| ③' FIN 重传
      |                               |
      | 若 A 已 CLOSED:               |  A 无此连接状态 -> 回 RST
      |                               |  B 收 RST: 连接异常终止
      | 若 A 在 TIME_WAIT:            |  A 仍有状态 -> 重发 ACK
      |------------------------------>|  B 正常进入 CLOSED
```

若 A 发完最后一个 `ACK` 就 `CLOSED` 并释放四元组，B 重传的 `FIN` 到达时 A 只能回 `RST`，B 会把已成功传输的数据判为异常终止；TIME_WAIT 让 A 保留状态以**重发 `ACK`**，并在收到重复 `FIN` 时重置计时器。

**理由二：让本次连接的旧报文在网络中全部消失。** 考虑四元组复用：关闭后立刻用同样的 `(源IP, 源端口, 目的IP, 目的端口)` 建连，若上一轮某个被延迟的报文仍在网络中，其序号落进新连接的接收窗口，就会被当作新数据交付应用层。`2MSL` 的物理含义正是：**一个报文从发出到被网络丢弃最多花 MSL，对端对它做出反应（如重传）再花 MSL，故 2MSL 后本轮连接的所有报文及其所有可能的响应都必然消失。**

> [!IMPORTANT]
> 记忆锚：**理由一防"对端收不到 ACK 而误判失败"，理由二防"旧报文污染同四元组的新连接"**。前者关于本次连接能否优雅结束，后者关于下一次连接能否正确开始。只答一条必被追问"还有吗"。

### 16.6.4 TIME_WAIT 过多的解法与代价

先判断量级是否真异常。本地端口由 `net.ipv4.ip_local_port_range` 决定（默认约 32768~60999，约 2.8 万个）。若服务是**客户端角色**（调外部接口、做代理转发），`TIME_WAIT` 消耗本地端口，到几万量级即报 `Cannot assign requested address`；若服务是**服务端角色**（固定监听端口），`TIME_WAIT` 不消耗端口，只占内存表项。

| 手段 | 真实作用 | 代价与边界 |
|---|---|---|
| `SO_REUSEADDR` | **对 TIME_WAIT 数量毫无影响**。只做两件事：允许 `bind` 处于 TIME_WAIT 的地址（重启不必等 2MSL），允许同一端口绑定不同本地 IP | 它不让 `TIME_WAIT` 变少，只让"绑定"不被拒。当成减少 TIME_WAIT 的手段是根因性误解 |
| `tcp_tw_reuse` | 允许**主动发起连接**的一方，在时间戳（`tcp_timestamps=1`）开启时复用 TIME_WAIT 中的四元组建新连接 | 只对主动发起方有效；必须开时间戳（复用后旧报文的序号保护由 PAWS 承担）；不改架构，只做缓解 |
| 缩短 `tcp_fin_timeout` | 只缩短 `FIN_WAIT_2` 的等待上限 | **完全不影响 `TIME_WAIT` 时长**（Linux 由内核常量 `TCP_TIMEWAIT_LEN` 固定）。当解法属彻底误读 |
| 连接池化 / 长连接 | 从根上减少连接创建次数，因而减少 `TIME_WAIT` 总量 | 需处理空闲超时、对端半关闭、失效连接重建（见第 18 章）。**唯一从源头解决的方向** |
| 让对端主动关闭 | 把 `TIME_WAIT` 推给对端 | 需协议层协商，且对端运维未必接受 |
| `tcp_tw_recycle` | 已废弃并移除 | 见下方警示 |

> [!CAUTION]
> `net.ipv4.tcp_tw_recycle` 在 4.12 及之后的内核中已被**彻底移除**，在旧内核上也**绝不应启用**。它依赖"同一源 IP 的时间戳必须单调递增"，而 NAT 设备后的多个客户端共用一个源 IP、各自时间戳并不同步，服务端会把时间戳更小的正常 `SYN` 当作过期报文直接丢弃——NAT 后的客户端出现**随机、大面积连不上**。

---

## 16.7 半关闭、SO_LINGER 与端口复用选项

| 操作 | 作用 | 能否继续读 | 对连接的影响 |
|---|---|---|---|
| `shutdown(fd, SHUT_WR)` | 只发 `FIN`，关闭发送方向 | **能**，接收方向仍可读 | 连接进入半关闭（`FIN_WAIT_1`/`FIN_WAIT_2`），fd 仍被占用 |
| `shutdown(fd, SHUT_RD)` | 关闭接收方向，对端再发数据则回 `RST` | 不能 | fd 仍被占用 |
| `shutdown(fd, SHUT_RDWR)` | 双向关闭并发 `FIN` | 不能 | 触发完整挥手 |
| `close(fd)` | fd 引用计数减 1；**减到 0 才发 `FIN`** | 取决于是否还有其他副本 | 释放 fd；引用计数未归零则连接**不会关闭** |

最后一行是高频陷阱：`fork` 或 `dup` 出来的 fd 共享同一打开文件表项，一方 `close` 只减引用计数，连接依旧活着（见第 7 章三层结构）。**`SO_LINGER` 四种组合**——定义 `struct linger { int l_onoff; int l_linger; }`：

| `l_onoff` | `l_linger` | `close()` 行为 | 用途与风险 |
|---|---|---|---|
| 0 | 忽略 | 默认：后台异步发完剩余数据并正常四次挥手 | 绝大多数场景 |
| 1 | 0 | **丢弃发送缓冲区未发数据，直接发 `RST`** | 强制跳过四次挥手、不产生 `TIME_WAIT`；代价是对端可能丢数据并被报"连接被重置" |
| 1 | >0 | `close()` 阻塞至多 `l_linger` 秒，等数据发完并收到对端 `ACK`；超时退化为发 `RST` | 适合关闭前必须确保送达的场景；风险是**阻塞调用线程**，在事件循环里会拖垮整个 loop |
| 1 | 负值 | 未定义/视实现而定，不应使用 | — |

> [!WARNING]
> "把 `SO_LINGER` 的 `l_linger` 设 0 来消灭 TIME_WAIT"是流传最广的偏方之一。它确实让主动关闭方发 `RST` 而不进 `TIME_WAIT`，但代价是：① 发送缓冲区里未确认的数据被静默丢弃；② 对端按异常终止处理，可能触发重试或告警；③ 绕过了理由二，旧报文污染新连接的风险重新出现。

**`SO_REUSEADDR` 与 `SO_REUSEPORT` 的差异**：前者管**能不能 bind**（允许绑定 TIME_WAIT 地址、允许同一端口绑定不同本地 IP）；后者管**谁来 accept**——允许多个 socket（甚至不同进程）绑定完全相同的地址与端口，内核按四元组哈希在它们之间**做负载均衡**，每个进程有独立队列，从而消除 `accept` 惊群（thundering herd）与锁竞争。注意 `SO_REUSEPORT` 要求同一连接始终分到同一 socket，故哈希必须稳定；它只解决分发，不解决"谁来关闭连接"。

---

## 16.8 TCP 状态机全图与状态×标志位反应表

```text
  [CLOSED] --passive open--> LISTEN --recv SYN/send SYN+ACK--> [SYN_RCVD]
      |                                                             |
      | active open / send SYN                     recv ACK / recv RST
      v                                       (移入 accept 队列 / 关闭)
  [SYN_SENT] --recv SYN+ACK / send ACK --+                      |
                                         v                      v
                          +----------------------------------+
                          |          ESTABLISHED             |  <-- 数据收发
                          +--+----------------------------+--+
           recv FIN / send ACK|                          | close / send FIN
                              v                          v
                      +--------------+          +----------------+
                      |  CLOSE_WAIT  |          |  FIN_WAIT_1    |
                      +------+-------+          +---+--------+---+
            close/send FIN   |          recv ACK   |        | recv FIN
            (不 close 即堆积) v      (对端未 close)  v        | send ACK
                      +--------------+     +------------+    | 同时关闭: 经
                      |  LAST_ACK    |     | FIN_WAIT_2 |    | CLOSING 汇入
                      +------+-------+     +-----+------+    |
                   recv ACK  |           recv FIN |         |
                             v           send ACK v         v
                          CLOSED <--2MSL-- +------------------+
                                          |    TIME_WAIT     |
                                          +------------------+
```
11 种状态是 `CLOSED`、`LISTEN`、`SYN_SENT`、`SYN_RCVD`、`ESTABLISHED`、`FIN_WAIT_1`、`FIN_WAIT_2`、`CLOSE_WAIT`、`CLOSING`、`LAST_ACK`、`TIME_WAIT`。TCP 是**事件驱动的有限状态机**：事件 = 收到的标志位组合 + 本端应用调用（`open`/`send`/`close`）+ 超时。

| 当前状态 | 收到 `ACK` | 收到 `FIN` | 收到 `RST` |
|---|---|---|---|
| `CLOSED` | 回 `RST` | 回 `RST` | 忽略 |
| `LISTEN` | 忽略 | 忽略 | 忽略 |
| `SYN_SENT` | 忽略（未置 `SYN` 的 ACK 无效） | 建连后再处理 | `connect()` 失败，→ `CLOSED` |
| `SYN_RCVD` | 移入 accept 队列，→ `ESTABLISHED` | 先建连再走 `CLOSE_WAIT` | → `CLOSED` |
| `ESTABLISHED` | 更新发送窗口 | 回 `ACK`，→ `CLOSE_WAIT` | 异常终止，→ `CLOSED` |
| `FIN_WAIT_1` | 确认我方 `FIN`：已收对端 `FIN` 则 → `TIME_WAIT`，否则 → `FIN_WAIT_2` | 回 `ACK`，→ `CLOSING` 或 `TIME_WAIT` | → `CLOSED` |
| `FIN_WAIT_2` | 忽略 | 回 `ACK`，→ `TIME_WAIT` | → `CLOSED` |
| `CLOSE_WAIT` | 忽略 | 忽略（已在关闭中） | → `CLOSED` |
| `CLOSING` | 确认我方 `FIN`，→ `TIME_WAIT` | 重发 `ACK` | → `CLOSED` |
| `LAST_ACK` | → `CLOSED` | 重发 `ACK` | → `CLOSED` |
| `TIME_WAIT` | 忽略 | **重发 `ACK` 并重置 2MSL 计时器** | 提前 → `CLOSED` |

所有非 `LISTEN`、非 `SYN_SENT` 的非 `CLOSED` 状态收到裸 `SYN` 都属异常，统一回 `RST`；`LISTEN` 收到 `SYN` 回 `SYN+ACK` 并进入 `SYN_RCVD`，`SYN_SENT` 收到对端 `SYN` 即"同时打开"，回 `SYN+ACK` 后进入 `SYN_RCVD`。

`TIME_WAIT` 行加粗项正是理由一在状态机层面的落地：收到对端重传的 `FIN` 时**不回 `RST`，而是重发 `ACK`**。

---

## 本章小结

| 关键结论 | 一句话表述 | 常见错误 |
|---|---|---|
| 不能两次 | 两次握手只让服务端单方面迁移状态，历史 SYN 会造成客户端不认的幽灵连接 | 只背"防止旧连接请求"，说不出旧 SYN 如何产生 |
| 不能四次 | 服务端 `SYN` 与对客户端 `SYN` 的 `ACK` 可放进同一报文，拆开无收益 | 说"四次也行，只是三次更快"，漏掉"不违反任何约束" |
| 两个队列 | SYN 队列溢出丢 `SYN`；accept 队列溢出看 `tcp_abort_on_overflow`，默认 0 时连接"成功后消失" | 把两者混成一个 `backlog`，忽略 `somaxconn` 取小值 |
| `CLOSE_WAIT` / `TIME_WAIT` | 前者是被动关闭方应用没调 `close()`（代码 bug）；后者是主动关闭方的正常等待 | 把两者一起归为内核参数问题 |
| `TIME_WAIT` 的 2MSL | ① 能重发最后一个 `ACK`；② 让旧报文消失，避免污染新四元组 | 只说一条；把 `tcp_fin_timeout` 当成控制 `TIME_WAIT` 的参数 |
| 端口复用选项 | `SO_REUSEADDR` 只管"能否 bind"，不减 TIME_WAIT；`SO_REUSEPORT` 管内核级分发 | 把 `SO_REUSEADDR` 当成 TIME_WAIT 优化手段 |
| `tcp_tw_recycle` | 因 NAT 时间戳假设错误已被移除 | 按旧博客去开启 |

## 自测题

**Q1（推导）** 请在不使用"三次握手"这个词的前提下，解释为什么服务端在第二个报文之后不能认为连接已建立，并给出一个能让错误模型暴露的资源耗尽场景。

<details><summary>答案与解析</summary>

核心论点是**连接是双方的共同状态，不是单方的承诺**。第二个报文只完成了"服务端告知自己的 ISN 并确认了客户端 ISN"，服务端据此进入 `ESTABLISHED`；但客户端尚未确认服务端的 ISN，也无法分辨该报文是回应本次请求还是上一轮的迟到回应。若模型在此结束，服务端为每条收到的 `SYN` 都分配了接收缓冲区与表项，而客户端可能根本没有对应状态。

资源耗尽场景：伪造源 IP 发送大量 `SYN`。两次模型下服务端对每个 `SYN` 立即完成建连并分配资源，**无法用"客户端是否回应第三次确认"筛除伪造请求**——因为根本没有第三次。常见错误：答"两次不可靠，数据可能丢失"，可靠性归第 17 章重传机制，与轮次无关。追问：① "客户端收到 `SYN+ACK` 后崩了，三次握手不也一样消耗服务端资源吗"→ 用 `SYN_RCVD` 超时回收与 syncookies 回答；② "`RST` 在这里起什么作用"→ 客户端无对应 socket 时内核回 `RST`，服务端收到即清理。

</details>

**Q2（判断）** 某 Java 服务出现大量 `CLOSE_WAIT`，运维建议调大 `tcp_max_syn_backlog` 与 `somaxconn`。判断该建议是否有效，并说明你会怎么定位。

<details><summary>答案与解析</summary>

**无效，方向性错误。** `CLOSE_WAIT` 表示"本端已收到对端 `FIN` 并回了 `ACK`，正等本端应用调用 `close()`"，其唯一出口是应用层 `close()`；两个队列参数只影响**连接建立阶段**的表项长度。

定位路径：`ss -tan state close-wait` 找连接与本地端口 → `ss -tanp` 或 `lsof -p <pid>` 映射到进程与 fd → 审查连接获取/释放代码。典型根因有三：① 连接池 `close`/`release` 不在 `finally` 或 `try-with-resources` 中，异常路径漏释放；② 连接池 `maxIdle`/`maxLifetime` 与对端空闲超时不匹配，对端先关、本端不感知；③ `finally` 里只提交事务而没归还连接到池。常见错误：把它与 `TIME_WAIT` 一起归为内核参数问题。追问：① "`CLOSE_WAIT` 占用 fd 吗"→ 占用，这正是它最终引发 `Too many open files` 的原因；② "调 `ulimit -n` 能否缓解"→ 只是把爆炸时间推后，属掩盖问题。

</details>

**Q3（计算）** 某服务端 `tcp_timestamps=0`，`ip_local_port_range` 为默认 32768~60999。它作为**主动发起方**每秒向同一后端的同一端口建立 5000 条短连接，且由本端主动关闭。判断是否会出现本地端口耗尽，并说明 `tcp_tw_reuse` 能否救场。

<details><summary>答案与解析</summary>

可用本地端口约 `60999 − 32768 + 1 ≈ 28232` 个。本端主动关闭 → 每条连接进 `TIME_WAIT` 并占住其四元组 60 秒（`TCP_TIMEWAIT_LEN = 60 s`）。稳态下处于 `TIME_WAIT` 的四元组数约 `5000 × 60 = 300000`，远超 28232，**约 5.6 秒就会耗尽端口空间**。要注意四元组是 `(源IP, 源端口, 目的IP, 目的端口)`，若后端有多个地址/端口，占用会按目的端组合分摊，瓶颈变成 `可用源端口数 × 目的端组合数`；本题限定同一后端同一端口，故必然耗尽。

`tcp_tw_reuse` **不能救场**：题目已设 `tcp_timestamps=0`，而它要求时间戳开启（依赖 PAWS 保证复用后旧报文不被误收）；即便开启时间戳，它也只在**发起新连接**时允许复用，属缓解而非根治。真正的解法是连接池/长连接（把 5000 次/秒的建连降到接近零），辅以扩大 `ip_local_port_range`、增加后端地址多样性。常见错误：答"把 `tcp_fin_timeout` 调小"，它控制 `FIN_WAIT_2`，不影响 `TIME_WAIT` 的 60 秒。追问：① "为什么 `tcp_tw_reuse` 需要时间戳"→ 复用同一四元组后，旧连接的迟到报文可能落入新连接窗口，PAWS 用时间戳单调性拒收；② "`SO_REUSEADDR` 有帮助吗"→ 只对"服务端重启时 bind 失败"有帮助。

</details>

**Q4（设计）** 要上线一个"每台机器每秒对下游发起上万次 HTTP 短连接"的网关。请给出减少连接管理开销的方案，按收益排序并说明每个方案的 TCP 层依据。

<details><summary>答案与解析</summary>

按收益排序：① **连接池 + 长连接（keep-alive）**，把建连与拆连的**次数**从每秒上万降到接近零，直接消除 `TIME_WAIT` 总量与握手 RTT（依据 16.6.4）；② **降低主动关闭方压力**，与下游协商空闲超时，连接池 `maxLifetime` 必须小于对端空闲超时，避免使用被对端静默关闭的连接（依据 16.6.2）；③ **扩大可用四元组空间**，调整 `ip_local_port_range` 并在下游增加 `(IP, 端口)` 组合（依据 Q3 计算）；④ **`SO_REUSEPORT` 做接入侧多进程负载均衡**，若同时对外提供入口，多进程各自 `listen` 同端口、由内核按四元组哈希分发，可消除惊群（16.7）；⑤ **谨慎使用 `tcp_tw_reuse`**，仅"本端主动发起 + 时间戳开启"有效且依赖 PAWS；⑥ **明确不要用** `tcp_tw_recycle`（已移除，NAT 场景必炸）与 `SO_LINGER` 置 0（丢数据且破坏 TIME_WAIT 的保护语义）。

常见错误：把 `SO_REUSEADDR` 排在前面——它只解决"重启时 `bind` 失败"与"多 IP 同端口"。追问：① "长连接会不会有时间戳回绕风险"→ 超过 24.8 天的连接需靠时间戳/序号回绕保护，故应设 `maxLifetime` 定期重建；② "改 HTTP/2 多路复用有没有帮助"→ 有，但那是第 18 章主题。

</details>

**Q5（辨析）** 面试官问："`TIME_WAIT` 是为了让最后一个 `ACK` 能重发，那我调小 `tcp_fin_timeout` 不就缩短了吗？"请指出其中的两处错误。

<details><summary>答案与解析</summary>

**错误一：`tcp_fin_timeout` 不控制 `TIME_WAIT`。** 它控制 `FIN_WAIT_2` 的存活上限（对端迟迟不发 `FIN` 时多久放弃），默认 60 秒；`TIME_WAIT` 时长由内核常量 `TCP_TIMEWAIT_LEN`（60 秒，即 `2MSL`，对应实现假设的 `MSL = 30 s`）决定，用户态无法通过 sysctl 修改。

**错误二：把 `TIME_WAIT` 的理由只归结为"重发 `ACK`"。** 还有第二条独立理由：让本轮连接的所有报文在网络中消失，避免污染使用相同四元组的新连接；即使能保证最后一个 `ACK` 一定送达，第二条理由依然要求等待约 `2MSL`。加分回答：可补"把主动关闭方角色转移到可控一侧是常见工程做法；彻底消除 `TIME_WAIT` 的方向是减少建连次数（连接池）或换用多路复用方案（QUIC，见第 18 章）"。常见错误：答"调小 `tcp_fin_timeout` 会让 `TIME_WAIT` 更快释放，但有风险"——把参数名与风险绑定，恰好暴露没读 `man 7 tcp`。追问：① "`MSL` 与 `2MSL` 是什么关系"→ `MSL` 是单程报文最大存活时间，`2MSL` 覆盖一去一回；② "为什么 Linux 用 60 秒"→ 内核把 `TCP_TIMEWAIT_LEN` 定为固定值，是网络半径与端口压力之间的折中。

</details>

## 面试高频问题与陷阱

| 面试问法 | 考查角度 | 陷阱 / 加分点 | 对应本章小节 |
|---|---|---|---|
| 为什么 TCP 三次握手不能改成两次？ | 是否理解"连接是双方共同状态" | 错误回答：只答"防止旧连接请求"就停住，说不出旧 SYN 怎么产生、为什么客户端会回 `RST`。加分：完整给出"延迟 SYN + 连接已关闭 + 客户端无状态"场景链 | 16.3 |
| 为什么三次握手不是四次？ | 是否理解标志位可合并 | 错误回答："四次也行，只是三次更快"——漏了"不违反任何约束"这一层，也没提同时打开与延迟确认捎带 | 16.2、16.6.1 |
| TCP 的初始序号为什么要随机化，不能从 0 开始？ | 序号空间与安全性 | 错误回答：只说"为了安全"。加分：两条理由分开讲（旧报文误收 + 序列号猜测攻击），并给出概率量级约 `1/2³²` | 16.2 |
| 第三次握手可以携带数据吗？ | 协议细节与工程取舍 | 错误回答："不可以，`SYN` 不能带数据"——混淆了第一次与第三次。加分：协议允许但默认不发，因服务端 `cwnd` 初值小、丢包会连带重传数据 | 16.2 |
| `ss -lnt` 里 `LISTEN` 状态的 `Send-Q` 和 `Recv-Q` 是什么意思？ | 两个队列与观测 | 错误回答："收发缓冲区大小"——那是 `ESTABLISHED` 下的含义。加分：`Recv-Q` 是 accept 队列当前长度、`Send-Q` 是上限，贴上限即溢出 | 16.4 |
| `CLOSE_WAIT` 很多是什么原因，怎么处理？ | 状态语义与排查 | 错误回答："调内核参数"或"提高 `ulimit`"。加分：断言这是应用没调 `close()` 的代码 bug，并给出连接池 `finally`/`try-with-resources` 的定位路径 | 16.6.2 |
| `TIME_WAIT` 为什么必须有，为什么是 2MSL 而不是 1MSL？ | 本章第一号难点 | 错误回答：只说"重发 ACK"。加分：第二条理由（旧报文消失，防污染同四元组新连接）+ 说明 `MSL` 是单程、`2MSL` 覆盖一去一回 | 16.6.3 |
| `SO_REUSEADDR` 能不能减少 `TIME_WAIT`？ | 参数真实语义 | 错误回答："能，开启后 `TIME_WAIT` 就少了"。正确：它只解决 `bind` 被 TIME_WAIT 拒绝的问题，数量不变 | 16.6.4、16.7 |
| `tcp_tw_recycle` 为什么被移除？ | 是否读过真实内核变更 | 错误回答："因为它不安全"这类空话。加分：NAT 后多客户端共源 IP、时间戳不同步，服务端按时间戳丢弃正常 `SYN` 导致随机连不上，4.12 起已移除 | 16.6.4 |
| `syncookies` 是怎么防 SYN Flood 的，有什么代价？ | 无状态化思想 | 错误回答："它扩大了半连接队列"。加分：状态编码进 ISN 的 MAC；代价是选项协商退化、cookie 有效期有限 | 16.5 |
| `shutdown(SHUT_WR)` 和 `close` 有什么区别？ | 半关闭与 fd 引用计数 | 错误回答："没区别"。加分：`shutdown` 只关发送方向、接收仍可读；`close` 是减引用计数，`dup`/`fork` 共享时可能**根本不发 `FIN`** | 16.7 |
| `SO_REUSEADDR` 和 `SO_REUSEPORT` 的区别是什么？ | 是否分清两个易混选项 | 错误回答："都是为了端口复用，差不多"。加分：前者管能否 `bind`，后者是内核按四元组哈希做负载均衡、消除惊群 | 16.7 |
| 服务端大量连接处于 `SYN_RCVD` 说明什么？ | 半连接状态与攻击识别 | 错误回答："正常现象，不用管"。加分：区分三种成因——客户端不回第三次 `ACK`（可能有防火墙丢包）、真实 SYN Flood、`tcp_max_syn_backlog` 偏小，并给出 `netstat -s` 对应计数器 | 16.4、16.5 |
| 同时关闭（simultaneous close）时状态怎么走？ | 状态机是否真的画过 | 错误回答："不可能同时关闭"。正确路径：`FIN_WAIT_1 → CLOSING → TIME_WAIT`，前提是双方都成为主动关闭方 | 16.6.1、16.8 |

## 延伸阅读

- RFC 793《Transmission Control Protocol》——状态机定义（含状态转移图）、ISN 选择、`TIME_WAIT` 与 `MSL` 的原始表述。
- RFC 1122《Requirements for Internet Hosts — Communication Layers》4.2.2 节——TCP 实现的最低要求，含半关闭与 `TIME_WAIT` 的强制项。
- RFC 6298《Computing TCP's Retransmission Timer》——握手期 `SYN` 重传与 RTO 初值（`1 s`）的规范来源。
- RFC 7323《TCP Extensions for High Performance》——窗口扩大因子、时间戳与 PAWS、SACK 选项，是理解"syncookies 牺牲了什么"的必备背景。
- Linux 手册页 `man 7 tcp`、`man 7 socket`——`tcp_tw_reuse`/`tcp_fin_timeout`/`tcp_syncookies`/`SO_LINGER`/`SO_REUSEPORT` 的权威语义，注意区分官方文档与网上偏方。
- Linux 内核文档 `Documentation/networking/ip-sysctl.rst`——`tcp_max_syn_backlog`、`tcp_abort_on_overflow` 与 accept 队列溢出的精确行为。
- 《TCP/IP 详解 卷 1：协议》第 17~18 章——连接建立与终止的报文级时序，可作抓包对照（第 19 章讲怎么读这些报文）。


---

> 作者: [破戒僧](https://onlyblacktea.cn)  
> URL: https://onlyblacktea.cn/book/backend/ch16/  

