3、TCP三次握手与四次挥手:状态转换图、半关闭状态、TIME_WAIT详解
TCP的连接管理,说白了就是三次握手和四次挥手。这两个过程,我当年刚入行时觉得很简单,不就是发几个包嘛。直到线上出了几次连接异常的问题,才意识到里面的门道有多深。今天咱们就把它彻底讲透。
3.1 三次握手:建立连接的过程
三次握手,是客户端和服务器建立TCP连接时,必须经历的三个步骤。为什么要三次?两次不行吗?嗯,这个问题我面试时经常问别人。
核心目的:确保双方都有发送和接收数据的能力,同时同步初始序列号(ISN)。
过程是这样的:
- 第一次握手(SYN):客户端发送一个SYN包,告诉服务器「我想连你」。此时客户端进入
SYN_SENT状态。 - 第二次握手(SYN+ACK):服务器收到后,回复SYN+ACK包,意思是「收到,我也准备好了」。服务器进入
SYN_RCVD状态。 - 第三次握手(ACK):客户端再发一个ACK包,表示「好的,开始传数据吧」。双方进入
ESTABLISHED状态。
我个人习惯用「打电话」来类比:你拨号(SYN),对方接听并说「喂?」(SYN+ACK),你回一句「喂,是我」(ACK)。通话建立。
小技巧:三次握手中,前两次不能携带数据,第三次可以。为什么?因为前两次还没确认对方能收,万一丢了还得重传,带数据就浪费了。
3.2 四次挥手:断开连接的过程
挥手比握手复杂一点。因为TCP是全双工的,双方都要关闭自己的通道。所以需要四次。
过程如下:
- 第一次挥手(FIN):主动关闭方发送FIN包,表示「我不发数据了」。进入
FIN_WAIT_1状态。 - 第二次挥手(ACK):被动关闭方回复ACK,表示「知道了」。进入
CLOSE_WAIT状态。主动方收到后进入FIN_WAIT_2。 - 第三次挥手(FIN):被动关闭方也发送FIN包,表示「我也不发了」。进入
LAST_ACK状态。 - 第四次挥手(ACK):主动关闭方回复ACK,然后进入
TIME_WAIT状态。被动方收到后关闭。
这里有个坑。我在项目中遇到过,服务端程序没处理好CLOSE_WAIT状态,导致大量连接堆积,最后把端口耗尽了。排查了半天才发现是对方关闭后,我方没调用close()。
注意:CLOSE_WAIT状态如果大量出现,八成是你的应用层代码忘了关闭socket。用netstat -n | grep CLOSE_WAIT可以快速定位。
3.3 状态转换图
下面这张图,是我用SVG画的TCP状态转换图。它把三次握手和四次挥手的所有状态变化串在了一起。你想想看,掌握了这张图,TCP连接管理你就通了八成。
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看一眼状态,八成能找到线索。