没有窗口,发一个段就要等一个确认:速度被往返时间锁死。 窗口的本质是「允许在路上同时存在多少数据」,不是「一次发多少」。
6.4.5 讲了「一个段多大」,这一节讲「一次能发几个段」。 6.4.8 的流控制和 6.4.9 的拥塞控制都会用窗口这个概念,这里先把地基打好。
一、停等协议的困境
最朴素的做法是「发一个、等一个确认」:这保证不会乱序,也不会把对方淹了。 但它的效率被「往返时间」锁死——一个往返只能送一个段,无论链路有多快。 算一下就知道有多糟:每段 1460 字节、往返 100 毫秒,一秒只能送 10 个段,也就是约 117 kbps。 哪怕链路是千兆的,也只能跑到这个数——因为时间全花在「等确认」上了。
二、窗口怎么解决它
做法很直接:不等了,一次发多个。 「窗口」就是「已经发出去但还没被确认的数据量上限」。 只要没超过这个上限,就可以继续发。
| 窗口大小 | 要几个往返 | 说明 |
|---|---|---|
| 1 | 10 | 停等,每段一个往返 |
| 4 | 3 | 一次发四个,最后一批不满 |
| 10 | 1 | 全部一次发出 |
第二行「最后一批不满」是个细节:10 个段、窗口 4,要发 3 轮(4 加 4 加 2)。 所以「窗口越大越好」在「数据量小」的时候并不成立——因为最后一个窗口的空位是浪费的。 第三行才是理想状态:一个往返把所有数据都送出去。 要做到这一点,窗口必须大于等于「在路上的数据量」。
三、带宽延迟积
「在路上的数据量」有个名字,叫带宽延迟积:链路带宽乘以往返时间。 它决定了「窗口至少要多小才不至于成为瓶颈」。 算一下常见的两个场景:同城 100 Mbps、往返 1 毫秒,积是 12.5 KB,八九个段就够; 跨洋 100 Mbps、往返 200 毫秒,积是 2.5 MB,要 1700 多个段。 所以「同样的带宽,跨洋的 TCP 更难跑满」——因为窗口要求大了几十倍。 这就是「长肥管道」问题:又长(往返大)又肥(带宽大)的链路,窗口成了瓶颈,而不是带宽。 TCP 后来加了「窗口缩放」选项,把窗口字段从 16 位扩到 30 位—— 不然最大只能表示 65535 字节,跑不满任何高速链路。
四、动手 Demo:窗口为什么能把速度提上来
10 个段、每段 1460 字节、往返 100 毫秒,比较三种窗口。 最后再算一次「填满 100 Mbps 需要多大窗口」。
seg, seg_bytes, rtt = 10, 1460, 0.1
for win in (1, 4, 10):
rounds = (seg + win - 1) // win
sec = rounds * rtt
print('窗口 %2d 要 %d 个往返 %.0f ms,吞吐 %.2f Mbps'
% (win, rounds, sec * 1000, seg * seg_bytes * 8 / sec / 1e6))
bdp = 100e6 * 0.1 / 8
print('带宽延迟积 %.2f MB,约 %d 个段' % (bdp / 1e6, int(bdp / 1460)))
实跑输出:
--------------------------------------------------------------------
窗口为什么能把速度提上来
--------------------------------------------------------------------
**没有窗口的时候,发一个段就要等一个确认**:**一个往返只能送一个段,速度被「往返时间」锁死。**
方式 要几个往返 总耗时(毫秒) 吞吐(Mbps)
------------------------------------------------------------------
停等(窗口 1) 10 1000 0.12
窗口 4 3 300 0.39
窗口 10 1 100 1.17
**三行的差距全是「往返次数」造成的**:**窗口 10 一次把 10 个段都发出去,只等一个往返。**
**所以「窗口」的本质是「允许在路上同时存在多少数据」**,
**而不是「一次发多少」**——**这两个说法在效果上一样,但后者容易理解错。**
**再算一个「填满链路需要多大窗口」**:
**100 Mbps 链路、往返 100 毫秒,带宽延迟积是 1.25 MB**,
**换算成段约 856 个**。
**也就是说,窗口小于这个数,链路就跑不满**——**这就是「长肥管道」问题**:
**跨洋链路带宽大、往返长,窗口不够就成了瓶颈。**
窗口 1 和窗口 10 差了整整十倍:差别全部来自「往返次数」。 所以「提高吞吐」最快的办法不是「加带宽」,而是「把窗口撑大」——在已经够快的链路上尤其如此。 「约 856 个段」这个数说明什么:100 Mbps、往返 100 毫秒的链路,要 856 个段才能跑满。 而默认窗口只有 65535 字节(约 45 个段), 所以「不开窗口缩放,就别想跑满」——这是长距离高速传输最常见的性能问题。 最后一个提醒:窗口不是越大越好。 窗口大意味着「一次发出去的包多」,丢一个就要重传,而且「丢包时已经白发了很多」。 所以窗口的合适值,取决于「链路的丢包率」。
30 秒实验
- 算一下:每段 1460、往返 50 毫秒、窗口 10,吞吐是多少?
- 想一下:为什么「窗口 4」要 3 个往返而不是 2.5 个?
- 想一下:窗口缩放为什么是必须的?
常见误区
- 以为「窗口是「一次发多少」」。 是「在路上的数据量上限」,发多少由 TCP 自己切。
- 以为「窗口越大越好」。 丢包时浪费也大,合适值取决于链路质量。
- 以为「带宽够就一定快」。 还要看带宽延迟积,窗口不够照样跑不满。
自测三问(点开对答案)
1. 窗口解决什么问题? —— **「发一个等一个」的低效**:允许一次发多个段,不必逐个等确认。 2. 带宽延迟积是什么? —— **带宽乘以往返时间**,等于「在路上的数据量」,决定窗口至少要多大。 3. 什么是长肥管道问题? —— **带宽大、往返长的链路,窗口成了瓶颈而不是带宽**,要靠窗口缩放解决。下一节讲「窗口里丢了一个包怎么办」。
- ☐ (本节完,下一节 6.4.7 窗口控制与重发控制)