适用读者:背过"三次握手四次挥手"但说不清"为什么"的人;被 TIME_WAIT 和连接数坑过的人。
前置:S02-B01(分层与 socket)。
实测环境:Ubuntu 24.04.1 / Linux 6.8 / iproute2;实测日期 2026-11-05。
覆盖旧文:A-1033(TCP 三次握手、四次挥手,104 点)。
覆盖扩展:E-S02-005(首部字段与选项)、E-S02-007(TIME_WAIT 与端口耗尽)。
上一篇:S02-B02 · 进程与线程:为什么"多开一个线程"不等于"多一份 CPU"
下一篇:S02-B04 · TCP 可靠传输与 UDP。
1. 一句话结论
三次握手的本质是"双方各自确认对方的收发能力",四次挥手的本质是"两个方向的连接要分别关闭"。
| 问题 | 答案 |
|---|---|
| 为什么是三次不是两次? | 两次只能让"发起方"确认,被动方无法确认自己的发送能力被对方收到 |
| 为什么是四次不是三次? | TCP 是全双工,两个方向需要各自 FIN/ACK;被动方收到 FIN 后可能还有数据要发,所以 ACK 与 FIN 不能合并 |
| TIME_WAIT 为什么必须存在? | 保证"最后的 ACK 能到达" + "让旧连接的残余包在网络中消亡" |
2. 三次握手:每一步解决什么
客户端 服务端
│ ── SYN(seq=x) ────────────────────► │ ① 我想连你,我的初始序号是 x
│ │ (服务端:SYN 队列 +1)
│ ◄────────── SYN+ACK(seq=y, ack=x+1) ──│ ② 我收到了(ack=x+1),我也想连你(seq=y)
│ │
│ ── ACK(ack=y+1) ───────────────────► │ ③ 我收到了你的 SYN
│ │ (连接进入 ESTABLISHED,accept 队列)
| 步 | 解决什么 | 如果没有会怎样 |
|---|---|---|
| ① SYN | 客户端告诉服务端"我要连,我的序号起点" | 服务端不知道有连接请求 |
| ② SYN+ACK | 服务端同时确认客户端(ack)并发出自己的 SYN | 客户端不知道"服务端是否收到我的 SYN" |
| ③ ACK | 客户端确认服务端的 SYN | 服务端不知道"自己的 SYN 是否被收到" → 这正是"两次不够"的原因 |
2.1 "两次为什么不够":一个具体反例
假设只有两次(SYN → SYN+ACK 就建连)。此时:
- 客户端确认了:我能发(SYN 到了)、我能收(收到 SYN+ACK)。
- 服务端确认了:我能收(收到 SYN)。
- 服务端没确认:我能发(我的 SYN+ACK 有没有到客户端?)
后果:如果 SYN+ACK 在路上丢了,客户端会重发 SYN,服务端却以为连接已建立,可能开始发数据——发出去的数据没人接收,白白占用资源。第三次 ACK 就是为了消掉这个不确定性。
另一个经典解释:防止历史重复连接。网络中滞留的旧 SYN 到达服务端时,两次握手会让服务端直接建连(浪费资源),三次握手让客户端有机会用 RST 拒掉。
2.2 两个队列(面试常问、排障常用)
| 队列 | 存什么 | 溢出表现 |
|---|---|---|
| SYN 队列(半连接队列) | 收到 SYN、还没完成三次握手的连接 | SYN Flood 攻击的目标;netstat -s | grep -i "SYNs to LISTEN" |
| accept 队列(全连接队列) | 已完成握手、等应用 accept() |
应用处理慢时溢出;ss -lnt 的 Recv-Q 持续 > 0 |
# 观察监听队列积压(Recv-Q 是当前排队数,Send-Q 是 backlog 上限)
$ ss -lnt
State Recv-Q Send-Q Local Address:Port
LISTEN 0 511 0.0.0.0:80
判据:Recv-Q 长期接近 Send-Q → 应用 accept 太慢,不是网络问题。
2.3 SYN Flood 与防护
攻击者发大量 SYN 不回 ACK → 半连接队列占满 → 正常用户连不上。
| 防护 | 机制 |
|---|---|
tcp_syncookies=1 |
不保存半连接状态,用 cookie 编码在序号里(默认开启) |
调大 tcp_max_syn_backlog |
提高忍耐度 |
调小 tcp_synack_retries |
更快放弃无效握手 |
3. 四次挥手:为什么不能合并成三次
主动关闭方 被动关闭方
│ ── FIN(seq=u) ───────────────────► │ ① 我没有数据要发了(但还能收)
│ ◄──────────────────── ACK(u+1) ──── │ ② 收到了(此时主动方进入 FIN_WAIT_2)
│ │ 【被动方可能还有数据要发】
│ ◄──────────────────── FIN(seq=w) ─── │ ③ 我也没有数据要发了
│ ── ACK(w+1) ──────────────────────► │ ④ 确认(主动方进入 TIME_WAIT)
为什么 ② 和 ③ 不能合并:被动方收到 FIN 时,它可能还有数据没发完(半关闭状态)。所以先 ACK(告诉对方"我知道你要关了"),等自己数据发完再发 FIN。合并就丢失了"半关闭"这个能力。
特殊情况:如果被动方也没有数据要发,ACK 与 FIN 可以在同一个包里发出(延迟确认机制下常见),这时看起来像"三次挥手"——但语义上仍是四次。
4. TIME_WAIT:实测它会累积(E-S02-007)
4.1 实测
$ ss -tan state time-wait | wc -l
6 # 初始
# 发起 30 次"主动关闭"的短连接后
$ ss -tan state time-wait | wc -l
36 # 增加 30
结论:TIME_WAIT 出现在"主动关闭"的一方。服务端如果是主动关闭方(如 nginx 关闭空闲 keepalive 连接),TIME_WAIT 就堆在服务端。
4.2 为什么必须有 TIME_WAIT(两个理由)
| 理由 | 说明 |
|---|---|
| 保证最后的 ACK 到达 | 如果 ACK 丢了,被动方会重发 FIN;此时主动方还在 TIME_WAIT 就能重发 ACK。若直接关闭,被动方会收到 RST |
| 让旧连接的残余包消亡 | 同一四元组短时间内复用,旧包到达会被误认为新连接的数据。TIME_WAIT 持续 2×MSL(Linux 约 60 秒)让旧包过期 |
4.3 什么时候会出事:端口耗尽
| 场景 | 症状 | 缓解 |
|---|---|---|
| 客户端频繁短连接(未复用) | Cannot assign requested address |
连接池/keepalive(根本解);tcp_tw_reuse=1(仅对出向连接有效) |
| 服务端主动关闭过多 | TIME_WAIT 堆积占端口 | 让客户端先关;调 tcp_max_tw_buckets(治标) |
| 大量 4xx 短请求 | 同上 | 上 keepalive + 限制 QPS |
注意:
tcp_tw_recycle在新内核已被移除(NAT 环境下会导致连接失败)。网上老文章还在推荐它——不要用。
5. 首部字段:哪些字段在握手/挥手里起作用(E-S02-005)
| 字段 | 作用 | 相关场景 |
|---|---|---|
| 源/目的端口 | 定位连接 | 四元组 |
| 序号(seq) | 字节流编号 | 可靠性基础(B04) |
| 确认号(ack) | 期望收到的下一个字节 | 同上 |
| 数据偏移 | 首部长度(20–60 字节) | 有选项时变大 |
| 标志位 | SYN/ACK/FIN/RST/PSH/URG | 握手挥手全靠它 |
| 窗口(window) | 接收方可接收的字节数 | 流量控制(B04) |
| 校验和 | 检测传输错误 | — |
| 选项 | MSS、Window Scale、SACK、时间戳 | 影响性能(见下) |
5.1 三个"看不见但很关键"的选项
| 选项 | 作用 | 不启用会怎样 |
|---|---|---|
| MSS | 协商每个段的最大数据量(通常 1460) | 可能分片,性能下降 |
| Window Scale | 把 16 位窗口放大到最多 1 GB | 高延迟链路吞吐上不去(BDP 受限) |
| SACK | 选择性确认,只重传丢的那段 | 丢一个包要重传后面全部 |
排障含义:ss -ti 里能看到 wscale、mss、sack 相关字段——高延迟高带宽场景下,"窗口太小"往往比"带宽不够"更常见。
6. 用命令观察连接状态
# 当前各状态连接数
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# 只看 TIME_WAIT 数量
ss -tan state time-wait | wc -l
# 连接详情(含 RTT、重传、拥塞窗口)
ss -ti state established
实测样例(B01 已出现过):
cubic wscale:7,7 rto:204 rtt:3.145/0.924 ... cwnd:10 ... retrans 相关字段
rtt:3.145/0.924= 平滑 RTT / 平均偏差(毫秒)cwnd:10= 拥塞窗口(单位 MSS)rto:204= 重传超时(毫秒)
这三项在 B04 与 J01 里会用到。
7. 五个误区
| 误区 | 真相 |
|---|---|
| "三次握手是为了同步序号" | 是原因之一,但核心是双向确认能力 |
| "TIME_WAIT 是 bug" | 是保护机制;只有主动关闭方才有 |
"tcp_tw_recycle=1 能解决 TIME_WAIT" |
新内核已移除;NAT 下会造成连接失败,不要用 |
| "服务端不用管 TIME_WAIT" | 服务端主动关闭 keepalive 连接时同样会堆 |
| "握手成功就一定能传数据" | 还要看 accept 队列、窗口、路由 MTU |
8. 自检
- 用"双向确认"解释为什么两次握手不够。
- 为什么 FIN 与 ACK 不能总是合并?
ss -lnt里Recv-Q持续接近Send-Q说明什么?- 服务端出现大量 TIME_WAIT,可能是什么原因?根治办法是什么?
Window Scale不启用时,什么场景会明显变慢?
9. 下一篇
S02-B04 · TCP 可靠传输与 UDP:谁在保证"不丢、不乱、不重"
握手只解决"能开始说话"。下一篇讲清楚:序号、确认、重传、窗口、拥塞控制如何协作保证可靠;以及 UDP 为什么在某些场景反而更好(含 QUIC 的思路)。会用 tc netem 注入延迟做实测对照。
本篇属于系列《S02 · 计算机与网络基础》。旧文来源:A-1033;扩展点 E-S02-005/007。TIME_WAIT 累积为 2026-11-05 实测(6 → 36)。