1. 从一次线上故障说起:为什么“关闭一个连接”这么难
大概半年前,我们线上有个网关服务半夜报警,文件描述符(file descriptor)直接被打满,新的连接根本进不来。当时第一反应是“连接泄漏”,赶紧上 ss -s 看了一眼,结果发现大量连接卡在 CLOSE_WAIT 状态,数量稳定在七千多,远超正常水位。
排除了代码里没关连接的低级问题之后,我盯着抓包文件里的 FIN 报文序列,才意识到问题出在“被动关闭方对半关闭状态的处理”上——对端发了 FIN,但我们的服务内核已经回了 ACK,应用层却迟迟没有调用 close,这条 TCP 连接就一直挂在半开半闭的状态,既不收数据也不发数据,像一扇没锁严的窗户,风一吹就吱呀作响,雨一飘就满屋是水。
这个场景,其实就是 TCP 半关闭(half-close)最经典的形态之一。所以想借这篇内容,把 TCP 连接“结束生命周期”这件事拆开来讲清楚:四次挥手为什么是四次、半关闭状态到底有什么用、CLOSE_WAIT 和 TIME_WAIT 哪个更可怕、以及我们在真实业务里怎么设计一套“优雅关闭”的机制。适合正在写网络服务的后端开发、中间件维护者,还有准备 TCP 面试题的同学们。
如果你对 TCP 的认知还停留在“三次握手建立连接、四次挥手断开连接”这个口诀,那这篇内容会帮你把口诀背后的原理和实战里的坑一次性补齐。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 四次挥手的底层拆解:谁的 FIN 先发、ACK 怎么凑、半关闭状态藏在哪一步
2.1 为什么必须是四次:全双工的对称关闭逻辑
TCP 是全双工协议,意思是数据在两个方向上独立传输,A 给 B 发数据的同时,B 也能给 A 发数据。正因为两个方向的数据流是独立的,关闭的时候也必须“各自为政”——每个方向都要单独关一次。
于是就有了四次挥手:
- 主动关闭方(假设是 A)发送 FIN,表示“我这边的数据发完了,我不再往这个方向发数据了”;
- 被动关闭方(B)收到 FIN 后回 ACK,表示“我收到了你关闭的请求”;
- B 在合适的时间发送自己的 FIN,表示“我这边数据也发完了,我也要关了”;
- A 回 ACK,确认 B 的关闭请求,连接彻底释放。
很多人问:为什么不是三次?把第 2 步和第 3 步合并成一次 ACK+FIN 不就好了?答案是不行,因为第 2 步和第 3 步之间,B 可能还要继续发数据。
举个例子:A 发送完请求后说“我不发了”,但 B 可能还要做长时间计算,算完再给 A 回一个大响应。在这个计算期间,B 必须先回 ACK 安抚 A:“你的 FIN 我收到了,但我还在工作,先别急着销毁连接。”如果 B 把 ACK 和 FIN 合并成一步提前发出去,A 就会以为 B 也彻底关闭了,这就不符合 TCP 全双工的语义。
所以四次挥手不是形式主义,而是一方“单方面关闭发送方向”这一行为在协议层面上的必然结果。这个“单方面关闭”,就是半关闭。
2.2 FIN 的语义:它不是“断开”,而是“我不再发送”
理清 FIN 的语义非常重要。FIN 的全称是 finish,但它的准确含义是“我这个方向的数据流结束了”,而不是“连接立刻没有了”。
我们再看一下标准状态转移:
- 主动关闭方 A 发出 FIN 后,进入
FIN_WAIT_1; - 收到 B 的 ACK 后进入
FIN_WAIT_2,此时 A 不再发数据,但仍然可以读数据; - B 收到 FIN 并回了 ACK 后进入
CLOSE_WAIT,此时 B 知道自己不再有新数据可读了,但仍然可以写数据; - B 完成所有数据发送后,发出自己的 FIN,进入
LAST_ACK; - A 收到 FIN 后回 ACK,进入
TIME_WAIT,等待 2MSL 后彻底关闭; - B 收到 ACK 后进入
CLOSED。
这里最容易忽略的就是 FIN_WAIT_2 和 CLOSE_WAIT 这组“等待”状态。在 A 发出 FIN 且收到 ACK 后,到 B 发出 FIN 之前的这段时间,就是半关闭窗口期。在这个窗口期里,A 不能发数据,但 B 可以。这正好覆盖了“请求-处理-响应”这种经典模式里,客户端不再发数据、但服务端还需要时间回数据的场景。
2.3 半关闭状态与“三次握手断开”的误区
有一个常见误区是,很多人以为可以“三次挥手”。确实有 TCP 实现允许把 FIN 和 ACK 合并在同一个报文里,比如双方同时都不再有数据要发时,B 可以直接回 ACK+FIN,这样抓包看起来只有三次报文交互。但这种场景是巧合而不是常态——只有当被动关闭方恰好也无需再发送数据时,合并才是合法的。业务流里如果强制要求“三次就断”,多半是通过 SO_LINGER 配合 RST 实现的,那就是强制重置连接,而不是优雅关闭了,后面会细讲。
所以准确的说法是:优雅关闭一定基于半关闭的能力,而半关闭意味着关闭动作天然拆成两个独立步骤,因此标准实现就是四次挥手。
3. 半关闭状态的真实价值:从 shutdown 与 close 的区别说起
3.1 一份服务器代码引发的思考:为什么我不能直接 close
有一次同事写了一个简单的 echo 服务,处理完请求后直接调 close() 关闭 socket。功能上没毛病,但有一个隐
