《TCP/IP详解 卷1》 笔记: TCP的超时与重传

来源:互联网 发布:开淘宝网店需要多少钱 编辑:程序博客网 时间:2024/06/09 19:39

引言

    TCP提供可靠的运输层。它使用的方法之一就是确认从另一端收到的数据。但数据和确认都有可能会丢失。TCP通过在发送时设置一个定时器来解决这种问题。如果当定时器溢出时还没有收到确认,它就重传该数据。对任何实现而言,关键之处就在于超时和重传的策略,即怎样决定超时间隔和如何确定重传的频率。

    对每个连接,TCP管理4个不同的定时器。
        1. 重传(retransmit)定时器使用于当希望收到另一端的确认。
        2. 坚持(persist)定时器使窗口大小信息保持不断流动,即使另一端关闭了其接收窗口。
        3. 保活(keepalive)定时器可检测到一个空闲连接的另一端何时崩溃或重启。
        4. 2MSL定时器测量一个连接处于TIME_WAIT状态的时间。

在本节我们将详细讨论重传定时器以及一些相关的问题,如拥塞避免。之后的章节讨论坚持定时器和保活定时器。

超时与重传的简单例子

    我们来看一个重传的例子。使用telnet在bsdi与svr4 discard服务之间建立TCP连接,先发送一行数据,然后断开它们之间的网线,再发送一行数据,tcpdump的输出结果如下:


    从上图中我们观察连续重传之间的时间差(称为超时重传时间,RTO),它们取整后分别为1、3、6、12、24、48和多个64秒。这个倍乘关系被称为“指数退避(exponential backoff)”。在多个64秒超时之后,TCP发送复位报文段。注意:实际上TCP设置的第一个超时时间是1.5秒,而不是1秒(原因见下文)。

往返时间RTT的例子

    TCP超时与重传中最重要的部分就是对一个给定连接的往返时间RTT的测量。由于路由器和网络流量均会变化,因此我们认为这个时间可能经常会发生变化,TCP应该跟踪这些变化并相应地改变其超时时间。

    我们先来看一个TCP如何测量RTT的例子。这个例子是从slip主机往广域网内的主机vangogh发送32768字节的数据,下图显示的是前5秒中数据和确认的传输过程:


    我们看到TCP在5秒的时间内获取到了3次RTT值,分别是1.0661秒、0.808秒和1.015秒。

    为什么只计算3次RTT值?这是因为大多数源于伯克利的TCP实现在任何时候对每个连接仅测量一次RTT值。在发送一个报文段时,如果给定连接的定时器已经被使用,则该报文段不被计时。在每次调用500ms的TCP的定时器例程时,就增加一个计数器来完成计时。这意味着,如果一个报文段的确认在它发送550ms后到达,则该报文段的往返时间RTT将是1个滴答(即500ms)或是2个滴答(即1000ms)。下图显示了本例中实际RTT与时钟滴答计数之间的关系:


    虚线是500ms定时器溢出的时刻。以1.061秒的RTT为例:TCP发送报文段1时的时刻为0,此时开始对报文段计时。在0.03秒的时刻,500ms定时器第一次溢出,计数器的值为1。在0.53秒的时刻,定时器第二次溢出,计数器值为2。在1.03秒的时候定时器第三次溢出,计数器的值为3。在1.061秒的时刻,收到包含报文段1的数据起始序号的ACK,因此不再计数,这个报文段的RTT为3个滴答,也就是1.5秒。

超时重传时间RTO的计算

    在测得一个RTT(注意:不是第1个RTT)后,TCP会使用下面的公式计算新的RTO:

        Err = M - A
        A = A + 0.125 * Err
        D = D + 0.25 * (|Err| - D)
        RTO = A + 4 * D

    其中M就是RTT。A称为平滑的RTT(估计器),初始值为0。D被称为平滑的平均偏差,初始值为3。要注意的是初始的RTO按RTO = A + 2 * D 计算,因此初始的RTO为6秒。还要注意的是A和D的初始值仅被用来计算初始RTO,之后会在测得RTT的值后重新初始化。

    此例中,两个计算RTO的过程如下:

        当第1个数据报文段的ACK(报文段2)到达时,经历了3个时钟滴答,估计器被初始化为
            A = M + 0.5 = 1.5 + 0.5 = 2 (因为经历3个时钟滴答,因此,M取值为1.5)
            D = A / 2 = 1
        在前面,A和D分别初始化为0和3,RTO的初始计算值为6。这是使用第1个RTT的测量结果M对估计器进行首次计算的初始值。计算的RTO值为

            RTO = A + 4 * D = 2 + 4 × 1 = 6
        当第2个数据报文段的ACK(报文段5)到达时,经历了1个时钟滴答(0.5秒),估计器按如下更新:
            Err = M - A = 0.5 - 2 = -1.5
            A = A + 0.125 * Err = 2 - 0.125 × 1.5 = 1.8125
            D = D + 0.25 * (|Err| - D) = 1 + 0.25 × (1.5 - 1) = 1.125
            RTO = A + 4 * D = 1.8125 + 4 × 1.125 = 6.3125

karn算法
    当超时发生时,RTO正如图21-1中显示的那样进行退避,分组以更长的RTO进行重传,然后收到一个确认。那么这个ACK是针对第一个分组的还是针对第二个分组呢?这就是所谓的重传多义性问题。[Karn and Partridge1987]规定,当一个超时和重传发生时,在重传数据的确认最后到达之前,不能更新RTT估计器,因为我们并不知道ACK对应哪次传输(也许第一次传输被延迟而并没有被丢弃,也有可能第一次传输的ACK被延迟)。并且,由于数据被重传,RTO已经得到了一个指数退避,我们在下一次传输时使用这个退避后的RTO。对一个没有被重传的报文段而言,除非收到了一个确认,否则不要计算新的RTO。
拥塞举例

    下图是例子中发生拥塞时的一个数据传输过程:


    看来报文段45丢失或损坏了,这一点无法从该输出上进行辨认。能够在主机slip上看到的是对第6657字节(报文段58)以前数据的确认(不包括字节6657在内)。紧接着的是带有相同序号的8个ACK。正是接收到报文段62,也就是第3个重复ACK,才引起自序号6657开始的数据报文段(报文段63)进行重传。的确,源于伯克利的TCP实现对收到的重复ACK进行计数,当收到第3个重复的ACK时,就假定一个报文段已经丢失并重传自那个序号起的一个报文段。这就是Jacobson的快速重传算法,该算法通常与他的快速恢复算法一起配合使用。
    注意到在重传后(报文段63),发送方继续正常的数据传输(报文段67、69和71)。TCP不需要等待对方确认重传。
现在检查一下在接收端发生了什么。当按序收到正常数据(报文段43)后,接收TCP将255个字节的数据交给用户进程。但下一个收到的报文段(报文段46)是失序的:数据的开始序号(6913)并不是下一个期望的序号(6657)。TCP保存256字节的数据,并返回一个已成功接收数据的最大序号加1(6657)的ACK。被vangogh接收到的后面7个报文段(48,50,52,54,55,57和59)也是失序的,接收方TCP保存这些数据并产生重复ACK。
    目前TCP尚无办法告诉对方缺少一个报文段,也无法确认失序数据。此时主机vangogh所能够做的就是继续发送确认序号为6657的ACK。当缺少的报文段(报文段63)到达时,接收方TCP在其缓存中保存第6657~8960字节的数据,并将这2304字节的数据交给用户进程。所有这些数据在报文段72中进行确认。请注意此时该ACK通告窗口大小为5888(8192-2304),这是因为用户进程没有机会读取这些已准备好的2304字节的数据。
拥塞避免算法
    在前一节介绍的慢启动算法是在一个连接上发起数据流的方法,但有时我们会达到中间路由器的极限,此时分组将被丢弃。拥塞避免算法是一种处理丢失分组的方法。该算法假定由于分组受到损坏引起的丢失是非常少的(远小于1%),因此分组丢失就意味着在源主机和目的主机之间的某处网络上发生了拥塞。有两种分组丢失的指示:发生超时和接收到重复的确认。如果使用超时作为拥塞指示,则需要使用一个好的RTT算法。
    拥塞避免算法和慢启动算法是两个目的不同、独立的算法。但是当拥塞发生时,我们希望降低分组进入网络的传输速率,于是可以调用慢启动来作到这一点。在实际中这两个算法通常在一起实现。
    拥塞避免算法和慢启动算法需要对每个连接维持两个变量:一个拥塞窗口cwnd和一个慢启动门限ssthresh。这样得到的算法的工作过程如下:
        1) 对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535个字节。
        2) TCP输出例程的输出不能超过cwnd和接收方通告窗口的大小。拥塞避免是发送方使用的流量控制,而通告窗口则是接收方进行的流量控制。前者是发送方感受到的网络拥塞的估计,而后者则与接收方在该连接上的可用缓存大小有关。
        3) 当拥塞发生时(超时或收到重复确认),ssthresh被设置为当前窗口大小(cwnd和接收方通告窗口大小的最小值,但最少为2个报文段)的一半。此外,如果是超时引起了拥塞,则cwnd被设置为1个报文段(这就是慢启动)。
        4) 当新的数据被对方确认时,就增加cwnd,但增加的方法依赖于我们是否正在进行慢启动或拥塞避免。如果cwnd小于或等于ssthresh,则正在进行慢启动,否则正在进行拥塞避免。慢启动一直持续到我们回到当拥塞发生时所处位置一半的时候才停止,然后转为执行拥塞避免。
    慢启动算法初始设置cwnd为1个报文段,此后每收到一个确认就加1。正如前一节描述的那样,这会使窗口按指数方式增长:发送1个报文段,然后是2个,接着是4个……。
    拥塞避免算法要求每次收到一个确认时将cwnd增加1/cwnd。与慢启动的指数增加比起来,这是一种加性增长。我们希望在一个往返时间内最多为cwnd增加1个报文段(不管在这个RTT中收到了多少个ACK),然而慢启动将根据这个往返时间中所收到的确认的个数增加cwnd。

    下图是慢启动和拥塞避免的一个可是化描述:


    我们以段为单位来显示 cwndssthresh,但它们实际上都是以字节为单位进行维护的。 在该图中,假定当cwnd为32个报文段时就会发生拥塞。于是设置ssthresh为16个报文段,而cwnd为1个报文段。在时刻0发送了一个报文段,并假定在时刻1接收到它的ACK,此时cwnd增加为2。接着发送了2个报文段,并假定在时刻2接收到它们的ACK,于是cwnd增加为4(对每个ACK增加1次)。这种指数增加算法一直进行到在时刻3和4之间收到8个ACK后cwnd等于ssthresh时才停止,从该时刻起,cwnd以线性方式增加,在每个往返时间内最多增加1个报文段。
快速重传与快速恢复算法

    从图21-7中我们看到,在收到一个失序的报文段时,TCP立即产生了一个ACK(一个重复的ACK)。该重复的ACK的目的在于让对方知道收到一个失序的报文段,并告诉对方自己希望收到的序号。

    由于我们不知道一个重复的ACK是由一个丢失的报文段引起的,还是由于仅仅出现了几个报文段的重新排序,因此我们等待少量重复的ACK到来。假如这只是一些报文段的重新排序,则在重新排序的报文段被处理并产生一个新的ACK之前,只可能产生1~2个重复的ACK(???)。如果一连串收到3个或3个以上的重复ACK,就非常可能是一个报文段丢失了。于是我们就重传丢失的数据报文段,而无需等待超时定时器溢出。这就是快速重传算法。接下来执行的不是慢启动算法而是拥塞避免算法。这就是快速恢复算法。

    在图21-7中可以看到在收到3个重复的ACK之后没有执行慢启动。相反,发送方进行重传,接着在收到重传的ACK以前,发送了3个新的数据的报文段(报文段67,69和71)。在这种情况下没有执行慢启动的原因是由于收到重复的ACK不仅仅告诉我们一个分组丢失了。由于接收方只有在收到另一个报文段时才会产生重复的ACK,而该报文段已经离开了网络并进入了接收方的缓存。也就是说,在收发两端之间仍然有流动的数据,而我们不想执行慢启动来突然减少数据流。
    这些算法通常按如下过程进行实现:
        1) 当收到第3个重复的ACK时,将ssthresh设置为当前拥塞窗口cwnd的一半。重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小。
        2) 每次收到另一个重复的ACK时,cwnd增加1个报文段大小并发送1个分组(如果新的cwnd允许发送)。
        3) 当下一个确认新数据的ACK到达时,设置cwnd为ssthresh(在第1步中设置的值)。这个ACK应该是在进行重传后的一个往返时间内对步骤1中重传的确认。另外,这个ACK也应该是对丢失的分组和收到的第1个重复的ACK之间的所有中间报文段的确认。这一步采用的是拥塞避免,因为当分组丢失时我们将当前的速率减半。

按每条路由进行度量

    较新的TCP实现在路由表项中维持许多我们在本章已经介绍过的指标。当一个TCP连接关闭时,如果已经发送了足够多的数据来获得有意义统计资料,且目的结点的路由表项不是一个默认的表项,那么下列信息就保存在路由表项中以备下次使用:平滑的RTT、平滑的均值偏差以及慢启动门限。

    当建立一个新的连接时,不论是主动还是被动,如果该连接将要使用的路由表项已经有这些度量的值,则用这些度量来对相应的变量进行初始化。
重新分组
    当TCP超时并重传时,它不一定要重传同样的报文段。相反,TCP允许进行重新分组而发送一个较大的报文段,这将有助于提高性能(当然,这个较大的报文段不能够超过接收方声明的MSS)。

阅读全文
0 0
原创粉丝点击