📚 第六章 TCP与UDP
📄 一句话概括

没有窗口,发一个段就要等一个确认:速度被往返时间锁死。 窗口的本质是「允许在路上同时存在多少数据」,不是「一次发多少」。

📝 本节和前面几节的关系

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 秒实验

  1. 算一下:每段 1460、往返 50 毫秒、窗口 10,吞吐是多少?
  2. 想一下:为什么「窗口 4」要 3 个往返而不是 2.5 个?
  3. 想一下:窗口缩放为什么是必须的?

常见误区

  1. 以为「窗口是「一次发多少」」。 是「在路上的数据量上限」,发多少由 TCP 自己切。
  2. 以为「窗口越大越好」。 丢包时浪费也大,合适值取决于链路质量。
  3. 以为「带宽够就一定快」。 还要看带宽延迟积,窗口不够照样跑不满。
自测三问(点开对答案) 1. 窗口解决什么问题? —— **「发一个等一个」的低效**:允许一次发多个段,不必逐个等确认。 2. 带宽延迟积是什么? —— **带宽乘以往返时间**,等于「在路上的数据量」,决定窗口至少要多大。 3. 什么是长肥管道问题? —— **带宽大、往返长的链路,窗口成了瓶颈而不是带宽**,要靠窗口缩放解决。

下一节讲「窗口里丢了一个包怎么办」。

相关笔记