【网络协议】TCP 窗口

前言

由于最近写了一篇关于 TCP 协议的文章,所以一些更加细节的内容,更偏向 TCP 独立协议的,拿出来独立记录。

为了获得最优的连接速率,使用 TCP 窗口来控制流速率(flow control),滑动窗口就是一种主要的机制。这个窗口允许源端在给定连接传送数据分段而不用等待目标端返回 ACK,一句话描述:窗口的大小决定在不需要对端响应(acknowledgement)情况下传送数据的数量。​ 官方定义:“The amount of octets that can be transmitted without receiving an acknowledgement from the other side”。

TCP 窗口机制

比如我让发送的每一个包都有一个 id,接收端必须对每一个包进行确认,这样设备 A 一次多发送几个片段,而不必等候 ACK,同时接收端也要告知它能够收多少,这样发送端发起来也有个限制,当然还需要保证顺序性,不要乱序,对于乱序的状况,我们可以允许等待一定情况下的乱序,比如说先缓存提前到的数据,然后去等待需要的数据,如果一定时间没来就 DROP 掉,来保证顺序性!

在 TCP/IP 协议栈中,滑动窗口的引入可以解决此问题,先来看从概念上数据分为哪些类

TCP header 中有一个 Window Size 字段,它其实是指接收端的窗口,即接收窗口,用来告知发送端自己所能接收的数据量,从而达到一部分流控的目的。其实 TCP 在整个发送过程中,也在度量当前的网络状态,目的是为了维持一个健康稳定的发送过程,比如拥塞控制。因此,数据是在某些机制的控制下进行传输的,就是窗口机制。发送端的发送窗口是基于接收端的接收窗口来计算的,也就是我们常说的 TCP 是有连接的发送,数据传输需要对端确认,发送的数据分为如下四类来看,图 1 和图 2 介绍的同一个东西

窗口滑动发送数据-1

  • 已经发送并且对端确认(Sent/ACKed),发送窗外,缓冲区外
  • 已经发送但未收到确认数据(Sent/UnACKed),发送窗内,缓冲区内
  • 允许发送但尚未防的数据 ​(Unsent/Inside),发送窗内,缓冲区内
  • 未发送暂不允许(Unsent/Outside),发送窗外,缓冲区内 ​

窗口滑动发送数据-2

  • Sent and Acknowledged:这些数据表示已经发送成功并已经被确认的数据,比如图中的前 31 个 bytes,这些数据其实的位置是在窗口之外了,因为窗口内顺序最低的被确认之后,要移除窗口,实际上是窗口进行合拢,同时打开接收新的带发送的数据
  • Send But Not Yet Acknowledged:这部分数据称为发送但没有被确认,数据被发送出去,没有收到接收端的 ACK,认为并没有完成发送,这个属于窗口内的数据。
  • Not Sent,Recipient Ready to Receive:这部分是尽快发送的数据,这部分数据已经被加载到缓存中,也就是窗口中了,等待发送,其实这个窗口是完全有接收方告知的,接收方告知还是能够接受这些包,所以发送方需要尽快的发送这些包
  • Not Sent,Recipient Not Ready to Receive: 这些数据属于未发送,同时接收端也不允许发送的,因为这些数据已经超出了接收端所接收的范围

对于接收端也是有一个接收窗口的,类似发送端,接收端的数据有 3 个分类,因为接收端并不需要等待 ACK 所以它没有类似的接收并确认了的分类,情况如下

  • Received and ACK Not Send to Process:这部分数据属于接收了数据但是还没有被上层的应用程序接收,也是被缓存在窗口内
  • Received Not ACK: 已经接收并,但是还没有回复 ACK,这些包可能输属于 Delay ACK 的范畴了
  • Not Received:有空位,还没有被接收的数据。

TCP 窗口就是这样逐渐滑动,发送新的数据,滑动的依据就是发送数据已经收到 ACK,确认对端收到,才能继续窗口滑动发送新的数据。可以看到窗口大小对于吞吐量有着重要影响,同时 ACK 响应与系统延时又密切相关。需要说明的是:如果发送端的窗口过大会引起接收端关闭窗口,处理不过来反之,如果窗口设置较小,结果就是不能充分利用带宽,所以仔细调节窗口对于适应不同延迟和带宽要求的系统很重要。

发送窗口和可用窗口

对于发送方来讲,窗口内的包括两部分,就是发送窗口(已经发送了,但是没有收到 ACK),可用窗口,接收端允许发送但是没有发送的那部分称为可用窗口。

  • Send Window : 20 个 bytes 这部分值是有接收方在三次握手的时候进行通告的,同时在接收过程中也不断的通告可以发送的窗口大小,来进行适应
  • Window Already Sent: 已经发送的数据,但是并没有收到 ACK。

窗口滑动发送数据-3

滑动窗口原理

TCP 并不是每一个报文段都会回复 ACK 的,可能会对两个报文段发送一个 ACK,也可能会对多个报文段发送 1 个 ACK【累计 ACK】,比如说发送方有 1/2/3 3 个报文段,先发送了 2,3 两个报文段,但是接收方期望收到 1 报文段,这个时候 2,3 报文段就只能放在缓存中等待报文 1 的空洞被填上,如果报文 1,一直不来,报文 2/3 也将被丢弃,如果报文 1 来了,那么会发送一个 ACK 对这 3 个报文进行一次确认。

举一个例子来说明一下滑动窗口的原理:

  1. 假设 32~45 这些数据,是上层 Application 发送给 TCP 的,TCP 将其分成四个 Segment 来发往 internet

  2. seg1 3234 seg2 3536 seg3 3741 seg4 4245 这四个片段,依次发送出去,此时假设接收端之接收到了 seg1 seg2 seg4

  3. 此时接收端的行为是回复一个 ACK 包说明已经接收到了 32~36 的数据,并将 seg4 进行缓存(保证顺序,产生一个保存 seg3 的 hole)

  4. 发送端收到 ACK 之后,就会将 32~36 的数据包从发送并没有确认切到发送已经确认,提出窗口,这个时候窗口向右移动

  5. 假设接收端通告的 Window Size 仍然不变,此时窗口右移,产生一些新的空位,这些是接收端允许发送的范畴

  6. 对于丢失的 seg3,如果超过一定时间,TCP 就会重新传送(重传机制),重传成功会 seg3 seg4 一块被确认,不成功,seg4 也将被丢弃

就是不断重复着上述的过程,随着窗口不断滑动,将真个数据流发送到接收端,实际上接收端的 Window Size 通告也是会变化的,接收端根据这个值来确定何时及发送多少数据,从对数据流进行流控。原理图如下图所示:

窗口滑动发送数据-4

滑动窗口动态调整

主要是根据接收端的接收情况,动态去调整 Window Size,然后来控制发送端的数据流量。

客户端不断快速发送数据,服务器接收相对较慢,看下实验的结果:

  • 包 175,发送 ACK 携带 WIN = 384,告知客户端,现在只能接收 384 个字节

  • 包 176,客户端果真只发送了 384 个字节,Wireshark 也比较智能,也宣告 TCP Window Full

  • 包 177,服务器回复一个 ACK,并通告窗口为 0,说明接收方已经收到所有数据,并保存到缓冲区,但是这个时候应用程序并没有接收这些数据,导致缓冲区没有更多的空间,故通告窗口为 0, 这也就是所谓的零窗口零窗口期间,发送方停止发送数据

  • 客户端察觉到窗口为 0,则不再发送数据给接收方

  • 包 178,接收方发送一个窗口通告,告知发送方已经有接收数据的能力了,可以发送数据包了

  • 包 179,收到窗口通告之后,就发送缓冲区内的数据了.

Wireshark抓包分析

TCP 窗口大小

最早 TCP 协议涉及用来大范围网络传输时候,其实是没有超过 56Kb/s 的 ​ 连接速度的。因此,TCP 包头中只保留了 16bit 用来标识窗口大小,允许的最大缓存大小不超过 64KB。为了打破这一限制,RFC1323 规定了 TCP 窗口尺寸选择,是在 TCP 连接开始的时候三步握手的时候协商的(SYN, SYN-ACK,ACK),会协商一个 Window size scaling factor,之后交互数据中的是 Window size value,所以最终的窗口大小是二者的乘积.

  • Window size value: 64 or 0000 0000 0100 0000 (16 bits)

​- Window size scaling factor: 256 or 2 ^ 8 (as advertised by the 1st packet)

  • The actual window size is 16,384 (64 * 256)

这里的窗口大小就意味着,直到发送 16384 个字节,才会停止等待对方的 ACK.随着双方回话继续,窗口的大小可以修改 window size value 参数完成变窄变宽,但是注意:Window size scaling factor 乘积因子必须保持不变。在 RFC1323 中规定的偏移(shift count)是 14,也就是说最大的窗口可以达到 Gbit,很大。

TCP 窗口的参数设置

TCP 窗口起着控制流量的作用,实际使用时这是一个双端协调的过程,还涉及到 TCP 的慢启动​(Rapid Increase/Multiplicative Decrease),拥塞避免,拥塞窗口和拥塞控制。可以记住,发送速率是由 min(拥塞窗口[cwnd],接收窗口[rwnd]),接收窗口在下文有讲。

TCP 窗口优化设置 ​

TCP​ 窗口既然那么重要,那要怎么设置,一个简单的原则是 2 倍的 BDP.这里的 BDP 的意思是 bandwidth-delay product,也就是带宽和时延的乘积,带宽对于网络取最差连接的带宽

1
buffer size = 2 * bandwidth * delay​

还有一种简单的方式,使用 ping 来计算网络的环回时延(RTT),然后表达为:

1
buffer size = bandwidth * RTT​

为什么是 2 倍?因为可以这么想,如果滑动窗口是 bandwidth\*delay,当发送一次数据最后一个字节刚到时,对端要回 ACK 才能继续发送,就需要等待一次单向时延的时间(可以统称为RTT/2),所以当是 2 倍时,刚好就能在等 ACK 的时间继续发送数据,等收到 ACK 时数据刚好发送完成,这样就提高了效率。

举个例子:带宽是 20Mbps,通过 ping 我们计算单向时延是 20ms,那么可以计算:20000000bps*8*0.02 = 52,428bytes​,因此我们最优窗口用 104,856 bytes = 2 x 52,428,所以说当发送者发送 104,856 bytes 数据后才需要等待一个 ACK 响应,当发送了一半的时候,对端已经收到并且返回 ACK(理想情况),等到 ACK 回来,又把剩下的一半发送出去了,所以发送端就无需等待 ACK 返回。

注意我们这里的 bps(bit peer second),所以转成 bytes 的时候需要注意 8 倍的转换

发现了么?这里的窗口已经明显大于 64KB 了,所以机制改善了。

TCP 拥塞控制

现在我们看看到底如何控制流量。TCP 在传输数据时和 windows size 关系密切,本身窗口用来控制流量,在传输数据时,发送方数据超过接收方就会丢包,流量控制,流量控制要求数据传输双方在每次交互时声明各自的接收窗口「rwnd」大小,用来表示自己最大能保存多少数据,这主要是针对接收方而言的,通俗点儿说就是让发送方知道接收方能吃几碗饭,如果窗口衰减到零,也就是发送方不能再发了,那么就说明吃饱了,必须消化消化,如果硬撑胀漏了,那就是丢包了。

流量控制

TCP 的拥塞控制主要依赖于 拥塞窗口(congestion window, cwnd) 和 慢启动阈值(slow start threshold, ssthresh)。cwnd 是发送端根据网络的拥塞程度所预设的一个窗口大小,而 ssthresh 则是慢启动窗口的阈值,cwnd 超过此阈值则转变控制策略。

TCP 拥塞控制的主要算法有 慢启动(Slow Start)拥塞避免(Congestion Avoidance)快速重传(Fast Retransmit)快速恢复(Fast Recovery)等。

慢启动(Slow Start)

虽然流量控制可以避免发送方过载接收方,但是却无法避免过载网络,这是因为接收窗口「rwnd」只反映了服务器个体的情况,却无法反映网络整体的情况。
s
为了避免网络过载,慢启动引入了拥塞窗口「cwnd」s 的概念,用来表示发送方在得到接收方确认前,最大允许传输的未经确认的数据「cwnd」「rwnd」相比不同的是:它只是发送方的一个内部参数,无需通知给接收方,其初始值往往比较小,然后随着数据包被接收方确认,窗口成倍扩大,有点类似于拳击比赛,开始时不了解敌情,往往是次拳试探,慢慢心里有底了,开始逐渐加大重拳进攻的力度。

拥塞窗口扩大

在慢启动的过程中,随着「cwnd」的增加,可能会出现网络过载,其外在表现就是丢包,一旦出现此类问题,「cwnd」的大小会迅速衰减,以便网络能够缓过来。

拥塞窗口和丢包

说明:网络中实际传输的未经确认的数据大小取决于「rwnd」和「cwnd」中的小值。

拥塞避免 ​

从慢启动的介绍中,我们能看到,发送方通过对「cwnd」大小的控制,能够避免网络过载,在此过程中,丢包与其说是一个网络问题,倒不如说是一种反馈机制,通过它我们可以感知到发生了网络拥塞,进而调整数据传输策略,实际上,这里还有一个慢启动阈值「ssthresh」的概念如果「cwnd」小于「ssthresh」,那么表示在慢启动阶段如果「cwnd」大于「ssthresh」,那么表示在拥塞避免阶段,此时「cwnd」不再像慢启动阶段那样呈指数级整整,而是趋向于线性增长,以期避免网络拥塞,此阶段有多种算法实现,通常保持缺省即可。

如何调整「rwnd」到一个合理值

很多时候 TCP 的传输速率异常偏低,很有可能是接收窗口「rwnd」过小导致,尤其对于时延较大的网络,实际上接收窗口「rwnd」的合理值取决于 BDP 的大小,也就是带宽和延迟的乘积。假设带宽是 100Mbps,延迟是 100ms,那么计算过程如下:

1
BDP = 100Mbps * 100ms = (100 / 8) * (100 / 1000) = 1.25MB​

此问题下如果想最大限度提升吞度量,接收窗口「rwnd」的大小不应小于 1.25MB。

如何调整「cwnd」到一个合理值

一般来说「cwnd」的初始值取决于 MSS 的大小,计算方法如下:

1
min(4 * MSS, max(2 * MSS, 4380))

具体来说,新建 TCP 连接时,cwnd 需初始化为一个或几个最大发送报文段大小(send maximum segment size, SMSS 或者有一些也叫 MSS)。具体规则(IW 为初始窗口大小):

1
2
3
4
5
6
7
IW = 1*(SMSS) (if SMSS <= 2190 bytes)

IW = 2*(SMSS) and not more than 2 segments (if SMSS > 2190 bytes)

IW = 3*(SMSS) and not more than 3 segments (if 2190 ≥ SMSS > 1095 bytes)

IW = 4*(SMSS) and not more than 4 segments (otherwise)

以太网标准的 MSS 大小通常是 1460,所以「cwnd」的初始值是 3MSS。当我们浏览视频或者下载软件的时候,「cwnd」初始值的影响并不明显,这是因为传输的数据量比较大,时间比较长,相比之下,即便慢启动阶段「cwnd」初始值比较小,也会在相对很短的时间内加速到满窗口,基本上可以忽略不计。

不过当我们浏览网页的时候,情况就不一样了,这是因为传输的数据量比较小,时间比较短,相比之下,如果慢启动阶段「cwnd」初始值比较小,那么很可能还没来得及加速到满窗口,通讯就结束了。这就好比博尔特参加百米比赛,如果起跑慢的话,即便他的加速很快,也可能拿不到好成绩,因为还没等他完全跑起来,终点线已经到了。

如果 TCP 连接一建立就向服务器大量发包,很容易导致拥塞。因此,新建立的连接不能一开始就大量发送数据包,而是应该根据网络状况,逐步地增加每次发送数据包的量,这就是慢启动。慢启动通常在新建立 TCP 连接或由于 RTO(重传超时) 而丢包时执行。

当 cwnd 值超过 ssthresh 值时,慢启动过程结束,进入拥塞避免阶段。在拥塞避免阶段,cwnd 将不再呈指数增长,而是呈线性增长。

  • 收到一个 ACK 时,cwnd = cwnd + 1/cwnd
  • 当每过一个 RTT 时,cwnd = cwnd + 1

这样放缓了拥塞窗口的增长速率,避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。

拥塞状态

  1. 等待 RTO 超时,重传数据包,此时 TCP 反应强烈:
    1. 将 ssthresh 降低为此时 cwnd 的一半
    2. 将 cwnd 重新设为初始值(IW)
    3. 重新进入慢启动阶段

      原则:加法增大、乘法减小。

  2. 连续收到 3 个 duplicate ACK 时,重传数据包,无须等待 RTO。

快速重传

TCP 在收到一个乱序的报文段时,会立即发送一个重复的 ACK,并且此 ACK 不可被延迟。

如果连续收到 3 个或 3 个以上重复的 ACK,TCP 会判定此报文段丢失,需要重新传递,而无需等待 RTO。这就叫做快速重传。

快速恢复

快速恢复是指快速重传后直接进入拥塞避免阶段而非慢启动阶段。总结一下快速恢复的步骤(以 SMSS 为单位):

  • 当收到 3 个重复的 ACK 时,将 ssthresh 设置为 cwnd 的一半(ssthresh = cwnd/2),然后将 cwnd 的值设为 ssthresh 加 3(cwnd = ssthresh + 3),然后快速重传丢失的报文段
  • 每次收到重复的 ACK 时,cwnd 增加 1(cwnd += 1),并发送 1 个 packet(如果允许的话)
  • 当收到新的 ACK 时,将 cwnd 设置为第一步中 ssthresh 的值(cwnd = ssthresh),代表恢复过程结束
    快速恢复后将进入拥塞避免阶段。

还有其他的充传相关的内容会放在其他文章中。

总结

从传输数据来讲,TCP/UDP 以及其他协议都可以完成数据的传输,从一端传输到另外一端,TCP 比较出众的一点就是提供一个可靠的,流控的数据传输,所以实现起来要比其他协议复杂的多,先来看下这两个修饰词的意义:

  1. Reliability ,提供 TCP 的可靠性,TCP 的传输要保证数据能够准确到达目的地,如果不能,需要能检测出来并且重新发送数据。

  2. Data Flow Control,提供 TCP 的流控特性,管理发送数据的速率,不要超过设备的承载能力