TCP 基础篇-第五篇:TCP 连接断开——四次挥手全解析
深入理解 TCP 的优雅关闭:状态迁移、TIME_WAIT 与常见陷阱
在 TCP 协议的学习中,三次握手往往被反复强调,但连接断开的过程——即“四次挥手”——同样至关重要。如果说握手是建立信任的仪式,那么挥手则是得体告别的艺术。不同于握手的三次交互,断开连接需要四次报文交换,这背后隐藏着 TCP 全双工通信的本质:每个方向都需要独立关闭。本文将逐帧拆解四次挥手的每个步骤,剖析状态机变迁,探讨 TIME_WAIT 与 CLOSE_WAIT 这两个“老大难”问题,并给出最佳实践建议,帮助你彻底掌握 TCP 连接终止的精髓。

一、为什么需要四次挥手?
TCP 提供全双工通信,意味着数据可以同时在两个方向上传输。因此,当一方想要终止连接时,它必须确保自己不再发送数据,同时也要确认对端已经完成数据发送。这就像两个人在电话交谈中,一方说“我说完了”(FIN),另一方可能回应“好的,我知道了”(ACK),但此时另一方可能还有话要说,所以它会在自己说完后再发送自己的“我说完了”(FIN),然后最初的一方再确认(ACK)。因此,每个方向都需要一个 FIN 和一个 ACK,总计四次报文。
对比三次握手(SYN, SYN-ACK, ACK),断开之所以多一次,是因为握手时双方可以同时交换 SYN(即 SYN 与 ACK 合并),而断开时,两个方向的 FIN 通常不能合并,因为双方可能在不同时刻完成数据发送。当然,如果双方同时发送 FIN(即同时关闭),则可能变为四次甚至三次,但常规场景下是四次。
二、四次挥手的标准流程
假设客户端(主动关闭方)发起断开请求,服务器(被动关闭方)响应。实际中任何一端都可以主动关闭,但流程对称。以下以客户端主动关闭为例:
第一次挥手(FIN):客户端应用调用
close()或shutdown(SHUT_WR),TCP 层发送一个 FIN 报文段,序列号为u(等于之前已发送数据的最后一个序列号加 1)。此时客户端进入FIN_WAIT_1状态,表示等待对方的 ACK。第二次挥手(ACK):服务器收到 FIN 后,发送 ACK 报文,确认号为
u+1,同时服务器进入CLOSE_WAIT状态。此时客户端收到 ACK 后进入FIN_WAIT_2状态,等待服务器发送它的 FIN。此时连接处于“半关闭”状态:客户端不再发送数据,但可以接收数据;服务器仍可发送数据。第三次挥手(FIN):当服务器应用也决定关闭连接(调用
close()),服务器发送 FIN 报文,序列号为w(服务器已发送数据的最后一个序列号+1),并进入LAST_ACK状态,等待客户端的最终 ACK。第四次挥手(ACK):客户端收到 FIN 后,发送 ACK 确认,确认号为
w+1,然后客户端进入TIME_WAIT状态,等待 2MSL(最大报文生存时间)后进入CLOSED。服务器收到 ACK 后立即进入CLOSED状态。
客户端状态: ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED 服务器状态: ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED
值得注意的是,TIME_WAIT 只在主动关闭方出现(通常是客户端,但服务器也可能主动关闭)。2MSL 的等待是为了确保最后一个 ACK 能到达对端,如果该 ACK 丢失,对端会重传 FIN,主动关闭方可以重发 ACK,同时防止旧连接的延迟报文干扰新连接。
三、状态机详解:从 FIN_WAIT_1 到 TIME_WAIT
四次挥手涉及多个 TCP 状态,每个状态都有其特定的意义和超时处理:
FIN_WAIT_1:主动关闭方发送 FIN 后的初始状态。如果在此状态下收到对端的 FIN+ACK(即同时关闭),则可能直接进入
TIME_WAIT。FIN_WAIT_2:收到对端对 FIN 的 ACK 后进入。此时主动方已无数据发送,但可以接收数据。如果对端一直不发送 FIN,则可能因
tcp_fin_timeout超时而强制关闭。CLOSE_WAIT:被动关闭方收到 FIN 并发出 ACK 后进入。此状态表示对端已关闭发送,但本端仍可发送数据。应用层需要适时调用
close()来发送 FIN,否则连接会一直停留在CLOSE_WAIT,造成资源泄漏。LAST_ACK:被动方发送 FIN 后等待主动方的最终 ACK。收到 ACK 后进入
CLOSED。TIME_WAIT:主动方发送最终 ACK 后进入,持续 2MSL(通常为 1~4 分钟)。这是为了保障连接的可靠关闭,并防止“旧连接数据”污染“新连接”。
⚠️ 常见误区: 许多开发者以为 close() 会立即关闭连接,但实际上它只是触发 FIN 发送,真正的关闭需要等待四次挥手完成。如果使用 SO_LINGER 零超时,则会发送 RST 强制关闭,绕开四次挥手,但一般不推荐。
四、TIME_WAIT 的奥秘与调优
TIME_WAIT 是 TCP 协议中最容易引发问题的状态之一。它的存在有两大理由:
确保最后的 ACK 能被对端收到:如果主动方发送的 ACK 丢失,被动方会重传 FIN,主动方必须能在 2MSL 内收到并重发 ACK,否则被动方无法正确关闭。
让延迟的重复报文自然消失:网络中可能存留旧连接的延迟数据包,如果立即建立新连接且使用相同的四元组(源IP、源端口、目标IP、目标端口),这些旧包可能被误认为是新连接的数据。2MSL 时间足以保证旧包在网络中被丢弃。
然而,在高并发服务器(如 Web 服务器)中,如果服务器主动关闭连接(例如短连接场景),大量 TIME_WAIT 会耗尽端口资源(因为端口号有限)。常见调优手段包括:
开启
tcp_tw_reuse(Linux 3.7+)允许在安全条件下重用 TIME_WAIT 连接,但需配合时间戳。调整
tcp_tw_recycle(已被废弃,且 NAT 下有问题,不建议使用)。调整
tcp_max_tw_buckets限制最大 TIME_WAIT 数量,超出后直接释放并记录警告。应用层设计上尽量让客户端主动关闭,避免服务器产生大量 TIME_WAIT。
在实践中,对于短连接服务(如 HTTP/1.0),服务器经常处于 TIME_WAIT 状态,可通过长连接(Keep-Alive)或协议升级(HTTP/2)来减少连接建立/断开的频率。
五、CLOSE_WAIT 泄漏:如何识别与修复
CLOSE_WAIT 是另一个常见的异常状态。它表示对端已经关闭发送(收到 FIN),但本端应用尚未调用 close()。如果大量连接堆积在 CLOSE_WAIT,说明应用代码存在资源泄漏——可能是忘记关闭套接字,或是业务逻辑阻塞导致未能及时处理关闭事件。排查方法:
使用
netstat -ant | grep CLOSE_WAIT观察数量。检查应用代码,确保每个
accept()返回的 socket 或每个连接对象在完成通信后都正确关闭。在服务端合理设置
SO_RCVTIMEO或SO_SNDTIMEO,防止长期阻塞。
如果 CLOSE_WAIT 持续增长,通常意味着程序有 bug,需要从代码层面根治,单纯调整内核参数无效。
六、同时关闭(Simultaneous Close)
当双方同时调用 close() 时,可能出现同时发送 FIN 的情况。此时,双方都会进入 FIN_WAIT_1 状态,收到对方的 FIN 后,各自发送 ACK,并进入 CLOSING 状态(而不是 FIN_WAIT_2),随后再收到 ACK 后进入 TIME_WAIT。虽然这种情况较少见,但 TCP 状态机对此有完善支持,确保最终都能正确关闭。
七、挥手过程中的异常处理
网络不可靠,挥手过程中可能发生丢包。TCP 通过超时重传机制来应对:
如果 FIN 丢失,发送方会重传 FIN(重传计时器)。
如果第二次的 ACK 丢失,主动方会重传 FIN(因为收不到 ACK),被动方会重复 ACK。
如果最终的 ACK 丢失,被动方(LAST_ACK)会重传 FIN,主动方(TIME_WAIT)收到后重发 ACK,并重置 TIME_WAIT 计时器。
如果主动方在 TIME_WAIT 期间收到 FIN,说明对端未收到 ACK,需要重新发送 ACK。
这些机制保证了即使在丢包环境下,连接也能最终关闭,不会永久卡住。
八、最佳实践:优雅关闭的几点建议
优先让客户端主动关闭,避免服务端大量 TIME_WAIT 占用端口。
使用连接池复用长连接,减少频繁握手/挥手开销。
监控系统状态,特别是
netstat中的 TIME_WAIT 和 CLOSE_WAIT 计数,设置告警阈值。在代码中正确处理关闭异常,如捕获
ECONNRESET等错误,避免因 RST 导致未完成四次挥手。谨慎使用
SO_LINGER,除非明确需要快速释放资源并接受数据丢失风险。
结语:挥手是仪式的终点,也是新连接的起点
四次挥手虽然比握手多一次交互,但每一步都蕴含深刻的工程设计智慧:全双工独立关闭、状态容错、延迟报文隔离——这些都是 TCP 能够成为互联网基石的重要原因。理解挥手过程,不仅能帮助你写出更健壮的网络程序,还能在排查连接泄漏、端口耗尽等问题时迅速定位根源。至此,我们已完成了 TCP 连接建立(三次握手)和断开(四次挥手)的完整闭环,下一篇章我们将进入 TCP 的“异常断链”专题——RST 报文,敬请期待!


客服1