bigywater.tech
网络协议 · 40

TCP 状态机:用 netstat 逐个观察 11 个状态

TCP 状态机的 11 个状态,在 netstat 里到底什么时候出现?CLOSE_WAIT 为什么会堆积,TIME_WAIT 留给谁?这篇给回环加延迟,每 20 ms 查一次 netstat,把一次连接从诞生到消失的每个状态和时刻记下来。

11 / 11
本机 netstat 里观察到的 TCP 状态数(10 个出现的状态加没有这一行的 CLOSED)
约 300 ms
服务端停在 SYN_RCVD 的时间:回 SYN-ACK 到收到第三个 ACK 的一个往返
2249 ms
客户端先 close 的场景里,客户端进入 TIME_WAIT 的时刻;服务端 LAST_ACK 之后直接消失

握手的状态

实测 · 本机执行输出
  场景 1:握手、数据、客户端先 close、服务端晚一秒才 close(单向延迟 150 ms,往返 300 ms;括号里是相对 connect 的时间):
    客户端:SYN_SENT(111 ms) → ESTABLISHED(411 ms) → FIN_WAIT_1(1075 ms) → FIN_WAIT_2(1378 ms) → TIME_WAIT(2249 ms)
    服务端:SYN_RCVD(263 ms) → ESTABLISHED(580 ms) → CLOSE_WAIT(1222 ms) → LAST_ACK(2110 ms) → (无)(2381 ms)
    监听套接字:LISTEN(8 ms)
  这个场景里见到的状态:CLOSE_WAIT、ESTABLISHED、FIN_WAIT_1、FIN_WAIT_2、LAST_ACK、LISTEN、SYN_RCVD、SYN_SENT、TIME_WAIT
  场景 2:客户端 connect 之后立刻 close,服务端也立刻 close(看 LAST_ACK)(单向延迟 150 ms,往返 300 ms;括号里是相对 connect 的时间):
    客户端:SYN_SENT(118 ms) → ESTABLISHED(405 ms) → FIN_WAIT_1(576 ms) → CLOSING(730 ms) → TIME_WAIT(871 ms)
    服务端:SYN_RCVD(254 ms) → FIN_WAIT_1(576 ms) → CLOSING(730 ms) → TIME_WAIT(871 ms)
  场景 3:两端同时 close(看 CLOSING)(单向延迟 150 ms,往返 300 ms;括号里是相对 connect 的时间):
    一端:SYN_SENT(135 ms) → ESTABLISHED(427 ms) → FIN_WAIT_1(1081 ms) → CLOSING(1241 ms) → TIME_WAIT(1386 ms)
    另一端:SYN_RCVD(254 ms) → ESTABLISHED(570 ms) → FIN_WAIT_1(1081 ms) → CLOSING(1241 ms) → TIME_WAIT(1386 ms)

客户端 SYN_SENT → ESTABLISHED;服务端 SYN_RCVD → ESTABLISHED,其中 SYN_RCVD 持续一个往返。服务端有没有 accept() 不影响状态。

11 个状态

实测 · 本机执行输出
    CLOSED      本次观察到  没有连接(netstat 里没有这一行)
    LISTEN      本次观察到  服务端 listen() 之后
    SYN_SENT    本次观察到  客户端发出 SYN、等 SYN-ACK
    SYN_RCVD    本次观察到  服务端收到 SYN、发出 SYN-ACK、等第三个 ACK
    ESTABLISHED 本次观察到  三次握手完成,数据传输阶段
    FIN_WAIT_1  本次观察到  主动关闭方发出 FIN、等对方的 ACK
    FIN_WAIT_2  本次观察到  主动关闭方的 FIN 已被 ACK,等对方的 FIN
    CLOSE_WAIT  本次观察到  被动关闭方收到 FIN、已 ACK,等本端应用 close()
    CLOSING     本次观察到  两端同时关闭:发出 FIN 后先收到对方的 FIN
    LAST_ACK    本次观察到  被动关闭方发出 FIN、等最后的 ACK
    TIME_WAIT   本次观察到  主动关闭方收到对方的 FIN 并回了 ACK,等 2MSL
  三个场景里 netstat 抓到了 10 个状态(LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、CLOSING、LAST_ACK、TIME_WAIT);CLOSED 本来就是“没有这一行”,算上它是 11 / 11

LISTEN、SYN_SENT、SYN_RCVD、ESTABLISHED、FIN_WAIT_1、FIN_WAIT_2、CLOSE_WAIT、CLOSING、LAST_ACK、TIME_WAIT 十个在 netstat 里都能抓到,CLOSED 就是没有这一行。

主动关闭、被动关闭与同时关闭

实测 · 本机执行输出
  非阻塞 connect 到一个丢弃 SYN 的地址,0.5 秒后 netstat 看到的状态:['SYN_SENT']
  场景 4:非阻塞 connect,服务端不 accept(accept 与状态无关,握手由内核完成)(单向延迟 150 ms,往返 300 ms;括号里是相对 connect 的时间):
    客户端:SYN_SENT(127 ms) → ESTABLISHED(432 ms) → FIN_WAIT_1(837 ms) → FIN_WAIT_2(1139 ms)
    服务端:SYN_RCVD(279 ms) → ESTABLISHED(579 ms) → CLOSE_WAIT(994 ms)
  服务端内核在回了 SYN-ACK 之后到收到客户端第三个 ACK 之前处于 SYN_RCVD;只要握手完成就是 ESTABLISHED——应用有没有调用 accept() 不影响状态,没被 accept 的连接排在全连接队列里(见 backlog 一篇)

主动方 FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT;被动方 CLOSE_WAIT(等本端应用 close)→ LAST_ACK → 消失;同时关闭两端 FIN_WAIT_1 → CLOSING → TIME_WAIT。大量 CLOSE_WAIT 说明应用没有及时 close()。

分步演示

TCP 状态机
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。全部在 lo0 上,延迟由 netlab 注入(单向 150 ms);状态靠每 20 ms 通过 netstat -an -p tcp 取一次快照,记录的是第一次看到的时刻,精度约几十毫秒;netstat 里没有的连接就是 CLOSED
    • 没有验证:真实网络上的同样路径;状态之间的超时(FIN_WAIT_2 的超时、TIME_WAIT 的 2MSL 时长,见下一篇);大量 CLOSE_WAIT / SYN_RCVD 堆积的诊断;其它系统的 netstat 对状态的命名差异
    京ICP备2026067502号-1京公网安备11010502063487号