TCP 连接管理:三次握手与四次挥手
三次握手和四次挥手,几乎每个学过网络的人都能把步骤复述出来,但真正能讲清楚的人不多。问题往往出在学习顺序上:很多人是先记住"为什么要三次"这个结论,却没先搞懂握手过程中,双方的状态和序列号到底是怎么一步步变化的。结论悬在空中,自然站不稳。
所以这一篇我们换个顺序:先把机制本身讲透——每一步发了什么报文、报文里装了哪些字段、双方的连接状态如何迁移——把这套运转过程看清楚之后,再回过头解释每一步是为了解决什么问题。
要先记住一件事:TCP 是一个有状态的协议。每一条连接的两端,内核里都各自维护着一个状态机。握手和挥手的全过程,本质上就是这两个状态机在报文的驱动下一步步迁移。盯住状态的变化,整件事就清楚了。
一、先认识几个关键字段和状态
要看懂机制,得先认识参与其中的"零件"。
报文头里我们这次重点关注三个标志位和两个编号:
- SYN:同步标志,置 1 表示这是一个用于建立连接、同步序列号的报文;
- ACK:确认标志,置 1 表示报文里的"确认号"字段有效;
- FIN:结束标志,置 1 表示发送方已经没有数据要发了;
- seq(序列号):本报文所携带数据的第一个字节的编号;
- ack(确认号):期望收到对方的下一个字节的编号,等于"已收到的最后一个字节编号 + 1"。
连接两端各自会经历一系列状态,建立阶段涉及:CLOSED、LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED;关闭阶段涉及:FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、LAST_ACK、TIME_WAIT、CLOSED。
下面所有内容,就是看这些状态在报文驱动下怎么走。先不用记,跟着流程走一遍自然就懂了。
二、三次握手的机制:状态与序列号怎么变
我们假设客户端主动连接服务端。服务端已经在 LISTEN 状态等待,客户端处于 CLOSED。先用一张时序图建立整体印象,再逐步拆解:
第一步——客户端发送 SYN:
客户端选一个初始序列号(ISN),设 seq = x,把 SYN 标志置 1,发出报文。
- 报文内容:
SYN=1, seq=x - 客户端状态:
CLOSED→SYN_SENT
注意 SYN 报文本身不携带数据,但它会消耗一个序列号,也就是说它逻辑上占了编号 x 这个位置。
第二步——服务端回 SYN + ACK:
服务端收到后,也选自己的初始序列号 seq = y,同时确认客户端的 SYN。
- 报文内容:
SYN=1, ACK=1, seq=y, ack=x+1 ack = x+1表示:“你的 x 我收到了,下一个我期望收到 x+1”- 服务端状态:
LISTEN→SYN_RCVD
这一个报文同时干了两件事:用 SYN 同步自己的序列号 y,用 ACK 确认对方的序列号 x。
第三步——客户端回 ACK:
客户端收到后,确认服务端的序列号。
- 报文内容:
ACK=1, seq=x+1, ack=y+1 ack = y+1表示:“你的 y 我也收到了”- 客户端状态:
SYN_SENT→ESTABLISHED - 服务端收到这个 ACK 后:
SYN_RCVD→ESTABLISHED
到这里,双方都进入 ESTABLISHED,连接建立完成,后续数据传输的序列号就分别从 x+1 和 y+1 接着往下走。
把机制串起来看,三次握手其实是两组"SYN + 对应 ACK":客户端的 SYN(x) 被服务端的 ACK(x+1) 确认,服务端的 SYN(y) 被客户端的 ACK(y+1) 确认。只不过中间那一次,服务端把"确认你的"和"同步我的"合并进了一个报文,所以看起来是三次而不是四次。
三、看懂机制后,再问:为什么必须三次
现在双方的状态迁移和序列号交换已经清楚了,我们再来回答那个经典问题,就会非常自然。
可靠通信要求两个方向的收发能力都被确认,也就是上面说的两组 SYN-ACK 都要完成。我们数一下,到第几步才凑齐:
- 第二步结束时:客户端确认了"我发的 x 服务端收到了",也知道"服务端能发能收";
- 第三步结束时:服务端才确认"我发的 y 客户端收到了"。
如果砍掉第三次,服务端永远无法确认"自己的 SYN(y) 到底有没有送达客户端"——它的序列号同步是单向的、没被确认的。少了这一步,可靠性的前提就缺了一角。
还有一个机制层面的理由——防止历史连接。假设客户端一个很早的 SYN 在网络里滞留许久才到达服务端,客户端其实早已放弃。如果只要两次握手,服务端一收到旧 SYN 就进入 ESTABLISHED 并分配资源。而有了第三次,客户端面对这个自己并不认账的连接,会回一个 RST 而不是 ACK,服务端就不会误建。
所以"第三次握手"不是多余的礼节,它在机制上补齐了"服务端序列号是否被确认"这唯一缺口,同时给了客户端否认历史连接的机会。
四、四次挥手的机制:为什么状态多了一倍
关闭连接的状态迁移比建立更复杂,因为 TCP 是全双工的——一条连接是两个方向的字节流,每个方向要各自独立地关闭。我们还是逐报文看状态怎么走。假设客户端主动关闭。
第一步——客户端发 FIN:
- 报文内容:
FIN=1, seq=u(u 是客户端当前序列号) - 客户端状态:
ESTABLISHED→FIN_WAIT_1 - 含义:"我这个方向的数据发完了。"FIN 同样消耗一个序列号。
第二步——服务端回 ACK:
- 报文内容:
ACK=1, ack=u+1 - 服务端状态:
ESTABLISHED→CLOSE_WAIT - 客户端收到这个 ACK 后:
FIN_WAIT_1→FIN_WAIT_2 - 含义:"知道你不发了。“但此时服务端可能还有数据没发完,所以它只确认、暂不发自己的 FIN。这时客户端→服务端方向已关,服务端→客户端方向仍开着——这就是所谓的"半关闭”。
第三步——服务端发 FIN:
等服务端把自己剩下的数据都发完,它才发 FIN。
- 报文内容:
FIN=1, ACK=1, seq=w, ack=u+1 - 服务端状态:
CLOSE_WAIT→LAST_ACK - 含义:“现在我这个方向也发完了。”
第四步——客户端回 ACK:
- 报文内容:
ACK=1, ack=w+1 - 客户端状态:
FIN_WAIT_2→TIME_WAIT - 服务端收到后:
LAST_ACK→CLOSED
机制上最关键的就是第二步和第三步不能合并:服务端收到 FIN 时手里很可能还有数据要发,所以"确认对方关闭"和"自己也要关闭"是两个时刻、两个报文。这正是挥手比握手多一次的根源——握手时服务端能把 ACK 和 SYN 合并,挥手时却不行。
五、TIME_WAIT 的机制:它在等什么
上面看到,主动关闭方在发出最后一个 ACK 后,没有直接 CLOSED,而是进入 TIME_WAIT,停留约 2 倍报文最大生存时间(2MSL)才真正关闭。从机制上讲,这段等待守着两件具体的事:
- 兜底最后那个 ACK。 万一第四步的 ACK 在网络里丢了,服务端(处于
LAST_ACK)会重发它的 FIN。如果客户端发完 ACK 就立刻CLOSED,就没人回应这个重发的 FIN,服务端会一直卡在LAST_ACK。停留在TIME_WAIT就是为了能重发这个 ACK。 - 让旧报文自然消亡。 等满 2MSL,能确保这条连接在网络里所有迟到的残留报文都已过期消失,不会窜进之后用相同四元组建立的新连接里造成污染。
六、把整套状态机连起来看
到这里,建立和关闭的机制都拆开了,我们用状态迁移把它串成一条完整的主线,这是这一篇最该记住的骨架:
- 建立:
CLOSED → SYN_SENT → ESTABLISHED(客户端);LISTEN → SYN_RCVD → ESTABLISHED(服务端)。 - 关闭:
ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED(主动方);ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED(被动方)。
每一次状态迁移,都是被一个具体报文(SYN / ACK / FIN)触发的。一旦你脑子里有这张状态机,再看 netstat/ss 输出里的那些状态,就知道每条连接卡在了哪一步、下一步在等什么报文。
七、抓包视角:让机制变得可见
理解状态机最快的办法,是用 tcpdump 或 Wireshark 抓一次真实连接。
抓一次完整的 HTTP 请求,你会清楚看到开头三个包 SYN → SYN, ACK → ACK,结尾四个包 FIN, ACK → ACK → FIN, ACK → ACK。更进一步,把抓包里的 Seq= 和 Ack= 字段和上面讲的 x、x+1、y、y+1 对上号,你会非常直观地看到序列号是怎么递进和确认的——这比任何流程图都让人记得牢。
八、和工程实践 / 后端开发的联系
把状态机和真实排障对应起来,这些知识立刻就有用了:
- CLOSE_WAIT 堆积:连接卡在
CLOSE_WAIT说明收到了对方的 FIN,但本端应用迟迟没调用 close(),挥手停在第二步之后走不下去。这几乎总是应用代码或连接池没正确关连接的 bug,是非常强的排障信号。 - TIME_WAIT 过多:大量连接停在
TIME_WAIT是主动关闭方的正常产物,尤其高并发短连接下。优化方向是长连接 / 连接池 / Keep-Alive,减少连接的建立与关闭次数。 - 连接建立的 RTT 开销:三次握手要花一个往返时间,这正是 HTTP Keep-Alive 和连接池存在的根本动机——复用已经处于
ESTABLISHED的连接,省掉反复握手。
九、学习这一部分最容易踩的坑
1. 跳过状态机,直接背"三次/四次"
数字是机制运转的结果。不盯住状态迁移和序列号变化,结论永远记不牢。
2. 以为挥手一定是四次
如果被动方收到 FIN 时已经没有数据要发,第二步的 ACK 和第三步的 FIN 可以合并,挥手就变成三次。四次是常见情况,不是铁律。
3. 把 CLOSE_WAIT 和 TIME_WAIT 搞混
CLOSE_WAIT 在被动关闭方,通常是应用没 close 的 bug;TIME_WAIT 在主动关闭方,多数是正常现象。二者排障方向完全相反,混了就会找错人。
4. 忽略 SYN/FIN 会消耗序列号
SYN 和 FIN 虽然不带数据,但各占一个序列号,所以确认号是 x+1 而不是 x。这点想不通,序列号就永远对不上。
总结
这一篇我们坚持先讲机制、再讲设计,核心要带走的是这张状态机和它背后的因果:
- TCP 是有状态协议,握手挥手的本质是两端状态机在 SYN / ACK / FIN 报文驱动下的迁移;
- 三次握手 = 两组"SYN + ACK"的序列号同步与确认,中间一次把 ACK 和 SYN 合并,所以是三次;
- 必须三次,是为了让服务端的序列号也被确认,并挡住历史连接;
- 四次挥手源于全双工——两个方向独立关闭,被动方收到 FIN 时可能还有数据要发,故 ACK 与 FIN 不能合并;
- TIME_WAIT 停留 2MSL,是为了兜底最后的 ACK 重传、并让旧报文自然消亡;
- 工程上,
CLOSE_WAIT指向应用没关连接,TIME_WAIT指向短连接过多,方向截然不同。
当你能在脑子里默画出那条 SYN_SENT → ESTABLISHED → FIN_WAIT → TIME_WAIT 的迁移链,并说清每一步是被哪个报文推动的,这部分就真正属于你了。
参考资源:
- 《计算机网络:自顶向下方法》
- 《TCP/IP 详解 卷一:协议》
- RFC 793 - Transmission Control Protocol
- Wikipedia - TCP state diagram