2506 字
约 8 分钟
7
S02-B03 · TCP 三次握手与四次挥手:为什么是三次不是两次

适用读者:背过"三次握手四次挥手"但说不清"为什么"的人;被 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. 自检

  1. 用"双向确认"解释为什么两次握手不够。
  2. 为什么 FIN 与 ACK 不能总是合并?
  3. ss -lnt 里 Recv-Q 持续接近 Send-Q 说明什么?
  4. 服务端出现大量 TIME_WAIT,可能是什么原因?根治办法是什么?
  5. 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)。

S02-B03 · TCP 三次握手与四次挥手:为什么是三次不是两次
https://www.clxhxhhr.top/posts/5184/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。