引言

连接建立有三次握手,连接关闭有四次挥手。握手讲的是"怎么把路修好",挥手讲的是"怎么把路安全拆掉"——这比握手复杂得多。

三次握手只需要双方确认对方能收到自己的消息,四次挥手则要处理一个更棘手的问题:双方各自都可能还有数据没发完。一方发完数据说"我要关了",另一方可能还要继续发送数据,等那边也发完后才能真正断开。这中间的任何一步没处理好,连接就没法干净利落地关闭,残留的状态会占用端口、消耗资源,甚至在重启时引发"端口仍被占用"的报错。

这篇文章把挥手过程中的每个状态、每个参数、每个常见问题全部讲透。


一、四次挥手完整时序

先看一张完整的时序图,把四次挥手的每一步标清楚:

被动关闭方(服务器) 主动关闭方(客户端) 被动关闭方(服务器) 主动关闭方(客户端) 应用层调用 close() 第一次挥手 · 主动端发送 FIN 第二次挥手 · 被动端发送 ACK 被动端处理完剩余数据 应用层调用 close() 第三次挥手 · 被动端发送 FIN 第四次挥手 · 主动端发送 ACK 等待 2MSL(Linux 默认 60 秒) 状态: C → CLOSED 状态: S → CLOSED FIN(seq=u) , ACK(ack=j+1) 状态: C → FIN_WAIT_1 ACK(ack=u+1) 状态: S → CLOSE_WAIT 状态: C → FIN_WAIT_2 FIN(seq=j+1) 状态: S → LAST_ACK ACK(ack=j+2) 状态: C → TIME_WAIT MSL1 超时后 → TIME_WAIT 结束 MSL2 超时后 → 连接完全关闭

每一步的状态变化:

阶段 主动关闭方 被动关闭方 说明
应用层调用 close() 触发关闭流程
第一次挥手 → FIN, 进入 FIN_WAIT_1 收到 FIN 数据已发完,请求关闭
第二次挥手 收到 ACK, 进入 FIN_WAIT_2 → ACK, 进入 CLOSE_WAIT 确认收到,进入半关闭
第三次挥手 等待对端 FIN → FIN, 进入 LAST_ACK 对端数据也发完了
第四次挥手 → ACK, 进入 TIME_WAIT 收到 ACK, 进入 CLOSED 确认收到,计时开始
计时结束 进入 CLOSED 连接完全关闭

为什么是四次而不是三次?

因为 TCP 是全双工协议。两个方向的数据流是独立的,一方发送 FIN 只是说"我这边的数据发完了",但另一方向的数据可能还没发完。所以需要两对 FIN/ACK:先关闭发送方向(第一、二次挥手),再关闭接收方向(第三、四次挥手)。


二、两个TIME_WAIT:FIN_WAIT_2 与 TIME_WAIT 的区别

这是挥手过程中最容易混淆的两个状态。

FIN_WAIT_2 出现在主动关闭端。主动端发送 FIN 并收到对端 ACK 后,进入 FIN_WAIT_2 状态,此时主动端已经不能发送数据,但仍然可以接收数据。主动端需要收到对端的 FIN 后才会进入 TIME_WAIT。

关键问题在这里:如果对端一直不发送那个 FIN(比如对端应用层卡住、没有调用 close()、或者网络丢包),主动端会永久卡在 FIN_WAIT_2。Linux 内核通过 net.ipv4.tcp_fin_timeout 控制 FIN_WAIT_2 的超时时间(默认 60 秒),超时后强制关闭连接。这个值不能设太短,否则对端稍有延迟就会导致连接被提前断开;但也不能设太长,否则大量 FIN_WAIT_2 连接会消耗内核内存。

TIME_WAIT 出现在主动关闭端收到对端的 FIN 并发送 ACK 之后,持续时间为 2MSL(Linux 默认 60 秒 × 2 = 120 秒)。TIME_WAIT 的存在有两个目的:

  • 防止旧连接的延迟数据包被新连接错误接收。 2MSL 是数据包在网络中可能存活的最大时间,确保旧连接的所有数据包都从网络中消失后,同一个四元组(源IP、源端口、目标IP、目标端口)的新连接才建立。
  • 确保被动关闭方收到最后的 ACK。 如果最后的 ACK 丢失,被动端会重传 FIN,主动端需要在这个重传窗口内仍然在线,以便重新发送 ACK。

主动端发送 FIN
进入 FIN_WAIT_1

收到对端 ACK?

超时 → 关闭连接

进入 FIN_WAIT_2

收到对端 FIN?

tcp_fin_timeout 超时?

强制关闭连接

进入 TIME_WAIT
等待 2MSL

MSL1 超时

MSL2 超时

连接完全关闭
状态 CLOSED

2MSL 为什么是 120 秒?

MSL(Maximum Segment Lifetime)是一个数据包在网络中可以存活的最大时间,由 RFC 793 规定不超过 2 分钟。Linux 将 MSL 实现为 60 秒,因此 TIME_WAIT = 2 × MSL = 120 秒。这个值在 Linux 中不可直接配置,但可以通过调整 net.ipv4.tcp_fin_timeout 间接影响 FIN_WAIT_2 的行为。

net.ipv4.ip_default_ttl(默认 64)是 IP 层的 TTL,和 MSL 是两个完全不同的概念。TTL 控制的是数据包经过路由器的最大跳数,每经过一个路由器减 1,到 0 就丢弃;MSL 是 TCP 层的概念,衡量的是时间维度而不是跳数。


三、TIME_WAIT 堆积:什么时候是问题,什么时候不是

TIME_WAIT 堆积几乎是每个高并发服务器都会遇到的现象。大量文章会直接说"开启 tcp_tw_reuse",但这个建议在很多场景下并不适用。

TIME_WAIT 在以下场景下是正常现象,不需要处理:

高并发短连接场景下,每次请求建立新连接然后立即关闭,主动端必然进入 TIME_WAIT。端口范围够用(默认 32768 ~ 60999,约 28000 个端口),2MSL = 120 秒内理论最大 TIME_WAIT 数量约 28000 × (120 / 平均连接时长)。如果平均连接时长是 1 秒,每秒 28000 个新连接,TIME_WAIT 数量约为 280 万——看起来很多,但内核的 TIME_WAIT 状态占用的内存极小(每个约几百字节),通常不构成实质问题。

TIME_WAIT 在以下场景下是真正的问题:

客户端使用固定源端口访问大量不同目标服务器。比如爬虫或 API 客户端,每个请求使用不同目标 IP 但相同源端口(客户端通常随机选择源端口,但端口范围有限),当 TIME_WAIT 连接占满可用端口范围时,新连接会因为"无可用端口"而失败。这种情况下 tcp_tw_reuse 确实有效。

服务器端监听端口(bind(0.0.0.0:80))收到连接后,TIME_WAIT 状态在本端——这时即使开启 tcp_tw_reuse,对服务器端的 TIME_WAIT 也完全无效。因为 tcp_tw_reuse 只对从本机主动发起的连接生效,对被动接收的连接无效。

TIME_WAIT 出现在哪端?

客户端主动发起连接

服务器被动接收连接

tcp_tw_reuse 有效?

出方向 NAT 场景
高并发爬虫/API 客户端

非 NAT 客户端
tw_reuse 不生效

服务器端 TIME_WAIT

tw_reuse 完全无效

解决方案:
SO_REUSEADDR / SO_LINGER
延长连接复用时间

真正的端口耗尽信号:

不是 TIME_WAIT 数量本身,而是 ss -s 输出中 ress(重用状态的数量)接近 0 且 outa(出站连接失败)上升,或者 netstat -ant 看到大量 TIME-WAITLocal Address 的端口号接近 60999 上限。这说明可用端口已经不够用了。


四、tcp_tw_reuse 的有效条件

net.ipv4.tcp_tw_reuse = 1 不是万能药。内核在以下条件下才会将新的出站连接重用到 TIME_WAIT 状态的 socket:

  • 本机作为客户端,主动连接外部服务器
  • net.ipv4.tcp_timestamps = 1(默认开启)——时间戳是 PAWS 机制的基础,也是 tw_reuse 的前提
  • 新连接的发出时间距离 TIME_WAIT 开始时间超过 1 秒
  • 目标地址是非本地地址(经过 NAT 或公网)

对于服务器端监听端口产生的 TIME_WAIT,tw_reuse 无效。真正的解决方案是:

  • SO_REUSEADDR:允许 bind 到处于 TIME_WAIT 状态的地址+端口组合。服务器重启时如果端口仍被占用,开启这个选项后可以立即重新监听,而不必等待 2MSL 结束。
  • 设置合理的 SO_LINGER:调用 setsockopt(sockfd, SOL_SOCKET, SO_LINGER, ...) 控制 close() 行为。 linger timeout = 0 时,close() 直接发送 RST 而不是走完四次挥手,适用于对数据完整性要求不高的场景,但会丢失尚未确认的数据。
  • 调整 net.ipv4.tcp_max_tw_buckets:限制 TIME_WAIT 状态的最大数量,超过后强制关闭最早的 TIME_WAIT 连接。默认值足够大(262144),不建议随意调小。

五、SO_REUSEADDR 与 SO_REUSEPORT 的精确区别

这两个选项名字相近,职责完全不同:

SO_REUSEADDR 解决的是"服务器重启后端口仍被占用"的问题。关闭时服务器进入 TIME_WAIT,120 秒内无法重新 bind 同一端口。开启 SO_REUSEADDR 后,内核允许 bind 到处于 TIME_WAIT 状态的地址+端口。这个选项在高可用服务重启场景(Keepalived 主备切换、滚动更新)下必须开启。

// 服务器端标准配置
int sock = socket(AF_INET, SOCK_STREAM, 0);
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
listen(sock, 128);

SO_REUSEPORT 是 Linux 3.9 引入的负载均衡机制。允许多个进程(或多个线程/多个 socket)绑定到相同的地址+端口,内核根据五元组哈希(源IP、源端口、目标IP、目标端口、协议)将新连接均匀分发到各个 socket。这个机制解决了"单进程 accept() 成为瓶颈"的问题。

# Nginx 多 worker 场景下的 reuseport 配置
stream {
    upstream backend {
        server 127.0.0.1:8080;
    }
    server {
        listen 443 reuseport;
        proxy_pass backend;
    }
}

reuseport 开启后,每个 Nginx worker 都有自己的 socket,内核负责分发连接,不再需要 accept() mutex 锁。多核场景下性能提升显著。需要注意的是,reuseport 依赖的内核负载均衡策略在哈希冲突严重时可能不均匀,可以通过 ss -ltnp 观察各 socket 的连接分布来验证。


六、挥手故障诊断

连接关闭过程中的异常排查:

连接关闭异常

症状是什么?

Connection reset by peer

Address already in use

连接卡在 FIN_WAIT_2

被动端收到 RST
原因:应用层提前 close()
或 SO_LINGER timeout=0

检查代码中 close() 调用时机
确认数据已完整发送

服务器重启时 bind 失败
TIME_WAIT 尚未结束
原因:SO_REUSEADDR 未开启

setsockopt SO_REUSEADDR
或等待 2MSL

tcp_fin_timeout 超时
FIN_WAIT_2 状态被强制关闭
原因:对端未调用 close()
或网络丢包导致 FIN 未到达

netstat -ant | grep FIN_WAIT2
确认对端是否存活
检查网络连通性


七、知识框架

四次挥手与
TIME_WAIT

挥手四阶段

第一次挥手 FIN 发送

第二次挥手 ACK 确认

第三次挥手 对端 FIN

第四次挥手 最后 ACK

关键状态

FIN_WAIT_1 等待 ACK

FIN_WAIT_2 等待对端 FIN

CLOSE_WAIT 对端已关闭本端

LAST_ACK 等待最后 ACK

TIME_WAIT 2MSL 等待期

计时参数

MSL 60秒 Linux实现

2MSL 120秒

tcp_fin_timeout FIN_WAIT_2超时

优化方案

tcp_tw_reuse 出方向 NAT 有效

SO_REUSEADDR 重启 bind 占用

SO_REUSEPORT 内核级负载均衡

tcp_max_tw_buckets 上限控制

诊断工具

ss -ti 连接状态与 RTT

netstat -ant TIME_WAIT 数量

net.ipv4.tcp_tw_reuse 当前值


总结

四次挥手比三次握手更复杂,原因在于双方的数据流是独立的,每个方向都需要单独确认关闭。FIN_WAIT_2 和 TIME_WAIT 是两个完全不同的状态,前者是等待对端 FIN 的过渡期,后者是确保旧数据包消失的安全等待期。

TIME_WAIT 堆积不一定需要处理——它在高并发短连接场景下是正常现象,只有当它导致了端口耗尽才是真正的问题。tcp_tw_reuse 只对客户端出方向 NAT 场景有效,对服务器端监听端口的 TIME_WAIT 无效。真正解决服务器端 TIME_WAIT 问题的方式是 SO_REUSEADDR 和合理的 linger 配置。

下一篇文章聚焦 DNS 解析——为什么有时候域名能 ping 通但访问不到,dig +trace 能告诉你 DNS 解析卡在了哪一层的哪一台服务器。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐