
常见的重传机制有:超时重传、快速重传、SACK、D-SACK。
在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK
确认应答报文,就会重发该数据,根据重传时间判断。
TCP 会在以下两种情况发生超时重传:
RTT
(Round-Trip Time 往返时延):数据发送时刻导接受到确认的时刻点的差值。比如主机A发送SYN报文开始,到收到ACK报文为止。
超时重传时间是以 RTO
(Retransmission Timeout 超时重传时间)表示。
RTO
设置时间过长,会造成网络的间隙时间增大,降低了网络传输效率RTO
设置时间过短,会造成不必要的重传,还没有接受到ACK报文,就直接进行了重发。综上所述,RTO
时间应该略大于一次报文的往返时间RTT
。
实际当中由于网络时常变化,因此RTT
是一个变化的值,所以RTO
也是一个变化的值。
每当遇到一次超时重传时,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。
超时周期可能会过长,引出「快速重传」机制来解决超时重发的时间等待。
根据ACK确认报文来判断报文丢失问题,进行重传。
当发送多个报文时,接收方发送ACK回应已经正确接受的报文,当发送seq5时,ACK回应多个ACK2,就表明中间有seq2没有正确接受,会重发seq2报文。
ACK报文对已经接受的报文确认,但不能确定后续需要重发seq2,还是seq2以后的所有报文,因为seq2以后的报文有部分是正确的接受的。
- 重传所有,会有资源、效率的浪费
- 重传部分,还有再判断后续是否有丢失的部分
由于快速重传不能确认重传哪些TCP报文,有了SACK
方法。
SACK
( Selective Acknowledgment), 选择性确认,在报文中增加了功能性字段。
这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
接受方每次回应已经确认的部分,如果中间有丢失部分,会增加SACK字段。
Duplicate SACK 又称 D-SACK
,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
在接收方成功接受报文,但是响应报文丢失的情况下,出现多次会触发超时重传机制,此时接收方会通过SACK告知发送方报文重复的部分,这部分SACK报文,称为D-SACK。
除了数据丢失重传会出现数据重传问题,还有网络延时造成报文重传,数据的重新接受问题,也可以通过SACK告知。
通过
D-SACK
可以区分数据包是发送方还是接收方丢失问题。是不是由网络延迟导致的问题
Linux 下可以通过
net.ipv4.tcp_dsack
参数开启/关闭这个功能(Linux 2.4 后默认打开)。
最早的思想,是TCP每发送一个数据,进行一次确认,然后再上一次数据包确认接受后,再发送下一个。
很明显这样的效率是低下的,RTT时间决定了通信的效率,由此引出了窗口这个概念。
窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
假设窗口大小为 3
个 TCP 段,那么发送方就可以「连续发送」 3
个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。
ACK报文是对前面所有正确接受报文的确认,假设ACK 600丢失了,可以通过ACK 700确认600之前的报文的正确接受。
TCP 头里有一个字段叫 Window
,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
所以,通常窗口的大小是由接收方的缓冲区(窗口)大小来决定的。
发送方缓存的数据四部分:
当收到接收方的新的部分ACK报文,那么发送窗口部分将会将部分数据变更为情况1,可用窗口向后移动部分字节。
在TCP滑动窗口中用两个绝对指针和一个相对指针来区分这四部分。
SND.WND:表示发送窗口的大小(大小是由接收方指定的)
绝对指针
SND.UNA
指向已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。SND.NXT
指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。相对指针
SND.UNA
指针加上 SND.WND
大小的偏移量,就可以指向 #4 的第一个字节了。可用窗口的大小 = SND.WND -(SND.NXT - SND.UNA)
根据处理的情况划分成三个部分:
主要用一个绝对指针和一个相对指针划分:
RCV.WND
:表示接收窗口的大小,它会通告给发送方。RCV.NXT
:是一个绝对指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。RCV.NXT
指针加上 RCV.WND
大小的偏移量,就可以指向 #4 的第一个字节了。发送窗口和接受方窗口近似相等,在实际中会变化,比如应用进程堆区数据非常快,接收方窗口也很大的空缺,会因此调整窗口大小。
用滑动窗口根据收发法的发送和接受能力,来提高发送的效率。但是还是要考虑接收方的处理能力,否则对方处理不过来,就会触发重发机制,导致网络流量的无端浪费。
为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据**「接收方」的实际接收能力**控制发送的数据量,这就是所谓的流量控制。
实际中发送窗口和接受窗口是会被OS调整的,因为发送窗口和接受窗口中存放的字节数都放在操作系统内存缓冲区,当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。
当应用程序没有即时读取收到的数据时,那么就会有个字节遗留在缓冲区,导致接受窗口缩小,通过ACK报文中,告知发送方接受窗口缩小,发送方收到后也调整发送发的窗口大小
当接收方的窗口收缩到0时,会告知发送方窗口变更为0,发送窗口也减小为0,窗口关闭。当发送方可用窗口变为 0 时,发送方实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变。
在实际中可能由于服务端繁忙,调整了接受缓冲区大小,但是在通过ACK报文回应变更时,晚了一步,发送方发出了超过接受缓冲区的报文,被直接丢弃,当发送方收到前面的ACK报文和窗口大小调整时,可能会出现负值情况。
为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。
在通过ACK报文告知发送端窗口为0时,会关闭窗口,等接收方处理完数据,再改变窗口大小时,万一这个ACK报文丢失,将会出现死锁。
为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。
如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。
窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST
报文来中断连接。
当接收方太忙,不能及时处理接受窗口内的数据,滞留在缓冲区当中,那么就会导致发送方的发送窗口越来越小。但发送窗口小到几个字节,为个位数字节准备一个TCP报文(本身TCP + IP报文就包含了40个字节),是十分不划算的。
糊涂窗口综合症的现象是可以发生在发送方和接收方:
当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0
,也就阻止了发送方再发数据过来。
等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。
使用 Nagle 算法
,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:
MSS
**(最大报文长度)**并且 数据大小 >= MSS
;ack
回包;只要上面两个条件都不满足,发送方一直在囤积数据,直到满足上面的发送条件。
实际当中Nagle算法是默认开启的,在强交互的程序中应该关闭Nagle算法。
必须同时满足上述两个条件,才能避免糊涂窗口综合征。
滑动窗口通过窗口提高了发送效率,流量控制避免了发送数据超过缓存,拥塞控制考虑了实际网络中可能产生的拥堵问题。
当网络发生用途时,出现包丢失重传,这样会造成恶性循环,造成更大的延迟和丢包。
拥塞控制的目的是避免「发送方」的数据填满整个网络。
拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。区别前面的发送和接受窗口,发送窗口大小swnd = min(cwnd, rwnd),接受窗口和拥塞窗口的最小值。
拥塞窗口 cwnd
变化的规则:
cwnd
就会增大;cwnd
就减少;如何判断网络拥塞,只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。
在TCP刚建立连接后,会经过一个缓慢发送数据包,不断增加cwnd窗口大小的过程,当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。(收到1个ACK + 1, 2 个 ACK + 2,指数级增加)
实际中会设置一个门限ssthresh
(slow start threshold),大于门限值后使用「拥塞避免算法」。
一般来说 ssthresh
的大小是 65535
字节,超过这个值使用拥塞避免算法
那么进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。
变成了线性增长,增长速度减缓,一直增长后,网络慢慢会进入拥塞的状况,就会出现丢包的现象,需要进行重传。
当触发重传机制,进入拥塞发生算法
根据重传的不同,主要是超时重传和快速重传,会有不同的拥塞发生算法。
当发生了超时重传:
ssthresh
设为 cwnd/2
,cwnd
重置为 1
(是恢复为 cwnd 初始化值,我这里假定 cwnd 初始化值 1)当收到三个重复的ACK,发生了快速重传:
ssthresh
和 cwnd
变化如下:cwnd = cwnd/2
,也就是设置为原来的一半;ssthresh = cwnd
;正如前面所说,进入快速恢复之前,cwnd
和 ssthresh
已被更新了:
cwnd = cwnd/2
,也就是设置为原来的一半;ssthresh = cwnd
;然后,进入快速恢复算法如下:
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了);
拥塞窗口cwnd = ssthreash + 3是因为收到了3个重复的ACK报文,增加窗口大小。
再收到重复ACK(重发收到后的报文), cwnd + 1,发送窗口的前部有报文没有被接受,导致后续的报文被阻塞占据了发送窗口的位置,并且是没有意义。所以每收到一个重复ACK,就意味着少了一个可用窗口,于是通过增加一个拥塞窗口来补偿,实现重传旧数据的同时,不干扰正常发送。因为swnd = min(cwnd(+1), rwnd)。
完成重传丢失报文后,收到新的确认报文,cwnd = ssthresh,进入拥塞避免阶段。
此时再次收到一条冗余的确认报文,表示发送端发出的报文又有一条离开网络到达了接收端(虽然不是接收端当前想要的一条),这说明网络中腾出了一条报文的空间,所以允许发送端再向网络中发送一条报文。但是由于当前序号最小的报文丢失,导致拥塞窗口cwnd
无法向前移动,于是只好将cwnd
增加1MSS
,于是发送端又可以发送一条数据段,提高了网络的利用率。