TCP拥塞控制
TCP基础
网络的传输层有两种方式——TCP
和UDP
,其中TCP
是基于连接的,而UDP
不需要连接。它们各自支持一些应用层协议,但也有些协议是两者都支持的,比如DNS
,我们通过DNS
来比较TCP
和UDP
的差别。当前我的计算机的ip
为192.168.199.134
,向DNS
服务器发起一个DNS
查询,以期获得tinylcy.me
所对应的ip
地址。DNS
默认使用UDP
的情况如下所示:
通过Wireshark
抓取这个过程的网络包,如下所示:
使用set vc
强制DNS
使用TCP
:
这个过程所有的网络包如下所示:
从以上两种情况可以得知,真正起查询作用的只有两个DNS
包。在使用UDP
的情况下,确实只需要两个包就可以完成DNS
查询,但是在使用TCP
时,要先用3
个包来建立连接。可以看到,如果DNS
使用TCP
,那么连接的成本将会远远大于DNS
查询本身,这对于繁忙的DNS
服务器来说无疑是巨大的压力,因此,DNS
服务器默认使用UDP
来传输数据。
但是大多数应用层协议还是基于TCP
,因为TCP
可靠的连接带来的好处更多。首先,需要理解几个重要的TCP
参数。
Seq
:表示数据段的序号。TCP
提供有序的传输,所以每个数据包都要标上一个序号。当接收方收到乱序的包时,有了这个序号就可以重新排序了。如下图所示,数据包6
的起始序号为1
,长度为20
,那么数据包7
的Seq
号即为1 + 20 = 21
。数据段7
的长度为433
,所以数据包8
的Seq
为21 + 433 = 454
。
Len
:对应数据包的长度,注意这个长度不包括TCP
头,很多情况下,包的Len = 0
,但其实是有TCP
头的,并且头部携带的信息量还很大。Ack
:确认号,如下图所示,数据段91
的Seq = 438
,Len = 20
,来自接收方的92
号数据包的Ack = 438 + 20 = 458
,表示收到了之前的458
字节。理论上,接收方回复的Ack
号恰好等于发送方的下一个Seq
号,所以可以看到93
号数据包的Seq = 458
。
由于网络传输的不确定性,因此接收方收到的数据包往往是乱序的,此时,接收方只需要根据Seq
号从小到大重新排序就好了,这样就保证了TCP
的有序性。若存在数据包的丢失,接收方通过前一个Seq + Len
的值与下一个Seq
的差,就能判断丢失了哪些包,这保证了TCP
的可靠性。
TCP窗口
TCP
传输数据包之后,需要等待确认包是否到达,这样就花费了一个往返时间。如果每发送一个包就停下来确认,那么在一个往返时间只能发送一个包,这样的传输效率太低。最快的方式就是一次把所有的包全部发送出去,然后一起确认,但是现实存在着限制:接收方的缓存(接收窗口)可能一下子接受不了这么多数据;网络带宽也不一定足够大,一次发太多数据会导致丢包事故。因此,发送方要知道接收方的接收窗口和网络这两个限制因素哪一个更加严格然后在其限制范围内尽可能的多发包。这个一口气能够发送的数据量就是所谓的TCP
发送窗口。
Window size value
如下图所示,每个包的TCP
层都含有Window size value
,这个值并不是指发送窗口的大小,而是在向对方声明自己的接收窗口。
151.101.24.133
向192.168.199.134
声明自己的接收窗口是28800
字节,192.168.199.134
收到之后,就会把自己的发送窗口限制在28800
字节内。
发送窗口大小
如果发送窗口的大小由接收窗口决定,那么可以通过Window size value
来判断,而当发送窗口的大小由网络因素决定,事情就会变得很复杂。大多数时候,甚至不确定哪个因素在起作用,只能大概推理。如果接收窗口的大小声明为0
,那么接收窗口肯定会起作用,因为不能再小了,可以推断对应的发送方的发送窗口就为0
。
发送窗口和MSS
发送窗口决定一次能够发送的字节数,而MSS
决定了这些字节需要分多少个包才能发完。举例:在发送窗口为16000
字节的情况下,如果MSS
是1000
字节,那么需要发送16
个包;如果MSS
为8000
字节,那么发送的包数即为2
。
TCP重传机制
网络之所以要限制发送窗口,是因为一次收到太多数据就会拥塞,拥塞的结果就是丢包,能导致网络拥塞的数据流称为拥塞点。发送方希望能够把发送窗口的控制在拥塞点以下,但是网络设备并不知道自己的拥塞点,即便知道了也无法通知给发送方。
目前并没有完美的解决方案,相对靠谱的策略是在发送方维护一个虚拟的拥塞窗口,并利用算法使它尽可能的接近真实的拥塞点,网络对发送窗口的限制,就是通过拥塞窗口来实现的。
- 连接刚刚建立,发送方对网络状况一无所知。如果一次发送太多的数据就可能遭遇拥塞,所以把发送方的拥塞窗口初始值定的很小,RFC的建议是
2
个、3
个或者4
个MSS
。 - 用过发出去的包都得到确认,表明还没有到达拥塞点,可以增大拥塞窗口。由于这个阶段发送拥塞的概率很低,所以增速可以快一些。
RFC
建议的算法是每收到n
个确认,可以把拥塞窗口增加n
个MSS
。这个过程的增速很快,但是由于基数低,传输速度还是比较慢的,所以被称为慢启动过程。 - 慢启动过程持续一段时间后,拥塞窗口达到一个较大的值。这时候传输速度比较快,触碰拥塞点的概率也大了,所以不能再采用翻倍的慢启动算法了,而是要更加缓慢一些。
RFC
建议是每个往返时间增加一个MSS
。这个过程被称为拥塞避免。从慢启动过渡到拥塞避免的临界窗口值很有讲究。如果之前发生过拥塞,那么就把该拥塞点作为参考依据,如果从来没有发生过拥塞就可以取相对较大的值,比如和最大接收窗口相等。
拥塞之后,对于发送方来说就是发出去的包得不到确认了,不过收不到确认也可能是网络延迟导致,所以发送方会等待一会儿再判断,如果迟迟收不到,那么就认定包已经丢失,只能重传了。这个过程被称为超时重传。从发出原始包到重传该包的时间被称为RTO
。
重传之后的拥塞窗口非常有必要调整,RFC
建议把拥塞窗口下降到1
个MSS
,然后再次进入慢启动过程。这一次从慢启动过程过渡到拥塞避免的临界窗口值就有参考依据了。RFC
建议临界窗口值为发生拥塞时没被确认的数据量的1/2
,但是不能小于2
个MSS
。
下图模拟了慢启动和拥塞避免两个过程,并且在此期间发生了超时重传。
可以看到超时重传对传输性能有着严重影响。因为RTO
阶段没有传输数据,相当于浪费了一段时间,并且拥塞敞口急剧减小,相当于接下来传的慢多了。
有时候拥塞很轻微,只有少量的包丢失,还有一些偶然因素,比如校验码不对,也会导致单个丢包。但是这种丢包和严重拥塞时的丢包并不一样,因为后续有包正常到达。当后续的包到达接收方时,会发现其Seq
比期望的大,所以接收方每收到一个包就Ack
一次期望的Seq
号。当发送方收到3
个或者以上的重复确认(Dup Ack
)时,就意识到相应的包已经丢失了,因此立即重传它。这个过程被称为快速重传。之所以称为快速,是因为它不需要像超时重传那样等待一段时间。
前面提到发送方在收到3
个或者以上的Dup Ack
时才会启动快速重传,这是因为网络包有时会乱序,乱序的包也会导致触发重复的Ack
,但是没有必要为了乱序而重传。由于一般乱序的距离不会相差太大,所以限定成3
个 或者以上可以很大程度上避免因为乱序而出发快速重传。