3. 网络协议基础:TCP三次握手与四次挥手、TCP连接状态变迁、TIME_WAIT与CLOSE_WAIT详解

说到高并发连接,TCP协议是绕不开的基础。很多人觉得TCP就是三次握手、四次挥手,背下来就完事了。但我在实际项目中踩过不少坑,发现真正理解这些状态变迁,才是解决连接问题的关键。

你想想看,一个高并发服务,动不动就是几万、几十万的连接。如果对TCP状态理解不透,出了问题根本无从下手。今天我就把这块掰开了讲清楚。

3.1 三次握手:建立连接的“双向确认”

TCP是面向连接的协议。建立连接前,双方得先确认“你行我也行”。这个过程就是三次握手。

核心流程:

  1. 客户端发送SYN:客户端说“我想跟你连”,同时带上自己的初始序列号(seq=x)。
  2. 服务端回复SYN+ACK:服务端说“收到,我也准备好了”,确认号是x+1,同时带上自己的序列号(seq=y)。
  3. 客户端发送ACK:客户端说“好,开始传数据”,确认号是y+1。

为什么是三次,不是两次?我刚开始学的时候也纳闷。说白了,两次握手没法防止“失效的连接请求”被服务端误接受。比如客户端发了个SYN,网络延迟了,客户端重试后连上了,结果之前那个SYN又到了服务端。如果是两次握手,服务端就直接建立连接了,白白浪费资源。三次握手让服务端等客户端的ACK,就能确认客户端确实“活着”。

我的经验:在高并发场景下,三次握手本身就有开销。每次握手都要走网络往返,延迟高的地区(比如跨洲)建连时间可能上百毫秒。我建议用连接池复用连接,减少握手次数。

3.2 四次挥手:优雅关闭的“双向告别”

断开连接比建立连接复杂。因为TCP是全双工的,双方都得关闭自己的发送通道。这就是四次挥手。

核心流程:

  1. 主动方发送FIN:主动方说“我不发数据了”,进入FIN_WAIT_1状态。
  2. 被动方回复ACK:被动方说“知道了”,进入CLOSE_WAIT状态。主动方收到后进入FIN_WAIT_2。
  3. 被动方发送FIN:被动方说“我也不发了”,进入LAST_ACK状态。
  4. 主动方回复ACK:主动方说“收到”,进入TIME_WAIT状态。被动方收到后关闭。

这里有个细节:被动方收到FIN后,可能还有数据没发完。所以它先回ACK,等数据发完再发FIN。这就是为什么挥手要四次,不能合并。

我曾经踩过的坑:有一次线上服务大量连接处于CLOSE_WAIT状态,查了半天发现是被动方(服务端)忘了调用close()。代码里处理完请求后没关闭socket,导致连接一直挂着。CLOSE_WAIT是应用层的锅,出现大量CLOSE_WAIT,基本就是代码没处理好。

3.3 TCP连接状态变迁图

下面这张图是我自己整理的TCP状态变迁。记住它,排查问题会快很多。

TCP连接状态变迁图 TCP连接状态变迁图 客户端 服务端 CLOSED SYN_SENT ESTABLISHED FIN_WAIT_1 FIN_WAIT_2 TIME_WAIT CLOSED LISTEN ESTABLISHED CLOSE_WAIT LAST_ACK SYN SYN+ACK ACK FIN ACK FIN ACK 关键说明 实线箭头表示三次握手(建立连接),虚线箭头表示四次挥手(断开连接) TIME_WAIT是主动关闭方最后停留的状态,持续2MSL(约2分钟)

3.4 TIME_WAIT:为什么需要等2MSL?

TIME_WAIT是主动关闭方发送完最后一个ACK后进入的状态。它要等2MSL(Maximum Segment Lifetime,最大报文生存时间),通常是2分钟。

为什么要等这么久?两个原因:

  • 保证被动方收到ACK:如果最后一个ACK丢了,被动方会重发FIN。主动方还在TIME_WAIT,就能重新回复ACK。如果直接关闭,被动方收不到ACK,会一直重试。
  • 防止旧连接干扰新连接:网络里可能还有延迟的报文。等2MSL,这些报文都过期了,新连接就不会收到旧数据。

我曾经遇到的线上问题:高并发短连接场景下,服务器上TIME_WAIT连接堆积到几万个。系统资源被耗尽,新连接建不了。后来我调整了内核参数,开启了tcp_tw_reuse和tcp_tw_recycle(注意:tcp_tw_recycle在NAT环境下有坑,建议谨慎使用),才缓解了这个问题。

3.5 CLOSE_WAIT:应用层的“僵尸连接”

CLOSE_WAIT是被动关闭方收到FIN后进入的状态。按理说,应用层应该尽快调用close(),发送FIN,进入LAST_ACK。但如果代码没处理好,CLOSE_WAIT就会一直挂着。

状态 出现方 常见原因 排查方法
TIME_WAIT 主动关闭方 短连接太多,2MSL等待 netstat -n | grep TIME_WAIT | wc -l
CLOSE_WAIT 被动关闭方 应用层未调用close() lsof -i :端口,检查进程
FIN_WAIT_2 主动关闭方 被动方未发送FIN 检查被动方是否卡住
SYN_RECV 服务端 半连接队列满 调整tcp_max_syn_backlog

我的排查习惯:线上出现连接问题,我第一件事就是执行ss -tan看状态分布。如果CLOSE_WAIT多,直接找代码里没关socket的地方。如果TIME_WAIT多,考虑连接池或调整内核参数。

3.6 高并发场景下的TCP优化建议

结合我多年的经验,给你几个实用的优化方向:

  • 减少TIME_WAIT:开启net.ipv4.tcp_tw_reuse,允许重用TIME_WAIT连接。注意:这个选项只对客户端有效。
  • 调整端口范围:net.ipv4.ip_local_port_range默认是32768-60999,高并发时可以扩大范围。
  • 增大半连接队列:net.core.somaxconn和net.ipv4.tcp_max_syn_backlog,防止SYN洪水。
  • 开启TCP快速打开:tcp_fastopen,减少握手延迟。

一句话总结:TCP状态变迁是排查连接问题的基本功。TIME_WAIT是主动方的“安全等待”,CLOSE_WAIT是被动方的“代码漏洞”。理解了这些,高并发连接优化就有了方向。