为什么我背了很多年 TCP 三次握手,还是总觉得差一点?
写在前面
TCP 三次握手这个东西,我学过很多次。
- 上学的时候学过
- 找工作的时候背过
- 面试的时候讲过
- 抓包的时候也看过
但很多年里,我一直有一种感觉:
好像懂了,又好像没真正懂。
因为网上大多数解释,虽然没错,但总是在说这些话:
- 确认双方收发能力
- 同步序列号
- 防止历史连接
这些结论都对。
但它们没有让我真的"想通"。
后来我才发现,问题不在答案不对,而在于我一直站在包的角度看 TCP。
我看到的是:
text
SYN
SYN + ACK
ACK
看到的是:
text
FIN
ACK
FIN
ACK
但我一直没先回答一个更底层的问题:
连接到底是什么?
一、连接不是一根线,而是内核里的一个状态对象
很多人脑子里对"连接"的想象,其实是一根线:
text
客户端 ---------------- 服务器
好像线通了,连接就建立了。
但 TCP 在内核里不是这么工作的。
从代码视角看,连接不是一根线,而是一个对象。
你可以把它理解成:
- 一块内存
- 一个变量
- 一个有状态的内核对象
这个对象里要记录很多东西:
- 当前状态是什么
- 自己的序列号走到哪里
- 对方的序列号确认到哪里
- 我下一步能不能继续
所以 TCP 真正管理的,不是"线通没通",而是:
这个对象现在能不能切到下一个状态。
这时候很多事情就变了。
你不再关心:
一共发了几个包?
而开始关心:
这个对象凭什么有资格切状态?
二、TCP 最关键的一点,不是我发了什么,而是我收到了什么
我后来觉得,TCP 里最容易被忽略的一点就是:
状态切换,盯的不是"我发了什么",而是"我收到了什么"。
为什么?
因为"发"只能说明你的意图。
你发了一个 SYN,只能说明:
我想建立连接。
它不能说明:
- 对方收到了
- 对方同意了
- 对方已经为这个连接建好了自己的状态
所以 TCP 真正决定状态能不能往下走的,不是"我已经发出去了",而是:
我有没有收到对方的 ACK。
这就是我后来真正想通的地方。
连接对象的状态切换,本质上是在等对方的确认把状态闭上。
三、这样再看三次握手,突然就顺了
如果只背包,三次握手是:
text
SYN
SYN + ACK
ACK
但如果换成"连接对象状态切换"的视角,它其实简单很多。
第一次:客户端发 SYN
这只能说明一件事:
我想连你。
客户端这边可以把自己的状态推进一点,但推进不到最终状态。
因为它还没收到任何确认。
也就是说,这时候连接对象还不能说"连接建立好了"。
第二次:服务器回 SYN + ACK
这时候服务器做了两件事:
- 告诉你:你的 SYN 我收到了
- 告诉你:我这边也为这个连接建好状态了
所以客户端这边一看,才知道:
- 对方收到我了
- 对方也能回我
客户端这边的对象,状态就可以继续推进。
但服务器那边还没完全闭上。
为什么?
因为服务器虽然把 SYN + ACK 发出去了,但它还不知道:
客户端到底有没有收到我这个回应。
第三次:客户端回 ACK
这一发很多人以前总觉得像废话。
其实不是。
这一发的意义在于:
它是服务器那边状态闭合所必须的一步。
服务器收到这个 ACK,才能确认:
我前面发出去的
SYN + ACK,对方确实收到了。
到这里,两边的连接对象状态才真正都闭上了。
所以三次握手为什么是三次?
不是因为"三"这个数字神圣。
而是因为:
要让两边的状态对象都闭合,三次刚好够。
四、为什么不是两次?因为两次只能让一边安心,不能让两边都安心
如果只有两次:
text
客户端 -> SYN
服务器 -> SYN + ACK
客户端大概还能觉得差不多了。
但服务器不行。
服务器会卡在一个很尴尬的状态里:
我知道你来找我了,我也回你了,但我不知道你到底有没有收到我。
也就是说,服务器那边的状态对象还没闭上。
所以两次不够。
它不够的根本原因,不是什么口诀,而是:
服务器没拿到那个能让自己状态切换的 ACK。
五、四次挥手也是同一个逻辑,只不过 FIN 不是 SYN
很多人握手背完以后,接着又会卡在挥手:
- 为什么通常是四次?
- 为什么有时候又是三次?
如果继续只盯包看,就会继续乱。
但如果你已经接受了前面那个视角,其实这里很好理解。
关键在于:
FIN 不是"连接立即关闭",而是"我这个方向没数据了"。
也就是说,TCP 是双向的。
你这个方向关了,不代表对方那个方向也马上关。
所以关闭连接,天然就比建立连接更容易拆成两段。
六、为什么通常是四次?因为 ACK 不能等,FIN 可以等
假设客户端先发一个 FIN。
服务器收到以后,首先必须立刻回一个 ACK。
因为客户端在等确认。
但服务器不一定能立刻发自己的 FIN。
因为它可能:
- 还有数据没发完
- 还有业务没处理完
- 还没准备好关闭自己这个方向
所以这里自然就会拆开:
- 先 ACK:我知道你这边发完了
- 再 FIN:我这边现在也发完了
所以四次挥手的本质不是"四步模板",而是:
ACK 不能等,FIN 可以等。
七、为什么有时候又会变成三次?因为 ACK 和 FIN 正好可以合并
如果服务器收到对方 FIN 的时候,自己也正好没数据要发了,那么它完全可以直接回:
text
ACK + FIN
这样四次就压成三次了。
所以这里没有什么神秘现象。
就是一句话:
能合并就合并,不能合并就分开。
决定因素不是模板,而是时机。
八、到这里,三次握手和四次挥手其实都被一条逻辑打通了
我后来重新看 TCP,真正补上的不是某个知识点,而是一种视角。
这套视角其实很简单:
1. 连接不是线,是内核里的状态对象
你甚至可以把它粗暴理解成:
一个变量,一块内存,一个有状态的对象。
2. 这个对象能不能切状态,不看我发了什么,看我收到了什么
更准确地说:
看我有没有收到对方的 ACK。
3. 所谓三次握手、四次挥手,本质上都在做一件事
闭状态。
这时候你再去看那些经典问题:
- 为什么是三次
- 为什么不是两次
- 为什么挥手通常是四次
- 为什么有时候又是三次
就会发现它们其实不是四个问题。
它们本质上都是一个问题:
连接对象的状态,到底什么时候才算闭合?
九、回到抓包和排障,真正该看的不是"发了几个包",而是谁还没等到 ACK
我后来再看抓包,思路就变了。
我不再先看:
- 这是第几次握手
- 这是第几个 FIN
- 这个 ACK 是不是多余
而是先看:
现在是谁的状态还没闭上?
比如:
只看到 SYN,没有 SYN + ACK
那意思不是抽象地说"握手没完成"。
而是:
客户端已经想建连接了,但客户端还没收到能让状态继续推进的 ACK。
看到 SYN、SYN + ACK,但看不到最后 ACK
那更具体地说就是:
服务器那边的状态还没闭上。
挥手时只看到 ACK,看不到 FIN
那不是"少了一步"。
而是:
对方先确认了你的关闭请求,但它自己那个方向还没结束。
一旦这么看,TCP 就不会再只是"背模板"。
它会变成一个很清楚的状态流转问题。