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 状态转换图

光看表格不够直观,我画了一张状态转换图。这张图我当年手抄过不下十遍,每次排查问题都要在脑子里过一遍。

TCP 状态转换图 TCP 11种状态转换图 CLOSED LISTEN SYN_SENT SYN_RCVD ESTABLISHED FIN_WAIT_1 CLOSE_WAIT FIN_WAIT_2 LAST_ACK CLOSING TIME_WAIT 被动打开 主动打开 收到SYN 发送SYN 收到SYN+ACK 收到ACK 主动关闭 收到FIN 收到ACK 收到FIN 应用关闭 收到FIN 收到ACK 收到ACK 2MSL超时 建立/基础状态 数据传输 主动关闭 被动关闭 同时关闭

核心要点:这张图里最值得关注的是两条路径——主动关闭路径(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 状态时,内核会做以下几件事:

  1. 释放接收和发送缓冲区:所有未发送和未确认的数据都会被丢弃
  2. 删除定时器:重传定时器、保活定时器等全部清除
  3. 更新内存统计:减少 tcp_memory_allocated 计数
  4. 通知应用层:通过 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」的一个循环。