简单聊聊 TCP 协议
- 如何实现可靠传输 ?
- 完全可靠
- 存在比特差错
- 存在丢包
- 流水线可靠数据传输协议
- 回退N步 (GBN)
- 选择重传 (ARQ)
- 小结
- TCP
- TCP 连接
- 报文段结构
- 序号和确认号
- 可靠数据传输
- 避免重传
- 超时时间加倍
- 快速重传
- 回退N步还是选择重传
- 流量控制
- 连接管理
- 拥塞控制
- 拥塞原因
- 拥塞控制方法
- 端到端拥塞控制
- 如何限制发送速率 ?
- 如何感知拥塞 ?
- 拥塞控制算法
- 慢启动
- 拥塞避免
- 快速恢复
- 小结
- 公平性
- 自私自利的UDP
- 并行TCP连接
- 网络辅助拥塞控制
- 小结
本文参考自定向下一书第三章整理而来。
如何实现可靠传输 ?
在网络世界中最重要的问题之一就是可靠传输,而运输层的TCP协议为上层实体提供的服务抽象就是: 数据可以通过一条可靠的信道进行传输。借助于可靠信道,传输数据比特就不会受到损坏或者丢失,而且所有数据都是按照其发送顺序进行交付。这也正是TCP向调用它的因特网所提供的服务模型。
TCP 是在不可靠的网络层之上实现的可靠数据传输协议,所以这里的关键在于如何去设计这样一个可靠数据传输协议呢 ?
完全可靠
- 假设底层的信道传输是绝对可靠的,那么我们直接正常收发数据即可
存在比特差错
- 如果底层信道可能会发生比特差错,那么两端此时都需要具备差错检测功能,同时需要引入消息确认机制,当任何一端接收到否定确认消息时,都需要重传对应的分组
基于上面重传机制实现的可靠数据传输协议也被成为自动重传请求(ARQ)协议
方案二中,发送方在发送完一个分组后,必须等待直到接收到ACK或NAK响应后,才能继续从上层获取更多的数据进行发送,因此该协议也被成为停止等待协议。
方案二最大的缺陷在于没有考虑ACK和NAK分组受损的可能性,我们可以为ACK或NAK分组也添加校验和进行差错检测,但是我们无法知道接收方是否正确接收了上一块发送的数据。
解决这个问题或许有三个思考方向:
- 由于ACK和NAK分组是针对数据分组重传引入的,所以我们可以考虑为ACK和NAK分组重传单独引入一组分组,用于ACK和NAK分组的重传需求,但是如果这里针对ACK和NAK分组重传对应的分组也出现差错了,那又该怎么办呢 ? 套娃不太现实,那样会陷入递归的死循环中,所以该方案不可取!
- 能否为ACK和NAK分组增加足够的校验和比特,使得发送双方不仅可以检测差错,还可恢复差错呢 ?
- 当发送方接收到含糊不清的ACK或NAK分组时,只需要重传当前数据分组即可,但是该方案会在信道中引入冗余分组。
解决这个问题的一个简单办法就是引入序列号机制,即为所有数据分组添加一个新的seq字段,用于表示当前分组的序号;在方案二中,当接收方接收到一个受损分组后,会响应一个NAK分组,但是当我们引入了序列号机制时,则可以通过对上次正确接收到的分组发送一个ACK,达成和NAK一样的效果;当发送方接收到对同一个分组的两个ACK (即接收到冗余ACK)时,就知道接收方没有正确接收到跟在被确认两次的分组后面的分组了。
基于序列号机制,我们便轻松实现了一个无NAK的可靠数据传输协议。
方案二假设底层信道不会产生分组丢失问题
存在丢包
- 方案三假定底层信道不仅会发生比特受损,还会丢包,那么这又引出了两个问题 : 怎样检测丢包以及丢包发生后我们应该做些什么。
检测丢包最直接的想法就是引入超时重传机制;站在发送方的角度来看,由于发送方不知道是一个分组丢失,还是一个ACK丢失,或者只是该分组或ACK过度延时。在所有这些情况下,都可以采用超时重传来解决。
为了实现超时重传,我们需要引入一个重传计时器,发送方需要做以下几件事情:
- 每次发送一个分组时(第一次分组和重传分组),便启动一个定时器
- 发送方还要能够响应定时器中断(重传当前未ACK的分组)
- 终止定时器 (不存在未ACK的分组时)
引入了超时重传机制后,我们来看一下当前协议在各种情况下的工作流程:
- 无丢包操作
- 分组丢失
- 丢失ACK
- 过早超时
方案三中最主要的一点就是超时时间的设置,但是不管怎样,我们已经可以利用检验和,序号,定时器,ACK机制来实现一个可靠的数据传输协议了。
流水线可靠数据传输协议
方案三所设计的停止等待协议最大问题就在于无法充分利用底层网络带宽,解决这个问题的办法就是采用流水线传输技术,如下图所示:
采用流水线传输技术后,我们需要针对方案三设计的停止等待协议进行如下改造:
- 停止等待协议中,我们其实只使用两个序列号0和1就足以了,但是流水线传输迫使我们必须增加序号范围,因为每个输送中的分组都必须有一个唯一的序号,而且也许有多个在传输中但是未被确认的报文。
- 协议的发送方和接收方两端也需要缓存多个分组;发送方至少能够缓存那些已经发送但还未确认的分组,接收方需要缓存那些已经正确接收的分组。
- 流水线的差错恢复也需要考虑是采用 回退N步(GBN) 还是 选择重传 (SR) ,这也决定了所需序号范围和对缓存区大小的要求。
回退N步 (GBN)
在回退N步(GBN)协议中,允许发送方发送多个分组,而无需等待确认,但它也受限于流水线中未确认的分组数不能超过某个最大允许数N。
发送方会在本端维护一个滑动窗口,大小为N,同时在发送端看到的发送缓存区的数据被分为了以下四部分:
- 已经被确认的字节流
- 已发送,还未被确认的字节流
- 准备就绪,待发送的字节流
- 空闲空间
随着协议的运行,该窗口在序号空间向前滑动。因此,N通常被称为窗口长度,GBN 协议也常被称为滑动窗口协议。
对于GBN协议来说,他必须处理以下三类事件:
- 当上层调用send接口发送数据时,发送器首先检查发送窗口是否已满,即是否有N个已经发送但未被确认的分组,如果未满,则产生一个分组并将其发送,并更新相关变量值;如果满了,发送器可以将数据暂时缓存起来,或者使用同步机制让调用方等待直到窗口未满时。
- 收到一个ACK响应时,采用累积确认的方式,表明接收方已经正确接收到序号为n的以前且包括n在内的所有分组。
- 当出现定时器超时事件时,发送方重传所有已发送但还未被确认过的分组;这里发送方可以仅使用一个定时器,作为最早的已发送但未被确认的分组所使用的定时器。如果收到一个ACK,但仍有已发送但未被确认的分组,则定时器被重新启动(刷新定时器)。如果没有已发送但未被确认的分组,则停止该定时器。
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协议关键要弄明白按序到达时什么意思
选择重传 (ARQ)
GBN协议允许发送方用多个分组“填充流水线”,因此避免了停等协议中所提到的信道利用率问题。然而,GBN本身也有一些情况存在着性能问题。尤其是当窗口长度和带宽时延积都很大时,在流水线中会有很多分组更是如此。单个分组的差错就能够引起GBN重传大量分组,许多分组根本没有必要重传。随着信道差错率的增加,流水线可能会被这些不必要重传的分组所充斥。
选择重传(SR)协议通过让发送方仅重传那些它怀疑在接收方出错(即丢失或受损)的分组而避免了不必要的重传。这种个别的、按需的重传要求接收方逐个地确认正确接收的分组。再次用窗口长度N来限制流水线中未完成、未被确认的分组数。然而,与GBN不同的是,发送方已经收到了对窗口中某些分组的ACK。
SR 接收方将确认一个正确接收的分组而不管其是否按序到达。失序的分组将被缓存直到所有丢失分组(即序号更小的分组)皆被收到为止,这时才可以将一批分组按序交付给上层。
下面我们先来看看SR发送方需要处理的事件有哪些:
- 从上层收到数据: 当从上层接收到数据后,SR发送方检查下一个可用于该分组的序号。如果序号位于发送方的窗口内,则将数据打包并发送;否则就像在GBN中一样,要么将数据缓存,要么将其返回给上层以便以后传输。
- 超时: 定时器再次被用来防止丢失分组。然而,现在每个分组必须拥有其自己的逻辑定时器,因为超时发生后只能发送一个分组。可以使用单个硬件定时器模拟多个逻辑定时器的操作。
- 收到ACK: 如果收到ACK,倘若该分组序号在窗口内,则SR发送方将那个被确认的分组标记为已接收。如果该分组的序号等于send_base,则窗口基序号向前移动到具有最小序号的未确认分组处。如果窗口移动了并且有序号落在窗口内的未发送分组,则发送这些分组。
再来看看SR接收方需要处理的事件有哪些:
- 收到的分组落在接收方的窗口内,一个选择ACK被回送给发送方。如果该分组以前没收到过,则缓存该分组。如果该分组的序号等于接收窗口的基序号
(rev_base)
,则该分组以及以前缓存的序号连续的(起始于rev_base的)
分组交付给上层。然后,接收窗口按向前移动分组的编号向上交付这些分组。 - 序号在
[rev_base - N,rev_base - 1]
内的分组被正确收到。在此情况下,必须产生一个ACK,即使该分组是接收方以前已确认过的分组。 - 其他情况。忽略该分组。
可以参考下图案例进行理解:
这里有一点很重要,就是接收方需要重新确认(而不是忽略)已收到过的那些序号小于当前窗口基序号的分组,这里可能是因为ack丢失,导致发送方进行产生了重发,所以需要回应一下。
小结
经过了上述讨论后,我们来总结一下确保可靠数据传输的机制和相关用途:
- 校验和 : 用于检测一个传输分组中的比特错误
- 定时器 : 用于超时/重传一个分组,可能因为该分组(或其ACK)在信道中丢失了。由于当一个分组延时但未丢失,或当一个分组已经被接收方收到但从接收方到发送方的ACK丢失时,可能产生超时事件,所以接收方可能会收到一个分组的多个冗余副本。
- 序号 : 用于为从发送方流向接收方的数据分组按顺序编号。所接收分组的序号间的空隙可使接收方检测出丢失的分组。具有相同序号的分组可使接收方检测出一个分组的冗余副本。
- 确认 : 接收方用于告诉发送方一个分组或一组分组已被正确地接收到了。确认报文通常携带着被确认的分组或多个分组的序号。确认可以是逐个的或累积的,这取决于协议。
- 否定确认 : 接收方用于告诉发送方某个分组未被正确地接收。否定确认报文通常携带着未被正确接收的分组的序号。
- 窗口,流水线 : 发送方也许被限制仅发送那些序号落在一个指定范围内的分组。通过允许一次发送多个分组但未被确认,发送方的利用率可在停止等待模式的基础上得到增加。窗口的长度可根据接收方接收和缓存报文的能力,网络中的拥塞程度或两者情况来进行设置。
上述方案假设过程中我们都默认分组在发送方和接收方之间的信道中不会被重新排序,但是实际网络运行过程中,分组重新排序是可能发生的。
分组重新排序的一个表现就是,一个具有序号或确认号x的分组的旧副本可能会出现,即使发送方或接收方的窗口中都没有包含x。对于分组重新排序,信道可被看成基本上是在缓存分组,并在将来任意时刻自然地释放出这些分组。由于序号可以被重新使用,那么必须小心,以免出现这样的冗余分组。实际应用中采用的方法是,确保一个序号不被重新使用,直到发送方“确信”任何先前发送的序号为x的分组都不再在网络中为止。通过假定一个分组在网络中的“存活”时间不会超过某个固定最大时间量来做到这一点。在高速网络的TCP扩展中,最长的分组寿命被假定为大约3分钟。Sunshine 1978 中描述了一种使用序号的方法,它能够完全避免重新排序问题,感兴趣可以自行去了解一下。
TCP
TCP是因特网运输层的面向连接的可靠的运输协议,其主要使用包括差错检测,重传,累积确认,定时器以及序号和确认号的首部字段实现可靠传输协议。
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 报文段结构如下所示:
下面针对其中部分重要字段展开说明:
- 32比特的序号字段和32比特的确认号字段。
- 16比特的接收窗口字段,该字段用于流量控制 --> 用于指示接收方愿意接受的字节数量。
- 4比特的首部长度字段,该字段指示了以32比特的字为单位的TCP首部长度。
由于TCP选项字段的原因,TCP首部的长度是可变的。(通常,选项字段为空,所以TCP首部的典型长度是20字节。)
- 可选与变长的选项字段,该字段用于发送方与接收方协商最大报文段长度(MSS)时,或在高速网络环境下用作窗口调节因子时使用。首部字段中还定义了一个时间戳选项。
- 6比特的标志字段(flag field):
- ACK比特用于指示确认字段中的值是有效的,即该报文段包括一个对已被成功接收报文段的确认。
- RST、SYN 和 FIN 比特用于连接建立和拆除。
- 在明确拥塞通告中使用了CWR和ECE比特。
- 当PSH比特被置位时,就指示接收方应立即将数据交给上层。
- 最后, URG比特用来指示报文段里存在着被发送端的上层实体置为“紧急”的数据。
- 紧急数据的最后一个字节由16比特的紧急数据指针字段指出。
- 当紧急数据存在并给出指向紧急数据尾指针的时候, TCP必须通知接收端的上层实体。
(在实践中, PSH、 URG和紧急数据指针并没有使用。为了完整性起见,我们才提到这些字段。)
序号和确认号
TCP把数据看成一个无结构的、有序的字节流。我们从TCP对序号的使用上可以看出这一点,因为序号是建立在传送的字节流之上,而不是建立在传送的报文段的序列之上。一个报文段的序号因此是该报文段首字节的字节流编号。
TCP是全双工的,因此主机A在向主机B发送数据的同时,也许也接收来自主机B的数据(都是同一条TCP连接的一部分)。从主机 B 到达的每个报文段中都有一个序号用于表示从 B 流向 A 的数据。主机A 填充进报文段的确认号是主机 A 期望从主机 B 收到的下一字节的序号。
TCP协议采用的是累积确认法,为了证明这一点,我们来看下面这个例子:
- 假设主机A已收到一个来自主机B的包含字节0~535的报文段,以及另一个包含字节900~1000的报文段。
- 由于某种原因,主机A还没有收到字节536~899的报文段。
- 在这个例子中,主机A为了重新构建主机B的数据流,仍在等待字节536(和其后的字节)。
- 因此,A到B的下一个报文段将在确认号字段中包含536。
- 因为TCP只确认该流中至第一个丢失字节为止的字节,所以TCP被称为提供累积确认。
当我们引入了序列号和确认号后,我们还可以思考一下下面这个问题:
- 主机A在收到第二个报文段(字节536~899)之前收到第三个报文段(字节900~1000)会发生什么呢?也就是说第三个报文段失序到达。
- 该微妙的问题是:当主机在一条TCP连接中收到失序报文段时该怎么办?有趣的是,TCP RFC并没有为此明确规定任何规则,而是把这一问题留给实现TCP的编程人员去处理。他们有两个基本的选择:
- 接收方立即丢弃失序报文段(如前所述,这可以简化接收方的设计);
- 接收方保留失序的字节,并等待缺少的字节以填补该间隔。
- 显然,后一种选择对网络带宽而言更为有效,是实践中采用的方法。
TCP协议中关于初始序列号的选择也是有讲究的:
- 一条 TCP连接的双方均可随机地选择初始序号。这样做可以减少将那些仍在网络中存在的来自两台主机之间先前已终止的连接的报文段,误认为是后来这两台主机之间新建连接所产生的有效报文段的可能性(它碰巧与旧连接使用了相同的端口号)。
关于TCP协议中的确认号,我们也需要注意下面这点:
- TCP协议通常会将对客户到服务器的数据的确认装载在一个承载服务器到客户的数据的报文段中;
- 这种确认被称为是被捎带(piggybacked)在服务器到客户的数据报文段中的。
- 同时,如上图所示,该报文段的确认号字段填入的是80,因为客户已经收到了字节流中序号为79及以前的字节,它现在正等待着字节80的出现。你可能认为这有点奇怪,即使该报文段里没有数据还仍有序号。这是因为TCP存在序号字段,报文段需要填入某个序号。
下面我们再来看看TCP中隐式NAK的实现:
- TCP通过使用肯定确认与定时器来提供可靠数据传输。
- TCP确认正确接收到的数据,而当认为报文段或其确认报文丢失或受损时,TCP会重传这些报文段。
- 有些版本的TCP还有一个隐式NAK机制
(在TCP的快速重传机制下,收到对一个特定报文段的3个冗余ACK就可作为对后面报文段的一个隐式NAK,从而在超时之前触发对该报文段的重传)
。 - TCP使用序号以使接收方能识别丢失或重复的报文段;由于TCP自己也无法明确地分辨一个报文段或其ACK是丢失了还是受损了,或是时延过长了。所以在发送方,TCP的响应是相同的:重传有疑问的报文段。
最后我们再来看看关于TCP中流水线的说明:
- TCP也使用流水线,使得发送方在任意时刻都可以有多个已发出但还未被确认的报文段存在。
- 当报文段长度与往返时延之比很小时,流水线可显著地增加一个会话的吞吐量。一个发送方能够具有的未被确认报文段的具体数量是由TCP的流量控制和拥塞控制机制决定的。
可靠数据传输
由于运输层报文段是被IP数据报携带着在网络中传输的,并且网络层服务是不可靠的,所以需要像TCP这样的可靠传输协议来提供一种可靠数据传输服务。
本节我们来看看TCP是如何使用我们上一节讲到的可靠传输技术来实现可靠的数据传输协议的。
首先是TCP发送方,其有3个与发送和重传有关的主要事件:
- 从上层应用程序接收数据,将数据封装在一个报文段中,并把该报文段交给IP层。每个报文段都包含一个序号,这个序号就是该报文段第一个数据字节的字节流编号。并且此时如果定时器没有为其他报文段而运行,则当报文段被传递给IP时,TCP就启动该定时器。
这里注意:
- 为每一个已经发送但未被确认的报文段都与一个定时器相关联,概念上是最简单的,但是定时器的管理却需要相当大的开销。因此,一般仅使用单一的重传定时器。
- 我们这里可以将定时器想象为与最早的未被确认的报文段相关联。
- 定时器超时 ,TCP 通过重传引起超时的报文段来响应超时事件,然后TCP重启定时器
- 收到ACK时 ,TCP 将 ACK 的值 y 与它的变量 SendBase 进行比较。TCP 状态变量 SendBase 是最早未被确认的字节的序号。(SendBase - 1 是指接收方已正确按序接收到的数据的最后一个字节的序号)。由于TCP本身采用累积确认,所以y确认了字节编号在y之前的所有字节都已经收到。如果y > SendBase ,则该ACK是在确认一个或多个先前未被确认的报文段。因此发送方更新它的Sendbase变量;如果当前有未被确认的报文段,TCP还要重新启动定时器。
// 假设发送方不受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:
- 主机A向主机B发送一个报文段。假设该报文段的序号是92,而且包含8字节数据。在发出该报文段之后,主机A等待一个来自主机B的确认号为100的报文段。虽然A发出的报文段在主机B上被收到,但从主机B发往主机A的确认报文丢失了。
- 在这种情况下,超时事件就会发生,主机A会重传相同的报文段。当然,当主机B收到该重传的报文段时,它将通过序号发现该报文段包含了早已收到的数据。因此,主机B中的TCP将丢弃该重传的报文段中的这些字节。
例子2:
- 在第二种情况中,主机A连续发回了两个报文段。第一个报文段序号是92,包含8字节数据;第二个报文段序号是100,包含20字节数据。
- 假设两个报文段都完好无损地到达主机B,并且主机B为每一个报文段分别发送一个确认。第一个确认报文的确认号是100,第二个确认报文的确认号是120。
- 现在假设在超时之前这两个报文段中没有一个确认报文到达主机A。当超时事件发生时,主机A重传序号92的第一个报文段,并重启定时器。只要第二个报文段的ACK在新的超时发生以前到达,则第二个报文段将不会被重传。
例子3:
- 假设主机A与第二种情况中一样,发送两个报文段。第一个报文段的确认报文在网络丢失,但在超时事件发生之前主机A收到一个确认号为120的确认报文。主机A因而知道主机B已经收到了序号为119以及之前的所有字节;所以主机A不会重传这两个报文段中的任何一个。
超时时间加倍
每当超时事件发生时,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;
回退N步还是选择重传
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上的应用进程不时地从该缓存中读取数据。我们定义以下变量:
- LastByteRead:主机B上的应用进程从缓存读出的数据流的最后一个字节的编号。
- LastByteRcvd:从网络中到达的并且已放入主机B接收缓存中的数据流的最后一个字节的编号。
由于TCP不允许已分配的缓存溢出,下式必须成立:
- LastByteRcvd - LastByteRead ≤ RcvBuffer
接收窗口用rwnd表示,根据缓存可用空间的数量来设置:
- rwnd = RevBuffer - [ LastByteRcvd - LastByteRead ]
由于该空间是随着时间变化的,所以rwnd是动态的。
连接是如何使用变量rwnd 来提供流量控制服务的呢?
- 主机B通过把当前的rwnd值放入它发给主机A的报文段接收窗口字段中,通知主机A它在该连接的缓存中还有多少可用空间。
- 开始时,主机B设定 rwnd=RcvBuffer。注意到为了实现这 一点,主机B必须跟踪几个与连接有关的变量。
主机A轮流跟踪两个变量,LastByteSent 和LastByteAcked,这两个变量的意义很明显。 注意到这两个变量之间的差LastByteSent - LastByteAcked,就是主机A发送到连接中但未 被确认的数据量。通过将未确认的数据量控制在值rwnd以内,就可以保证主机A不会使主机B的接收缓存溢出。因此,主机A在该连接的整个生命周期须保证:
- LastByteSent - LastByteAcked ≤ rwnd
对于这个方案还存在一个小小的技术问题。为了理解这一点,假设主机B的接收缓存已经存满,使得rwnd=0。在将rwnd=0通告给主机A之后,还要假设主机B没有任何数据要发给主机A。此时,考虑会发生什么情况?
- 因为主机B上的应用进程将缓存清空,TCP并不向主机A发送带有rwnd新值的新报文段;
- 事实上,TCP仅当在它有数据或有确认要发时才会发送报文段给主机A。
- 这样,主机A不可能知道主机B的接收缓存已经有新的空间了,即主机A被阻塞而不能再发送数据!
- 为了解决这个问题,TCP规范中要求:当主机B的接收窗口为0时,主机A继续发送只有一个字节数据的报文段。这些报文段将会被接收方确认。最终缓存将开始清空,并且确认报文里将包含一个非0的rwnd值。
描述了TCP的流量控制服务以后,我们在此要简要地提一下UDP并不提供流量控制,报文段由于缓存溢出可能在接收方丢失。
- 例如,考虑一下从主机A上的一个进程向主机B上的一个进程发送一系列UDP报文段的情形。对于一个典型的UDP实现,UDP将在一个有限大小的缓存中加上报文段,该缓存在相应套接字(进程的门户)“之前”。进程每次从缓存中读取一个完整的报文段。如果进程从缓存中读取报文段的速度不够快,那么缓存将会溢出,并且将丢失报文段。
连接管理
在本节中,我们来看看如何建立和拆除一条TCP连接。假设运行在一台主机(客户)上的一个进程想与另一台主机(服务器)上的一个进程建立一条连接。客户应用进程首先通知客户TCP,它想建立一个与服务器上某个进程之间的连接。客户中的TCP会用以下方式与服务器中的TCP建立一条TCP连接:
- 客户端的TCP首先向服务器端的TCP发送一个特殊的TCP报文段。该报文段中不包含应用层数据。但是在报文段的首部中的一个标志位(即SYN比特)被置为1。因此,这个特殊报文段被称为SYN报文段。另外,客户会随机地选择一个初始序号(client_isn),并将此编号放置于该起始的TCP SYN报文段的序号字段中。该报文段会被封装在一个IP数据报中,并发送给服务器。为了避免某些安全性攻击,在适当地随机化选择client_isn方面有着不少有趣的研究。
- 一旦包含TCP SYN报文段的IP数据报到达服务器主机,服务器会从该数据报中提取出TCP SYN报文段,为该TCP连接分配TCP缓存和变量,并向该客户TCP发送允许连接的报文段。这个允许连接的报文段也不包含应用层数据。但是,在报文段的首部却包含3个重要的信息。首先,SYN比特被置为1。其次,该TCP报文段首部的确认号字段被置为client_isn+1。最后,服务器选择自己的初始序号(server_isn),并将其放置到TCP报文段首部的序号字段中。这个允许连接的报文段实际上表明了:“我收到了你发起建立连接的SYN分组,该分组带有初始序号client_isn。我同意建立该连接。我自己的初始序号是server_isn。”该允许连接的报文段被称为SYN - ACK报文段。
- 在收到SYN - ACK报文段后,客户也要给该连接分配缓存和变量。客户主机则向服务器发送另外一个报文段;这最后一个报文段对服务器的允许连接的报文段进行了确认(该客户通过将值 server_isn+1放置到TCP报文段首部的确认字段中来完成此项工作)。因为连接已经建立了,所以该SYN比特被置为0。该三次握手的第三个阶段可以在报文段负载中携带客户到服务器的数据。
一旦完成这3个步骤,客户和服务器主机就可以相互发送包括数据的报文段了。在以后每一个报文段中,SYN比特都将被置为0。注意到为了创建该连接,在两台主机之间发送了3个分组。由于这个原因,这种连接创建过程通常被称为3次握手。
参与TCP连接的两个进程中任何一个都能终止该连接。当连接结束后,主机中的“资源”(即缓存和变量)将被释放。假设某客户打算关闭连接:
- 客户应用进程发出一个关闭连接命令。这会引起客户TCP向服务器进程发送一个特殊的TCP报文段。这个特殊的报文段让其首部中的一个标志位即FIN比特被设置为1。
- 当服务器接收到该报文段后,就向发送方回送一个确认报文段。然后,服务器发送它自己的终止报文段,其FIN比特被置为1。
- 最后,该客户对这个服务器的终止报文段进行确认。
- 此时,在两台主机上用于该连接的所有资源都被释放了。
在一个TCP连接的生命周期内,运行在每台主机中的TCP协议在各种TCP状态(TCP state)之间变迁。下图说明了客户TCP会经历的一系列典型TCP状态:
- 客户TCP开始时处于CLOSED(关闭)状态。
- 客户的应用程序发起一个新的TCP连接。这引起客户中的TCP向服务器中的TCP发送一个SYN报文段。在发送过SYN报文段后,客户TCP进入了SYN_SENT状态。当客户TCP处在SYN_SENT状态时,它等待来自服务器TCP的对客户所发报文段进行确认且SYN比特被置为1的一个报文段。
- 收到这样一个报文段之后,客户TCP进入ESTABLISHED(已建立)状态。
- 当处在ESTABLISHED状态时,TCP客户就能发送和接收包含有效载荷数据(即应用层产生的数据)的TCP报文段了。
- 假设客户应用程序决定要关闭该连接。
(注意到服务器也能选择关闭该连接。)
这引起客户TCP发送一个带有FIN比特被置为1的TCP报文段,并进入FIN_WAIT_1状态。 - 当处在FIN_WAIT_1状态时,客户TCP等待一个来自服务器的带有确认的TCP报文段。当它收到该报文段时,客户TCP进入FIN_WAIT_2状态。
- 当处在FIN_WAIT_2状态时,客户等待来自服务器的FIN比特被置为1的另一个报文段;当收到该报文段后,客户TCP对服务器的报文段进行确认,并进入TIME_WAIT状态。
- 假定ACK丢失,TIME_WAIT状态使TCP 客户重传最后的确认报文。在TIME_WAIT状态中所消耗的时间是与具体实现有关的,而典型的值是30秒、1分钟或2分钟。经过等待后,连接就正式关闭,客户端所有资源(包括端口号)将被释放。
上面我们站在客户端的视角,看了整个TCP状态转换图,下面我们站在服务器端的视角,来看看TCP状态转换过程:
初始时服务器正在监听客户发送其SYN报文段的端口。我们来考虑当一台主机接收到一个TCP报文段,其端口号或源IP地址与该主机上进行中的套接字都不匹配的情况。
- 例如,假如一台主机接收了具有目的端口80的一个TCP SYN分组,但该主机在端口80不接受连接(即它不在端口80上运行Web服务器)。
- 则该主机将向源发送一个特殊重置报文段。该TCP报文段将RST标志位置为1。
- 因此,当主机发送一个重置报文段时,它告诉该源“我没有那个报文段的套接字。请不要再发送该报文段了”。当一台主机接收一个UDP分组,它的目的端口与进行中的UDP套接字不匹配,该主机发送一个特殊的ICMP数据报。
下面再来简单介绍一下nmap端口扫描工具的实现原理:
- 为了探索目标主机上的一个特定的TCP端口,如端口6789,nmap将对那台主机的目的端口6789发送一个特殊的TCP SYN报文段。有3种可能的输出:
- 源主机从目标主机接收到一个TCP SYN - ACK报文段。因为这意味着在目标主机上一个应用程序使用TCP端口6789运行,nmap返回“打开”。
- 源主机从目标主机接收到一个TCP RST报文段。这意味着该SYN报文段到达了目标主机,但目标主机没有运行一个使用TCP端口6789的应用程序。但攻击者至少知道发向该主机端口6789的报文段没有被源和目标主机之间的任何防火墙所阻挡。
- 源什么也没有收到。这很可能表明该SYN报文段被中间的防火墙所阻挡,无法到达目标主机。
拥塞控制
在前面几节中,我们已经分析了面临分组丢失时用于提供可靠数据传输服务的基本原理及特定的TCP机制。我们以前讲过,在实践中,这种丢包一般是当网络变得拥塞时由于路由器缓存溢出引起的。分组重传因此作为网络拥塞的征兆(某个特定的运输层报文段的丢失)来对待,但是却无法处理导致网络拥塞的原因,因为有太多的源想以过高的速率发送数据。为了处理网络拥塞原因,需要一些机制以在面临网络拥塞时遏制发送方。
拥塞原因
这里以书上三个循序渐进的例子为例进行说明:
- 两个发送方和一台具有无穷大缓存的路由器
随着分组的到达速率接近链路容量时,分组会经历巨大的排队时延。
- 两个发送方和一台具有有限缓存的路由器
随着分组的到达速率接近链路容量时,发送方必须执行重传来补偿因为缓存溢出而丢弃的分组。但是这样做,又可能在发送方遇到大时延时进行不必要的重传,从而加剧传输链路本身的拥塞程度,因为链路上的路由器需要转发不必要的分组副本。
- 四个发送方和具有有限缓存的多台路由器以及多跳路径
当一个分组沿一条路径被丢弃时,每个上游路由器用于转发该分组到丢弃该分组而使用的传输容量最终都会被浪费掉。
拥塞控制方法
关于拥塞控制的方法,可以根据网络层是否为运输层拥塞控制提供显式帮助,来区分拥塞控制方法:
- 端到端拥塞控制。在端到端拥塞控制方法中,网络层没有为运输层拥塞控制提供显式支持。即使网络中存在拥塞,端系统也必须通过对网络行为的观察(如分组丢失与时延)来推断之。
- TCP采用端到端的方法解决拥塞控制,因为IP层不会向端系统提供有关网络拥塞的反馈信息。TCP报文段的丢失(通过超时或3次冗余确认而得知)被认为是网络拥塞的一个迹象,TCP会相应地减小其窗口长度。我们还将看到关于TCP拥塞控制的一些最新建议,即使用增加的往返时延值作为网络拥塞程度增加的指示。
- 网络辅助的拥塞控制。在网络辅助的拥塞控制中,路由器向发送方提供关于网络中拥塞状态的显式反馈信息。这种反馈可以简单地用一个比特来指示链路中的拥塞情况。该方法在早期的IBM SNA[Schwartz 1982]、DEC DECnet [Jain 1989; Ramakrishnan 1990] 和ATM [Black 1995]等体系结构中被采用。更复杂的网络 反馈也是可能的。
- 例如,在ATM可用比特率(Available Bite Rate,ABR)拥塞控制中,路由器显式地通知发送方它(路由器)能在输出链路上支持的最大主机发送速率。如上面所提到的,默认因特网版本的IP和TCP采用端到端拥塞控制方法。然而,IP和TCP也能够选择性地实现网络辅助拥塞控制。
对于网络辅助的拥塞控制,拥塞信息从网络反馈到发送方通常有以下两种方式:
- 直接反馈信息可以由网络路由器发给发送方。这种方式的通知通常采用了一种阻塞分组(choke packet)的形式(主要是说:“我拥塞了!”)。
- 更为通用的第二种形式的通知是,路由器标记或更新从发送方流向接收方的分组中的某个字段来指示拥塞的产生。一旦收到一个标记的分组后,接收方就会向发送方通知该网络拥塞指示。注意到后一种形式的通知至少要经过一个完整的往返时间。
端到端拥塞控制
上一节说过:TCP必须使用端到端拥塞控制而不是使网络辅助的拥塞控制,因为IP层不向端系统提供显式的网络拥塞反馈。
TCP所采用的方法是让每一个发送方根据所感知到的网络拥塞程度来限制其能向连接发送流量的速率。如果一个TCP发送方感知从它到目的地之间的路径上没什么拥塞,则TCP发送方增加其发送速率;如果发送方感知沿着该路径有拥塞,则发送方就会降低其发送速率。但是这种方法提出了三个问题。第一,一个TCP发送方如何限制它向其连接发送流量的速率呢?第二,一个TCP发送方如何感知从它到目的地之间的路径上存在拥塞呢?第三,当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢?
如何限制发送速率 ?
我们首先分析一下TCP发送方是如何限制向其连接发送流量的。之前说过,TCP连接的每一端都是由一个接收缓存、一个发送缓存和几个变量(LastByteRead、rwnd等)组成。运行在发送方的TCP拥塞控制机制跟踪一个额外的变量,即拥塞窗口(congestion window)。拥塞窗口表示为cwnd,它对一个TCP发送方能向网络中发送流量的速率进行了限制。特别是,在一个发送方中未被确认的数据量不会超过cwnd与rwnd中的最小值,即
- LastByteSent - LastByteAcked ≤ min | 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和其后的三个冗余ACK)被解释为跟随该四个ACK的报文段的“丢包事件”的一种隐含的指示。从拥塞控制的观点看,该问题是TCP发送方应当如何减小它的拥塞窗口长度,即减小其发送速率,以应对这种推测的丢包事件。
- 一个确认报文段指示该网络正在向接收方交付发送方的报文段,因此,当对先前未确认报文段的确认到达时,能够增加发送方的速率。确认的到达被认为是一切顺利的隐含指示,即报文段正从发送方成功地交付给接收方,因此该网络不拥塞。拥塞窗口长度因此能够增加。
- 带宽探测。给定ACK指示源到目的地路径无拥塞,而丢包事件指示路径拥塞,TCP调节其传输速率的策略是增加其速率以响应到达的ACK,除非出现丢包事件,此时才减小传输速率。因此,为探测拥塞开始出现的速率,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发送速率起始慢,但在慢启动阶段以指数增长。
但是,何时结束这种指数增长呢?慢启动对这个问题提供了几种答案:
- 首先,如果存在一个由超时指示的丢包事件(即拥塞),TCP发送方将cwnd设置为1MSS并重新开始慢启动过程。它还将第二个状态变量的值ssthresh(“慢启动阈值”的速记)设置为 cwnd/2,即当检测到拥塞时将 ssthresh 置为拥塞窗口值的一半。
- 慢启动结束的第二种方式是直接与ssthresh的值相关联。因为当检测到拥塞时 ssthresh 设为 cwnd 的值一半,当到达或超过ssthresh的值时,继续使 cwnd 翻番可能有些鲁莽。因此,当 cwnd的值等于ssthresh时,结束慢启动并且TCP转移到拥塞避免模式。我们将会看到,当进入拥塞避免模式时,TCP更为谨慎地增加cwnd。
- 最后一种结束慢启动的方式是,如果检测到 3 个冗余 ACK,这时 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的拥塞避免算法行为相同。与慢启动的情况一样,cwnd的值被设置为1个MSS,当丢包事件出现时,ssthresh的值被更新为cwnd值的一半。
- 然而,前面讲过丢包事件也能由一个三个冗余ACK事件触发。在这种情况下,网络继续从发送方向接收方交付报文段(就像由收到冗余ACK所指示的那样)。因此TCP对这种丢包事件的行为,相比于超时指示的丢包,应当不那么剧烈:TCP将cwnd的值减半(为使测量结果更好,对于已收到的3个冗余的ACK要加上3个MSS),并且当收到3个冗余的ACK,将ssthresh的值记录为cwnd的值的一半。接下来进入快速恢复状态。
快速恢复
在快速恢复中,对于引起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越长,路由器中的拥塞越严重。
最后贴出一张慢启动,拥塞避免,快速恢复三个状态的转换图:
- 初始状态为慢启动,拥塞窗口翻倍增长,当窗口大小大于等于ssthresh阈值时,进入拥塞避免状态。当出现三次冗余ACK时,进入快速恢复状态。出现超时或者接收到新的ACK情况,还是保持慢启动状态不变。
- 拥塞避免状态下,如果发生超时丢包,则退回慢启动状态。如果出现三次冗余ACK则转变为快速恢复状态。
- 快速恢复状态下,如果发生超时丢包,则退回慢启动状态。如果接受到新的ACK,则转变为拥塞避免状态。
拥塞窗口阈值大小和拥塞窗口大小的变化,可以参考上图学习,这里就不再赘述。
公平性
考虑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的连接享用更高的吞吐量。
自私自利的UDP
我们刚才已经看到,TCP拥塞控制是如何通过拥塞窗口机制来调节一个应用程序的传输速率的。许多多媒体应用如因特网电话和视频会议,经常就因为这种特定原因而不在TCP上运行,因为它们不想其传输速率被扼制,即使在网络非常拥塞的情况下。
相反,这些应用宁可在UDP上运行,UDP是没有内置的拥塞控制的。当运行在UDP上时,这些应用能够以恒定的速率将其音频和视频数据注入网络之中并且偶尔会丢失分组,而不愿在拥塞时将其发送速率降至“公平”级别并且不丢失任何分组。
从TCP的观点来看,运行在UDP上的多媒体应用是不公平的,因为它们不与其他连接合作,也不适时地调整其传输速率。因为TCP拥塞控制在面临拥塞增加(丢包)时,将降低其传输速率,而UDP源则不必这样做,UDP源有可能压制TCP流量。当今的一个主要研究领域就是开发一种因特网中的拥塞控制机制,用于阻止UDP流量不断压制直至中断因特网吞吐量的情况。
并行TCP连接
即使我们能够迫使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连接建立和慢启动对时延的影响。我们观察到在许多重要场合,连接建立和慢启动会对端到端时延产生严重影响。
最后,我们再来看看其他的一些有趣协议:
- 数据报拥塞控制协议(Datagram Congestion Control Protocol,DCCP)[RFC 4340]提供 了一种低开销、面向报文,类似于UDP的不可靠服务,但是具有应用程序可选择的拥塞控制形式,该机制与TCP相兼容。如果某应用程序需要可靠的或半可靠的数据传送,则这将在应用程序自身中执行。DCCP被设想用于诸如流媒体等应用程序中,DCCP能够利用数据交付的预定时间和可靠性之间的折中,但是要对网络拥塞做出响应。
- 在谷歌的Chromium 浏览器中实现了QUIC(Quick UDP Intemnet Connections)协议 [lyengar 2016],该协议通过重传以及差错检测、快速连接建立和基于速率的拥塞控制算法提供可靠性,而基于速率的拥塞控制算法是以TCP友好特性为目标,这些机制都是在UDP之上作为应用层协议实现的。2015年年初,谷歌报告从Chrome浏览器到谷歌服务器的大约一半请求运行在QUIC之上。
- DCTCP(数据中心TCP)[Alizadeh 2010]是一种专门为数据中心网络设计的TCP版本,使用ECN以更好地支持短流和长流的混合流,这种混合流代表了数据中心负载的特征。
- 流控制传输协议(Stream Control Transmission Protocol,SCTP)[RFC 4960,RFC 3286] 是一种可靠的、面向报文的协议,该协议允许几个不同的应用层次的“流”复用到单个SCTP连接上(一种称之为“多流”的方法)。从可靠性的角度看,在该连接中的不同流被分别处理,因此在一条流中的分组丢失不会影响其他流中数据的交付。当一台主机与两个或更多个网络连接时,SCTP也允许数据经两条出路径传输,还具有失序数据的选项交付和一些其他特色。SCTP的流控制和拥塞控制算法基本上与TCP中的相同。
- TCP友好速率控制(TCP-Friendly Rate Control,TFRC)协议[RFC 5348]是一种拥 塞控制协议而不是一种功能齐全的运输层协议。它定义了一种拥塞控制机制,该机制能被用于诸如DCCP等其他运输协议(事实上在DCCP中可供使用的两种应用程序可选的协议之一就是TFRC)。TFRC的目标是平滑在TCP 拥塞控制中的“锯齿”行为,同时维护一种长期的发送速率,该速率“合理地”接近TCP的速率。使用比TCP更为平滑的发送速率,TFRC非常适合诸如IP电话或流媒体等多媒体应用,这种平滑的速率对于这些应用是重要的。TFRC是一种“基于方程”的协议,这些协议使用测得的丢包率作为方程的输入,即使用方程估计一个TCP会话在该丢包率下TCP的吞吐量将是多大。该速率则被取为TFRC的目标发送速率。