本文参考自定向下一书第三章整理而来。
在网络世界中最重要的问题之一就是可靠传输,而运输层的TCP协议为上层实体提供的服务抽象就是: 数据可以通过一条可靠的信道进行传输。借助于可靠信道,传输数据比特就不会受到损坏或者丢失,而且所有数据都是按照其发送顺序进行交付。这也正是TCP向调用它的因特网所提供的服务模型。
TCP 是在不可靠的网络层之上实现的可靠数据传输协议,所以这里的关键在于如何去设计这样一个可靠数据传输协议呢 ?
基于上面重传机制实现的可靠数据传输协议也被成为自动重传请求(ARQ)协议
方案二中,发送方在发送完一个分组后,必须等待直到接收到ACK或NAK响应后,才能继续从上层获取更多的数据进行发送,因此该协议也被成为停止等待协议。
方案二最大的缺陷在于没有考虑ACK和NAK分组受损的可能性,我们可以为ACK或NAK分组也添加校验和进行差错检测,但是我们无法知道接收方是否正确接收了上一块发送的数据。
解决这个问题或许有三个思考方向:
解决这个问题的一个简单办法就是引入序列号机制,即为所有数据分组添加一个新的seq字段,用于表示当前分组的序号;在方案二中,当接收方接收到一个受损分组后,会响应一个NAK分组,但是当我们引入了序列号机制时,则可以通过对上次正确接收到的分组发送一个ACK,达成和NAK一样的效果;当发送方接收到对同一个分组的两个ACK (即接收到冗余ACK)时,就知道接收方没有正确接收到跟在被确认两次的分组后面的分组了。
基于序列号机制,我们便轻松实现了一个无NAK的可靠数据传输协议。
方案二假设底层信道不会产生分组丢失问题
检测丢包最直接的想法就是引入超时重传机制;站在发送方的角度来看,由于发送方不知道是一个分组丢失,还是一个ACK丢失,或者只是该分组或ACK过度延时。在所有这些情况下,都可以采用超时重传来解决。
为了实现超时重传,我们需要引入一个重传计时器,发送方需要做以下几件事情:
引入了超时重传机制后,我们来看一下当前协议在各种情况下的工作流程:
方案三中最主要的一点就是超时时间的设置,但是不管怎样,我们已经可以利用检验和,序号,定时器,ACK机制来实现一个可靠的数据传输协议了。
方案三所设计的停止等待协议最大问题就在于无法充分利用底层网络带宽,解决这个问题的办法就是采用流水线传输技术,如下图所示:
采用流水线传输技术后,我们需要针对方案三设计的停止等待协议进行如下改造:
在回退N步(GBN)协议中,允许发送方发送多个分组,而无需等待确认,但它也受限于流水线中未确认的分组数不能超过某个最大允许数N。
发送方会在本端维护一个滑动窗口,大小为N,同时在发送端看到的发送缓存区的数据被分为了以下四部分:
随着协议的运行,该窗口在序号空间向前滑动。因此,N通常被称为窗口长度,GBN 协议也常被称为滑动窗口协议。
对于GBN协议来说,他必须处理以下三类事件:
GBN 协议采用累积确认方式,即如果接收方收到了一个分组序号为n,并且该分组按序到达,那么接收方会为分组n发送一个ACK,并将该分组中的数据全部交付给上层。如果分组未按序到达,那么接收方会丢弃该分组,并为最近按序接收的分组重新发送ACK。注意到因为一次交付给上层一个分组,如果分组k已接收并交付,则所有序号比k小的分组也以及交付。因此,使用累积确认是GBN一个自然的选择。
在GBN协议中,接收方会丢弃所有失序分组,又因为接收方必须按序将数据交付给上层。假定现在期望接收分组n,而分组n+1却到了。因为数据必须按序交付,接收方可能缓存(保存)分组n+1,然后,在它收到并交付分组n后,再将该分组交付到上层。然而,如果分组n丢失,则该分组及分组n+1最终将在发送方根据GBN重传规则而被重传。
因此,接收方只需丢弃分组n+1即可。这种方法的优点是接收缓存简单,即接收方不需要缓存任何失序分组,从而接收缓冲区只需要维护一个nextSeq表示下一个按序接收的分组的序号即可。
当然,丢弃一个正确接收的分组的缺点是随后对该分组的重传也许会丢失或出错,因此甚至需要更多的重传。
上图给出了窗口长度为4个分组的GBN协议的运行情况。因为该窗口长度的限制,发送方发送分组0~3,然后在继续发送之前,必须等待直到一个或多个分组被确认。当接收到每一个连续的ACK(例如ACK0和ACK1)时, 该窗口便向前滑动,发送方便可以发送新的分组(分别是分组4和分组5)。在接收方存在分组2丢失时,分组3、4和5被发现是失序分组也会被丢弃。
搞懂GBN协议关键要弄明白按序到达时什么意思
GBN协议允许发送方用多个分组“填充流水线”,因此避免了停等协议中所提到的信道利用率问题。然而,GBN本身也有一些情况存在着性能问题。尤其是当窗口长度和带宽时延积都很大时,在流水线中会有很多分组更是如此。单个分组的差错就能够引起GBN重传大量分组,许多分组根本没有必要重传。随着信道差错率的增加,流水线可能会被这些不必要重传的分组所充斥。
选择重传(SR)协议通过让发送方仅重传那些它怀疑在接收方出错(即丢失或受损)的分组而避免了不必要的重传。这种个别的、按需的重传要求接收方逐个地确认正确接收的分组。再次用窗口长度N来限制流水线中未完成、未被确认的分组数。然而,与GBN不同的是,发送方已经收到了对窗口中某些分组的ACK。
SR 接收方将确认一个正确接收的分组而不管其是否按序到达。失序的分组将被缓存直到所有丢失分组(即序号更小的分组)皆被收到为止,这时才可以将一批分组按序交付给上层。
下面我们先来看看SR发送方需要处理的事件有哪些:
再来看看SR接收方需要处理的事件有哪些:
(rev_base)
,则该分组以及以前缓存的序号连续的(起始于rev_base的)
分组交付给上层。然后,接收窗口按向前移动分组的编号向上交付这些分组。[rev_base - N,rev_base - 1]
内的分组被正确收到。在此情况下,必须产生一个ACK,即使该分组是接收方以前已确认过的分组。这里有一点很重要,就是接收方需要重新确认(而不是忽略)已收到过的那些序号小于当前窗口基序号的分组,这里可能是因为ack丢失,导致发送方进行产生了重发,所以需要回应一下。
经过了上述讨论后,我们来总结一下确保可靠数据传输的机制和相关用途:
上述方案假设过程中我们都默认分组在发送方和接收方之间的信道中不会被重新排序,但是实际网络运行过程中,分组重新排序是可能发生的。
分组重新排序的一个表现就是,一个具有序号或确认号x的分组的旧副本可能会出现,即使发送方或接收方的窗口中都没有包含x。对于分组重新排序,信道可被看成基本上是在缓存分组,并在将来任意时刻自然地释放出这些分组。由于序号可以被重新使用,那么必须小心,以免出现这样的冗余分组。实际应用中采用的方法是,确保一个序号不被重新使用,直到发送方“确信”任何先前发送的序号为x的分组都不再在网络中为止。通过假定一个分组在网络中的“存活”时间不会超过某个固定最大时间量来做到这一点。在高速网络的TCP扩展中,最长的分组寿命被假定为大约3分钟。Sunshine 1978 中描述了一种使用序号的方法,它能够完全避免重新排序问题,感兴趣可以自行去了解一下。
TCP是因特网运输层的面向连接的可靠的运输协议,其主要使用包括差错检测,重传,累积确认,定时器以及序号和确认号的首部字段实现可靠传输协议。
TCP 协议是面向连接的协议,连接建立前,需要先经历三次握手,断开时,需要经历四次挥手。TCP 连接的组成包括: 一台主机上的缓存,变量和与进程连接的套接字,以及另一台主机上的另一组缓存,变量和与进程连接的套接字。这两台主机之间的网络元素(路由器,交换机和中继器)中,没有为该连接分配任何缓存和变量。
这里的"连接"是一条逻辑连接,其共同状态仅保留在两个通信端系统的TCP程序中,中间的路由器对TCP连接完全视而不见,它们看到的是数据报,而不是连接。
其次,TCP 连接总是点对点的,不支持多播,发起连接的进程被称为客户端进程,而另一个进程被称为服务器进程。连接的建立由客户端发送一个特殊的TCP报文段开启,服务器用另一个特殊的TCP报文段来响应。最后,客户端再用第三个特殊报文段作为响应。前两个报文段不承载有效载荷,也就是不包含应用层数据;而第三个报文段可以承载有效载荷,由于这两台主机之间发送了三个报文段,所以这种连接建立过程通常被称为三次握手。
一旦建立起了一条TCP连接,两个应用进程之间就可以相互发送数据了。TCP会在两端都维护一个发送缓存和接收缓存,TCP会在方便的时候,从发送缓存中取出最多MSS(最大报文段长度)大小的数据。
- MSS通常根据最初确定的由本地发送主机发送的最大链路层帧长度(即所谓的
最大传输单元(Maximum Transmission Unit,MTU)
)来设置。- 设置该MSS要保证一个TCP 报文段(当封装在一个IP数据报中)加上TCP/IP首部长度(通常40字节)将适合单个链路层帧。
- 以太网和PPP链路层协议都具有1500字节的MTU,因此MSS的典型值为1460字节。已经提出了多种发现路径MTU的方法,并基于路径MTU值设置MSS
(路径 MTU是指能在从源到目的地的所有链路上发送的最大链路层帧[RFC 1191])
。注意到MSS是指在报文段里应用层数据的最大长度,而不是指包括首部的TCP报文段的最大长度
。
TCP为每块客户数据配上一个TCP首部,从而形成多个TCP报文段(TCP segment)。这些报文段被下传给网络层,网络层将其分别封装在网络层IP数据报中。然后这些IP数据报被发送到网络中。当TCP在另一端接收到一个报文段后,该报文段的数据就被放入该TCP连接的接收缓存中。
由于TCP协议已经遵循MSS在传输层进行了分段处理,所以网络层就无需通过IP协议再次分片和重组了,与之相对的就是UDP协议还需要借助IP层进行分片处理:
- Linux 1.2.13 – IP分片重组源码分析
tcp 报文段结构如下所示:
下面针对其中部分重要字段展开说明:
由于TCP选项字段的原因,TCP首部的长度是可变的。(通常,选项字段为空,所以TCP首部的典型长度是20字节。)
(在实践中, PSH、 URG和紧急数据指针并没有使用。为了完整性起见,我们才提到这些字段。)
TCP把数据看成一个无结构的、有序的字节流。我们从TCP对序号的使用上可以看出这一点,因为序号是建立在传送的字节流之上,而不是建立在传送的报文段的序列之上。一个报文段的序号因此是该报文段首字节的字节流编号。
TCP是全双工的,因此主机A在向主机B发送数据的同时,也许也接收来自主机B的数据(都是同一条TCP连接的一部分)。从主机 B 到达的每个报文段中都有一个序号用于表示从 B 流向 A 的数据。主机A 填充进报文段的确认号是主机 A 期望从主机 B 收到的下一字节的序号。
TCP协议采用的是累积确认法,为了证明这一点,我们来看下面这个例子:
当我们引入了序列号和确认号后,我们还可以思考一下下面这个问题:
TCP协议中关于初始序列号的选择也是有讲究的:
关于TCP协议中的确认号,我们也需要注意下面这点:
下面我们再来看看TCP中隐式NAK的实现:
(在TCP的快速重传机制下,收到对一个特定报文段的3个冗余ACK就可作为对后面报文段的一个隐式NAK,从而在超时之前触发对该报文段的重传)
。最后我们再来看看关于TCP中流水线的说明:
由于运输层报文段是被IP数据报携带着在网络中传输的,并且网络层服务是不可靠的,所以需要像TCP这样的可靠传输协议来提供一种可靠数据传输服务。
本节我们来看看TCP是如何使用我们上一节讲到的可靠传输技术来实现可靠的数据传输协议的。
首先是TCP发送方,其有3个与发送和重传有关的主要事件:
这里注意:
- 为每一个已经发送但未被确认的报文段都与一个定时器相关联,概念上是最简单的,但是定时器的管理却需要相当大的开销。因此,一般仅使用单一的重传定时器。
- 我们这里可以将定时器想象为与最早的未被确认的报文段相关联。
// 假设发送方不受TCP流量和拥塞控制的限制,来自上层数据的长度小于MSS,且数据传送只在一个方向进行
NextSeqNum = InitialSeqNumber
SendBase = InitialSeqNumbder
loop (永远) {
switch (事件)
事件: 从上面应用程序接收到数据
生成具有序号NextSeqNum的TCP报文段
if (定时器当前没有运行)
启动定时器
向IP传递报文段
NextSeqNum = NextSeqNum + len(data)
break;
事件: 定时器超时
重传具有最小序号但仍未应答的报文段
启动定时器
break;
事件: 收到ACK,具有ACK字段值y
if(y > SendBase){
SendBase=y;
if (当前存在未应答报文段)
启动定时器
}
break;
}
关于TCP的可靠传输实现,这里有四点需要我们注意。
TCP实际采用了GBN混合ARQ的策略,所以可以避免不必要的数据报重传,如下面这三个例子所示:
例子1:
例子2:
例子3:
每当超时事件发生时,TCP重传具有最小序号的还未被确认的报文段。只是每次TCP重传时都会将下一次的超时间隔设为先前值的两倍,而不是用从EstimatedRTT和DevRTT推算出的值。
例如,假设当定时器第一次过期时,与最早的未被确认的报文段相关联的TimeoutInterval是0.75秒。TCP就会重传该 报文段,并把新的过期时间设置为1.5秒。如果1.5秒后定时器又过期了,则TCP将再次重传该报文段,并把过期时间设置为3.0 秒。因此,超时间隔在每次重传后会呈指数型增长。然而,每当定时器在另两个事件(即收到上层应用的数据和收到ACK)中的任意一个启动时,TimeoutInterval 由最近的EstimatedRTT值与DevRTT值推算得到,相当于重置回默认值。
这种修改提供了一个形式受限的拥塞控制。定时器过期很可能是由网络拥塞引起的,即太多的分组到达源与目的地之间路径上的一台(或多台)路由器的队列中,造成分组丢失或长时间的排队时延。在拥塞的时候,如果源持续重传分组,会使拥塞更加严重。相反,TCP使用更文雅的方式,每个发送方的重传都是经过越来越长的时间间隔后进行的。
超时触发重传存在的问题之一是超时周期可能相对较长。当一个报文段丢失时,这种长超时周期迫使发送方延迟重传丢失的分组,因而增加了端到端时延。
幸运的是,发送方通常可在超时事件发生之前通过注意所谓冗余ACK来较好地检测到丢包情况。冗余ACK(duplicate ACK)就是再次确认某个报文段的ACK,而发送方先前已经收到对该报文段的确认。要理解发送方对冗余ACK的响应,我们必须首先看一下接收方为什么会发送冗余ACK。
下表总结了TCP接收方的ACK生成策略。当TCP接收方收到一个具有这样序号的报文段时,即其序号大于下一个所期望的、按序的报文段,它检测到了数据流中的一个间隔,这就是说有报文段丢失。这个间隔可能是由于在网络中报文段丢失或重新排序造成的。因为TCP不使用否定确认,所以接收方不能向发送方发回一个显式的否定确认。相反,它只是对已经接收到的最后一个按序字节数据进行重复确认(即产生一个冗余ACK)即可。
下表中允许接收方不丢弃失序报文段,实际的TCP协议栈实现过程中,接收方也都会缓存失序到达的报文段,而非直接丢弃,这一点和ARQ协议做法一致。
因为发送方经常一个接一个地发送大量的报文段,如果一个报文段丢失,就很可能引起许多一个接一个的冗余ACK。如果TCP发送方接收到对相同数据的3个冗余ACK,它把这当作一种指示,说明跟在这个已被确认过3次的报文段之后的报文段已经丢失。一旦收到3个冗余ACK,TCP就执行快速重传(fast retransmit),即 在该报文段的定时器过期之前重传丢失的报文段。
对于采用快速重传的TCP,可用下面的伪代码进行表述:
事件: 收到ACK,具有ACK字段值y
if ( y > SendBase ) {
SendBase = y
if (存在未应答的报文段)
启动定时器
} else {
// 对已经确认的报文段的一个冗余ACK
对y收到的冗余ACK数加1
if(y==3)
// TCP快速重传
重新发送具有序号y的报文段
}
break;
TCP是一个GBN协议还是一个SR协议?前面讲过,TCP确认是累积式的,正确接收但失序的报文段是不会被接收方逐个确认的。
因此,TCP发送方仅需维持已发送过但未被确认的字节的最小序号(SendBase)和下一个要发送的字节的序号(NextSeqNum)。在这种意义下,TCP看起来更像一个GBN风格的协议。但是TCP和GBN 协议之间有着一些显著的区别。许多TCP实现会将正确接收但失序的报文段缓存起来。另外考虑一下,当发送方发送的一组报文段1,2,···,N,并且所有的报文段都按序无差错地到达接收方时会发生的情况。
进一步假设对分组 N 的确认报文丢失,但是其余 N - 1 个确认报文在分别超时以前到达发送端,这时又会发生情况。在该例中,GBN不仅会重传分组n,还会重传后面的分组n+1,n+2,… N 。而对于TCP来说,其将重传至多一个报文段n。此外,如果对报文段n+1的确认报文在报文段n超时之前到达,TCP压根就无需重传报文段n。
这一部分内容在避免重传一小节讲过,不清楚可以回看。
对TCP提出的一种修改意见是所谓的 选择确认(selective acknowledgment),它允许TCP接收方有选择地确认失序报文段,而不是累积地确认最后一个正确接收的有序报文段。当将该机制与选择重传机制结合起来使用时(即跳过重传那些已被接收方选择性地确认过的报文段),TCP看起来就很像我们通常的SR协议。因此,TCP的差错恢复机制也许最好被分类为GBN协议与SR协议的混合体。
前面讲过,一条TCP连接的每一侧主机都为该连接设置了接收缓存。当该TCP连接收到正确、按序的字节后,它就将数据放入接收缓存。相关联的应用进程会从该缓存中读取数据,但不必是数据刚一到达就立即读取。事实上,接收方应用也许正忙于其他任务,甚至要过很长时间后才去读取该数据。如果某应用程序读取数据时相对缓慢,而发送方发送得太多、太快,发送的数据就会很容易地使该连接的接收缓存溢出。
TCP为它的应用程序提供了流量控制服务以消除发送方使接收方缓存溢出的可能性。流量控制因此是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。前面提到过,TCP发送方也可能因为IP网络的拥塞而被遏制;这种形式的发送方的控制被称为拥塞控制。
- 流量控制和拥塞控制采取的动作非常相似(对发送方的遏制),但是它们显然是针对完全不同的原因而采取的措施。
- 现在我们来讨论TCP 如何提供流量控制服务的。为了能从整体上看问题,我们在本节都假设TCP是这样实现的,即TCP接收方丢弃失序的报文段。
TCP通过让发送方维护一个称为接收窗口(receive window)的变量来提供流量控制。通俗地说,接收窗口用于给发送方一个指示—该接收方还有多少可用的缓存空间。因为TCP是全双工通信,在连接两端的发送方都各自维护一个接收窗口。
假设主机A通过一条TCP连接向主机B发送一个大文件。主机B为该连接分配了一个接收缓存,并用RcvBuffer来表示其大小。主机B上的应用进程不时地从该缓存中读取数据。我们定义以下变量:
由于TCP不允许已分配的缓存溢出,下式必须成立:
接收窗口用rwnd表示,根据缓存可用空间的数量来设置:
由于该空间是随着时间变化的,所以rwnd是动态的。
连接是如何使用变量rwnd 来提供流量控制服务的呢?
主机A轮流跟踪两个变量,LastByteSent 和LastByteAcked,这两个变量的意义很明显。 注意到这两个变量之间的差LastByteSent - LastByteAcked,就是主机A发送到连接中但未 被确认的数据量。通过将未确认的数据量控制在值rwnd以内,就可以保证主机A不会使主机B的接收缓存溢出。因此,主机A在该连接的整个生命周期须保证:
对于这个方案还存在一个小小的技术问题。为了理解这一点,假设主机B的接收缓存已经存满,使得rwnd=0。在将rwnd=0通告给主机A之后,还要假设主机B没有任何数据要发给主机A。此时,考虑会发生什么情况?
描述了TCP的流量控制服务以后,我们在此要简要地提一下UDP并不提供流量控制,报文段由于缓存溢出可能在接收方丢失。
- 例如,考虑一下从主机A上的一个进程向主机B上的一个进程发送一系列UDP报文段的情形。对于一个典型的UDP实现,UDP将在一个有限大小的缓存中加上报文段,该缓存在相应套接字(进程的门户)“之前”。进程每次从缓存中读取一个完整的报文段。如果进程从缓存中读取报文段的速度不够快,那么缓存将会溢出,并且将丢失报文段。
在本节中,我们来看看如何建立和拆除一条TCP连接。假设运行在一台主机(客户)上的一个进程想与另一台主机(服务器)上的一个进程建立一条连接。客户应用进程首先通知客户TCP,它想建立一个与服务器上某个进程之间的连接。客户中的TCP会用以下方式与服务器中的TCP建立一条TCP连接:
一旦完成这3个步骤,客户和服务器主机就可以相互发送包括数据的报文段了。在以后每一个报文段中,SYN比特都将被置为0。注意到为了创建该连接,在两台主机之间发送了3个分组。由于这个原因,这种连接创建过程通常被称为3次握手。
参与TCP连接的两个进程中任何一个都能终止该连接。当连接结束后,主机中的“资源”(即缓存和变量)将被释放。假设某客户打算关闭连接:
在一个TCP连接的生命周期内,运行在每台主机中的TCP协议在各种TCP状态(TCP state)之间变迁。下图说明了客户TCP会经历的一系列典型TCP状态:
(注意到服务器也能选择关闭该连接。)
这引起客户TCP发送一个带有FIN比特被置为1的TCP报文段,并进入FIN_WAIT_1状态。上面我们站在客户端的视角,看了整个TCP状态转换图,下面我们站在服务器端的视角,来看看TCP状态转换过程:
初始时服务器正在监听客户发送其SYN报文段的端口。我们来考虑当一台主机接收到一个TCP报文段,其端口号或源IP地址与该主机上进行中的套接字都不匹配的情况。
下面再来简单介绍一下nmap端口扫描工具的实现原理:
在前面几节中,我们已经分析了面临分组丢失时用于提供可靠数据传输服务的基本原理及特定的TCP机制。我们以前讲过,在实践中,这种丢包一般是当网络变得拥塞时由于路由器缓存溢出引起的。分组重传因此作为网络拥塞的征兆(某个特定的运输层报文段的丢失)来对待,但是却无法处理导致网络拥塞的原因,因为有太多的源想以过高的速率发送数据。为了处理网络拥塞原因,需要一些机制以在面临网络拥塞时遏制发送方。
这里以书上三个循序渐进的例子为例进行说明:
随着分组的到达速率接近链路容量时,分组会经历巨大的排队时延。
随着分组的到达速率接近链路容量时,发送方必须执行重传来补偿因为缓存溢出而丢弃的分组。但是这样做,又可能在发送方遇到大时延时进行不必要的重传,从而加剧传输链路本身的拥塞程度,因为链路上的路由器需要转发不必要的分组副本。
当一个分组沿一条路径被丢弃时,每个上游路由器用于转发该分组到丢弃该分组而使用的传输容量最终都会被浪费掉。
关于拥塞控制的方法,可以根据网络层是否为运输层拥塞控制提供显式帮助,来区分拥塞控制方法:
对于网络辅助的拥塞控制,拥塞信息从网络反馈到发送方通常有以下两种方式:
上一节说过:TCP必须使用端到端拥塞控制而不是使网络辅助的拥塞控制,因为IP层不向端系统提供显式的网络拥塞反馈。
TCP所采用的方法是让每一个发送方根据所感知到的网络拥塞程度来限制其能向连接发送流量的速率。如果一个TCP发送方感知从它到目的地之间的路径上没什么拥塞,则TCP发送方增加其发送速率;如果发送方感知沿着该路径有拥塞,则发送方就会降低其发送速率。但是这种方法提出了三个问题。第一,一个TCP发送方如何限制它向其连接发送流量的速率呢?第二,一个TCP发送方如何感知从它到目的地之间的路径上存在拥塞呢?第三,当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢?
我们首先分析一下TCP发送方是如何限制向其连接发送流量的。之前说过,TCP连接的每一端都是由一个接收缓存、一个发送缓存和几个变量(LastByteRead、rwnd等)组成。运行在发送方的TCP拥塞控制机制跟踪一个额外的变量,即拥塞窗口(congestion window)。拥塞窗口表示为cwnd,它对一个TCP发送方能向网络中发送流量的速率进行了限制。特别是,在一个发送方中未被确认的数据量不会超过cwnd与rwnd中的最小值,即
为了关注拥塞控制(与流量控制形成对比),我们后面假设TCP接收缓存足够大,以至可以忽略接收窗口的限制;因此在发送方中未被确认的数据量仅受限于cwnd。我们还假设发送方总是有数据要发送,即在拥塞窗口中的所有报文段要被发送。
上面的约束限制了发送方中未被确认的数据量,因此间接地限制了发送方的发送速率。为了理解这一点,我们来考虑一个丢包和发送时延均可以忽略不计的连接。因此粗略地讲,在每个往返时间(RTT)的起始点,上面的限制条件允许发送方向该连接发送cwnd个字节的数据,在该RTT结束时发送方接收对数据的确认报文。因此,该发送方的发送速率大概是cwnd/RTT字节/秒。通过调节 cwnd的值,发送方因此能调整它向连接发送数据的速率。
我们接下来考虑 TCP发送方是如何感知在它与目的地之间的路径上出现了拥塞的。我们将一个TCP发送方的“丢包事件”定义为:要么出现超时,要么收到来自接收方的3个冗余ACK。
当出现过度的拥塞时,在沿着这条路径上的一台(或多台)路由器的缓存会溢出,引起一个数据报(包含一个 TCP报文段)被丢弃。丢弃的数据报接着会引起发送方的丢包事件(要么超时或收到3个冗余ACK),发送方就认为在发送方到接收方的路径上出现了拥塞的指示。
考虑了拥塞检测问题后,我们接下来考虑网络没有拥塞这种更为乐观的情况,即没有出现丢包事件的情况。在此情况下,在TCP的发送方将收到对于以前未确认报文段的确认。如我们将看到的那样,TCP将这些确认的到达作为一切正常的指示,即在网络上传输的报文段正被成功地交付给目的地,并使用确认来增加拥塞窗口的长度(及其传输速率)。
注意到如果确认以相当慢的速率到达(例如,如果该端到端路径具有高时延或包含一段低带宽链路),则该拥塞窗口将以相当慢的速率增加。在另一方面,如果确认以高速率到达,则该拥塞窗口将会更为迅速地增大。因为TCP使用确认来触发(或计时)增大它的拥塞窗口长度,TCP被说成是自计时(self-clocking)的。
给定调节cwnd值以控制发送速率的机制,关键的问题依然存在:TCP发送方怎样确定它应当发送的速率呢?如果众多TCP发送方总体上发送太快,它们能够拥塞网络,进一步导致网络拥塞加剧。
TCP使用下列指导性原则回答这些问题:
- TCP发送方的行为也许类似于要求(并得到)越来越多糖果的孩子,直到最后告知他/她“不行!”,孩子后退一点,然后过一会儿再次开始提出请求。
- 注意到网络中没有明确的拥塞状态信令,即ACK和丢包事件充当了隐式信号,并且每个TCP发送方根据异步于其他TCP发送方的本地信息而行动。
本节我们来看看广受赞誉的TCP拥塞控制算法,该算法包括3个部分: 慢启动,拥塞避免和快速恢复。
慢启动和拥塞避免时TCP的强制部分,两者的差异在于对收到的ACK做出反应时增加cwnd长度的方式。同时慢启动比拥塞避免能更快地增加cwnd的长度。快速恢复是推荐部分,并非是必需的。
当一条TCP连接开始时,cwnd的值通常初始为一个MSS的较小值,这就使得初始发送速率大约为MSS/RTT。由于对TCP发送方而言,可用带宽可能比MSS/RTT大得多,TCP发送方希望迅速找到可用带宽的数量。因此,在慢启动状态,cwnd的值以1个MSS开始并且每当传输的报文段首次被确认就增加1个MSS。
在上图所示的例子中,TCP向网络发送第一个报文段并等待一个确认。当该确认到达时,TCP发送方将拥塞窗口增加一个MSS,并发送出两个最大长度的报文段。这两个报文段被确认,则发送方对每个确认报文段将拥塞窗口增加一个MSS,使得拥塞窗口变为4个MSS,并这样下去。这一过程每过一个RTT,发送速率就翻番。因此,TCP发送速率起始慢,但在慢启动阶段以指数增长。
但是,何时结束这种指数增长呢?慢启动对这个问题提供了几种答案:
ssthresh 是慢启动阈值,当拥塞窗口大小大于等于该值时,会从慢启动状态转变为拥塞避免状态;当拥塞窗口大小小于该值时,会从拥塞避免状态转变为慢启动状态。
一旦进入拥塞避免状态,cwnd的值大约是上次遇到拥塞时的值的一半,即距离拥塞可能并不遥远!因此,TCP无法每过一个RTT再将cwnd的值翻番,而是采用了一种较为保守的方法,每个RTT只将cwnd的值增加一个MSS。这能够以几种方式完成。一种通用的方法是对于TCP发送方无论何时到达一个新的确认,就将cwnd增加一个MSS(MSS/cwnd)字节。
例如,如果MSS是1460字节并且cwnd是14600字节,则在一个RTT内发送10个报文段。每个到达ACK(假定每个报文段一个ACK)增加1/10 * MSS的拥塞窗口长度,因此在收到对所有10个报文段的确认后,拥塞窗口的值将增加了一个MSS。
但是何时应当结束拥塞避免的线性增长(每RTT 1MSS)呢?
在快速恢复中,对于引起TCP进入快速恢复状态的缺失报文段,对收到的每个冗余的ACK,cwnd的值增加一个MSS。最终,当对丢失报文段的一个ACK到达时,TCP在降低cwnd后进入拥塞避免状态。如果出现超时事件,快速恢复在执行如同在慢启动和拥塞避免中相同的动作后,迁移到慢启动状态:当丢包事件出现时,cwnd的值被设置为1个MSS,并且ssthresh的值设置为cwnd值的一半。
快速恢复是TCP推荐的而非必需的构件。有趣的是,一种称为TCP Tahoe的TCP早期版本,不管是发生超时指示的丢包事件,还是发生3个冗余ACK指示的丢包事件,都无条件地将其拥塞窗口减至1个MSS,并进入慢启动阶段。TCP的较新版本TCP Reno,则综合了快速恢复。
下图展示了Reno 版TCP 和 Tahoe 版TCP 的拥塞控制窗口演化情况。在该图中,初始阈值等于8个MSS,在前8个传输回合,Tahoe 和 Reno 都采取了相同的动作。 拥塞窗口在慢启动阶段以指数速度快速爬升,在第4轮传输时到达了阈值(慢启动阶段)
。然后拥塞窗口以线性速度爬升(拥塞避免阶段)
,直到在第8轮传输后出现3个冗余ACK。注意到当该丢包事件发生时,拥塞窗口值为12xMSS。于是ssthresh的值被设置为0.5xcwnd=6x MSS。在TCP Reno下,拥塞窗口被设置 为cwnd=9MSS,然后线性地增长(快速恢复阶段)
。在TCP Tahoe 下,拥塞窗口被设置为1个MSS,然后呈指数增长,直至到达 ssthresh (慢启动阶段)
。
在深入了解慢启动、拥塞避免和快速恢复的细节后,现在有必要退回来回顾一下全局。忽略一条连接开始时初始的慢启动阶段,假定丢包由3个冗余的ACK而不是超时指示,TCP的拥塞控制是:每个RTT内cwnd线性(加性)增加1MSS,然后出现3个冗余ACK事件时cwnd减半(乘性减)。因此,TCP拥塞控制常常被称为加性增、乘性减(Additive-Increase, Multiplicative-Decrease,AIMD)拥塞控制方式。AIMD拥塞控制引发了下图中所示的“锯齿”行为,这也很好地图示了我们前面TCP检测带宽时的直觉,即TCP线性地增加它的拥塞窗口长度(因此增加其传输速率),直到出现3个冗余ACK事件。然后以2个因子来减少它的拥塞窗口长度,然后又开始了线性增长,探测是否还有另外的可用带宽。
许多TCP实现采用了Reno算法,同时也存在很多Reno算法的变种,比如TCP Vegas算法,该算法试图在维持较好的吞吐量的同时避免阻塞。
该算法基本思想是:
快要发生分组丢失是通过观察RTT来预测的,分组的RTT越长,路由器中的拥塞越严重。
拥塞窗口阈值大小和拥塞窗口大小的变化,可以参考上图学习,这里就不再赘述。
考虑K条TCP连接,每条都有不同的端到端路径,但是都经过一段传输速率为R bps的瓶颈链路。(所谓瓶颈链路,是指对于每条连接,沿着该连接路径上的所有其他段链路都不拥塞,而且与该瓶颈链路的传输容量相比,它们都有充足的传输容量。)假设每条连接都在传输一个大文件,而且无 UDP 流量通过该段瓶颈链路。如果每条连接的平均传输速率接近R/K,即每条连接都得到相同份额的链路带宽,则认为该拥塞控制机制是公平的。
TCP的AIMD算法公平吗?尤其是假定可在不同时间启动并因此在某个给定的时间点可能具有不同的窗口长度情况下,对这些不同的TCP连接还是公平的吗?TCP趋于在竞争的多条TCP连接之间提供对一段瓶颈链路带宽的平等分享。
我们考虑有两条TCP连接共享一段传输速率为R的链路的简单例子,如下图所示:
我们将假设这两条连接有相同的MSS和RTT(这样如果它们有相同的拥塞窗口长度,就会有相同的吞吐量),它们有大量的数据要发送,且没有其他TCP连接或UDP数据报穿越该段共享链路。我们还将忽略TCP的慢启动阶段,并假设TCP连接一直按CA模式(AIMD)运行。
上图描绘了两条TCP连接实现的吞吐量情况。如果TCP要在这两条TCP连接之间平等地共享链路带宽,那么实现的吞吐量曲线应当是从原点沿45°方向的箭头向外辐射(平等带宽共享)。理想情况是,两个吞吐量的和应等于R。(当然,每条连接得到相同但容量为0的共享链路容量并非我们所期望的情况!)所以我们的目标应该是使取得的吞吐量落在图中平等带宽共享曲线与全带宽利用曲线的交叉点附近的某处。
假定TCP窗口长度是这样的,即在某给定时刻,连接1和连接2实现了由上图中A点所指明的吞吐量。因为这两条连接共同消耗的链路带宽量小于R,所以无丢包事件发生,根据TCP的拥塞避免算法的结果,这两条连接每过一个RTT都要将其窗口增加1个MSS。因此,这两条连接的总吞吐量就会从A点开始沿45°线前行(两条连接都有相同的增长)。最终,这两条连接共同消耗的带宽将超过R,最终将发生分组丢失。
假设连接1和连接2实现B点指明的吞吐量时,它们都经历了分组丢失。连接1和连接2于是就按二分之一减小其窗口。所产生的结果实现了C点指明的吞吐量,它正好位于始于B点止于原点的一个向量的中间。因为在C点,共同消耗的带宽小于R,所以这两条连接再次沿着始于C点的45°线增加其吞吐量。最终,再次发生丢包事件,如在D点,这两条连接再次将其窗口长度减半,如此等等。你应当搞清楚这两条连接实现的带宽最终将沿着平等带宽共享曲线在波动。还应该搞清楚无论这两条连接位于二维空间的何处,它们最终都会收敛到该状态!虽然此时我们做了许多理想化的假设,但是它仍然能对解释为什么TCP会导致在多条连接之间的平等共享带宽这个问题提供一个直观的感觉。
在理想化情形中,我们假设仅有TCP连接穿过瓶颈链路,所有的连接具有相同的RTT值,且对于一个主机—目的地对而言只有一条TCP连接与之相关联。实践中,这些条件通常是得不到满足的,客户—服务器应用因此能获得非常不平等的链路带宽份额。特别是,已经表明当多条连接共享一个共同的瓶颈链路时,那些具有较小RTT的连接能够在链路空闲时更快地抢到可用带宽(即较快地打开其拥塞窗口),因而将比那些具有较大RTT的连接享用更高的吞吐量。
我们刚才已经看到,TCP拥塞控制是如何通过拥塞窗口机制来调节一个应用程序的传输速率的。许多多媒体应用如因特网电话和视频会议,经常就因为这种特定原因而不在TCP上运行,因为它们不想其传输速率被扼制,即使在网络非常拥塞的情况下。
相反,这些应用宁可在UDP上运行,UDP是没有内置的拥塞控制的。当运行在UDP上时,这些应用能够以恒定的速率将其音频和视频数据注入网络之中并且偶尔会丢失分组,而不愿在拥塞时将其发送速率降至“公平”级别并且不丢失任何分组。
从TCP的观点来看,运行在UDP上的多媒体应用是不公平的,因为它们不与其他连接合作,也不适时地调整其传输速率。因为TCP拥塞控制在面临拥塞增加(丢包)时,将降低其传输速率,而UDP源则不必这样做,UDP源有可能压制TCP流量。当今的一个主要研究领域就是开发一种因特网中的拥塞控制机制,用于阻止UDP流量不断压制直至中断因特网吞吐量的情况。
即使我们能够迫使UDP流量具有公平的行为,但公平性问题仍然没有完全解决。这是因为我们没有什么办法阻止基于TCP的应用使用多个并行连接。例如,Web浏览器通常使用多个并行TCP连接来传送一个Web页中的多个对象。(多条连接的确切数目可以在多数浏览器中进行配置。)当一个应用使用多条并行连接时,它占用了一条拥塞链路中较大比例的带宽。举例来说,考虑一段速率为R且支持9个在线客户—服务器应用的链路,每个应用使用一条TCP连接。如果一个新的应用加入进来,也使用一条TCP连接,则每个应用得到差不多相同的传输速率R/10。但是如果这个新的应用这次使用了11个并行TCP连接,则这个新应用就不公平地分到超过R/2的带宽。Web流量在因特网中是非常普遍的,所以多条并行连接并非不常见。
自20世纪80年代后期慢启动和拥塞避免开始标准化以来,TCP已经实现了端到端拥塞控制的形式,即一个TCP发送方不会收到来自网络层的明确拥塞指示,而是通过观察分组丢失来推断拥塞。
对于IP和TCP的扩展方案也已经提出并已经实现和部署,该方案允许网络明确向TCP发送方和接收方发出拥塞信号。这种形式的网络辅助拥塞控制称为明确拥塞通告(ExplicitCongestion Notification,ECN)。
在网络层,IP数据报首部的服务类型字段中的两个比特(总的说来,有四种可能的值)被用于ECN。
路由器所使用的一种ECN比特设置指示该路由器正在历经拥塞。该拥塞指示则由被标记的IP数据报所携带,送给目的主机,再由目的主机通知发送主机,如上图所示。RFC3168没有提供路由器拥塞时的定义;该判断是由路由器厂商所做的配置选择,并且由网络操作员决定。然而,RFC3168推荐仅当拥塞持续不断存在时才设置ECN比特。发送主机所使用的另一种ECN比特设置通知路由器发送方和接收方是ECN使能的,因此能够对于ECN指示的网络拥塞采取行动。
如上图所示,当接收主机中的TCP通过一个接收到的数据报收到了一个ECN拥塞指示时,接收主机中的TCP通过在接收方到发送方的TCP ACK报文段中设置ECE(明确拥塞通告回显)比特,通知发送主机中的TCP收到拥塞指示。接下来,TCP发送方通过减半拥塞窗口对一个具有ECE拥塞指示的ACK做出反应,就像它对丢失报文段使用快速重传做出反应一样,并且在下一个传输的TCP发送方到接收方的报文段首部中对CWR(拥塞窗口缩减)比特进行设置。
除了TCP以外的其他运输层协议也可以利用网络层发送ECN信号。数据报拥塞控制协议(Datagram Congestion Control Protocol,DCCP)[RFC 4340]提供了一种低开销、控制 拥塞的类似UDP不可靠服务,该协议利用了ECN。DCTCP(数据中心TCP)[Alizadeh 2010]是一种专门为数据中心网络设计的TCP版本,也利用了ECN。
运输层协议能够提供可靠数据传输,即使下面的网络层是不可靠的。我们也看到了提供可靠的数据传送会遇到许多微妙的问题,但都可以通过精心地结合确认、定时器、重传以及序号机制来完成任务。
本文也详细地研究了TCP协议,它是因特网中面向连接和可靠的运输层协议。我们知道TCP是非常复杂的,它涉及了连接管理、流量控制、往返时间估计以及可靠数据传送。事实上,TCP比我们描述的要更为复杂,即我们有意地避而不谈在各种TCP实现版本中广泛实现的各种TCP补丁、修复和改进。然而,所有这些复杂性都对网络层应用隐藏了起来。如果某主机上的客户希望向另一台主机上的服务器可靠地发送数据,它只需要打开对该服务器的一个TCP套接字,然后将数据注人该套接字。客户—服务器应用程序则乐于对TCP的复杂性视而不见。
最后,我们从广泛的角度研究了拥塞控制,我们知道了拥塞控制对于网络良好运行是必不可少的。没有拥塞控制,网络很容易出现死锁,使得端到端之间很少或没有数据能被传输。在本文中我们学习了TCP实现的一种端到端拥塞控制机制,即当TCP连接的路径上判断不拥塞时,其传输速率就加性增;当出现丢包时,传输速率就乘性减。这种机制也致力于做到每一个通过拥塞链路的TCP连接能平等地共享该链路带宽。我们也深入探讨了TCP连接建立和慢启动对时延的影响。我们观察到在许多重要场合,连接建立和慢启动会对端到端时延产生严重影响。
最后,我们再来看看其他的一些有趣协议: