接收窗口是怎么告诉发送方“慢一点”的?接收方不读数据会发生什么?窗口大小怎么决定吞吐?这篇抓接收方每个 ACK 里的窗口,看它随缓冲区变化,再用 netlab 的 100 ms 往返验证吞吐 ≈ 窗口 ÷ RTT。
接收方(客户端,connect 之前设的 SO_RCVBUF)通告的窗口缩放系数 3(窗口字段 × 2^3);SO_RCVBUF 读回 342972 字节;发送方 SO_SNDBUF 146988 字节
第几个 ACK 确认号(已收到字节) 窗口字段(原始) 实际窗口 = 字段 × 2^3
1 0 42872 342976
2 16332 40831 326648
3 32664 38790 310320
4 48996 36749 293992
5 65328 34708 277664
6 … … …
7 294808 6028 48224
8 311140 3987 31896
9 327472 1946 15568
10 327680 1920 15360
共 23 种(确认号, 窗口)的 ACK(只算接收方读数据之前的);窗口恰好为 0 的出现 0 种,窗口没有恰好降到 0:最小是 15360 字节(小于一个 MSS,发送方因此停发)
接收方什么都没读时,接收缓冲区里可读的字节数(FIONREAD)327680;最后一个 ACK 通告的窗口 15360 字节,小于一个 MSS(16344)——此时发送方不会再发小包;发送方的 send() 一共让内核收下了 474460 字节 ≈ 接收缓冲区 327680 + 发送方自己的发送缓冲区(SO_SNDBUF 146988),再往后 send() 返回 EAGAIN——这就是流量控制;接收方后来把全部数据读完共 474460 字节接收方在每个 ACK 里通告还能收多少(窗口字段 × 缩放系数)。接收方不读,窗口越来越小,小于一个 MSS 时发送方停发,send() 最终返回 EAGAIN。
接收方读速约 1 MB/s:发送方在 2 秒里 send() 出去 4.8 MB(含进入缓冲区的),接收方读走 1.9 MB,折合接收方 0.9 MB/s 接收方读速约 20 MB/s:发送方在 2 秒里 send() 出去 35.8 MB(含进入缓冲区的),接收方读走 33.0 MB,折合接收方 16.5 MB/s 接收方读速不限速:发送方在 2 秒里 send() 出去 5926.1 MB(含进入缓冲区的),接收方读走 5926.1 MB,折合接收方 1974.4 MB/s 发送速率被接收方的读速限制住,不会无限塞满网络和对方内存:接收窗口是对端告诉你“我还能收多少”
发送方的实际速率被接收方的读速限制住,不会无限制地塞满对方的内存。
接收方 SO_RCVBUF = 16384(读回 326640):3 秒读到 9.06 MB,平均 3.02 MB/s;理论上限 读回的窗口 ÷ RTT = 3.27 MB/s 接收方 SO_RCVBUF = 65536(读回 342972):3 秒读到 9.13 MB,平均 3.04 MB/s;理论上限 读回的窗口 ÷ RTT = 3.43 MB/s 接收方 SO_RCVBUF = 262144(读回 277644):3 秒读到 9.16 MB,平均 3.05 MB/s;理论上限 读回的窗口 ÷ RTT = 2.78 MB/s 接收方 SO_RCVBUF = 1048576(读回 1061580):3 秒读到 32.37 MB,平均 10.79 MB/s;理论上限 读回的窗口 ÷ RTT = 10.62 MB/s 窗口越小,在往返时间长的链路上越吃亏:发送方每个往返最多发一个窗口的数据,然后必须等 ACK(见 BDP 一篇)
每个往返最多发一个窗口的数据,吞吐 ≈ 窗口 ÷ RTT;往返越长,窗口越要大。macOS 对小于约 256 KiB 的 SO_RCVBUF 设置没有照办。
接收方读了 400 次、每次 100 字节(共 40000 字节):这段时间里抓到“窗口更新”ACK(确认号不变、窗口字段变了)1 个;窗口更新的前几次(实际窗口,字节):[(7688, 269000)] 每次窗口变大的幅度:[261312]...——接收方不会读走 100 字节就立刻通告,而是等空出来的空间累积到一定程度(这里与 MSS 16344 / 缓冲区的一半相关)才发一个窗口更新,避免发送方反复发很小的包(糊涂窗口综合征,RFC 1122)
读走少量数据时接收方不会马上通告新窗口,避免糊涂窗口综合征。