bigywater.tech
网络协议 · 57

半开连接:对端消失时本端多久才知道

“对端挂了,我什么时候才知道?”取决于怎么挂的:进程崩溃、主机断电、网线被拔,线路上留下的痕迹完全不同。这篇用 kill -9、SIGSTOP、SO_LINGER 0 和丢包注入逐种实测,看哪些情形本端立刻知道,哪些会形成半开连接。

18 / 40
在 50% 丢包下关闭服务端时,RST 丢失、客户端毫无察觉的半开连接个数
FIN+ACK
kill -9 对端进程后线上出现的包:内核替进程收尾
75.0 s
connect 到丢 SYN 的黑洞后失败的时间

进程死 ≠ 主机消失

实测 · 本机执行输出
  先正常通信:收到 b'pong'
  SIGKILL 之后线上的包:
    + 0.000 s  服务端→客户端  FIN+ACK  载荷 0 字节
    + 0.000 s  客户端→服务端  ACK      载荷 0 字节
  客户端 recv 返回 b''(空 = 对端有序关闭 FIN)

对端进程被 kill -9,内核发 FIN,本端立刻读到 EOF。真正悄无声息的是整台机器或连接状态消失。

冻住的对端

实测 · 本机执行输出
  对端冻住后客户端一直写,直到发送缓冲区和对端接收缓冲区都满,共写进 1400312 字节(1.34 MiB)后 send 返回 EWOULDBLOCK;线上服务端(内核)一直在回 ACK,共 33 个纯 ACK

SIGSTOP 的进程内核仍然 ACK,线路上看不出异常,只是窗口最终会满。

明确的通知

实测 · 本机执行输出
  客户端 send 报错:[Errno 32] Broken pipe
  第二次 send 报错:EPIPE Broken pipe
  线上的包:
    + 0.000 s  服务端→客户端  RST+ACK  载荷 0 字节

对端用 SO_LINGER(1,0) 关闭会发 RST,本端下一次 send 就是 EPIPE。

半开

实测 · 本机执行输出
  40 个连接里:客户端已被通知(ECONNRESET 或 EOF)的 22 个;毫无察觉的半开 18 个(RST 在 netlab 里被丢了)
  对这些半开连接各发一次数据后(服务端没有这个连接,会回 RST;RST 同样有 50% 被丢):{'ECONNRESET': 14, '仍无察觉': 4}

RST 在路上丢了,本端还是 ESTABLISHED。只有本端发数据去问,对端才会回 RST;这个 RST 也可能再丢。

黑洞的等待

实测 · 本机执行输出
  connect 到 example.com:81 在 75.0 秒后失败:ETIMEDOUT Operation timed out
  系统参数:net.inet.tcp.keepinit: 75000;net.inet.tcp.msl: 15000

connect 到黑洞 75.0 秒才超时,等于 keepinit。已建立连接遇黑洞要等多久,本篇没有测。

分步演示

半开连接
本步的对象

    本文用到的工具与边界

    • 本机 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 丢包 50% 制造 RST 丢失(丢包上限 50%),是模拟;第五节对 example.com:81;第二节写进字节数每次运行不同,以最后一次为准
    • 没有验证:主机断电、拔网线后已建立连接的重传超时(黑洞下的 tcp_maxrtx 等);对端重启后收到老连接数据的真实 RST(用 SO_LINGER 模拟的);半开连接上 keepalive 发现的时间(上一篇用丢包代替);NAT 表项过期造成的半开;各种 TCP 栈(Linux)的差异
    京ICP备2026067502号-1京公网安备11010502063487号