1. 如何实现可靠传输
我们知道,TCP 发送的报文段是交给 IP 层传送的,但 IP 层只提供尽最大努力交付的服务,也就是说,TCP下面的网络提供的是不可靠的传输。因此,TCP必须采取适当的措施才能使得两个运输层实体之间的通信变得可靠。
理想的传输条件有以下两个特点:
- 传输信道不产生差错。
- 不管发送方以多快的速度发送数据,接收方总是来得及处理收到的数据。
然而现实中的网络都不具备上述两个理想条件。但我们可以使用一些可靠传输协议来使得本来不可靠的信道来实现可靠传输。下面介绍最简单的停止等待协议。
在计算机网络发展初期,通信链路不太可靠,因此链路层传送数据都要采用可靠的传输协议。其中最简单的就是这里的停止等待协议。但现在运输层并不使用这个协议,TCP实现可靠传输的原理比这复杂得多。这里的停止等待协议只是为了介绍实现可靠传输的基本原理。
停止等待协议
无差错情况
停止等待协议可以用下图来说明:
A 向 B 发送数据包 M1 ,发送完后就暂停运行,等待 B 收到 M1 后针对 M1 给出一个确认,A 收到 M1 的确认后再恢复运行继续发送 M2 ,依此类推。
出现差错
假设 B 收到 M1 后发现报文出现了差错,或者 M1 在传输的过程中压根就没传到 B ,或者 B 发送的针对 M1 的确认没有传到 A ,这些情况都属于出现了差错,而最终的表现的共同点都是 A 没收到 M1 的确认。而停止等待协议规定发送方发送一个分组后设置一个超时计时器,只要等到计时器到期后还没收到确认,就认定刚才发送的分组出现了差错,于是开始超时重传,即重新发送刚才那个分组。而如果在计时器到期前收到了确认就解除该计时器,然后接着发送后面的分组重复上述步骤。
这里应该注意三点:
- A发送完分组后必须保留该分组的副本直到收到针对该分组的确认,这样才能保证重传。
- 分组必须被编号,这样才能方便区分发送的多个分组。
- 超时计时器设置的超时时间应当比分组传输的平均往返时间长一点,太短的话会频繁触发重传浪费网络资源,太长又会导致通信效率的降低。
确认丢失和确认迟到
考虑两种情况确认丢失和确认迟到。
第一种 B 发送的针对 M1 的确认丢失了,等到 A 的计时器到期后 A 会重传 M1 ,重点关注 B 再次收到 M1 后的动作。B 发现重复收到 M1 后会丢弃重复的 M1 不向上层交付,之后重新发送一个针对 M1 的确认,因为 A 之所以重传 M1 就是因为没有收到针对 M1 的确认。
第二种 B 发送的针对 M1 的确认没有丢失而是堵在了网络中,等待 A 的计时器到期后又重传 M1 ,B 像上面说的丢弃重复的 M1 然后重传确认。但后 A 在收到这个确认后又收到了在网络中绕了好几圈才到的第一次确认,A 收到重复确认后什么也不做。
这两种情况下发送端和接收端的动作都是刻意设计的,可以想到这些动作的合理性。
这个可靠传输协议通常被称为自动重传请求ARQ(Automatic Repeat reQuest)。
总结如何实现可靠传输
总结上面的可靠传输协议,可以发现可靠传输可以用下面三点实现传输不出现差错:
- 发送方为每一个发送的数据包编号
- 接收方收到一个数据包后发送对应的确认
- 发送方为每个发送的数据包设一个计时器,计时器到期未收到确认则重传该数据包。
使用上面的机制,我们就可以在不可靠的传输网络上实现可靠的通信。
信道利用率
从上面的描述可以看出,A 在发送完一个数据段后必须等待接收到确认后才能发送下一个数据段,这很明显对信道的利用率是非常的低的,即双方大部分时间都在等待。
所以为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是采用流水线传输。如下图。流水线传输就是发送方可以连续发送分组而不用一直等待前一个分组的确认,这样就大大的提高了信道利用率。
连续ARQ协议
基于流水线传输,人们设计出了连续 ARQ 协议。所谓连续 ARQ 协议设置了一个发送窗口,所谓的发送窗口就是发送端要发送的完整消息的一个子区间,所有在发送窗口内的数据都可以不用等待确认就发送出去,这样不仅用流水线技术提高了信道利用率,同样也限制了发送端的最大发送速度,防止接收方因为发送速度过快而来不及接收。
滑动窗口是 TCP 实现可靠传输的精髓之一,所以下来开始正式了解 TCP 是如何实现可靠传输的。
2. TCP可靠传输的实现
记住上面讨论的精髓,即实现可靠传输的三个条件:编号、确认、超时重传。
以字节为单位的滑动窗口
首先,TCP 把传输的数据按照字节编号,而 TCP 的滑动窗口表示了全部数据的一个子集。如下图,每个小块都表示一个编号的字节。图中是 A 的发送窗口。
发送窗口介绍
先来讨论 A 的发送窗口。发送窗口表示:在没有收到任何对应的确认之前,A 可以随意把发送窗口内的数据都发送出去。凡是已经发送过但没有收到确认的数据都必须暂时保留,以便超时重传的时候使用。
图中可以看出当前 A 的发送窗口为 20 ,即 A 此时可以无限制的发送 20 个字节给 B ,所以如果发送窗口越大,那么 A 一次就能发送更多的数据给 B ,也就有可能获得更高的数据传输率,但过大的发送窗口可能导致 B 接收不过来,从而频繁重传降低效率。
发送窗口后沿之前的数据表示已发送并受到确认的数据,这部分数据显然不需要继续保留了。而发送窗口前沿之后的数据不允许发送。
可知后沿有两种可能的变化,即不动或者前移。当 A 收到一些数据的确认后,就可以将发送窗口的后沿前移,以表示这些数据已经确认收到了。而后沿不可能后移,因为不能撤销之前已经确认的数据。前沿也有两种可能的变化,不动或者前移。当 A 收到数据的确认后,同时还会将前沿前移,因为在这种情况下我们假设发送窗口的大小是不变的,你收到一些确认了当然就要滑动你的窗口让更多的数据可以发送。但我们后面也会看到,发送窗口是可能动态变化的。即前沿也是有可能收缩的,但 TCP 的标准强烈不建议这样做,因为可能收缩掉的那部分数据已经发送了,而你一收缩就可能产生一些冲突。
发送窗口的实例
如下图所示,A 此时发送了编号为 31 到 41 的所有数据给 B 。
所以这时候发送窗口内又被分为两部分:已发送但未收到确认的部分和允许发送但未发送的部分,可以把允许发送但尚未发送的这部分窗口称为可用窗口。
接收窗口介绍
看完了发送窗口的情况再来看 B 的接收窗口的情况,B 的接收窗口内的序号同样为为 31 到 50 ,31 之前的数据表示已经发送确认,而 50 之后的数据不允许接受。在上图中,B 收到了序号 32 和 33 的数据但没收到序号 31 的数据,这种情况下 B 只能对按序收到的最高序号给出确认。即 B 发送的确认报文中的确认号还是 31 而不是其他值。而对 32 、33 号数据可能暂存也可能直接丢弃,取决于具体的实现。
当 A 把发送窗口内的数据全部发送完并且没有收到任何确认的时候,A 只能等待,而不能对发生的情况做任何臆测。
窗口与缓存
TCP的每一方都有两个缓存,发送缓存与接收缓存。应用程序将要发送的数据写入发送缓存中,而把接收到的数据存到接收缓存供应用程序读取。而所谓的发送缓存和接收缓存其实都是内存中的一块区域而已。如下图所示:
首先来看发送方的情况。
发送缓存用来存放:
- 发送应用程序写入到发送缓存中的未发送数据。
- TCP已发送但尚未收到确认的数据。
- 已发送且已收到确认的数据可以直接从发送缓存中删除。
发送窗口是发送缓存的一部分。已经确认的数据应该从发送缓存中删除,所以发送窗口和发送缓存的后沿重合。而发送缓存前沿和发送窗口前沿之间的数据就是已经写入缓存但还不允许发送的数据。应用程序应该控制写入缓存的速率防止发送缓存被填满。
再来看接收方的情况。
接收缓存用来存放:
- 按序到达的,但未被应用程序读取的数据。
- 未按序到达的数据。
- 按序到达且已被应用程序读取的数据可以从接收缓存中删除。
如果收到的数据段被检测出有差错,则要丢弃。如果接收程序来不及读取收到的数据,接收缓存最终就会被填满,使接收窗口减小到零。反之,如果接收应用程序及时从接收缓存中读取数据,接收窗口就可以增大,但最大不能超过接收缓存的大小。
综上所述,强调三点:
- 第一,发送方的发送窗口会参考接收方的接收窗口的大小来调整,但一定不能超过接收窗口的大小,因为这样可能导致发送出的数据接收方根本就不允许接收。但发送窗口也不总是设置的和接收窗口一样大。这是因为通过网络传送窗口值会有一定时间的滞后,并且发送窗口还根据拥塞控制来调整,拥塞控制后面介绍。
- 第二,对于不按序到达的数据如何处理,TCP并无规定,接收方可以直接丢弃或者是暂存下来,等到缺少的字节收到后,再按序交付上层的应用程序。
- 第三,TCP要求接收方必须有累积确认的功能,即接收方可以自选合适的时候发送确认,也可以在有数据要发送的时候捎带上确认,这样可以减少传输开销。但请注意两点,一是发送方不应过分推迟发送确认,因为可能会导致不必要的重传。TCP标准规定,确认延迟的时间不应超过500ms。若收到一连串具有最大长度的数据段,必须每隔一个数据段就发送一次确认。二是捎带确认实际上很少见,因为很少需要同时在两个方向上发送数据。
总结:TCP何时发送确认
- 收到数据后最多延迟500ms发送确认
- 收到连续的具有最大报文段长度的报文段,必须每隔1个报文端发送一个确认
- 如果可以稍带,则立刻捎带确认
- 重复收到某已经确认的数据,则立刻发送确认
- 收到未按序到达的报文段,立刻确认最高的按序到达的报文段(这种情况在后面的快重传可以看到)
超时重传时间的选择
前面讲到,TCP 的发送方规定发出一个报文后,在规定的时间内没有收到确认则需要重传。这个概念很简单,但重传时间的选择则是 TCP 最复杂的问题之一。
TCP 采用了一种自适应的算法,它记录每一个报文发出的时间,以及收到相应确认的时间。这两个时间之差就是报文段往返时间RTT。TCP保留了RTT的一个加权平均往返时间RTTS(又称为平滑往返时间,S 表示 Smoothed ,因为加权,所以平滑)。每当第一次测量到 RTT 样本的时候,RTTS 就取值为该 RTT 的值。往后每测量到一个新的RTT样本,就按下式重新计算一次RTTS:
new RTTS = (1 - α) * (old RTTS) + α * (new RTT sample)
上式中,0 <= α <= 1。若 α 很接近于0,表示新的 RTTS 与旧值变化不大,收到新 RTT 样本的影响较小。而如果 α 很接近于 1,表示新的 RTTS 可能变化会很大,主要受到新 RTT 样本的影响。TCP 标准建议 α 取值为 1/8。
显然,超时计时器设置的超时重传时间RTO(Retransmission Time-Out)应该略大于上面的 RTTS 。标准建议使用下式计算 RTO :
RTO = RTTS + 4 * RTTD
RTTD 是 RTT 变化量的加权平均值,它与 RTTS 和新的 RTT 之差有关。标准建议这样计算 RTTD 。当第一次测量的时候,RTTD 取值为测量到的 RTT 样本值的一半。在以后的测量中,则使用下式计算加权平均的 RTTD :
new RTTD = (1 - β) * (old RTTD) + β * |RTTS - new RTT sample|
这里的 β 是一个小于1的系数,它的推荐值是 1/4。
上面说的方法还有很多细节问题需要补充。即如果出现了重传怎么办?
发出一个报文段,经过重传时间后还没收到确认,于是重传报文段。经过一段时间后收到了一个确认。那么,如何知道这个确认是针对第一次发送的报文段的确认还是针对第二次发送的报文段的确认。而这个问题其实对于 RTTS 的关系很大。
如果这是针对第二次报文的确认却被当成了第一次,则算出的 RTTS 和 RTO 都会偏大。
如果这是针对第一次报文的确认却被当成了第二次,则算出的 RTTS 和 RTO 都会偏小,这必然会导致报文段被过多的重传,而最终可能导致一个恶性循环。
综上所述,Karn 提出了一个算法:在计算 RTTS 的时候,只要报文段重传了,就不采用其往返时间的样本。但这还有问题,可能链路的时延忽然增大了很多,于是出现了大量的重传报文,这时候按理来说应该适当增大 RTO ,但 Karn 算法却直接丢弃掉了这些重传的报文,这样,RTO 就无法更新。
于是要对 Karn 算法进行修改。方法是:报文段每重传一次,就把重传时间 RTO 增大一些。典型的做法是直接翻一倍。
总之,Karn 算法能够使运输层区分开有效的和无效的往返时间样本,从而改进了往返时间的估测,使计算更加合理。
3. TCP的流量控制
滑动窗口实现流量控制
一般来说,我们总是希望数据传输的更快,但过快的发送数据可能导致来不及接收从而占满接收方的接收缓存。所谓流量控制(flow control)就是让发送发的发送速率不要太快,要让接收方来得及接收。
可以发现滑动窗口的设计恰好就解决流量控制的问题。因为 TCP 中一条很重要的规则就是发送方的发送窗口大小不能超过接收方给出的接收窗口的数值。于是当接收方发现数据传输的过快,上层应用程序积累了很多数据没有接收的时候,就会减小自己的接收窗口,于是发送方发现后也会相应的减小自己的发送窗口的大小(记得 TCP 报文头部中的接收窗口大小吗),于是数据传输的速率就降了下来。当接收方的接收缓存被未处理的数据占满后,就会向发送方发送自己的接收窗口已经为 0 的报文,这样接收方也就会将发送窗口设为 0,从而停止数据传输。这种停止状态会持续到接收方处理了一定的数据后,接收窗口增大,于是向发送方发送的确认里会说自己的接收窗口已经增大了,于是传输重新开始。
现在试想这样一种情况,传输由于接收方发送的零窗口报文而停止,过了一段时间接收方有多余的缓存了,于是重新向发送方发送了一个确认,里面表示自己的窗口已经恢复了。但这个确认报文在传输的过程中丢失了。于是发送方一直在等待接收方发送一个非零窗口的报文,而接收方也一直在等待发送方发送数据。如果没有其他措施,这种互相等待的死锁局面会持续下去。
为了解决这个问题,TCP 为每一个连接设有一个持续计时器(persistence timer)。只要 TCP 的一方收到零窗口通知就启动持续计时器,若计时器到期还没有收到窗口恢复的报文,就发送一个零窗口探测报文(仅携带1字节数据),而对方就在确认这个探测报文段的时候给出了自己的窗口值。如果窗口还为 0 ,则重新设置持续计时器。如果不是 0 ,则死锁局面被打破。
TCP应当何时发送数据
TCP 是面向字节流的协议。如何理解这句话呢?我们知道 Linux 提供了系统调用供应用程序发送数据,在应用程序调用系统调用后,系统调用将数据放入发送缓存中。而这些数据将和之前存放在发送缓存的数据接在一起,构成一个字节流。例如你调用系统调用的时候要发送的是 "Hello"
,但 TCP 只是把这些数据放到发送缓存中,其不保证 "Hello"
是完整的在一个报文段被发出去的,例如接收窗口前移只把 He
包了进来,则可能这次发送的数据只有 He
,而下一个报文段才会发送 llo
。而 UDP 完全不同,其是面向数据报的,即你用系统调用指定要发送的数据后,我这一个数据报一定会把这些数据全部发送出去,不管最后的 UDP 数据报多大或者多小。
前面说滑动窗口的时候,我们说只要是在发送窗口中的数据都可以随意发送,那么 TCP 到底是如何控制发送数据的策略的呢,其有下面三个机制:
- 只要缓存中存储的数据达到 MSS(定义在 TCP 头部)的大小的时候,就组成一个报文段发送出去。
- 发送应用程序指明要立即发送数据,即 PUSH 标志。
- 发送方的一个计时器的期限到了,就把已有的缓存数据装入报文段发送出去。
但如何控制发送数据的时机还是一个复杂的问题。
TCP实现中广泛应用了Nagle算法。算法如下,若发送应用进程把要发送的数据逐个字节的送到 TCP 的发送缓存,则发送方就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来。当发送方收到第一个字节的确认后,再把发送窗口中剩余所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存。只有收到前一个报文段的确认后才继续发送下一个报文段。Nagle算法还规定,当到达的数据已经达到发送窗口大小的一半或者达到一个MSS的长度时,就立即发送一个报文段。这样做,可以有效提高网络吞吐量。
但可以想到,一些交互性较强的应用,Nagle算法可能很不友好,例如游戏中你只按了一下攻击键,而这个数据却被存储在缓存中等待前一个报文段的确认,这就很影响游戏体验。所以可以使用 TCP_NODELAY 选项来禁用 Nagle 算法。
另一个问题叫做糊涂窗口综合征。假设接收方缓存已满,而上层应用程序一次只从缓存中读取一个字节数据,TCP发现接收缓存中出现了1字节的空余空间后,立即给发送方发送一个窗口为 1 的报文,于是发送方又只发送 1 字节数据过来,这样来来去去就会导致网络效率很低。
解决方法就是让接收方等待一段时间,使得接收缓存已有足够空间容纳一个最长的报文段,或者等到接收缓存中有一半的空闲空间,此时再通知发送方自己窗口的变化。
4. TCP的拥塞控制
TCP不仅可以根据接收方的接收速度来动态改变发送速度,从而做到流量控制。还能根据两个端点之间的网络状况来动态的改变发送数据的速度已达到降低网络负荷的目的,可以说真是很神奇了。
什么是拥塞控制
在某段时间内,若对网络中某资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏——产生拥塞(congestion)。
网络拥塞往往是很多因素累积引起的。例如,某个节点的缓存空间太小,导致到达的数据无法排队而不得不丢弃,此时就算你将缓存空间增大但由于处理器的处理速度没有提升,可能排队的数据越来越多,导致这些数据都发生重传,而重传的数据注入网络后又加重了网络的负担,最终形成了恶性循环。所以往往只提升某一个因素的性能还是无法解决拥塞,因为瓶颈转移到了别处。
拥塞控制与流量控制
拥塞控制就是防止过多的数据注入网络,这样可以使网络中的路由器或链路不致过载。拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及降低网络传输性能的所有因素。但处于TCP的两端对于拥塞的视角是,只要迟迟收不到数据或者确认,就能知道网络出现了拥塞,但无法确定是网络中的哪一处出现了拥塞。
而之前介绍的流量控制往往解决的是在给定的发送端和接收端之间的点对点的通信量的控制。流量控制只需要做到抑制发送端的发送速率,以便接收端来得及接收。
TCP拥塞控制方法
TCP进行拥塞控制的算法有四种,即慢开始(slow-start)、拥塞避免(congestion avoidance)、快重传(fast retransmit)和快恢复(fast recovery)。
慢开始
TCP为每个发送方都维持两个窗口:
- 拥塞窗口 cwnd(congestion windows)
- 接收方窗口 rwnd
拥塞窗口的大小取决于网络的拥塞程度,并且动态的在变化。
发送窗口 = Min [ cwnd, rwnd ] 。
- 当 cwnd < rwnd 的时候,是网络拥塞限制发送窗口的值。
- 当 rwnd < cwnd 的时候,是接收方的接受能力限制发送窗口的值。
发送方控制拥塞窗口变化的流程是:只要网络没有出现拥塞,拥塞窗口就可以再增大一点,以便增大发送窗口的值使得更多的报文段可以被发送出去。但只要网络出现拥塞或者可能出现拥塞,就必须减小拥塞窗口的大小,从而减小发送窗口的值使得注入网络的流量减少,以缓解网络出现的拥塞。
发送方又如何知道网络出现了拥塞呢?由于现代通信链路的传输质量一般都很好,所以暂且可以认为只要出现了超时就一定是通信链路出现了堵塞,而不是数据传输过程中出现了差错。因此,判断网络拥塞的依据就是出现了超时。
下面介绍拥塞窗口 cwnd 的大小是如何变化的。从慢开始算法开始。
慢开始算法的思路是这样的,在主机刚刚开始发送报文段时可先设置拥塞窗口 cwnd = 1(注意,拥塞控制中的单位全部是一个 MSS ,即先设置拥塞窗口的大小为 1 个 MSS 的大小)。
在每收到一个报文段的确认后,就将拥塞窗口加1,即增加一个 MSS 。即下图所示:
一开始发送方的 cwnd = 1,所以只发送了一个数据段 M1 ,接收方确认后 cwnd 增加 1 变成了 2 ,所以此时发送方一次性可以发送 M2 和 M3 ,接收方确认了 M2 和 M3 后发送方的 cwnd 又增加 2 ,变成了 4 ,所以此时可以一次性发送 4 个报文段。所以可以看出,每经过一个传输轮次,发送方 cwnd 就翻一倍。
这里引入了一个名词,传输轮次。可以理解为传输轮次就是一次性把发送窗口中所有数据发送出去并最终收到所有对应的确认所经历的时间,理想情况下近似为一个 RTT。
注意这里的慢开始中的慢并不是指 cwnd 的增长速度慢,事实上 cwnd 在这种情况下几乎是指数级增长。而是指TCP启动后先设置 cwnd 等于1,使得发送方一开始只能发送一个报文。与之对比的是一开始就设置一个大 cwnd 值导致一下子把许多报文段注入网络。
顺便指出,图中的情况是理想条件下的场景。事实上现实中很难出现多个报文段刚好一起发出又刚好一起收到对应的确认,所以就很难看见 cwnd 刚好一倍一倍的增长的场景。但只要记住基本原理:只要收到一个报文段的确认,就将拥塞窗口加1。
拥塞避免
为了防止拥塞窗口 cwnd 增长过快引起网络拥塞,还需要设置一个慢开始门限 ssthresh。
- 当 cwnd < ssthresh 时,使用上述慢开始算法。
- 当 cwnd > ssthresh 时,转而使用拥塞避免算法。
拥塞避免算法的思路是让拥塞窗口缓慢的增大而不是指数级增大,这样可以防止发送窗口增大的过快导致链路出现拥塞。即每经过一个 RTT(或者传输轮次)后就把发送方拥塞窗口的值加1,而不是像慢开始一样成倍增加。因此拥塞避免阶段有加法增大的特点。即按照线性规律增长。
出现拥塞
而不管是慢开始阶段还是拥塞避免阶段,只要发送方判断网络出现了拥塞(即出现重传),就要把慢开始门限 ssthresh 设置为出现拥塞时的拥塞窗口的一半。关于为什么是拥塞窗口的一半而不是原来 ssthresh 的一半,可以考虑当一段时间网络通信质量很差,ssthresh 降低到一个很低的水平,但后来网络恢复了,如果再次出现拥塞时还是设置成 ssthresh 的一半的话,ssthresh 就永远无法增大了。
之后把拥塞窗口 cwnd 重新设置为1,执行慢开始算法。
这样做的目的是迅速减小主机发送到网络中的分组数,使得发生拥塞的网络节点有足够的时间恢复。
接下来看一个慢开始和拥塞避免的实例:
图中的点 1 之前执行慢开始算法,点1的时候 cwnd 等于ssthresh 的值,于是转而执行拥塞避免算法。点 2 表示出现了超时重传,于是 ssthresh 设为当前拥塞窗口的一半即 12 ,然后将拥塞窗口置为 1 重启慢开始算法,点 3 转而执行拥塞避免算法。
不同的是点 4 处发生的事,此时发送方一连收到了 3 个对同一报文段的重复确认(图中标记为3-ACK)。对这个问题有如下的讨论:
快重传
有时,个别报文段在网络中丢失,但实际上网络并未出现拥塞,其表现为这个报文段之后的好几个报文段都顺利送达了,这说明可能网络并未拥塞,可能只是这一个报文段出现了某些问题。所以在这种情况下,我们不希望错误的判断成网络出现拥塞,从而导致重新执行慢开始算法。
那怎么办呢?TCP 规定收到未按序到达的报文段后要立刻再次确认按序到达的最大字节,所以如果出现了上面的情况,某个报文段在网络中丢失,但之后的三个报文段都正确的到达接收方,则按照规定接收方会连续确认三次按序到达的最高字节。而发送方发现连续三个对同一报文段的确认后,就知道此时出现了单个报文段的丢失。
如图所示,M3 在传输的过程中丢失了,接收方连续收到了三个不按序到达的报文段,于是连续三次发送了重复的确认 M2 给发送方。发送方收到 3 个连续的对 M2 的重复确认后,就知道接收方确实没收到 M3 ,则此时立刻重传 M3 ,这样就可以防止接收方的计时器超时从而导致再次慢开始。而且此时还需要再执行快恢复算法:
快恢复
再看这个图,上图中的点4,发送方知道现在只是丢失了个别报文段。于是不启动慢开始算法,而是执行快恢复算法。这时,发送方的 ssthresh 设置为拥塞窗口的一半,同时设置拥塞窗口也等于自己的一半即等于新的 ssthresh,于是立即执行拥塞控制算法。
总结
下图总结了TCP的拥塞控制可能出现的所有情况。
主动队列管理AQM
上一节的四种拥塞控制方法主要是TCP端点所采取的策略,但并没有介绍网络层设备可能会对拥塞所采取的措施,事实上它们之间有着密切的联系。
例如一个路由器对某些分组处理的时候耗费了过长的时间,那么就可能导致这些分组中的 TCP 报文过了很久才到达目的地,就可能导致发送方的重传计时器过期从而使发送方误以为网络出现了拥塞从而执行慢开始算法。
网络层中对 TCP 拥塞控制影响最大的就是路由器的分组丢弃策略。最简单的情况下,路由器的排队队列通常都是按照 FIFO 先进先出的策略进行排队的,如果这个路由器收到太多的分组,就可能导致后面到达的分组无法排队从而被丢弃。这就叫做尾部丢弃策略(tail-drop policy)。
路由器的尾部丢弃策略可能导致一连串的分组被丢弃,如果这些分组大部分都属于同一个TCP连接的话,可能发送方会多次启用慢开始算法,从而使数据发送速率降低到一个很小的水平,更严重的是,网络中可能同时存在很多条 TCP 连接,如果这些 TCP 的很多分组都被路由器丢弃了,就可能会影响到多条 TCP ,结果使得许多 TCP 连接在同一时间突然都进入到慢开始状态。这在 TCP 的术语中称为全局同步(global syncronization)。全局同步使得全网的通信量忽然下降了很多,而在网络恢复之后,其通信量又忽然增大了很多。
为了避免全局同步的出现,1998 年提出了主动队列管理(Active Queue Management)。所谓主动就是不要等待排队队列已经填满了才开始丢弃到来的分组,这样就太被动了,应当在队列长度到达了某个值得警惕的数值的时候就开始主动丢弃到来的分组。这样可以提醒那些发送方网络出现了拥塞,应当降低发送速率。AQM 有多种实现方法,其中流行多年的就叫做随机早期检测RED(Random Early Detection)。
实现 RED 时路由器需要维护两个参数,即最小丢弃门限值和最大丢弃门限值。当每个分组到达的时候,RED 就按照规定的算法计算当前的队列长度。
- 如果当前队列长度小于最小丢弃门限值的时候,就把新到的分组放入队列进行排队。
- 如果当前队列长度大于最大丢弃门限值的时候,直接把新到的分组丢弃。
- 如果当前队列长度大于最小丢弃门限值小于最大丢弃门限值的时候,就按照某一丢弃概率 p 把新到的分组丢弃(即分组丢弃具有随机性)。p 可以不是一个常数,而是一个随队列长度增大而增大的数。
由此可见,RED不是等到队列实在不能再容纳新的分组后才开始丢弃的,而是在检测到网络拥塞的早期征兆时就开始以概率 p 开始丢弃的。从而避免全局性拥塞控制的发生。
但经过多年的实践证明,RED 的使用效果并不理想。因此2015年后标准开始不建议使用RED,不过替代品都还在试验阶段,暂时还没有比较完美的替代品出现。