前面一节咱们介绍完了TCP协议,这部分,将要介绍,TCP是如何实现可靠传输的。
TCP的可靠传输
1.滑动窗口
上一节我们介绍TCP报文段头部的时候说得到,"窗口"这个部分,"窗口"的内容就是发送/接收的数据的字节总量(窗口是以字节为单位)。
发送方A有发送窗口,接收方B有接收窗口。头部的"窗口"要配合"确认号"才能确定,要发送哪些数据和具体接收哪些数据。
现假定A收到了B发来的确认报文段中,其中窗口是20(字节),而确认号是31(这表明B期望收到的下一个序号是31,而序号31之前的数据已经收到了),A就构造出自己的发送窗口,如下图:
我们现在来讨论一下A的发送窗口:
在没有收到B确认的情况下,A可以连续把窗口内的数据都发送出去。凡是发送过的数据,会暂时保留一份副本,以便超时重传时使用。
发送窗口的位置由窗口前沿和窗口后沿的位置共同决定。
后沿: 上图中后沿的后面部分(左边)表示已经发送并且收到了确认。这些数据不在保留副本。
后沿变化的情况有两种:不动(没有收到新的确认号(>31))和前移(收到了新的确认号)。后沿是不会向后移动的,因为不能撤销掉已收到的确认。
前沿: 上图中前沿的前面部分(右边)表示不允许发送的数据。
前沿变化情况有三种:
向前移动: 收到新的确认后(>31)的通常情况
不动: 没有收到确认;收到了新的确认号(35),但窗口值变小了变成了15,那前沿的位置还是在50那。
收缩:窗口值变得更小。如上面新的确认号是35,但窗口值变成了10,那前沿的位置就到了45的位置。不过TCP的标准强烈不赞成这样做。
后沿前沿的移动,正好就把窗口滑动的特点展示出来了。
下面我们再通过举例子,再详细介绍滑动窗口的特点
假定A发送了窗口中序号为31~41的数据(黑色小方框表示),表示已发送但未收到确认。而42~50号的数据是允许发送但尚未发送的。如下图。
上图中,要描述发送窗口的状态需要三个指针:P1,P2,P3。而小于P1的是已经发到并且收到确认的部分,大于P3是不允许发送的部分。
P3 - P1 = A的发送窗口(通知窗口)
P2 - P1 = 已发送但尚未收到确认的字节数。
P3 - P2 = 允许发送但尚未发送的字节数(可用窗口或有效窗口)
再说一下B的接收窗口。先看下图。
B接收窗口内的序号(31~50)是允许接收的。在上图中,32,33的被接收到了,而31的数据没有收到(也许丢失,也许滞留在网络中)。而B只能将按顺序收到的数据中,将最高号作为确认号发出。由于31号还没有收到,所以B发送的确认报文仍然是31,而不是32或33。
现在假设B收到了31号数据,并把31~33的数据交给应用程序,然后B会删除这些数据。接着把接收窗口向前移动3个序号,同时给A发送确认号34,窗口只20。
此时A、B窗口移动后的图如下:
A收到了收到了B发送的34号确认号,20的窗口值,所以P1移至34,P3移至53。但P2指针没有动。
而我们可以看到B还接收到了37、38、40。但这些数据没有按序到达,所以只能先暂存在接收窗口中。
然后,A再继续发送完序号42~53的数据后,指针P2向前移动和P3重合。如下图:
窗口内的序号已经用完。但还没有收到B的确认。如果过了一段时间A还没有收到确认,就只能超时重传这部分数据,直到B收到确认位置。
如果A收到确认号落在发送窗口内,那么A可以使窗口向前滑动,发送新的数据。
2. 发送/接收缓存
发送方的应用进程把字节流写入TCP的发送缓存;接收方的应用进程从TCP的接收缓存中读取字节流。下图画出了TCP缓存和窗口之间的关系:
我们先来看(a)图:
可以看到数据量大小是发送缓存>应用程序>发送窗口。
发送缓存用来暂时存放:
1: 应用程序传给TCP准备发送的数据。
2: TCP已经发送但未收到确认的数据。
应用程序必须控制写入缓存的速率,不能太快,否则发送缓存就是会没有存放数据的空间。(I/Obuffer)
再看一下(b)图:
接收缓存用来暂时存放:
1: 按序到达的、但尚未被接收应用程序读取的数据
2: 未按序到达的数据
如果收到的分组被检测出有差错,则要丢弃。如果应用程序来不及读取收到的数据,接收缓存最终就会被填满,使接收窗口减小到0。反之,如果应用程序能够及时从接收缓存中读取收到的数据,接收窗口就可以增大,但不能超过接收缓存的大小。(b)中还指出了,下一个期望收到的字节号,这个字节号也就是接收方给发送方的报文段的首部中的确认号。
经过讨论,可以强调3点:
1. A的发送窗口是根据B的接收窗口来设置的。
2. TCP对不按序到达的数据会先临时存放在接收窗口中,等到缺少字节收到后,再上交付到应用程序。
3. TCP要求接收方必须有积累确认的功能,这样可以减小传输开销。接收方可以在合适的时候发送确认,也可以在自己有数据要发送时把确认信息顺便捎带上。
3. 超时重传选择
3.1超时重传时间设置
如果超时时间设置的太短,就会引起报文段不必要的重传,增大网络负荷;如果设置的太长,又增加了网络的空闲时间,降低了传输效率。所以超时重传时间设置应该是动态变化的。
RTT(报文段的往返时间): 一个报文段发出的时间,到收到确认的时间。
RTTs(加权平均往返时间或平滑往返时间): 根据RTT计算出来的,超时重传时间。
新的RTTs = (1-α)*(旧的RTTs) + α*(新的RTT样本)。新的RTT即动态变化的重传时间。
RFC2988推荐的α值为1/8。用这种方法得出的加权平均往返时间RTTs就比测量出的RTT值更加平滑。
3.2超时计时器的设置
RTO(超时计时器设置的超时重传时间),应该略大于RTTs。RTO就是真正用来计算超时重传的时间设置。
RTO=RTTs + 4 * RTTd
RTTd是RTT的偏差的加权平均值。当第一次测量时,RTTd值为测量到的RTT值的一半,往后的测量中,RTTd的计算公式如下:
新的RTTd = (1-β) * (旧的RTTd) + β * |RTTs - 新的RTT样本|
β的推荐值是1/4。
3.3Karn算法
重传后,如何判定此确认报文段是对先发送的报文段的确认,还是对后来重传报文段的确认?重传报文段和原来的报文段完全一样,因此源主机在收到确认后无法作出正确的判断,而RTTs的值对正确的判断影响很大。
Karn提出了一个算法:在计算RTTs时,只要报文段重传了,就不采用重传报文段的往返时间样本(即上面公式中新的RTT)。这样得出的RTTs和RTO就比较准确。
但有个问题,就是超时重传时间无法更新。于是修正后的Karn算法是:报文段每重传一次,就把超时重传时间RTO增加大一些。典型做法是取新的重传时间为2倍的旧的重传时间。当不发生重传时,才根据上面RTO的公式计算超时重传时间。
选择确认SACK
在第一章介绍报文段头部选项的时候有介绍SACK的作用:只传送缺少的数据,不重传已经正确到达接收方的数据。
然而SACK文档没有指明发送方应当怎样响应SACK。因此大多数的实现所有未被确认的数据块。