「各位 007 的代码搬运工、年轻的网工朋友们,大家好!」
如果在计算机网络界搞一个“最熟悉的陌生人”排行榜,「TCP 协议」 绝对霸榜。
每天面试都在背:“三次握手,四次挥手”。但你真的懂它吗?为什么不能两次握手?为什么分手非要拖拖拉拉挥四次?
其实,TCP 协议不仅是一个严谨的技术标准,更是一位「讲礼貌、守规矩、甚至有点“强迫症”的绅士」。今天我们就脱下枯燥的协议外衣,用一种(稍微)不正经的方式,聊聊 TCP 的情感史。
第一幕:三次握手 —— 确认眼神,遇上对的人
「场景:」 客户端(Client)想找 服务端(Server)建立连接。
在不可靠的网络世界里,想建立一段可靠的“感情”(连接),必须确认「双方的收信能力和发信能力」都没问题。
剧情是这样的:
「第一次握手 (SYN)」
- 「客户端」羞涩地发了一条微信:“在吗?我想跟你聊聊。(SYN=1, seq=x)”
- 此时客户端状态:SYN_SENT(已发送,焦急等待中)
「第二次握手 (SYN + ACK)」
- 「服务端」
- 「服务端」:“在的在的!我听到了(ACK=x+1)。我也想跟你聊聊(SYN=1, seq=y),你能听到我吗?”
- 此时服务端状态:SYN_RCVD(已收到,期待回信)
- 「敲黑板:」 这里服务端很聪明,把“确认收到”(ACK)和“我也发起连接”(SYN)合并在一个包里发回去了,省了一次快递费。
「第三次握手 (ACK)」
- 「客户端」看到回信,激动万分:“听到了!我们可以开始了!(ACK=y+1)”
- 此时双方状态:ESTABLISHED(连接建立,锁死!)
灵魂拷问:为什么要三次?两次不行吗?
「面试官最爱问:」 如果我不回第三次,咱俩能好吗?
「答案是:不行。」
如果只有两次:
客户端发了第一封信 SYN 1,因为网络卡顿在路上堵了。客户端急了,又发了一封 SYN 2。
SYN 2 到了,两人建立了连接,聊完断开了。
这时候,那个迷路的 SYN 1 突然到了!服务端以为客户端又要复合,赶紧回了 SYN+ACK 并傻傻等着。
但客户端心里想:“我没理你啊?”于是直接忽略。
「结果:」 服务端一直维持着这个半死不活的连接,浪费资源(这就是著名的 「SYN Flood」 攻击的原理雏形)。
所以,第三次握手是为了「防止已经失效的连接请求突然又传到了服务端,造成误会」。
第二幕:四次挥手 —— 分手应该体面,谁都不要说抱歉
「场景:」 聊完了,该挂电话了。TCP 是「全双工」的(两条独立的通道),所以分手的过程比牵手要麻烦,必须「双向确认」。
剧情是这样的:
「第一次挥手 (FIN)」
- 「客户端」:“宝,我没话说了,我要挂了。(FIN=1, seq=u)”
- 客户端进入:FIN_WAIT_1
「第二次挥手 (ACK)」
- 「服务端」
- 服务端进入:CLOSE_WAIT(半关闭状态)
- 「注意!」 这时候服务端可能还有话没说完(比如还有数据没传完),所以它只是先确认“我知道你要走”,但还没说“我也要走”。客户端还得等着。
「第三次挥手 (FIN)」
- 「服务端」把剩下的话说完,终于决定放手:“好了,我也没话说了,互删吧。(FIN=1, seq=w)”
- 服务端进入:LAST_ACK(最后确认)
「第四次挥手 (ACK)」
- 「客户端」:“好,收到。再见,不,再也不见。(ACK=w+1)”
- 客户端进入:TIME_WAIT(注意!不是直接 CLOSED)
- 服务端收到 ACK 后:CLOSED(彻底解脱)
灵魂拷问:为什么要四次?
因为「回复 ACK」(我知道你想分)和「发送 FIN」(我也想分)通常不能同步。服务端收到分手请求时,应用层可能还在处理数据(比如还在写日志、查库),内核只能先自动回一个 ACK 稳住对方,等应用层处理完调用 close() 后,才能发 FIN。
第三幕:避坑指南 —— 那些关于状态的灵异事件
1. TIME_WAIT:为什么分手后还要再等 2 分钟?
你以为发完最后一次 ACK 就结束了?不,客户端还要在 「TIME_WAIT」 状态下等待 「2MSL」(报文最大生存时间,通常是 1-4 分钟)。
「这是 TCP 最深情的设定:」
- 「为了确保最后一声“再见”对方能听到。」 如果最后的 ACK 丢了,服务端没收到,它会以为你没听见它的分手宣言,会重发 FIN。如果你直接关机了,服务端就永远在等,这不合适。
- 「为了让旧连接的“幽灵”彻底消失。」 2MSL 的时间足以让网络中所有关于这段感情的旧数据包自然消亡,防止它们跑出来干扰下一段新连接(串号)。
2. CLOSE_WAIT:服务器卡住的罪魁祸首
如果你发现服务器上有大量的 「CLOSE_WAIT」 状态,别甩锅给网络,「这通常是 Bug!」
这说明客户端已经发起了分手(FIN),服务器系统内核也回了(ACK),但是「你的代码」(应用层)没有调用 close() 关闭连接!
「原因:」 可能是死循环、数据库卡死、或者忘了写关闭连接的代码。赶紧去查代码吧!
📝 总结
TCP 协议告诉我们:
- 「开始要慎重:」
- 「结束要体面:」 给对方处理收尾工作的时间,不要突然失联(四次挥手)。
- 「分手后要冷静:」 给彼此一点缓冲期(TIME_WAIT),确保过去彻底过去,未来才能干净开始。
希望大家的代码永远 ESTABLISHED,人生永远没有 404!
「点个“在看”,你的握手永远不丢包!」 👇