为了防止网络的拥塞现象,TCP提出了一系列的拥塞控制机制.主要包括以下几个:
1: 慢启动(Slow start)
2: 拥塞避免(Congestion avoidance)
3: 快速重传(Fast retransmit)
4: 快速恢复(Fast Recovery)
5: 选择性应答( selective acknowledgement,SACK)算法
TCP的拥塞控制主要原理依赖于一个拥塞窗口(cwnd)来控制,TCP还有一个对端通告的接收窗口(rwnd)用于流量控制. 由于需要考虑拥塞控制和流量控制两个方面的内容,因此TCP的真正的发送窗口=min(rwnd, cwnd)。但是rwnd是由对端确定的,网络环境对其没有影响,所以在考虑拥塞的时候我们一般不考虑rwnd的值,我们暂时只讨论如何确定cwnd值的大小.
慢启动(Slow start):
慢启动通过逐步增大拥塞窗口的值来控制网络拥塞。
传输轮次定义:
在TCP的拥塞避免中,我们规定:每发送拥塞窗口值个数的TCP数据段(有效数据承载),并且全部收到发送方对这些数据的ACK确认,我们就称完成了1个传输轮次(所谓的一轮次,也就是一个RTT时间)
例如,拥塞窗口=4,当发送方发送了4个TCP报文段,并收到这4个TCP报文段的ACK确认,我们就称完成了一个传输轮次
慢启动机制规定:
.拥塞窗口的初始值为1
.每收到一个对发出的数据段的ACK确认,便将拥塞窗口的值增加1,cwnd++ (呈线性上升)
.每当过了一个RTT(Round Trip Time),cwnd = cwnd*2; 呈指数让升
.阈值ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入“拥塞避免算法”
我们可以发现,每完成一次传输轮次,拥塞窗口的值就翻倍,即拥塞窗口随着传输轮次的增加成指数增长。
随着传输轮次的增加,拥塞窗口的值会变得很大,因此TCP拥塞控制給慢启动增加一个阈值(又称慢启动门限(ssthresh),当拥塞窗口>阈值时,就要进行尝试拥塞避免。
当 拥塞窗口 < 阈值 时,使用慢启动算法
当 拥塞窗口 > 阈值 时,使用拥塞避免算法
当 拥塞窗口 = 阈值时,既可以使用慢启动算法,也可时使用拥塞避免算法
随着网络拥塞的出现和变化,阈值也会不断变化。TCP拥塞控制中,阈值的初始值为16
拥塞避免(Congestion avoidance):
拥塞避免算法的思路是让拥塞窗口缓慢地增大,呈线性增长,
即:每当收到一个ACK,cwnd = cwnd + 1/cwnd
即: 每当过了一个RTT,cwnd = cwnd + 1
拥塞避免是指在拥塞避免阶段把拥塞窗口控制为按线性规律增长,使网络比较不容易出现拥塞,而不是完全能够避免拥塞。
1.TCP连接进行初始化的时候,cwnd=1,ssthresh=16
2.在慢启动算法开始时,cwnd的初始值是1,每次发送方收到一个ACK拥塞窗口就增加1,当ssthresh =cwnd时,就启动拥塞控制算法,拥塞窗口按照规律增长
3.当cwnd=24时,网络出现超时,发送方收不到确认ACK,此时设置ssthresh=12,(二分之一cwnd),设置cwnd=1,然后开始慢启动算法,当cwnd=ssthresh=12,慢启动算法变为拥塞控制算法,cwnd按照线性的速度进行增长
拥塞发生:当发生丢包进行数据包重传时,表示网络已经拥塞。分两种情况进行处理:
.把ssthresh降低为cwnd值的一半
.把cwnd重新设置为1
.重新进入慢启动过程
快速重传(Fast retransmit):
.把ssthresh设置为cwnd的一半
.把cwnd再设置为ssthresh的值(具体实现有些为ssthresh+3*MSS)
.重新进入拥塞避免阶段
快重传机制要求接收方每收到一个失序的TCP报文段后就立即发出重复确认(为了使发送方及早知道没有到达对方)而不要等待自己发送数据时才进行确认.
快重传算法规定:发送方只要连续收到3个重复确认就应当立即重传未被确认的报文段.
快速恢复(Fast Recovery):
当发送端收到连续三个重复的确认时,就执行“乘法减小”算法,把慢开始门限 ssthresh 减半。但接下去不执行慢开始算法。
由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,即拥塞窗口 cwnd 现在不设置为 1,而是设置为慢开始门限 ssthresh 减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。
.当收到3个重复ACK时,把ssthresh设置为cwnd的一半,把cwnd设置为ssthresh的值加3,然后重传丢失的报文段,加3的原因是因为收到3个重复的ACK,表明有3个“老”的数据包离开了网络,(cwnd = sshthresh + 3 * MSS (3的意思是确认有3个数据包被收到了))
.重传Duplicated ACKs指定的数据包
.如果再收到重复的ACK时,拥塞窗口增加1。
.当收到新的数据包的ACK时,把cwnd设置为第一步中的ssthresh的值。原因是因为该ACK确认了新的数据,说明从重复ACK时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态。
在快速恢复阶段,每收到重复的ACK,则cwnd加1;收到非重复ACK时,置cwnd= ssthresh,转入拥塞避免阶段;如果发生超时重传,则置ssthresh为当前cwnd的一半,cwnd = 1,重新进入慢启动阶段。
Reno快速恢复阶段退出条件:收到非重复ACK。
可以看出Reno的快速重传算法是针对一个包的重传情况的,然而在实际中,一个重传超时可能导致许多的数据包的重传,因此当多个数据包从一个数据窗口中丢失时并且触发快速重传和快速恢复算法时,问题就产生了。因此NewReno出现了,它在Reno快速恢复的基础上稍加了修改,可以恢复一个窗口内多个包丢失的情况。具体来讲就是:Reno在收到一个新的数据的ACK时就退出了快速恢复状态了,而NewReno需要收到该窗口内所有数据包的确认后才会退出快速恢复状态,从而更一步提高吞吐量
如果你仔细思考一下上面的这个算法,你就会知道,上面这个算法也有问题,那就是——它依赖于3个重复的Acks。注意,3个重复的Acks并不代表只丢了一个数据包,很有可能是丢了好多包。但这个算法只会重传一个,而剩下的那些包只能等到RTO超时,于是,进入了恶梦模式——超时一个窗口就减半一下,多个超时会超成TCP的传输速度呈级数下降,而且也不会触发Fast Recovery算法了。
选择性应答( selective acknowledgement,SACK)算法:
.由计算pipe变化决定,不再是计算cwnd变化
.由SACK携带信息决定,反应更迅速
采用SACK,ACK可包含额外信息,使得发送端在每个RTT时间内可以填补多个空缺.
利用SACK option携带的信息,我们能够提前知道哪些数据包丢失了。NewReno每个RTT内只能恢复
一个丢失的数据包,所以如果丢失了N个数据包,那么Fast Recovery就要持续N*RTT的时间,当N比较
大时,这是一段相当长的时间。而SACK则没有这个限制,依靠SACK option的信息,它能够同时恢复
多个数据包,更加快速和平稳的恢复。
client和 server建立 连接发送SYN的时候, 会相互协商双方是否SACK.
一般是通过tcp头option字段协商是否支持sack, 请查看下列截图:
如果TCP发送方能够了解接收方当前的空洞(以及序列空间中超出空洞的乱序数据块),它就能在报文段丢失或接收方遗漏时更好地进行重传工作。接收方通过SACK选项能够提供确认信息描述乱序数据,以帮助发送方有效地利用信息进行重传.
SACK信息保存于SACK选项中,包含了接收方已经接收的数据块的序列号范围,每个范围被称作一个SACK块,由一对32位的序列号表示。因此,一个SACK选项包含了n个SACK块,长度为(8n+2)个字节,增加的2个字节用于保存SACK选项的种类与长度。由于TCP头部选项的空间有限,因此一个报文段中发送的最大的SACK块数目为3(假设使用了时间戳选项)。虽然只用SYN报文段才能包含“选择确认”选项,但是只要发送方已经发送了该选项,SACK块就能通过任何报文段发送出去.
带选择确认的重传
合理采用SACK信息能更快地填补数据空洞,且能减少不必要的重传,原因在于一个RTT内能获知多个空缺。当采用SACK时,一个ACK可包含3个告知失序数据的SACK信息,每个SACK信息包含32位的序列号,代表接收端存储的失序数据的起始至最后一个序列号(加1)
备注:
当client由于缓冲为0,会用0窗口通知server端,server间隔一定时间会发送心跳keep-alive, client端继续发送0窗口,如果client缓冲数据被读取后,会发送窗口更新给server,此时,server可以继续发送数据了. 在播放视频文件过程中,这种情况会一直出现,主要是由于client端来不及处理导致的
下列两篇链接对tcp拥塞控制讲解的非常好:
https://www.jianshu.com/p/69695f332a71
https://www.jianshu.com/p/b25cdf09ece2