一. 前言
在TCP的传输过程中,由于网络情况的原因,如网络堵塞、数据包绕道从而会导致发送的数据包丢失,而为了保证传输的可靠性,即发送端发送的每一个报文段接收方都能接收到,TCP采取了一系列的处理措施来保证网络的顺畅与解决数据包丢失的问题,这些措施包括超时重传、慢启动、拥塞避免以及快重传与快恢复。
二. TCP保证传输可靠性的措施
1. 超时重传机制
由于发送方在发送数据包后,由于某些情况如接收方主机宕机,网络线路断开等,从而导致数据包发出去后,发送方无法得到接收方的ack响应。此时发送方无法知道接收方是否已经接收到了该数据包,为了传输的可靠,在超过了一定的等待ack时限后,发送发会主动再次发送接收方未响应ack的数据包,这就是超时重传。
对于重传的等待超时时限RTO(Retransmission TimeOut),有两个计算方式,一个是超时重传失败后的指数退避,另一个是正常接收ack情况下根据往返时间RTT(Round Trip Time)和RTT均值偏差来进行更新计算。
指数退避
首先看个简单例子:
该例子中,可以发现在完成连接建立后,除了第一个发送的报文段被确认后,第二个发送的报文段发生了超时并执行了重传,可以观察到重传报文段为7~18行,19行由于超时重传的最终时限到了,使得发送方放弃连接从而发送复位信号。可以观察到每次重传的时间间隔为1.0136、3.0001、6.0002、11.9905、24.0007、48.0013、64.0018…可以发现每次的重传时间为上一次的两倍,单词可以观察到第一次的重传间隔时间为1s左右,而不是1.5s,其原因如下图所示:
由于第一次计时的启动会发生在一个滴答内的任意一时刻,因此导致其显示计时为1.0136s,但在时钟滴答上却是经历了3个滴答,也就是是为1.5s。
这种在重传失败后,RTO的更新方式就是指数退避,即下一次的RTO为上一次的两倍,直到达到RTO的上限为止,本例中的RTO上限为64s。
根据RTT与RTT均值偏差进行更新计算
RTT与RTO更新计算公式
正常情况下,接收方即便能正常接收ack,但由于其并不能预知下一个ack的到来会不会超时,所以会实时计算并更新报文段的RTT和RTO,在最初的TCP规范中,RTT和RTO的计算方式为:
其中平滑因子$\alpha$取值为0.9,所得R为平滑RTT,时延离散因子$\beta$取值为2。
但由于该计算方式对RTT大范围变化时的情况不敏感,会在网络达到饱和时存在很多不必要的重传,增加网络负载。为了解决该问题,引入了跟踪RTT方差来进行计算,而为了方便计算,使用均值偏差来代替标准偏差,更改后的计算方式为:
其中A为平滑RTT,M为刚实际测量的RTT,Err则为实际RTT和平滑RTT之差,D为平滑均值偏差。增量g取值1/8,h取值1/4。A和D共同用来计算下一个重传时间RTO,可以看出较大的RTT波动会加快RTO上升。
实际RTT测量过程
观察以下例子:
当一个报文段发出时,若当前定时器没有被启动,则定时器启动开始计算当前报文发出的RTT,同时记录下该报文段的起始序号,当收到一个ack的确认序号包含该起始序号后,就关闭该定时器,然后根据新测量的RTT,使用前面的更新计算公式计算新的RTT和RTO。从例子中可以看到,启动定时器的报文为1、3、6三个报文段,而像4、7和9报文段,由于在它们发出时,定时器已处于启动状态,因此不会去测量它们的RTT。 再观察三个测量所得RTT,第一个RTT显示为1.061s,而插口排错信息显示该过程历经了3个TCP时钟滴答,RTT测量实际为1500ms;第二个RTT显示为0.808s,而它的RTT实际测量为1个TCP时钟滴答;第三个RTT显示为1.015s,而它的RTT实际测量为2个TCP时钟滴答,下图给出了RTT显示时间与实际测量时间的关系:
在RTT测量中,使用的是TCP 500ms的时钟滴答。图的上端为500ms滴答的时刻(虚线表示),下端为定时启动和关闭的时刻(实线表示),可以看到,第一个1.061s的测量历经了3个虚线,即表明经历了3个时钟滴答,RTT实际测量值为1500ms,而第二个0.808s的测量历经了1个虚线,为1个时钟滴答,RTT实际测量值为500ms,而第三个1.015s历经了2个虚线,为2个时钟滴答。
Karn算法
Karn算法内容为
1.当一个超时和重传发生时,在重传数据确认最后到达前不更新平滑RTT。
2.若数据被重传,则RTO进行指数退避,且在下一次传输时使用这个退避后的RTO。
3.只有当一个没有被重传的报文段得到了确认才对RTO进行更新。
RTT与RTO更新的完整执行流程
在RTT与RTO的更新执行流程在整个TCP传输过程中可分为两部分。第一部分为连接建立阶段,第二部分为数据传输阶段。
连接建立阶段:
在该阶段,RTT与RTO计算公式中的A和D分别被初始化为0和3s,初始RTO计算为 $RTO = A + 2D$,初始RTO计算为6s,若此时SYN信号没有收到应答,则大约6s后执行重传,第一次执行重传则先计算$RTO = A + 4D$,结果为12s,由于为第一次重传,需做一次指数退避,则最终RTO为24s,若后面依旧重传失败则继续执行指数避让。直到重传成功。
数据传输阶段:
根据Karn算法,该阶段开始依旧采用上一阶段最后所得的RTO,若该阶段一开始就发生超时重传,则RTO直接执行指数退避。若正常接收ack,则初始平滑RTT为$A = M + 0.5$,初始平滑均值偏差为$D = A/2$,RTO计算为$RTO = A + 4D$,往后的过程若正常接收ack则按照前面所说的更新公式进行计算,若发生重传则执行RTO指数退避。
2. 快速重传机制
不同于超时重传,快重传发生时,发送方依旧可以收到接收方的ack确认,然而只是其收到的ack确认中的确认序号一直是某一个相同的序号,即表示,接收方没接收到该序号的报文段,因此在重复发送该确认序号的ack以请求该报文段。此时发送方在收到三个相同确认序号的ack后立即将该丢失的报文段进行重发而无需等待超时,该机制即为快速重传机制,可以看出快速重传机制提高了传输的效率。例子如下图所示:
该例子中,发送方win大小为4096,接收方win大小为8192(图中已将其省略给出)
可以看到45号报文段丢失,而58号应答确认收到44报文段后,即便后续的46~59号报文段都被接收方接收了,可其由于没有收到45号报文段,其一直发送的ack确认序号为6657(58号报文为首次确认,而60、61、62为前三次重复的ack)而不会去确认后面失序了的报文段,在收到三次重复ack后,发送方立即发起重传,直到45号报文段接收成功后,才将其以及其后续发送的所有报文段一起进行ack应答(72号报文段),可以看到72号报文段win大小为5888,即前面所有发送的报文段刚被确认还没来得及被应用层取走。
此处解释为什么要是重复ack为3个才重传,由于在接收方在对几个报文段重新排序时其也会发送重复的ack,但这种下,其在重新排序处理完成并发出新的ack之前,其最多只能发送1~2个重复ack,因此一般收到第三个ack即大概率表明报文段丢失了。
3. 拥塞避免算法
拥塞避免算法一般都和慢启动算法一起结合使用,在一般的TCP传输过程,前半期为慢启动,而后半期为拥塞避免。慢启动和拥塞避免都维护着两个变量:拥塞窗口cwnd和慢启动门限ssthresh
慢启动
为了防止TCP一开始进行数据传输时一下子向网络中注入过多的数据而导致网络快速堵塞,从而采用慢启动策略。所谓慢启动,即TCP开始传输数据时先将cwnd大小设置为1个报文段大小进行发送(发送数据量为cwnd值和通告窗口值间的最小值),当发送了1个报文段,其相应收到1个ack确认后,其下一次发送所能发送的报文数量将增加1变成2,下一次发送2个报文段,收到2个ack则下一次发送量变成4…以此不断得将报文段发送量翻番,以指数增加的形式增加发送数据的量,即为慢启动策略。
拥塞避免
当cwnd的数量以指数增长的形式增长超过ssthresh值后,由于再以指数形式增加可能会很快导致网络堵塞,于是便转入执行拥塞避免策略,相对于慢启动策略,拥塞避免策略即在每个报文段接收到ack后,其不再以指数形式增长,而是每收到一个ack,则cwnd增长$1/cwnd$,这是一种线性增长,我们希望在一个RTT内最多为cwnd增加1个报文段(无论这个RTT中收到了多少个ack)
拥塞避免阶段,新的cwnd值按以下方法计算:
其中$\frac{segsize}{8}$ 为某些版本的错误TCP实现,如4.3BSD和4.4BSD
快速恢复
拥塞发生的情况表现为快重传或超时。
当发生快重传时,为了稳定网络情况,此时需要减小cwnd,但由于发生快重传时,收到了重复的ack,只有在接收方收到后续的报文段时才会响应重复的ack,此时表明在收发两端还是存在这流动的数据,而为了不使得数据流突然骤降,因此此时选择快速恢复策略而不是慢启动。
所谓快速恢复策略即为:
1)当收到3个重复ack时,ssthresh设为当前cwnd的一半,并重传报文段,然后设置cwnd为ssthresh加3倍报文段的大小(这里的加3倍报文段大小的原因是因为受到了3个重复ack且cwnd没有超过ssthresh)
2)在之后每收到一个重复ack,cwnd增加1个报文段大小并发送1个分组(如果新的cwnd大小允许发送)。
3)当下一个新的ack到达时,将cwnd设置为ssthresh(步骤1中设置的值)加上1个报文段大小(此处加1个报文段大小时因为此处收到了新的ac且cwnd没有超过ssthresh)
若发生的是超时,则不会执行快速恢复,而是会先将ssthresh设为当前cwnd的一半,并将cwnd设为1个报文段大小即重新执行慢启动。
拥塞避免的例子
以下来通过一个拥塞避免的例子来观察cwnd和ssthresh的变化:
此例中可以观察到:
1)起初cwnd初始化为一个报文段大小即256字节,ssthresh为65535字节,在连接建立阶段就发生了超时重传,则cwnd不变,而ssthresh将为cwnd的一半,但由于ssthresh的最小值应为2个报文段大小,因此其变成了512字节。
2)而后续可以看到,在慢启动阶段cwnd的增加按照收到的ack个数来进行增加,而当cwnd超过了ssthresh后进入拥塞避免阶段,cwnd增加按照前面介绍的计算公式进行。
在该例子的后续阶段(对应图5):
可以观察到:
1)当发生重复确认时,三次重复确认的前两次并不会增加cwnd。
2)而在第3个重传ack到来时候,ssthresh变了当前cwnd的一半,且cwnd变成了ssthresh加3倍报文段大小。
3)而后每收到1个重复ack则cwnd增加1个报文段大小,直到新数据确认到后,cwnd变成了ssthresh加一个报文段大小。
4)注意到重传之后获取新数据确认之前,并非每收到一个重复ack就可以新发送一个报文段,要想发送新报文段,则需比较当前cwnd与已发送而未确认数据的大小,只有cwnd大小比已发送而未确认数据的大小大时才可发送新的报文段。
4. 按每条路由进行度量
在较新的TCP实现中,其在路由表项中维持着许多我们前面介绍过的指标,包括被平滑的RTT、被平滑均值偏差记忆慢启动门限等。如果在TCP连接关闭之前已经发送了足够多的数据(16个窗口的数据,即16个RTT采样,这能使被平滑的RTT不会偏离正确结果的5%)来获得有意义的统计度量值并且当目的结点的路由表项不是一个默认的表项,那就把所获的统计度量值保存在路由表项中以备下次使用。当下一次新连接建立的时候,不论主动建立还是被动建立,若该连接将要使用的路由表项有这些度量值,就可以直接将这些值来对相应变量进行初始化。
总结一句话:上一次连接所得到的有意义统计度量值会进行保存,而下一次连接建立时会根据这些度量值进行初始化。
5. ICMP差错
ICMP(Internet Control Message Protocol)为网络控制报文协议,其报文封装在IP包里,其主要分为查询报文类型和差错报文类型,我们平时用的ping即是查询报文类型,而tracerout使用的则是差错报文类型。
ICMP常见的差错为源站抑制、主机不可达和网络不可达
TCP对ICMP差错的处理:
1)源站抑制:将cwnd设置为1个报文段来发起慢启动,但不改变慢启动门限ssthresh。
2)主机不可达或网络不可:实际上被TCP忽略,TCP不会放弃连接而是会试图重复发送该出错的数据,尽管最终会导致超时放弃,TCP在处理这样的一个超时错误时使用的是其他错误码而不是“连接超时”
6. 重新分组
TCP在发生超时重传时,它重传的不一定是同样的报文,它在准备重传时,如果发现后续还有数据,其可以将本来要发送的数据与后面的数据进行整合后一起进行重传。