3、TCP三次握手与四次挥手:状态转换图、半关闭状态、TIME_WAIT详解

TCP的连接管理,说白了就是三次握手和四次挥手。这两个过程,我当年刚入行时觉得很简单,不就是发几个包嘛。直到线上出了几次连接异常的问题,才意识到里面的门道有多深。今天咱们就把它彻底讲透。

3.1 三次握手:建立连接的过程

三次握手,是客户端和服务器建立TCP连接时,必须经历的三个步骤。为什么要三次?两次不行吗?嗯,这个问题我面试时经常问别人。

核心目的:确保双方都有发送和接收数据的能力,同时同步初始序列号(ISN)。

过程是这样的:

  1. 第一次握手(SYN):客户端发送一个SYN包,告诉服务器「我想连你」。此时客户端进入SYN_SENT状态。
  2. 第二次握手(SYN+ACK):服务器收到后,回复SYN+ACK包,意思是「收到,我也准备好了」。服务器进入SYN_RCVD状态。
  3. 第三次握手(ACK):客户端再发一个ACK包,表示「好的,开始传数据吧」。双方进入ESTABLISHED状态。

我个人习惯用「打电话」来类比:你拨号(SYN),对方接听并说「喂?」(SYN+ACK),你回一句「喂,是我」(ACK)。通话建立。

小技巧:三次握手中,前两次不能携带数据,第三次可以。为什么?因为前两次还没确认对方能收,万一丢了还得重传,带数据就浪费了。

3.2 四次挥手:断开连接的过程

挥手比握手复杂一点。因为TCP是全双工的,双方都要关闭自己的通道。所以需要四次。

过程如下:

  1. 第一次挥手(FIN):主动关闭方发送FIN包,表示「我不发数据了」。进入FIN_WAIT_1状态。
  2. 第二次挥手(ACK):被动关闭方回复ACK,表示「知道了」。进入CLOSE_WAIT状态。主动方收到后进入FIN_WAIT_2。
  3. 第三次挥手(FIN):被动关闭方也发送FIN包,表示「我也不发了」。进入LAST_ACK状态。
  4. 第四次挥手(ACK):主动关闭方回复ACK,然后进入TIME_WAIT状态。被动方收到后关闭。

这里有个坑。我在项目中遇到过,服务端程序没处理好CLOSE_WAIT状态,导致大量连接堆积,最后把端口耗尽了。排查了半天才发现是对方关闭后,我方没调用close()。

注意:CLOSE_WAIT状态如果大量出现,八成是你的应用层代码忘了关闭socket。用netstat -n | grep CLOSE_WAIT可以快速定位。

3.3 状态转换图

下面这张图,是我用SVG画的TCP状态转换图。它把三次握手和四次挥手的所有状态变化串在了一起。你想想看,掌握了这张图,TCP连接管理你就通了八成。

TCP状态转换图 客户端 服务端 CLOSED SYN_SENT ESTABLISHED FIN_WAIT_1 FIN_WAIT_2 TIME_WAIT CLOSED LISTEN SYN_RCVD ESTABLISHED CLOSE_WAIT LAST_ACK ① SYN ② SYN+ACK ③ ACK ① FIN ② ACK ③ FIN ④ ACK 三次握手 四次挥手 ACK确认

3.4 半关闭状态

半关闭,是TCP的一个特性。它允许一方关闭发送通道,但保留接收通道。说白了就是「我不说了,但我还能听」。

什么时候用?举个例子。我做过一个文件上传服务,客户端发完文件后调用shutdown(SHUT_WR),告诉服务器「文件发完了」。但客户端还能接收服务器的响应(比如上传成功或失败)。这就是半关闭的典型场景。

关键点:半关闭通过shutdown()函数实现,而不是close()。close()会直接关闭整个连接,而shutdown()可以只关一半。

代码示例:

// 客户端:关闭写端,保留读端
shutdown(sockfd, SHUT_WR);

// 此时服务端read()会返回0,表示对方关闭了写端
// 但客户端仍然可以read()服务端发来的数据

避坑指南:我曾经在写一个聊天程序时,用了close()代替shutdown(),结果对方还没读完数据连接就断了。后来改成shutdown(SHUT_WR)才解决问题。记住:close()是粗暴的,shutdown()是优雅的。

3.5 TIME_WAIT详解

TIME_WAIT,是四次挥手中最让人头疼的状态。它持续2MSL(Maximum Segment Lifetime,报文最大生存时间),通常为1-4分钟。

为什么要有TIME_WAIT?两个原因:

  • 确保最后一个ACK到达:如果ACK丢了,被动关闭方会重发FIN。TIME_WAIT状态能处理这个重传。
  • 防止旧连接干扰新连接:同一个端口对(IP+端口)如果被重用,旧连接在网络中滞留的报文可能被误认为是新连接的数据。

我在项目中遇到过,高并发服务器上TIME_WAIT堆积到几万个,导致新连接无法建立。为什么呢?因为端口被占满了。

注意:TIME_WAIT过多时,可以调整内核参数net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle。但tcp_tw_recycle在NAT环境下有坑,我建议慎用。稳妥的做法是开启tcp_tw_reuse,配合tcp_timestamps使用。

查看TIME_WAIT的命令:

# 统计TIME_WAIT数量
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

# 或者用ss命令(更快)
ss -tan | awk 'NR>1 {++S[$1]} END {for(a in S) print a, S[a]}'

3.6 常见问题与排查

问题 现象 排查思路
连接建立失败 connect()超时 检查服务端是否监听、防火墙是否放行
CLOSE_WAIT堆积 连接数持续增长 检查应用层是否忘了close()
TIME_WAIT过多 新连接无法建立 开启tcp_tw_reuse或调整端口范围
半连接攻击 SYN_RCVD大量堆积 开启tcp_syncookies

嗯,TCP的状态管理就这些。掌握了三次握手、四次挥手、半关闭和TIME_WAIT,你就能应对绝大多数连接问题了。下次线上出问题,别慌,先netstat看一眼状态,八成能找到线索。