「TCP 三次握手」大概是计算机网络里被问得最多的一道题。但大部分人的答案停在「为了保证可靠」——这句话是对的,却等于没说。
真正值得搞清楚的是三个更具体的问题:为什么是三次而不是两次?为什么挥手要四次?TIME_WAIT 到底在等什么?这篇逐帧拆开讲。
先看几个字段
TCP 头里跟握手挥手相关的只有四个东西:
| 字段 | 作用 |
|---|---|
| SYN | 请求建立连接,同时用掉一个序号 |
| ACK | 确认收到对方的序号 |
| FIN | 我没有数据要发了(半关闭) |
| seq / ack | 序号和确认号,用来排序和去重 |
记住一件事:SYN 和 FIN 各占一个序号。这一点决定了后面挥手时 ack 号为什么会跳 1。
三次握手
客户端 服务器 | --- SYN seq=x ------------------> | 第一次:我想连你,我的起始序号是 x | | 第三次:确认收到你的 y,连接建立 | | |============= 数据传输 ============== |
具体一点,用抓包会看到的顺序:
- 客户端发
SYN, seq=1000 - 服务器回
SYN, ACK, seq=5000, ack=1001 - 客户端发
ACK, seq=1001, ack=5001
注意 ack 是 x+1 而不是 x——因为 SYN 本身占用了一个序号。
为什么是三次,不是两次
这是全文最关键的一段。
两次握手的话,客户端发 SYN,服务器回 SYN+ACK,双方就认为连上了。问题出在网络里的旧报文上。
设想这个场景:
- 客户端发了一个 SYN,因为网络拥堵卡在某个路由器里很久
- 客户端等不到响应,超时重发了一个新的 SYN,建连、传数据、关闭
- 那个卡了很久的旧 SYN 此时终于到了服务器
如果是两次握手,服务器收到旧 SYN 就会立刻认为连接建立,分配资源、等待数据。但客户端根本不认为这条连接存在——于是服务器就挂着一条永远不会被使用的连接,白占资源。
三次握手正好能挡住它:服务器回 SYN+ACK 之后,客户端发现这个 ack 对不上自己当前的序号,直接发 RST 拒绝。连接没建起来。
说白了,第三次握手的唯一作用,是让客户端确认「服务器收到的那个请求,是我现在想要的这个,不是以前的某个残影」。
为什么不是四次
既然多一次更稳,为什么不干脆四次?
因为第二次把两个动作合并了:SYN(我也要建立连接)和 ACK(确认收到你的 SYN)。一台机器收到 SYN 之后,本来就要同时做这两件事,合并成一个包没有任何信息损失,还省了一次往返。
所以握手的下限是三次:发起 → 回应并确认 → 确认回应。少一次挡不住旧报文,多一次是纯浪费。这是「刚好够用」的设计,不是「越多越好」。
四次挥手
客户端 服务器 | --- FIN, seq=u ------------------> | 第一次:我没数据要发了 | <-- ACK, ack=u+1 ----------------- | 第二次:收到了(此时服务器可能还在发) | | 第四次:好的,你关吧 | | 客户端进入 TIME_WAIT,等 2MSL
关键点在第二次和第三次之间:服务器收到 FIN 之后,不一定马上也能关。
比如你上传完文件,客户端数据发完了,可以关。但服务器这边可能还有一堆积压的响应没发给你。所以它先回一个 ACK 表示「知道你要关了」,然后继续把剩下的数据发完,等自己也空了,才发自己的 FIN。
这就是「半关闭」:连接的两个方向各自独立关闭。客户端关了自己的发送方向,仍能接收;服务器还能继续发。
如果服务器也刚好没数据了,第二次和第三次可以合并成一个包——你会看到抓包里只有三个包,但语义上仍然是四次动作。
TIME_WAIT 到底在等什么
客户端发完最后一个 ACK 后,不立刻释放连接,而是进入 TIME_WAIT 状态,等 2MSL(通常 60 秒)。这段时间它做什么?
第一件事:保证最后那个 ACK 能到达。
如果这个 ACK 在路上丢了,服务器会重发 FIN。客户端此时如果已经彻底关掉,收到一个不认识的 FIN,只能回 RST,服务器那边的连接就会异常终止而不是优雅关闭。留在 TIME_WAIT 就是为了能重发 ACK。
第二件事:让旧连接的残余报文在网络里彻底消亡。
MSL 是报文在网络中的最大生存时间,等 2MSL,意味着这条连接的所有报文要么已经到达,要么已经过期。这样新连接用同样的四元组建立时,不会被旧报文污染。
为什么是 2 倍而不是 1 倍?因为要覆盖一个来回:你去 + 它回。
看两种状态,能定位两类问题
服务器上跑 ss -ant | awk '{print $1}' | sort | uniq -c,如果发现某个状态特别多,含义是不同的:
大量 TIME_WAIT —— 主动关闭太多
TIME_WAIT 出现在主动关闭的一方。数量多通常是这个原因:
- 程序用短连接,每次请求都新建、用完就关
- 你是服务端,但配置成了服务端主动关闭(比如设置了
Connection: close) - 爬虫或压测在疯狂建短连接
危害主要是占满本地端口。从一个 IP 访问另一个固定 IP:端口,可用四元组最多约 6 万个,TIME_WAIT 堆到几万就建不了新连接了。
解决办法按优先级:改用连接复用(keep-alive / 连接池),这是根治;然后才是调内核参数(tcp_tw_reuse)缓解。
大量 CLOSE_WAIT —— 代码忘了关
CLOSE_WAIT 表示对方已经发了 FIN,我这边还没关。
这个状态的堆积几乎百分之百是程序 bug:拿到连接用完没调用 close(),或者异常分支里漏了释放。因为是被动方,操作系统不会替你关,只能等进程结束。
看到 CLOSE_WAIT 成千上万,别去调内核参数,去查代码——通常是数据库连接池泄漏、HTTP 客户端没用 finally 释放。
用抓包验证
Wireshark 里过滤 tcp.flags.syn==1 && tcp.flags.ack==0 只看握手请求;过滤 tcp.flags.fin==1 只看挥手。Linux 上临时抓一下:
tcpdump -i eth0 -nn 'tcp port 80 and (tcp[tcpflags] & (tcp-syn|tcp-fin) != 0)'
看到 [S] 是 SYN,[S.] 是 SYN+ACK,[.] 是 ACK,[F.] 是 FIN+ACK。对照上面的时序图,一眼能看出卡在哪一步:
- 只有
[S]没有[S.]→ 请求没到服务器,或被防火墙拦了 - 有
[S.]但没有最后的[.]→ 回程被拦,常见于安全组只放了单向 - 反复出现
[S]间隔递增 → SYN 重传,是典型的超时
小结
三次握手解决的是旧报文误建连,四次挥手解决的是连接的两个方向要分别关闭,TIME_WAIT 解决的是最后一个 ACK 可能丢失和旧报文污染新连接。
把这三件事记住,比背”三次握手保证可靠传输”有用得多。面试官问为什么是三次,答「防止历史连接被服务端误接受」才算答到点上。
相关的实操工具:想验证某个端口通不通,看本站的 TCP/UDP 端口速查;想算网段,用 子网计算器。










暂无评论内容