3、TCP状态机全景图:11种状态总览、状态转换图、CLOSED状态详解
说实话,TCP 状态机是每个网络工程师都绕不开的坎。我记得刚入行那会儿,面试官问我「TCP 有多少种状态」,我脱口而出「11 种」。结果他接着问「那你给我画一下状态转换图」,我当场就卡住了。
后来在线上排查问题多了,我才真正理解——状态机不是背出来的,是用出来的。你想想看,每次用 netstat -anp 看到一堆 TIME_WAIT、CLOSE_WAIT,如果不懂状态机,你连问题出在哪都不知道。
3.1 11种状态总览
TCP 连接从建立到关闭,一共会经历 11 种状态。我习惯把它们分成三组:
- 连接建立阶段:CLOSED、LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED
- 主动关闭阶段:FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT
- 被动关闭阶段:CLOSE_WAIT、LAST_ACK、CLOSING
下面这张表把每种状态的核心含义列清楚了,建议你收藏起来,排查问题时对照着看。
| 状态 | 含义 | 典型场景 |
|---|---|---|
| CLOSED | 初始状态,没有连接 | socket 刚创建或关闭后 |
| LISTEN | 服务端等待客户端连接 | 调用 listen() 后 |
| SYN_SENT | 客户端发送 SYN 后 | 调用 connect() 后 |
| SYN_RCVD | 服务端收到 SYN,回复 SYN+ACK | 三次握手第二步 |
| ESTABLISHED | 连接已建立,可以传输数据 | 三次握手完成 |
| FIN_WAIT_1 | 主动关闭方发送 FIN 后 | 调用 close() 后 |
| FIN_WAIT_2 | 主动关闭方收到对端 ACK | 半关闭状态 |
| TIME_WAIT | 等待 2MSL 后彻底关闭 | 主动关闭方最后阶段 |
| CLOSE_WAIT | 被动关闭方收到 FIN,等待应用关闭 | 服务端常见问题点 |
| LAST_ACK | 被动关闭方发送 FIN,等待最终 ACK | 被动关闭最后一步 |
| CLOSING | 双方同时关闭时的中间状态 | 少见,但存在 |
我的经验:线上排查时,CLOSE_WAIT 和 TIME_WAIT 是出现频率最高的异常状态。CLOSE_WAIT 堆积通常意味着应用层忘了调用 close(),TIME_WAIT 过多则要考虑调整内核参数。
3.2 状态转换图
光看表格不够直观,我画了一张状态转换图。这张图我当年手抄过不下十遍,每次排查问题都要在脑子里过一遍。
核心要点:这张图里最值得关注的是两条路径——主动关闭路径(ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSED)和被动关闭路径(ESTABLISHED → CLOSE_WAIT → LAST_ACK → CLOSED)。线上 90% 的异常都出在这两条路径上。
3.3 CLOSED 状态详解
CLOSED 是 TCP 状态机的起点,也是终点。说白了,它就是「没有连接」的状态。但你别小看它,很多坑恰恰出在这个「什么都没有」的状态上。
3.3.1 CLOSED 的本质
CLOSED 状态对应的是 socket 数据结构中 tcp_state 字段的初始值。在内核里,它长这样:
/* include/net/tcp_states.h */
enum {
TCP_ESTABLISHED = 1,
TCP_SYN_SENT,
TCP_SYN_RECV,
TCP_FIN_WAIT1,
TCP_FIN_WAIT2,
TCP_TIME_WAIT,
TCP_CLOSE,
TCP_CLOSE_WAIT,
TCP_LAST_ACK,
TCP_LISTEN,
TCP_CLOSING, /* 注意:CLOSING 是新增的 */
};
注意看,内核里 TCP_CLOSE 的值是 7,不是 0。我当年调试时踩过这个坑——以为 0 是 CLOSED,结果查了半天发现是 7。
3.3.2 什么时候进入 CLOSED?
进入 CLOSED 状态有几种情况:
- socket 刚创建时:调用
socket()后,状态就是 CLOSED - 连接正常关闭后:TIME_WAIT 超时后回到 CLOSED
- 连接异常终止时:收到 RST 包,直接跳到 CLOSED
- 调用
close()但连接未建立时:比如connect()失败后
我曾经踩过的坑:有一次线上服务出现大量连接超时,排查了半天发现是客户端在 connect() 失败后没有重置 socket 状态,导致后续复用同一个 socket 时状态还是 CLOSED,但内核里的资源已经乱了。解决方案很简单——每次 connect() 失败后重新创建 socket。
3.3.3 CLOSED 状态下的内核行为
当 socket 处于 CLOSED 状态时,内核会做以下几件事:
- 释放接收和发送缓冲区:所有未发送和未确认的数据都会被丢弃
- 删除定时器:重传定时器、保活定时器等全部清除
- 更新内存统计:减少
tcp_memory_allocated计数 - 通知应用层:通过
epoll或select返回POLLHUP事件
这里有个细节很多人不知道——CLOSED 状态的 socket 仍然可以调用 listen() 或 connect()。内核不会报错,但会重新初始化状态机。我见过有人利用这个特性做 socket 复用,但说实话,风险很大,不建议这么干。
3.3.4 如何查看 CLOSED 状态?
用 netstat 或 ss 命令时,CLOSED 状态的连接默认是不显示的。因为 CLOSED 意味着连接已经不存在了,显示出来没有意义。但如果你非要看,可以加 -a 参数:
# 查看所有状态,包括 CLOSED
netstat -an | grep CLOSED
# 或者用 ss,更高效
ss -t state closed
不过说实话,我工作这么多年,几乎没遇到过需要查看 CLOSED 状态的场景。真正需要关注的是 TIME_WAIT 和 CLOSE_WAIT,这两个才是线上问题的重灾区。
个人习惯:我排查问题时,一般先用 ss -t state established 看正常连接,再用 ss -t state time-wait 和 ss -t state close-wait 看异常状态。CLOSED 状态基本不看,除非要确认 socket 是否真的被释放了。
3.3.5 CLOSED 与 RST 的关系
还有一个容易混淆的点:收到 RST 包后,状态会直接跳到 CLOSED,而不是经过正常的四次挥手流程。这意味着:
- 不会进入 TIME_WAIT 状态
- 不会等待 2MSL
- 数据可能丢失
为什么会这样?因为 RST 表示「连接异常终止」,内核认为没必要走正常关闭流程了。我遇到过一种情况:服务端负载过高时,内核会自动发送 RST 来拒绝新连接,这时候客户端看到的就是 Connection reset by peer。
嗯,CLOSED 状态就讲到这里。虽然它看起来简单,但理解了 CLOSED,你才能更好地理解其他 10 种状态是怎么来的、怎么没的。状态机这东西,说白了就是「从 CLOSED 开始,最终回到 CLOSED」的一个循环。