前言
由于最近写了一篇关于 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 介绍的同一个东西
- 已经发送并且对端确认(Sent/ACKed),发送窗外,缓冲区外
- 已经发送但未收到确认数据(Sent/UnACKed),发送窗内,缓冲区内
- 允许发送但尚未防的数据 (Unsent/Inside),发送窗内,缓冲区内
- 未发送暂不允许(Unsent/Outside),发送窗外,缓冲区内
- 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。
滑动窗口原理
TCP 并不是每一个报文段都会回复 ACK 的,可能会对两个报文段发送一个 ACK,也可能会对多个报文段发送 1 个 ACK【累计 ACK】
,比如说发送方有 1/2/3 3 个报文段,先发送了 2,3 两个报文段,但是接收方期望收到 1 报文段,这个时候 2,3 报文段就只能放在缓存中等待报文 1 的空洞被填上,如果报文 1,一直不来,报文 2/3 也将被丢弃,如果报文 1 来了,那么会发送一个 ACK 对这 3 个报文进行一次确认。
举一个例子来说明一下滑动窗口的原理:
假设 32~45 这些数据,是上层 Application 发送给 TCP 的,TCP 将其分成四个 Segment 来发往 internet
seg1 32
34 seg2 3536 seg3 3741 seg4 4245 这四个片段,依次发送出去,此时假设接收端之接收到了 seg1 seg2 seg4此时接收端的行为是回复一个 ACK 包说明已经接收到了 32~36 的数据,并将 seg4 进行缓存(保证顺序,产生一个保存 seg3 的 hole)
发送端收到 ACK 之后,就会将 32~36 的数据包从发送并没有确认切到发送已经确认,提出窗口,这个时候窗口向右移动
假设接收端通告的 Window Size 仍然不变,此时窗口右移,产生一些新的空位,这些是接收端允许发送的范畴
对于丢失的 seg3,如果超过一定时间,TCP 就会重新传送(重传机制),重传成功会 seg3 seg4 一块被确认,不成功,seg4 也将被丢弃
就是不断重复着上述的过程,随着窗口不断滑动,将真个数据流发送到接收端,实际上接收端的 Window Size 通告也是会变化的,接收端根据这个值来确定何时及发送多少数据,从对数据流进行流控。原理图如下图所示:
滑动窗口动态调整
主要是根据接收端的接收情况,动态去调整 Window Size,然后来控制发送端的数据流量。
客户端不断快速发送数据,服务器接收相对较慢,看下实验的结果:
包 175,发送 ACK 携带 WIN = 384,告知客户端,现在只能接收 384 个字节
包 176,客户端果真只发送了 384 个字节,Wireshark 也比较智能,也宣告 TCP Window Full
包 177,服务器回复一个 ACK,并通告窗口为 0,说明
接收方已经收到所有数据,并保存到缓冲区,但是这个时候应用程序并没有接收这些数据,导致缓冲区没有更多的空间
,故通告窗口为 0, 这也就是所谓的零窗口
,零窗口
期间,发送方停止发送数据客户端察觉到窗口为 0,则不再发送数据给接收方
包 178,接收方发送一个窗口通告,告知发送方已经有接收数据的能力了,可以发送数据包了
包 179,收到窗口通告之后,就发送缓冲区内的数据了.
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 | IW = 1*(SMSS) (if SMSS <= 2190 bytes) |
以太网标准的 MSS 大小通常是 1460,所以「cwnd」的初始值是 3MSS
。当我们浏览视频或者下载软件的时候,「cwnd」初始值的影响并不明显,这是因为传输的数据量比较大,时间比较长,相比之下,即便慢启动阶段「cwnd」初始值比较小,也会在相对很短的时间内加速到满窗口,基本上可以忽略不计。
不过当我们浏览网页的时候,情况就不一样了,这是因为传输的数据量比较小,时间比较短,相比之下,如果慢启动阶段「cwnd」初始值比较小,那么很可能还没来得及加速到满窗口,通讯就结束了。这就好比博尔特参加百米比赛,如果起跑慢的话,即便他的加速很快,也可能拿不到好成绩,因为还没等他完全跑起来,终点线已经到了。
如果 TCP 连接一建立就向服务器大量发包,很容易导致拥塞。因此,新建立的连接不能一开始就大量发送数据包,而是应该根据网络状况,逐步地增加每次发送数据包的量,这就是慢启动。慢启动通常在新建立 TCP 连接或由于 RTO(重传超时) 而丢包时执行。
当 cwnd 值超过 ssthresh 值时,慢启动过程结束,进入拥塞避免阶段。在拥塞避免阶段,cwnd 将不再呈指数增长,而是呈线性增长。
- 收到一个 ACK 时,cwnd = cwnd + 1/cwnd
- 当每过一个 RTT 时,cwnd = cwnd + 1
这样放缓了拥塞窗口的增长速率,避免增长过快导致网络拥塞,慢慢的增加调整到网络的最佳值。
拥塞状态
- 等待 RTO 超时,重传数据包,此时 TCP 反应强烈:
- 将 ssthresh 降低为此时 cwnd 的一半
- 将 cwnd 重新设为初始值(IW)
- 重新进入慢启动阶段
原则:加法增大、乘法减小。
- 连续收到 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 比较出众的一点就是提供一个可靠的,流控的数据传输,所以实现起来要比其他协议复杂的多,先来看下这两个修饰词的意义:
Reliability ,提供 TCP 的可靠性,TCP 的传输要保证数据能够准确到达目的地,如果不能,需要能检测出来并且重新发送数据。
Data Flow Control,提供 TCP 的流控特性,管理发送数据的速率,不要超过设备的承载能力