TCP实现可靠传输的实现
目录
- TCP实现可靠传输的实现
- ARQ协议
- 停止等待协议(古老)
- 连续ARQ协议
- 累计重传(回退N帧的ARQ协议)
- 缓存确认(选择重传ARQ协议)
- 超时重传的时间选择
- TCP的流量控制
- 零窗口探测报文段
- Nagle算法
- TCP的拥塞控制
- TCP的拥塞控制方法
- 路由器主动队列管理AQM
- 参考
ARQ协议
为了方便表示我们在本小节中将A叫做发送方,而B叫做接收方。
停止等待协议(古老)
停止等待
就是每发送完一个分组就停止发送,等待对方的确认。在收到确认后再发送下一个分组。这其实就有点类似与半双工了。
停止等待协议的工作原理可以通过下图了解。
理解停止等待协议只要根据上图看图说话就行。在无错误状态下A发送消息后在收到B返回的确认消息后再发出下一条消息。上图中的(b)则说明了若出现发送方报文出错重传的机制。下图展现了另外两种可能出现重传的情况。
有一些细节需要注意:
- A在发送完一个分组后,必须暂时保留已发送的分组的副本
- 分组和确认分组都必须进行编号。这样才能明确时哪一个发送出去的分组收到了确认,而哪一个分组还没有收到确认。比如出现已失效的连接请求报文段
- 超时计时器设置的重传时间应当比数据在分组传输的平均往返时间更长一些。需要考虑到网络中出现的时延和拥塞问题。
停止等待协议的优点是比较简单,但是信道利用率低。
根据上图我们,此时信道的利用率U可以粗略地通过下式来计算:
U
=
T
D
T
D
+
R
T
T
+
T
A
U = \frac{T_D}{T_D + RTT + T_A}
U=TD+RTT+TATD
连续ARQ协议
通过前面的介绍,我们可以知道停止等待协议的传输效率太低。我们可以想到用下图这种流水线的传输方式,显然这样可以提高信道的利用率。但是这也会带来新的问题,可以可以通过滑动窗口协议搭配不同的确认方式来解决
累计重传(回退N帧的ARQ协议)
滑动窗口协议限定了一次性发送数据的条数,同时限定了如何发送下一组请求。我们搭配着上面这副图来理解累计重传的原理。
例如,发送方一次性发送了5个分组,但是第二个分组丢失了。这个 时候接收方只能对第一个分组进行确认。发送方无法确认后面四个分组中还有哪个是传到了的,于是只好将后后面的四个分组全部重传。这也就出现了上图中滑动窗口向右滑动一个序号的原因,这就叫做Go-back-N(回退N)。
采用累计确认的方式时,接收方不必对收到的分组逐个发送确认,而是在收到几个分组后,对按序到达的最后一个分组发送确认
通过这种多个重传的案例,可以清楚地知道连续ARQ在通信线路质量不好时带来的负面影响。
缓存确认(选择重传ARQ协议)
我看别的很多都称这种方式为选择重传,但感觉根据自己的理解这个叫做缓存确认更容易理解。通过上图我们可以看到,在接收方可发送方分别需要维护一个发送缓存和一个接收缓存。
同样的我们对照着下图,来理解缓存确认的运作原理。例如,A已经发送了34-41分组中的数据,但是其中37、38、40都没有按照顺序被B收到。对于这三个未按序到达的数据而言可能是提早到、晚到、没传到。我举以下两个例子:
① 37、38、40 都提前到了
在数据提前到的时候,这些数据会先被B存放在自己的接收缓存中。当收到了36号数据后37、38依次接上并分别发送确认报文。40则在收到39后再接入。(如果过程中出现超时,也会触发A的重传)
②37没传到 38、40都提前到了
这个时候接收方最高发送的确认报文是36。而37号报文由于超时,A重新发送37这部分的数据,同时滑动窗口也会往后面移动,并发送42、43、44这三个序号的数据。
这里其实每次A收到确认号落在发送窗口内(考虑到“已失效的连接请求报文段”),那么A就可以使滑动窗口继续向前移动。
发送缓存用来暂时存放:
- 发送应用程序传送给发送方TCP准备发送的数据
- TCP已发出但尚未收到确认的数据
接收缓存用来暂时存放:
- 按序到达的、但尚未被接受应用程序读取的数据
- 未按序到达的数据
根据前面的了解,我们需要注意以下三点:
- 虽然A的发送窗口是根据B的接收窗口设置的,但再同一时刻,A的发送窗口并不总是和B的接收窗口一样大(可以小于接收窗口)。
- 对于不按序到达的数据如何处理,TCP标准并无明确规定。如果接收方将不按序到达的数据一律丢弃,这样对网络资源的利用不利。因此TCP通常对不按序到达的数据先临时存放再接收窗口中,等到字节流中缺少字节收到后,再按序交付上层的应用程序。
- TCP要求接收方必须有累计确认的功能,这样可以减小传输开销。接收方可以在合适的时候发送确认,也可以在自己有数据要发送的时候把确认信息顺便捎带上。但TCP规定,确认的推迟时间不应超过0.5秒。
超时重传的时间选择
TCP采用了一种自适应算法,它记录了一个报文段发出的时间,以及收到相应的确认的时间(可以去看TCP尾部的选项字段)。这两个时间之差就是报文段的往返时间RTT。TCP保留了一个RTT的一个加权平均往返时间RTTs(也叫平滑的往返时间)。每当第一次测量到RTT样本值。但以后每测量到一个新的RTT样本,就按下式重新计算依次RTTs:
新的
R
T
T
s
=
(
1
−
α
)
×
(
旧的
R
T
T
s
)
+
α
×
(
新的
R
T
T
样本
)
新的RTTs = (1 - \alpha) \times (旧的RTTs) + \alpha \times (新的RTT样本)
新的RTTs=(1−α)×(旧的RTTs)+α×(新的RTT样本)
一般认为RTT样本值对新的RTTs的值影响较大,RFC 3298 推荐的
α
\alpha
α值为0.125。**超时重传时间 RTO(Retransmission Time-Out)**应略大于上面得到的平滑RTTs。RFC 6298推荐使用下式计算RTO:
R
T
O
=
R
T
T
s
+
4
×
R
T
T
D
RTO = RTTs + 4 \times RTT_D
RTO=RTTs+4×RTTD
而
R
T
T
D
RTT_D
RTTD是RTT的偏差的加权平均值,它与RTTs和新的RTT样本之差有关。
新的
R
T
T
D
=
(
1
−
β
)
×
(
旧的
R
T
T
D
)
+
β
×
∣
R
T
T
s
−
新的
R
T
T
样本
∣
新的RTT_D = (1 - \beta) \times (旧的RTT_D) + \beta \times|RTTs - 新的RTT样本|
新的RTTD=(1−β)×(旧的RTTD)+β×∣RTTs−新的RTT样本∣
这里的
β
\beta
β是一个小于1的系数,它的推荐值是0.25.
通过上面的公式我们就可以计算出重传时间RTO。但是我们在此时要考虑到超时重传时出现时:**如何判定此报文段时对先发送的报文段的确认,还是对后来重传报文段的确认?**由于重传的报文段和原来的报文段完全一样,因源主机在收到确认后,就无法做出正确的判断,而正确的判断对确定平均RTTs 的值关系很大,影响关系可以通过下图看出。
于是又了Karn算法:在计算加权平均RTTS时,只要报文段重传了,就不采用其往返时间样本。这样就得出了加权平均RTTS和RTO就较准确。
但是若出现:报文段的时延突然增大很多。若不考虑重传的报文段重传时间,继续按照原来的RTO。将会导致出现大量的重传。
于是有了改进,方法是:报文段每重传依次,就把超时重传时间RTO增大一些(经典的做法是2倍)。当不再发生报文段的重传时,才根据RTO的计算公式计算超时重传。
TCP的流量控制
所谓的流量控制就是让发送方的速率不要太快,要让接收方来得及接收。(注意:流量控制是一种接收方对发送方来讲的机制,是一个端到端的问题)
利用滑动窗口机制可以比较方便地在TCP连接上实现对发送方的流量控制。
现在我们假定数据流向是单向的,也就是说发送方A只负责发送数据,不负责处理和接受数据,接收方B只负责处理和接收数据,并且对接收到的数据进行应答(ACK)。
在A和B建立连接的时候,B将自己的接收窗口RWND(receiver window)设置为400字节。这时候A将自己的发送窗口也设置为400,如下图所示。A给B连续发送了3个TCP报文段,每个报文段都包含100字节的数据,其中第三个TCP报文段因某种网络原因丢失。同时A收到了来自B的应答报文段,B调整接收窗口rwnd = 300。此时发送窗口应该是在301-500.于是A就继续发送301-400,401-500这两段报文。这时也触发了A201-300这段报文的超时重传。随后收到了对401-500这段报文的确认(这个时候说明前五段报文都已经被B收到了),并设置rwnd = 100。于是A根据rwnd再次调节发送窗口为100字节,此时发送窗口移动到了501-600的位置。在收到501-600这段报文后,根据rwnd = 0将发送窗口设置为0。
此时A需要等待B发送的新的接收窗口大小(rwnd > 0)才能继续发送数据。于是B向A发送更新自己rwnd的报文,但是意外出现了,这个更新报文段丢失,此时将会导致A在等待B的rwnd,B在等待A的新数据,于是出现了死锁局面。
零窗口探测报文段
为了防止这种意外的发生,我们设置A在收到B的0接收窗口大小(rwnd=0)时会自动启动一个“持续计时器”,当持续计时器timeout时,如果还没有收到来自B的rwnd更新报文段,则会发送一个零窗口探测报文段(携带1字节数据),当B接收到这个TCP报文段后,会给A回复一个rwnd的更新,如果此时rwnd > 0,那么A就可以继续发送数据,如果rwnd=0,则重新启动一个持续计时器,重复上述步骤。
这也出现了另外一个问题糊涂窗口综合征
:当B对应的交互式应用进程每次仅仅接收缓存中的1字节数据(这样使接收缓存空间仅腾出1字节),如果这个时候刚好收到了零窗口探测报文段,就会将rwnd设置为1,这样发送方就只能发送来1个字节的数据.这样进行下去,使网络的效率很低。
解决这个问题的方案是:
让接受方等待一段时间,使得接收缓存已有足够空间容纳一个最长报文段MSS,或者等到接收缓存已有一般空闲空间。只要出现两种情况之一,接收方就发出确认报文,并向发送方通知当前的窗口大小。
Nagle算法
为理解Nagle算法 ,我们可以设想以下场景:TCP的发送应用进程把要发送的数据逐字节地送到TCP发送缓存。这个时候为了提高网络的吞吐量,Nagle算法规定:
若应用进程把要发送的数据逐个字节地送到TCP的发送缓存,则发送方就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来。当发送方收到对第一个数据字符的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存。Nagle算法还规定,当已到达发送缓存的数据已达到发送窗口的一半或已到达报文段的最大长度时,就立即发送一个报文段。
TCP的拥塞控制
在某段时间内,如果网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫做拥塞。可以把出现网络拥塞的条件写成,如下式子:
∑
对资源的需求
>
可用资源
\sum 对资源的需求 > 可用资源
∑对资源的需求>可用资源
拥塞控制就是防止过多的数据注入到网络中,这样可用使网络中的路由器或链路不至于过载。拥塞控制是一个全局性的过程。
- 提供的负载 代表单位时间内输入给网络的分组数目。
- 吞吐量 代表单位时间内从网络输出的分组数目
TCP的拥塞控制方法
TCP进行拥塞控制的算法有四种:慢开始、拥塞避免、快重传和快恢复。
下面考虑的拥塞窗口是基于滑动窗口协议的。发送发维护了一个**拥塞窗口cwnd(congestion window)**的状态变量。同时认为发送方超时计时器启动时,就判断网络出现拥塞。因为传输出差错而丢失分组的可能性比较小。
慢开始算法
慢开始就是由小到达逐渐增大注入网络中的数据字节,或者说是从小到大逐渐增大拥塞窗口数值。(RFC5861规定初始的cwnd设置为不超过2至4个)。
慢开始规定,在每收到一个对新报文段的确认后,可用把拥塞窗口增加最多一个SMSS的数值。为了避免cwnd过大,还需要设置一个慢开始门限 ssthresh:
- 当cwnd < ssthresh时,使用慢开始算法
- 当cwnd > ssthresh时,使用拥塞避免算法
- 当cwnd = ssthresh时,既可以使用慢开始算法,也可使用拥塞避免算法。
拥塞避免算法
拥塞避免算法就是让cwnd缓慢增大。执行算法的过程大概是这样的:每经过一个往返时间RTT,发送方的拥塞窗口cwnd的大小就增加1。拥塞避免
并非完全避免拥塞,而是让拥塞窗口增长得缓慢一些。
快重传算法
在发现有分组丢失,认为网络出现了拥塞。如上图中的2号点。于是设置ssthresh = cwnd / 2、cwnd = 1。此时进入慢开始。在快重传算法中,接收方需要对发送方的数据立即做出确认,即使是失序且已接受的报文段也要做重复确认。如果发送方收到连续多条重复确认,发送方知道了只是丢失了个别报文段,于是开启快恢复。
快恢复算法
使ssthresh = cwnd / 2,同时设置cwnd = ssthresh,并开始执行拥塞避免算法。
路由器主动队列管理AQM
前面讨论的TCP拥塞控制并没有和网络层采取的策略联系起来。网络层的策略对TCP影响最大的就是路由器的分组丢弃策略。路由器会维护一个先进先出的队列,当队列已满,后续排队的分组都将被丢弃。这就叫做尾部丢弃策略。
主动队列管理AQM(active Queue Management) 可用由不同的实现方法。其中**随机早期检验RED(Random Early Edtection)**是其中比较流行的。使用RED需要路由器维护两个参数:最小门限和最大门限。当每一个分组到达的时,RED就按照规定的算法先计算当前的平均队列的长度。并按照一下三种情况处理新的分组:
- 平均队列的长度 < 最小门限,则把新到的分组放入队列进行排队
- 平均队列的长度 > 最大门限,则把新到达的分组丢弃
- 最小门限 < 平均队列的长度 < 最大门限,按照一定的概率p,把新到达的分组丢弃(体现了分组丢弃的随机性)。
RED随机丢弃分组(对应第三种情况),是因为检测到网络拥塞的早期征兆。
参考
- 《计算机网络 第7版》
- 《计算机网络 第8版》
- TCP的流量控制