TCP发送的数据和确认都可能丢失,TCP通过在发送时设置一个定时器来解决这种问题,当定时器溢出时还没收到确认,它就重传该数据。
对于每个连接,TCP管理4个不同定时器:
1.重传定时器用于希望收到另一端的确认。
2.坚持定时器使窗口大小信息保持不断流动,即使另一端关闭了其接收窗口。
3.保活定时器可检测到一个空闲连接的另一端何时崩溃或重启。
4.2MSL定时器测量一个处于TIME_WAIT状态的时间。
观察TCP的重传机制,先建立一个连接,发送一些分组证明一切正常,然后拔掉电缆,发送更多数据,再观察TCP的行为:
以下是tcpdump的结果:
前三行是TCP的正常建立过程,第四行是发送方发送的hello,world(12个字符+回车+换行),第五行是其确认。之后拔掉接收方以太网电缆,第六行表示and hi被发送,之后第7~18行是这个报文段的12次重传过程。第十九行是发送方TCP最终放弃并发送一个复位信号的过程。
检查连续重传之间的时间差,它们取整后分别为1、3、6、12、24、48和多个64秒。实际上第一次发送后设置的超时时间实际为1.5秒,但实际发送却是在1.0136秒后,原理就是定时器在固定时间溢出而等待却是随机的,即第一次发送后的超时时间为随机的0~1.5秒之间的某值,此后,该重传时间每次增加1倍直到64秒,这个倍乘关系称为指数退避。
首次分组传输与复位信号传输之间的时间差约为9分钟,该时间在目前的TCP实现中是不可变的。但Solaris 2.2允许管理者改变这个时间,且其默认值为2分钟而非常用的9分钟。
一个给定连接的RTT由于路由器和网络流量的变化,这个值也是会不断变化的,TCP应该追踪这些变化并相应地改变其超时时间。
TCP必须测量发送一个特定序号的字节到收到包含此字节的确认之间的RTT,可用M表示所测量到的RTT,最初的TCP规范使用低通过滤器来更新一个被平滑的RTT估计器:
以上公式中,α是一个推荐值为0.9的平滑因子。
RFC 793推荐的重传超时时间RTO(Retransmission TimeOut)的值应设为:
以上公式中,β是一个推荐值为2的时延离散因子。
[Jacobson 1988]分析指出在RTT变化范围很大时,以上方法无法跟上这个变化,从而引起不必要的重传,当网络已经处于饱和状态时,不必要的重传会增加网络的负载,对网络而言是在火上浇油。
除了被平滑的RTT估计器,需要做的还有跟踪RTT的方差,在往返时间变化起伏很大时,基于均值和方差来计算RTO比以上方法能提供更好的响应。以下是Jacobson描述的RTO计算公式:
以上公式中,M是测量到的RTT,A是被平滑的RTT,D是被平滑的均值偏差,Err是刚得到的测量结果与当前RTT估计器之差,增量g起平均作用,取为1/8,偏差的增益是h,取值为0.25,当RTT变化时,较大的偏差增益使RTO快速上升。
Jacobson指明了一种使用整数运算来计算这些公式的方法,并被许多实现所采用,这也是h、g、倍数4均是2的乘方的一个原因,这样计算只通过移位而不需要乘除即可完成。
假定一个分组被发送,超时发生时,该分组被重传,然后收到一个确认,这个ACK是针对第一个分组还是第二个分组是不知道的,这是重传多义性问题。[Karn and Partridge 1987]规定,当超时和重传发生时,不能更新RTT估计器,因为我们不知道ACK对应哪次传输,由于数据被重传,RTO已经得到了一个指数退避,下一次传输时使用这个退避后的RTO。对于之后没有被重传的报文段而言,收到了确认后再次计算RTO。
使用sock程序将32768字节的数据从主机slip发送到主机vangogh.cs.berkeley.edu上的丢弃服务:
该命令执行32个写1024字节的操作,由于以上连接的链路MTU为296字节,因此这些操作会产生128个报文段,每个报文段包含256字节的用户数据,整个传输过程的时间为45秒,以下是前5秒钟数据和确认的传输:
上图中左边显示计算了三次RTT,大括号表示对哪些报文段进行了计时,并不是所有报文段都被计时。
大多源于伯克利的TCP实现在任何时候对每个连接仅测量一次RTT值,发送一个报文段时,如果给定连接的定时器已经被使用,则该报文段不计时。
每次调用500ms的TCP定时器例程时,就增加一个计数器来完成计时,这意味着,如果一个报文段的确认在它发送550ms后到达,则该报文段的往返时间将是1个滴答(500ms)或2个滴答(1000ms)。
报文段中的起始序号会被记录下来,当收到一个包含这个序号的确认后,该定时器就被关闭,如果ACK到达时数据没有被重传,则被平滑的RTT和被平滑的均值偏差基于这个新测量进行更新。
如上图,第一个RTT虽然tcpdump显示是1.061秒,但实际取的是1500ms,第二个RTT实际取的是500ms,第三个RTT实际取的是1000ms,实际RTT与时钟滴答计数之间的关系:
传输过程中有数据报文段丢失的情况:
报文段45丢失或损坏了,之后发送端连续发送了8个后续的256字节报文段,直到发送端接收到第3个对6657字节之前数据的ACK,才开始重新发送报文段45的内容,源于伯克利的TCP实现对收到的重复ACK进行计数,当收到第3个重复ACK时,就假定这个报文段已经丢失并重传这个报文段,这就是Jacobson的快速重传算法。
我们可以看到在重传后(报文段63),发送方继续正常地进行数据传输,TCP不需等待对方确认重传。
接收端收到正常数据(报文段43)后,接收TCP将255个字节交给用户进程,但下一个收到的报文段(报文段46)是失序的,数据的开始序号是6913而非6657,TCP保存这失序的256字节,并返回一个已成功接收数据的最大序号加1(6657)的ACK,后面收到的7个报文段也都是失序的,接收方TCP保存这些数据并产生重复ACK。当缺少的报文段到达时,接收方TCP在其缓存中保存第6657~8960字节的数据, 并将这2304字节的数据交给用户进程,所有这些数据在报文段72中进行确认,此时ACK的通告窗口大小为5888(8192-2304),这是因为用户进程没有机会读取这些已准备好的2304字节数据。
慢启动算法是在一个连接上发起数据流的方法,有时我们会到达中间路由器的极限,此时分组被丢弃。拥塞避免算法是处理丢失分组的方法,该算法假定由于分组受到损坏引起的丢失是非常少的(远小于1%),因此分组丢失就意味着在源主机和目的主机之间的某处网络上发生了拥塞,有两种分组丢失的表现:发生超时和接收到重复的确认。
拥塞避免算法和慢启动算法是两个目的不同、独立的算法,但当拥塞发生时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来做到这一点,实际中这两个算法通常在一起实现。
拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口cwnd和一个慢启动门限ssthresh,算法工作过程如下:
1.对一个给定连接,初始化cwnd为1个报文段,ssthresh为65535字节。
2.TCP输出例程的输出不能超过cwnd和接收方通告的窗口大小。拥塞避免是发送方使用的流量控制,而通告窗口是接收方进行的流量控制,前者是发送方感受到的网络拥塞的估计,后者与接收方在该连接上的可用缓存大小有关。
3.当拥塞发生时,ssthresh被设置为当前窗口大小的一半,但最少为2个报文段,如果是超时引起了拥塞,则cwnd被设置为1个报文段(这就是慢启动)。
4.当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否正在进行慢启动或拥塞避免,如果cwnd小于等于ssthresh,则正在进行慢启动,否则正在进行拥塞避免,慢启动一直持续到拥塞发生时所处位置的一半才停止。
拥塞避免算法要求每次收到一个确认将cwnd增加1/cwnd,这是一种加性增长,我们希望在一个往返时间内最多为cwnd增加1个报文段,不管在这个RTT中收到了多少个ACK,而慢启动将根据这个往返时间中所收到的确认的个数增加cwnd。
所有4.3 BSD和4.4 BSD都在拥塞避免中将增加值不正确地设置为1个报文段大小除以8,这是错误的,并在以后的版本中不再使用。
上图中,假设当cwnd为32个报文段时会发生拥塞,于是设置ssthresh为16个报文段。
正如上图,术语慢启动并不完全正确,它只是采用了比引起拥塞更慢的分组传输速率。
在收到一个失序的报文段时,TCP需要产生一个重复的ACK,这个ACK不应被迟延,该ACK的目的在于让对方知道收到一个失序的报文段,并告诉对方自己希望收到的序号。
由于我们不知道一个重复的ACK是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重新排序,因此我们等待少量重复的ACK到来,假如只是一些报文段的重新排序,则可能只会产生1~2个重复的ACK,如果一连串收到3个或3个以上的重复ACK,就非常可能是一个报文段丢失了,于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出,这就是快速重传算法,接下来执行拥塞避免算法(而非慢启动算法),结合起来就是快速恢复算法。
快速恢复算法中不使用慢启动算法的原因是,接收方只有收到另一个报文段时才会产生重复的ACK,这说明收发两端之间仍然有流动的数据,而我们不想突然减少数据流。
快速重传算法步骤:
1.当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口cwnd的一半,重传丢失的报文段,将cwnd设为ssthresh加上3倍的报文段大小。
2.每次收到另一个重复的ACK,cwnd增加1个报文段大小。
3.当下一个确认新数据的ACK到达时,设置cwnd为ssthresh(第1步中设置的值),这个ACK应该是对丢失的分组和接收方收到的最新的数据之间的所有中间报文段的确认。
cwnd开始时被初始化为1,单位为报文段长度,如果MSS为256字节,则cwnd被初始化为256字节。
以下是初始SYN重传并接着发送了前7个数据报文段时cwnd和ssthresh的值(MSS为256字节):
当SYN的超时发生时,ssthresh被设为其最小取值(512字节,本例中表示2个报文段),为进入慢启动阶段,cwnd被置为1个报文段长度(256字节,值不变)。
当收到SYN和ACK时,没有对这两个变量做任何修改,因为新数据还没有被确认。
当ACK 257到达时,因为cwnd小于等于ssthresh,因此仍然处于慢启动阶段,于是将cwnd增加为512字节,当收到ACK 513时,进行同样的处理。
当ACK 769到达时,不再处于慢启动状态,而是进入了拥塞避免状态,新的cwnd按以下方法计算,结果为885字节:
cwnd以字节数而非报文段数来维护,因此这就是前面提到的增加1/cwnd。
当最初的2个重复ACK(报文段60和61)到达时,它们被计数,而cwnd不变,当第三个ACK到达时,ssthresh被设为cwnd的一半(四舍五入到报文段大小的倍数),而cwnd被设为ssthresh加上所收到的重复的ACK数乘报文段大小(即1024+3*256),然后发送重传数据。
之后又有5个重复ACK到达,每次cwnd增加1个报文段长度,最后一个新的ACK到达时,cwnd被设为ssthresh并进入正常的拥塞避免过程,cwnd设为ssthresh的值,取值为1280。
在快速重传阶段,我们收到报文段66、68、70中的重复ACK后才发送新的数据,而不是收到报文段64、65中的重复的ACK后就发送新数据,这是cwnd的取值与未被确认的数据大小比较的结果,报文段65到达前,cwnd为2048,但此时未被确认的数据有2304字节(9个报文段大小),因此不能发送任何数据,而当报文段65到达后,cwnd被设为2304,此时我们仍不能发送,而当报文段66到达时,cwnd等于2816,大于未被确认的2560字节,因此可以发送一个新的数据报文段。
较新的TCP实现中维护许多有关链路的值。当一个TCP连接关闭时,如果已经发送了足够多的数据,且目的节点的路由表项不是默认表项,则以下信息保存在路由表中备用:被平滑的RTT、被平滑的均值偏差和慢启动门限。足够多的数据指16个窗口的数据,这样可得到16个RTT采样,从而使被平滑的RTT过滤器能集中在正确结果的5%以内。
管理员可以用route命令设置给定路由的以下度量:被平滑的RTT、被平滑的均值偏差、慢启动门限、输出的带宽时延积、输入的带宽时延积。
基于伯克利的实现对ICMP差错的处理:
1.接收到源站抑制差错报文时,将拥塞窗口cwnd置为1个报文段大小来发起慢启动,但慢启动门限不变。
2.接收到主句不可达或网络不可达时忽略,因为这些是短暂现象。这可能是由于中间路由器被关闭导致选路协议花费数分钟才能稳定到另一个替换路由。
早期的BSD实现在任何时候收到一个主机不可达或网络不可达的ICMP差错时会不正确地放弃连接。
当TCP超时并重传时,它不一定要重传相同的报文段,TCP允许重新分组而发送一个较大的报文段。如电缆拔掉后,报文段超时,之后接上电缆,可能会发送一个包含超时报文段的更大的报文段。