前言
让我们来回忆一下TCP,TCP位于传输层(也有人称之为运输层),TCP提供可靠交付的服务,无差错、不丢失,不重复,并且按序到达 ,这句话出自教科书。其实这个不丢失我觉得可以理解为就算是有个数据包丢失的情况下,TCP提供的超时重传也能保证你能收到完整的数据包。丢失的最朴素的场景就是数据包被确定要走哪一片光缆的时候,这片光纤被挖断了,我大学的时候某个月,光纤就老是被挖断,那被分配到这片光缆上的数据包就可以理解为丢失了。那丢失了怎么办呢,再重传一次。但其实在网络这里,重传并不是一件简单的事情,因为有的时候,数据包未必丢失,也可能是迟到,除此之外,还要考虑效率问题。
让我们从最简单的模型谈起
全双工的通信的意思是,双方既是发送方也是接收方,但是为了讨论问题方便,我们目前只考虑A发送数据,而B接收,这里是讨论可靠传输的原理,所以不考虑 数据包在哪一个层次进行传输,因此把传送的数据单元称之为分组(传输层传送的数据单元叫报文段,网络层传输的协议数据单元叫做IP数据包)。让我们先从最简单的模型谈起,也就是停止等待协议,停止等待的意思是,每发送完一个分组就停止发送,等待对方的确认。在收到确认之后再发送下一个分组。
在这种模型下有以下几种情况:
- 无差错
这种情况最简单,A发送分组M1,发送完就暂停发送,等待B的确认。B收到了M1就向A发送确认。A在收到了对M1的确认之后,就再发送下一个分组M2。
- 出现差错
分组在传输过程中出现差错。B接收M1时检测出了差错,就丢弃了M1,其他什么也不做(不通知A收到有差错的分组,在可靠传输的协议中,也可以检测出有差错时发送“否认报文”给对方。这样做的好处是能够让发送方及早知道出现差错。不过由于这样处理会使协议变得复杂,现在实用的可靠传输协议都不使用这种否认报文了)。也可能是M1在传输的过程中丢失了,这时B当然什么都不知道。在这两种情况下,B都不会发送任何信息,一般的可靠传输协议是这么设计的: A只要超过了一段时间仍然没有收到确认,就认为刚才发送的分组丢失了,因而重传前面发送过的分组。这叫超时重传。要实现超时重传,就要在每发送完一个分组时设置一个超时计时器,如果在超时计时器到期之前收到了对方的确认,就撤销已设置的超时计时器。
- 确认丢失和确认迟到
B发送的对M1的确认丢失了。A在设定的超时重传时间内没有收到确认,并无法知道是自己发送的分组出错、丢失,或者B发送的确认丢失了。因此A的超时计时器丢失之后就会重传M1。假定在这种情况下B就又收到了M1,对于B来说处理这个重传的M1就要采取两个行动:
- 丢弃这个重复的分组M1,不向上层进行交付。
- 向A发送确认,不能认为已经发送过确认就不再发送。因为A之所以重传M1,就表示没有收到对M1的确认。
再有一种情况,B对M1的确认报文没有丢失,而是迟到了,在这种情况下A可能就会重复收到确认。对重复确认的处理很简单:收下后,就丢弃。B仍然会收到重复的M1,并且同样丢弃重复的M1,并重传确认分组。
通常A最终总是可以收到对所有发出的分组的确认。如果A不断重传分组但总是收不到确认,就说明通信线路太差,不能进行通信。想象一下,你的手机在弱网环境下,发出去的消息会转圈圈,如果转了很长时间也没发出去,微信就认为这条消息没发出去。
使用上述的确认和重传机制,我们就可以在不可靠的传输网络上实现可靠的通信。介绍这个模型主要是为了引出为了可靠传输会遇到哪些问题,这个模型的传输效率是非常低的,信道的利用率只有5.66%,这意味着信道在大多数时间内都是空闲的。
为了提高传输效率,发送方采取了流水线传输,流水线传输就是发送方可以连续发送多个分组,不必每发送一个分组就停顿下来,等待对方的确认。当使用流水线传输时,就引出了连续ARQ协议和滑动窗口协议。滑动窗口协议比ARQ协议复杂,我们这里先介绍连续ARQ协议,再介绍滑动窗口协议。
连续ARQ协议
上面是一个典型TCP报文首部格式,我这里不打算一一介绍TCP首部中各个字段的意思,这里只介绍一些我们本篇所需要的字段:
- 源端口和目的端口
各占两个字节,分别写入源端口号和目的端口号
- 窗口
占两个字节,窗口值是[0, 2^16^-1] 之间的整数,是指接收方允许对方发送的数据量,之所以有这个限制,原因就在于接收方的数据缓存空间是有限的。
窗口字段明确指出了现在允许对方发送的数据量。窗口值经常在动态的变化着。
- 序号
占4字节。序号范围为是[0,2^32^ -1], 共2^32^ 个序号,序号到达最大值之后,又回到0。在一个TCP连接中传送的字节流中每一个字节都按顺序编号,TCP报文首部的序号值则指的是本报文段所发送的数据的第一个字节的序号。
- 确认号
占4字节,是期望收到对方下一个报文段的第一个数据字节的序号。
TCP是双工的协议,会话的双方都可以同时接收、发送数据。TCP会话的双方都各自维护一个“发送窗口”和一个“接收窗口”。其中各自的“接收窗口”大小取决于应用、系统、硬件的限制(TCP传输速率不能大于应用的数据处理速率)。各自的“发送窗口”则要求取决于对端通告的“接收窗口”,要求相同
下图表示的是发送方维持的发送窗口:
表示的意义是:位于发送窗口内的5个分组都可以连续发送出去,而不需要等待对方的确认。这样,信道利用率就提高了。
连续ARQ协议规定,发送方没收到一个确认,就把发送窗口向前滑动一个分组的位置。接收方一般都采用累积确认的方式。这就是说,接收方不必对收到的分组逐个发送确认,而是在收到几个分组之后,对按序到达的最后一个分组发送确认,这就表示: 到这个分组为止的所有分组都已经正确收到了。
但是累积确认有优点也有缺点, 优点是:容易实现。缺点是不能向发送方反映出接收方已经正确收到的所有分组的信息。
举一个例子: 发送方发送了五个分组,中间第三个分组丢失了,注意我们上面的确认方式是对按序到达的最后一个分组发送确认,我们收到了1、2、4、5,就只能发送对2的确认,于是发送方就只好将3、4、5再重传一次,我们将这种情况称之为Go-back-N(回退N),表示需要再退回来重传已经发送过的N个分组。可见当通信线路质量不好时,连续ARQ协议会带来负面的影响。对于这种协议TCP打上的补丁为选择确认SACK,我们后面会讲。
滑动窗口协议
下面我们讨论TCP中的滑动窗口协议,为了讨论问题,我们还是先假定数据传输只在一个方向进行,即A发送数据,B接收数据,这个模型其实已经足够说明滑动窗口协议了。
现假定A收到B发来的确认报文段,其中窗口是10字节,而确认号是31(这表明B期望收到的下一个序号是32),而序号30为止的数据已经收到了),根据这两个数据,A就构造出来自己的滑动窗口:
上面的发送窗口表示:在没收到B的确认的情况下,A可以连续把窗口内的数据都发送出去。凡是已经发送过的数据,在未收到确认之前都必须暂时保留,以便在超时重传时使用。发送窗口也可能变小,当对方通知的窗口缩小,但TCP的标准强烈不建议这样做。因为很可能发送方在收到这个通知以前已经发送了窗口的很多数据,现在又要收缩窗口,不让发送这些数据,这样就会产生一些错误。
现在假定A发送了序号为31-35的数据,这时发送窗口位置并未发生改变,但发送窗口内靠后面有5个字节表示已发送但未收到确认。而发送窗口靠前面的五个字节(36-40)是表示允许发送但尚未发送的。
从以上所述可以看出,要描述一个发送窗口的状态需要是三个指针: P1,P2和P3:
小于P1的是已发送并已收到确认的部分,而大于P3的是不允许发送的部分。P3-P1=A的发送窗口,P2-P1 = 已经发送但尚未收到确认的字节数。P3-P2=允许发送但当前尚未发送的字节数。
再看下B的接收窗口。B的接收窗口大小是10.在接收窗口外面,到30号位置的数据是已经发送过确认。因此B可以不再保留这些数据,接收窗口内的序号(31-40)是允许接收的。在下面的图中,B收到了序号为32-33的数据。这些数据没有按序到达,因为序号为31的数据没有收到(也许丢失了,也许滞留在网路中的某处),因为31没收到,所以B发送的确认报文段中的确认号仍然是31(即期望收到的序号),而不能是32或33.
现在假定B收到了A重传的31-33的数据,并把序号31-33的交付给应用层的程序,然后B删除这些数据。接着把接收窗口向前移动3个序号,同时给A发送确认,其中窗口值仍然是10,但确认号是34。这表明B已经收到了到序号33为止的数据。
收到确认之后,A的可用滑动窗口变大,这个时候如果A发送完序号36-43的数据,P2会和P3重合。发送窗口内的序号已经用完,但还没有再收到确认。A的发送窗口已满,就不能再发送数据,必须停止发送数据。此时存在两种情况:
- 数据包还在路上,未到达B
- B已经给A发送了确认,但是还在路上。
为了保证可靠传输,A经过一段时间之后(在超时计时器到期之后),就会重传这部分数据,重新设置超时计时器,直到收到B的确认位置。如果A收到确认号落在发送窗口内,那么A就可以发送窗口继续向前滑动,并发送新的数据。
选择确认-SACK
上面的论述中还有一个问题,就是B遗失了中间序号的数据包,会请求A再重传,但是在上面的讨论中窗口值较小,影响还不是很大,但是如果窗口值很大,遗失了中间的一个包,这样有的时候可能导致网络拥堵,对于这种情况,TCP提出了选择确认。我们举一个例子来说明选择确认的原理.
假设A收到了以下三个字节流:
从图中我们可以看到缺少了1001-1500、3001-3500,一个直白的思路就是,收到这三个字节流的时候,算出缺少的区间,再请求回传的时候带上这些区间。我们上面画出了TCP首部,目前看来没有哪个字段能够提供边界信息。RFC 2018对此做了规定,如果要使用选择确认SACK,那么在建立TCP连接时候,就必须在TCP的首部的选项中添加上“允许SACK”的选项,而双方必须事先商定好。
超时时间的选择
上面我们提到了超时重传,这里来进行详尽的讨论,设置固定的超时时间是完全不可取的,因为不同地区的网络质量不同,所以这个超时时间应当是自适应的,这也是TCP的思路,TCP采用了一种自适应算法,记录一个报文段发出的时间,以及收到相应的确认的时间。这两个时间之差就是报文段的往返时间RTT。TCP保留了RTT的一个加权平均往返时间RTTs(这又被称为平滑的往返时间,S表示Smoothed。因为进行的是加权平均,因此得出的结果更加平滑)。
每当第一次测量到RTT样本时,RTTs值就取为所测量到的RTT样本值。但以后每测量到一个新的RTT样本,就按下面的共识重新计算一次RTTs:
新的RTTs = (1 - α) × (旧的RTTs)+ α × (新的RTT样本)
在上式中,0 ≤ α ≤ 1. 若α很接近0,则表示新的RTTs值和旧的差距不大,若α接近于1,则表示新的RTTs受新的RTT样本影响较大。RFC 6298推荐α值为1/8。
显然超时计时器设置的超时重传时间RTO(Retransmission Time-Out)应当略大于上面得出的加权平均往返时间RTTs。RFC6298推荐使用下面的算式计算RTO: RTO = RTTs + 4 × RTTD。而RTTD是RTT的偏差加权平均值,它与RTTs和新的RTT样本之差有关。RFC6298建议这样计算RTTD。当第一次测量时,RTTD值取为测量到的RTT样本值的一半。在以后的测量中,则使用下式计算加权平均的RTTD:
新的RTT~D~ = (1 - β) × (旧的RTTD) + β × |RTTs - 新的RTT样本|。
这个β是个小于1的系数,它的推荐值是1/4, 即0.25。
上面说的往返时间测量,事实上实现起来相当的复杂,我们这里举一个例子:发送一个报文段,设定的重传时间到了,还没有收到确认。于是重传报文段,过了一段时间后,收到了确认报文段。现在的问题是: 如何判定此确认报文段是对先发送的报文的确认,还是对后来重传的报文段的确认? 由于重传的报文段和原来的报文段完全一样,因此源主机在收到确认后,就无法作出正确的判断,而正确的判断对确定加权RTTs的值关系很大。
若将收到的确认当作是对重传报文段的确认, 但却被源主机当成是对原来的报文段的确认,则这样计算出的RTTs和超时重传时间RTO就会偏大。后面再发送的报文段又是经过重传之后才收到确认报文段,则按此方法得出的超时重传时间RTO就会越来越长。
同样,若收到的确认是对原来报文段的确认,但被当成是对重传报文段的确认。则由此计算出来的RTTs和RTO都会偏小。这就必然导致报文段过多地重传,这样就会导致报文会被过多重传。
根据以上所述,Karn提出了一个算法:在计算加权平均RTTs时,只要报文重传,就不采用其往返时间样本。这样得出的加权平均RTTS和RTO就比较准确。
但是这个算法还遗漏了一点,就是如果网络时延突然增大了很多。因此在原来得出的重传时间内,不会收到确认报文段。于是重传报文段,但根据Karn的算法,不考虑重传的报文段的往返时间样本,这样,超时重传时间就无法更新。
因此我们需要为Karn的算法打上一个补丁: 报文段每重传一次,就把超时重传时间RTO增大一些。典型的做法是取新的重传时间为旧的重传时间的2倍。当不发生报文段的重传时,才根据上面给出的:RTO = RTTs + 4 × RTT~D~ 计算超时时间。实践证明,这种策略是比较合理的。
总结一下
TCP的可靠传输依赖于滑动窗口和超时重传,我们从最简单的停止等待协议谈起,为了确认接收方确实收到了分组,接收方需要发送确认给发送方,停止等待协议对信道的利用率比较低,我们不大可能采用这种算法来做,TCP采用了滑动窗口来提升信道的利用率,为了防止分组在途中丢失或者出了差错,TCP引入了超时重传,超时重传时间的计算是一个自适应算法。本篇基本上取材于《计算机网络(第7版)》的TCP一节,用自己的方式梳理了一下。