bigywater.tech
网络协议 · 50

零窗口与窗口探测:接收方处理不过来时

接收方处理不过来、窗口变成 0 之后,发送方怎么办?万一窗口重新打开的通知丢了会不会永远卡住?这篇把接收缓冲区塞满,抓发送方的零窗口探测包,再让接收方重新读数据看恢复要多久。

每 5.0 秒
零窗口探测包的间隔(1 字节载荷,序号是窗口外的下一个字节),本机没有看到指数增长
0.21 ms
接收方读走数据后“窗口更新”ACK 发出的延迟(随后 0.58 ms 就有新数据)
976 字节
接收窗口的最后一个取值——比 1000 字节的写入单位还小,发送方已经发不动了

窗口变成 0

实测 · 本机执行输出
  接收缓冲区 342972 字节;发送方 send() 共收下 488000 字节;FIONREAD 可读 342000 字节
  接收方发出的 ACK 40 条(含 dummynet 之外无重复),其中窗口字段为 0 的 0 条;窗口的最后几个取值(实际窗口 = 字段 × 2^3,字节):[7976, 5976, 3976, 2976, 1976, 976]

接收方不读,窗口逐步缩小;小于发送方要发的包时就停发,等缓冲区吃满时通告窗口 0。

persist 探测

实测 · 本机执行输出
  零窗口从 5.10 秒(相对抓包开始)起;之后的 25 秒里发送方又发出了 4 个数据包(探测)
    探测 1:距零窗口 +5.0 秒,载荷 1 字节,序号(相对)= 342976
    探测 2:距零窗口 +10.0 秒,载荷 1 字节,序号(相对)= 342976
    探测 3:距零窗口 +15.0 秒,载荷 1 字节,序号(相对)= 342976
    探测 4:距零窗口 +20.0 秒,载荷 1 字节,序号(相对)= 342976
  相邻探测的间隔(秒):[5.0, 5.0, 5.0]
  每个探测后接收方回的 ACK 的窗口字段:[0](0 = 仍然没有空间;确认号 没有增加:探测的字节没有被接收)

窗口 0 时发送方起一个 persist 定时器,每 5 秒发一个 1 字节的探测包,接收方回当前窗口。这样即使窗口更新 ACK 丢了也不会永远死锁。

恢复

实测 · 本机执行输出
  接收方在读数据之前,发送方一个 send() 已经阻塞了 0.5 秒;接收方读走 100000 字节后,这个 send() 在 1.3 ms 内返回(send 耗时 0.51 秒)
  读数据之后第一个“窗口更新”ACK:读之后 0.21 ms,窗口字段 33176(非 0);之后第一个新数据包:读之后 0.58 ms
  窗口更新 ACK 是纯 ACK,不会被重传;万一丢了,靠探测包重新问到窗口(第二节),这就是为什么探测必须存在

接收方一读数据就主动发窗口更新 ACK,发送方毫秒级恢复;只有更新丢了才要等下一次探测。

分步演示

零窗口与窗口探测
本步的对象

    本文用到的工具与边界

    • 本机 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 上,接收方都在 connect 之前设置 SO_RCVBUF(实际缓冲区读回 342972 字节);第二节的 25 秒抓包期间接收方一直不读;窗口 = 窗口字段 × 2^3
    • 没有验证:探测间隔是否随失败次数增长到上限(只看了 4 次,固定 5.0 秒);窗口更新 ACK 丢失时的恢复(需要丢包);其它系统的 persist 定时器参数;零窗口时对端关闭或崩溃的探测行为
    京ICP备2026067502号-1京公网安备11010502063487号