TCP 连接断开这件事,在排障时经常是争论的焦点。服务端说是客户端先断的,客户端说是服务端踢的人,双方各执一词。其实 TCP 协议本身的设计会留下明确的痕迹:谁先发送 FIN 或 RST,谁就是主动关闭方。只要掌握正确的观察方法,这个问题完全可以给出确定性的答案。本文将从协议流程、系统状态、抓包分析三个层面展开讲解。

先理解四次挥手与主动关闭方的角色
TCP 连接的关闭遵循四次挥手流程。第一个发送 FIN 报文的一方被称为主动关闭方,另一方则是被动关闭方。主动关闭方发出 FIN 后进入 FIN_WAIT_1 状态,收到对方 ACK 后进入 FIN_WAIT_2,等收到对方的 FIN 并回复 ACK 后,进入 TIME_WAIT 状态并停留 2MSL 时长才彻底释放。而被动关闭方收到 FIN 后进入 CLOSE_WAIT 状态,此时连接的关闭权交到了自己手里,只有当应用程序调用 close 之后,它才会发出自己的 FIN,进入 LAST_ACK 状态。
这个流程里有两个非常关键的状态值得记住:TIME_WAIT 只会出现在主动关闭方,CLOSE_WAIT 只会出现在被动关闭方。这就是判断谁主动关闭的第一条重要线索。如果一台服务器上堆满了大量 TIME_WAIT,说明这台机器上的应用在频繁主动关闭连接;反之如果堆满 CLOSE_WAIT,说明对端在大量关闭连接,而本端应用没有及时调用 close,这往往意味着代码存在连接泄漏的 bug。
除了正常的四次挥手,还有一种粗暴的关闭方式:发送 RST 报文。RST 通常出现在异常场景,比如进程崩溃时操作系统回收套接字、向已关闭的连接写数据、防火墙强制掐断连接等。收到 RST 的一方会立刻释放连接,不经过任何挥手流程。因此抓包时如果看到的是 RST 而不是 FIN,就要往异常方向排查。
通过系统状态和抓包定位主动关闭方
第一步可以先用系统命令观察连接状态分布。在 Linux 上用 netstat -antp 或更快的 ss -antp 查看当前所有 TCP 连接的状态。如果在服务端机器上看到连接处于 CLOSE_WAIT 状态,可以确定是客户端先发起的关闭;如果看到 FIN_WAIT_1、FIN_WAIT_2 或 TIME_WAIT,则说明服务端是主动关闭方。Windows 上同样可以用 netstat -ano 配合任务管理器定位到具体进程。
状态观察只能覆盖尚未释放的连接,对于已经消失的连接就需要抓包了。tcpdump 是最直接的工具,抓包命令如下:
# 抓取指定端口的TCP报文,显示详细标志位 tcpdump -i any -nn 'tcp port 8080 and (tcp[tcpflags] & (tcp-fin|tcp-rst) != 0)' # 或者完整记录所有报文,事后用Wireshark分析 tcpdump -i any -nn -w capture.pcap 'tcp port 8080'
抓到的报文里,关注 FIN 和 RST 标志位的方向。假设客户端 IP 为 192.168.1.10,服务端为 192.168.1.20。如果看到 192.168.1.10.54321 > 192.168.1.20.8080: Flags [F] 出现在前面,说明客户端先发了 FIN,客户端是主动关闭方。如果第一个 FIN 来自服务端方向,结论相反。若看到 Flags [R] 或 Flags [R.],则是对端异常断开,需要进一步确认是进程崩溃、超时策略还是中间设备所为。
用 Wireshark 分析 pcap 文件时,可以打开 Statistics 菜单中的 Flow Graph 功能,它会以时间线的方式画出双向报文,四个 FIN 和 ACK 的先后顺序一目了然,这是演示和汇报时最直观的方式。
常见关闭场景的典型特征与排查建议
不同的关闭原因在报文层面有不同的指纹。第一种是服务端主动超时踢连接,常见于 Nginx 的 keepalive_timeout 到期、负载均衡器的 idle timeout。特征是服务端先发 FIN,服务端机器上会出现 TIME_WAIT 堆积。如果客户端报错频繁,可以检查中间链路各层的超时配置是否小于客户端的请求间隔。
第二种是客户端正常关闭,比如短连接模式下一个请求处理完就关连接。特征是客户端先发 FIN,服务端短暂处于 CLOSE_WAIT 后跟着关闭,服务端也会有一定量的 TIME_WAIT(因为它也要回 FIN 完成挥手)。这类情况是正常行为,但如果客户端使用短连接且并发量极大,服务端 TIME_WAIT 会急剧增长,此时应考虑启用 tcp_tw_reuse 或改造为长连接。
第三种是异常 RST,常见的诱因包括:应用进程被 kill 或崩溃、向已经被对端关闭的连接继续写数据(对端会回 RST)、连接队列溢出、防火墙或 NAT 设备清理会话。排查时可以从系统日志、应用日志、dmesg 输出中寻找线索。下面是一段用于观察连接状态的示例脚本:
# 统计各状态的TCP连接数量,快速判断是否存在异常
ss -ant | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# 查看某个端口上处于CLOSE_WAIT的连接
ss -antp state close-wait '( dport = :8080 or sport = :8080 )'最后在代码层面给出几点建议:服务端应确保请求处理完毕后及时释放套接字资源,避免 CLOSE_WAIT 泄漏;高并发短连接场景优先使用长连接加心跳保活;对连接关闭事件打日志时记录errno和关闭原因,这样即使事后没有抓包,也能通过日志还原是谁发起的关闭。掌握状态分布加报文方向这两把尺子,判断 TCP 连接的关闭方向就不再靠猜了。