在《初始篇》中,我们将网络比作一个复杂的物流系统。其中,TCP(传输控制协议)扮演了“可靠特快专递”的角色。
对于后端开发者而言,TCP 不仅仅是面试题中的那几张流程图。它是所有应用层协议(HTTP, RPC, MySQL 连接)的基石。在生产环境中,无论是服务端的CLOSE_WAIT堆积报警,还是高并发下的连接池耗尽,根源往往都能追溯到 TCP 的状态流转上。
面试官之所以热衷于问“三次握手”和“四次挥手”,并非为了考察你的背诵能力,而是看你是否理解:在一个不可靠的通信信道(IP 层)之上,如何通过严谨的状态机设计,构建出一个可靠的通信连接?
今天,我们深入 TCP 的心脏,拆解这套支撑起整个互联网的连接管理机制。
一、 三次握手:不仅是“你好”
很多同学对三次握手的理解停留在这个层面:“A 问 B 在吗?B 说在。A 说知道了。”
这只是表象。TCP 三次握手的核心目的,其实是为了同步双方的初始序列号。
TCP 是基于字节流的,为了保证数据不丢、不重、按序到达,每一个字节都需要有一个编号。握手过程,就是双方互相告知:“我要开始发数据了,我的起始编号是 X,请你确认。”
1. 握手全流程推演
第一次握手 (SYN):
客户端发送一个 SYN 报文(SYN=1),并随机生成一个初始序列号 client_isn。此时,客户端处于 SYN_SENT 状态。
潜台词:我要建立连接,我的数据将从这个编号开始。
第二次握手 (SYN + ACK):
服务端收到 SYN 后,回复一个 SYN + ACK 报文。
它包含两层意思:一是 ACK,确认收到了客户端的序号(ack = client_isn + 1);二是 SYN,服务端也生成自己的初始序列号 server_isn。此时,服务端处于 SYN_RCVD 状态。
潜台词:收到了,你的起始编号我记住了。我也要建立连接,这是我的起始编号,请你也确认一下。
第三次握手 (ACK):
客户端收到后,回复一个 ACK 报文(ack = server_isn + 1)。此时,客户端连接建立,进入 ESTABLISHED 状态。服务端收到这个 ACK 后,也进入 ESTABLISHED 状态。
潜台词:收到了,你的起始编号我也记住了。连接建立成功。
硬核面试题 1:为什么是 3 次?2 次不可以吗?
这是面试中最高频的问题。很多人会回答“为了防止失效的连接请求突然到达服务端”,这没错,但不够深刻。核心原因有两点:
防止历史连接的初始化:
假设网络拥堵,客户端发送了 旧的 SYN (Seq=90),然后又发送了 新的 SYN (Seq=100)。
如果只有 2 次握手:服务端收到旧的 SYN (90) 后,立刻回个 ACK 并建立连接。客户端发现:“不对啊,我想要的是 100,你怎么回我 90?” 于是客户端发送 RST 中止连接。但此时服务端已经建立了连接,浪费了资源。
如果是 3 次握手:服务端收到旧的 SYN (90) 回复 SYN+ACK。客户端收到后,发现 Seq 不对,直接回一个 RST(复位报文),告诉服务端“这个是旧的,别理它”。只有三次握手,才能让客户端有“验证”的机会,从而阻止历史连接。
同步双方序列号:
TCP 是全双工的(双向通信)。2 次握手只能保证“客户端 -> 服务端”的序列号被确认,无法保证“服务端 -> 客户端”的序列号被客户端确认。只有一来一回再一去,双方的初始序列号才能都被可靠同步。
硬核面试题 2:什么是 SYN Flood 攻击?
原理:攻击者伪造海量 IP,发送大量的
SYN包给服务端,但就是不回第三次ACK。后果:服务端的半连接队列 (Syn Queue)被瞬间填满,导致无法处理正常的连接请求,CPU 也会因为不断重发 SYN+ACK 而飙升。
防御:调大半连接队列、开启
tcp_syncookies(不将半连接放入队列,而是通过 Cookie 验证)。
二、 四次挥手:优雅的离场
建立连接需要热情,断开连接则需要耐心。因为 TCP 是全双工的,这意味着通信的双方都有独立的发送和接收能力。
断开连接的本质是:双方都要单独关闭自己的“发送通道”。
1. 挥手全流程推演
第一次挥手 (FIN):
客户端打算关闭连接,发送 FIN 报文。此时客户端进入 FIN_WAIT_1 状态。
潜台词:我没有数据要发给你了,我打算关闭我的发送通道。
第二次挥手 (ACK):
服务端收到 FIN,回一个 ACK。此时服务端进入 CLOSE_WAIT 状态。
潜台词:知道了。但请你等一下,我可能还有数据没发完,你先别彻底挂断,继续听我说。
注意:此时连接处于半关闭状态。客户端不能发数据了,但还可以收数据。
第三次挥手 (FIN):
服务端把剩下的数据发完了,也想断开了。于是发送 FIN 报文。此时服务端进入 LAST_ACK 状态。
潜台词:我的数据也发完了,现在我也要关闭我的发送通道了,再见。
第四次挥手 (ACK):
客户端收到 FIN,回一个 ACK。此时客户端进入 TIME_WAIT 状态,等待 2MSL 时间后才真正关闭 (CLOSED)。服务端收到 ACK 后,立刻进入 CLOSED。
潜台词:好的,收到了,一路顺风。
硬核面试题 3:为什么握手是 3 次,挥手却是 4 次?
握手时:服务端收到 SYN 后,因为没有历史负担,它可以把“确认收到” (ACK) 和“我也要连接” (SYN) 合并在一个包里发送(即 SYN+ACK),所以 3 次就够了。
挥手时:服务端收到客户端的 FIN 时,它可能还有数据在缓存区里没发完。它必须先回一个 ACK 说“我知道你想断了”,然后继续把数据发完。等事情都处理好了,再发自己的 FIN。ACK 和 FIN 很难合并发送,所以需要分两步,总共就是 4 次。
三、 生产环境的噩梦:CLOSE_WAIT 与 TIME_WAIT
在后端面试和线上故障排查中,TCP 的状态流转是重灾区。你必须死磕这两个状态。
1. CLOSE_WAIT(被动关闭方)
出现位置:被动关闭连接的一方(通常是服务端)。
现象:如果你用
netstat命令发现服务端有成千上万个CLOSE_WAIT状态,说明系统这就快挂了。原因:回顾四次挥手,CLOSE_WAIT 出现在“收到对方 FIN,回复 ACK”之后,等待“发送己方 FIN”之前。
如果你一直卡在这里,说明服务端程序没有调用 close() 关闭连接。
排查思路:这通常是代码 Bug。比如在数据库连接池、HTTP Client 中,处理完请求后忘记释放连接,或者异常处理逻辑中漏掉了关闭连接的代码。
2. TIME_WAIT(主动关闭方)
出现位置:主动发起关闭的一方(客户端,或者服务端主动断开连接时)。
现象:高并发场景下,如果服务器充当 Client 去调第三方接口,可能会积累大量的
TIME_WAIT,导致端口耗尽。面试必问:为什么客户端在发完最后一个 ACK 后,还要等待 2MSL(报文最大生存时间的 2 倍)才关闭?
防丢包:如果第四次挥手的 ACK 丢了,服务端会重发 FIN。如果客户端早早关机了,服务端就收不到响应了。
TIME_WAIT就是为了“兜底”,确保有足够的时间处理可能重传的 FIN。防混淆:网络中可能存在“迷路”的旧数据包。等待 2MSL 可以让这些残存在网络中的旧报文自然消亡,防止它们“尸变”跑到了下一个复用该端口的新连接中,造成数据错乱。
📝 总结
TCP 的连接管理,本质上是在不可靠的物理网络之上,通过协商(握手)、确认(ACK)和状态机(State Machine)强行构建出的一种逻辑上的可靠性。
三次握手:是为了同步序列号,防止历史连接困扰。
四次挥手:是因为全双工通信,发送和接收要分别关闭。
状态监控:
CLOSE_WAIT意味着代码泄露,TIME_WAIT意味着主动关闭和端口占用。
理解了这些,当你再看到线上的网络报错时,脑海中浮现的就不再是枯燥的定义,而是数据包在网线两端交互的生动画面。
如果这篇博文帮你看清了 TCP 的真面目,欢迎点赞收藏!