2. TCP连接管理:三次握手建立连接、四次挥手断开连接、半关闭状态与TCP状态转换图
TCP 的连接管理,说白了就是两个端点之间怎么「对上暗号」开始聊天,以及怎么「体面告别」结束对话。我做了这么多年网络排查,发现很多线上问题归根结底都是连接管理没搞明白。今天咱们就把这块彻底吃透。
2.1 三次握手:建立连接
为什么叫「三次握手」?因为需要交换三个报文段才能把连接建起来。你想想看,如果只有两次,服务端怎么知道客户端已经准备好接收数据了?
核心流程:
- 第一次握手(SYN): 客户端发送 SYN 报文,初始序列号设为
x。客户端进入SYN_SENT状态。 - 第二次握手(SYN+ACK): 服务端收到后,回复 SYN+ACK,确认号
ack=x+1,自己的初始序列号为y。服务端进入SYN_RCVD状态。 - 第三次握手(ACK): 客户端收到后,发送 ACK,确认号
ack=y+1。双方进入ESTABLISHED状态。
我个人习惯把三次握手比作打电话:
- 客户端:「喂,能听到吗?」(SYN)
- 服务端:「听到了,你能听到我吗?」(SYN+ACK)
- 客户端:「听到了,开始说吧。」(ACK)
嗯,这里要注意:第三次握手是可以携带数据的。但前两次不行,因为连接还没建立,数据发过去也没人认。
避坑指南: 我曾经遇到过一个问题——客户端大量发送 SYN 但不完成第三次握手,这叫 SYN 洪水攻击。服务端会维护一个半连接队列,如果队列满了,正常用户的连接就进不来了。解决方案是开启 SYN Cookie,把连接信息编码到 SYN+ACK 的序列号里,等收到第三次握手再重建。
2.2 四次挥手:断开连接
断开连接比建立连接复杂,因为 TCP 是全双工的——两个方向可以独立关闭。所以需要四次挥手。
核心流程:
- 第一次挥手(FIN): 主动关闭方发送 FIN,序列号为
u。进入FIN_WAIT_1状态。 - 第二次挥手(ACK): 被动关闭方回复 ACK,确认号
ack=u+1。进入CLOSE_WAIT状态。主动方收到后进入FIN_WAIT_2。 - 第三次挥手(FIN): 被动关闭方发送 FIN,序列号为
v。进入LAST_ACK状态。 - 第四次挥手(ACK): 主动关闭方回复 ACK,确认号
ack=v+1。进入TIME_WAIT状态,等待 2MSL 后关闭。
为什么是四次而不是三次?因为服务端收到 FIN 后,可能还有数据没发完,不能马上关闭。所以先回 ACK 告诉客户端「我知道了」,等数据发完再发 FIN。说白了,ACK 和 FIN 是分开的。
常见问题: 我见过很多开发者在写网络程序时,服务端收到 FIN 后直接调用 close(),导致客户端还在读数据就被强制关闭了。正确的做法是:先调用 shutdown(SHUT_RD) 关闭读方向,等数据发完再 close()。
2.3 半关闭状态
半关闭是 TCP 的一个特性——允许一端关闭自己的发送通道,但保留接收通道。这在某些场景下非常有用。
举个例子:客户端发完请求后,调用 shutdown(SHUT_WR) 告诉服务端「我说完了,你慢慢处理」。服务端知道客户端不会再发数据了,但还能继续发响应。等响应发完,服务端再关闭自己的发送通道。
实战经验: 我曾经在做一个 HTTP 代理服务器时,利用半关闭状态解决了「请求-响应边界」的问题。客户端发完请求后立即半关闭,代理就知道请求结束了,可以开始转发。如果不这样做,代理就得自己解析 HTTP 头部来找到请求结束位置,麻烦得多。
半关闭的 API 调用:
// 关闭发送方向,保留接收方向
shutdown(sockfd, SHUT_WR);
// 关闭接收方向,保留发送方向
shutdown(sockfd, SHUT_RD);
// 完全关闭
shutdown(sockfd, SHUT_RDWR);
注意:close() 会立即关闭两个方向,而 shutdown() 可以精细控制。我建议在需要优雅关闭的场景下优先使用 shutdown()。
2.4 TCP 状态转换图
TCP 的状态机是整个协议最核心的部分。我当年考 CCIE 时,这张图背了不下 50 遍。但后来发现,光背没用,得理解每个状态的含义和转换条件。
下面这张 SVG 图展示了完整的 TCP 状态转换过程:
2.5 关键状态详解
| 状态 | 所属端 | 含义 | 常见问题 |
|---|---|---|---|
LISTEN |
服务端 | 等待客户端连接 | 端口被占用时无法进入此状态 |
SYN_SENT |
客户端 | 已发送 SYN,等待 SYN+ACK | 对端无响应时卡在此状态 |
SYN_RCVD |
服务端 | 已收到 SYN,等待 ACK | 半连接队列满时丢包 |
ESTABLISHED |
双方 | 连接已建立,可传输数据 | 正常状态,无需担心 |
FIN_WAIT_1 |
主动关闭方 | 已发送 FIN,等待 ACK | 对端不回复时卡住 |
FIN_WAIT_2 |
主动关闭方 | 已收到 ACK,等待 FIN | 对端不关闭时卡住 |
TIME_WAIT |
主动关闭方 | 等待 2MSL 后关闭 | 大量 TIME_WAIT 耗尽端口 |
CLOSE_WAIT |
被动关闭方 | 已收到 FIN,等待应用关闭 | 应用未调用 close() 时泄漏 |
LAST_ACK |
被动关闭方 | 已发送 FIN,等待 ACK | 对端不回复时卡住 |
重点提醒: TIME_WAIT 状态经常被误解。它存在的两个原因:
- 保证最后一个 ACK 能到达对端(如果丢了,对端会重发 FIN)
- 让网络中残留的报文段过期,避免影响新连接
2MSL 是报文最大生存时间的两倍,通常为 1-4 分钟。我曾经在压测时遇到大量 TIME_WAIT 导致端口耗尽,后来通过开启 tcp_tw_reuse 和 tcp_tw_recycle 解决。但注意,tcp_tw_recycle 在 NAT 环境下会出问题,慎用。
2.6 实战排查技巧
最后分享几个我常用的排查命令:
# 查看当前所有 TCP 连接状态统计
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'
# 查看特定端口的连接状态
ss -tna | grep :8080
# 查看 TIME_WAIT 状态的连接详情
ss -tna state time-wait
# 抓取三次握手过程
tcpdump -i eth0 'tcp port 80 and (tcp-syn or tcp-ack)'
嗯,这里要注意:ss 命令比 netstat 快得多,我建议优先用 ss。尤其是在生产环境,netstat 读取 /proc 文件系统,连接数多时会很慢。
个人经验: 有一次线上服务突然大量报错「Connection reset by peer」,我一看 ss -tna,发现 CLOSE_WAIT 状态堆积了上千个。排查后发现是业务代码在收到 FIN 后没有调用 close(),导致服务端一直卡在 CLOSE_WAIT。修复后问题立刻消失。所以,如果你看到 CLOSE_WAIT 堆积,十有八九是应用层忘了关闭 socket。