TCP带有流控功能,英文叫flow control。这是因为在现实情况下,TCP接收端处理数据的能力是有限的,所以TCP发送的数据最好不要超过接收端的处理能力,否则会有悲剧发生。轻则多余的无法处理的数据会被丢弃,重则会导致接收端的TCP协议栈崩溃。

TCP的流控采用“允许制”的,简单地说发送端所能发送的字节数,必须在接收端允许的窗口大小之内。

一些相应的名词:

  • 接收窗口:接收端可以接收的字节数
  • 发送窗口:允许发送端发送的字节数

稍微思考一下就知道流控的过程有以下步骤:

  1. 接收端:计算接收窗口大小
  2. 接收端:把接收窗口大小通告发送端
  3. 发送端:使用发送窗口控制发送的数据大小,使其不超过接收窗口大小

计算接收窗口大小

RFC793搬来的接收窗口的图,假设接收窗口大小为1:

       1          2          3
   ----------|----------|----------
          RCV.NXT    RCV.NXT
                    +RCV.WND

接收窗口由两个变量表示:

  • RCV.NXT 下一个待接收的字节的序列号,在上图的例子中为2
  • RCV.WND 接收窗口的大小,在上图的例子中为1

所以接收窗口是一个半开半闭区间[RCV.NXT, RCV.NXT+RCV.WND),以上图为例,是[2, 3)

把接收窗口大小通告发送端

TCP的头部定义一个16bit的字段,用于通告接收窗口的大小:

    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Data |           |U|A|P|R|S|F|                               |
   | Offset| Reserved  |R|C|S|S|Y|I|            Window             |
   |       |           |G|K|H|T|N|N|                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

16bit的Window字段最多可以表示65535个字节的接收窗口大小。如果在带宽很大的情况下,传输速率会受接收窗口大小的影响,那时候就要想办法对窗口大小进行扩张了。一个办法是使用RFC7323定义的“TCP Window Scale Option”对Window字段进行乘数扩展。

使用发送窗口控制发送的数据大小

RFC793搬来的发送窗口的图,

           1         2          3          4
      ----------|----------|----------|----------
             SND.UNA    SND.NXT    SND.UNA
                                  +SND.WND

发送窗口由三个变量控制:

  • SND.UNA 首个已发送,但是未被应答的序列号,在上图中是2
  • SND.NXT 下一个待发送的序列号,在上图中是3
  • SND.WND 发送窗口的大小,根据RCV.WND的大小来更新,在上图中是2

由上图可知,发送窗口也是一个半开半闭区间[SND.UNA, SND.UNA+SND.WND),以上图为例,是[2, 4)。发送窗口中已经发送的数据用半开半闭区间[SND.UNA, SND.NXT)表示,此处的规则是 SND.NXT <= SND.UNA+SND.WND,以此来限制发送速率。

窗口滑动的一些问题

可以看到不管是接收窗口还是发送窗口,都是以[A, B)的形式来定义的。随着数据的交互,A随着数据的交互往前推进,向B靠拢,这样窗口就变小了。对于发送窗口而言,需要接收端发来Window字段来更新B。但有时候如果接收端先发来了一个很大Window值,然后又发来一个很小的Window值,可能会导致B朝左边移动,使发送窗口紧缩(shrink)。TCP标准强烈不建议接收端这么做,于此同时又建议发送端要做好准备,即使出现这种情况也要很好处理。