TCP已经在从1200 b/s的拨号SLIP链路到以太数据链路上运行了许多年。在80年代和90年代初期,以太网是运行TCP/IP最主要的数据链路方式。虽然TCP在比以太网速率高的环境(如T2电话线、FDDI及千兆比网络)中也能够正确运行,但在这些高速率环境下,TCP的某些限制就会暴露出来。
路径MTU发现
路径MTU即当前在两个主机之间的路径上任何网络上的最小MTU。路径MTU发现在IP首部中继承并设置“不要分片(DF)”比特,来发现当前路径上的路由器是否需要对正在发送的IP数据报进行分片。如果一个待转发的IP数据报被设置DF比特,而其长度又超过了MTU,那么路由器将返回ICMP不可达的差错。
TCP的路径MTU发现按如下方式进行:在连接建立时,TCP使用输出接口或对端声明的MSS中的最小MTU作为起始的报文段大小。路径MTU发现不允许TCP超过对端声明的MSS。如果对端没有指定一个MSS,则默认为536。
一旦选定了起始的报文段大小,在该连接上的所有被TCP发送的IP数据报都将被设置DF比特。如果某个中间路由器需要对一个设置了DF标志的数据报进行分片,它就丢弃这个数据报,并产生一个ICMP的“不能分片”差错。
如果收到这个ICMP差错,TCP就减少段大小并进行重传。如果路由器产生的是一个较新的该类ICMP差错,则报文段大小被设置为下一跳的MTU减去IP和TCP的首部长度。如果是一个较旧的该类ICMP差错,则必须尝试下一个可能的最小MTU。当由这个ICMP差错引起的重传发生时,拥塞窗口不需要变化,但要启动慢启动。
由于路由可以动态变化,因此在最后一次减少路径MTU的一段时间以后,可以尝试使用一个较大的值(直到等于对端声明的MSS或输出接口MTU的最小值)。RFC 1191推荐这个时间间隔为10分钟。
在对非本地目的地,默认的MSS通常为536字节,路径MTU发现可以避免在通过MTU小于576(这非常罕见)的中间链路时进行分片。对于本地目的主机,也可以避免在中间链路(如以太网)的MTU小于端点网络(如令牌环网)的情况下进行分片。但为了能使路径MTU更加有用和充分利用MTU大于576的广域网,一个实现必须停止使用为非本地目的制定的536的MTU默认值。MSS的一个较好的选择是输出接口的MTU(当然要减去IP和TCP的首部大小)。
大分组还是小分组
常规知识告诉我们较大的分组比较好 [Mogul 1993,15.2.8节],因为发送较少的大分组比发送较多的小分组“花费”要少(假定分组的大小不足以引起分片,否则会引起其他方面的问题)。这些减少的花费与网络(分组首部负荷)、路由器(选路的决定)和主机(协议处理和设备中断)等有关。
长肥管道
我们把一个连接的容量表示为:
$$capacity(b) = bandwidth(b/s) \times round - triptimes(s)$$
并称之为带宽时延乘积。也可称它为两端的管道大小。
当这个乘积变得越来越大时,TCP的某些局限性就会暴露出来。
具有大的带宽时延乘积的网络被称为长肥网络(Long Fat Network,即LFN,发音为“elefan(t)s”),而一个运行在LFN上的TCP连接被称为长肥管道。管道可以被水平拉长(一个长的RTT),或被垂直拉高(较高的带宽),或向两个方向拉伸。使用长肥管道会遇到多种问题。
- TCP首部中窗口大小为16 bit,从而将窗口限制在65535个字节内。
- 在一个长肥网络LFN内的分组丢失会使吞吐量急剧减少。
- 许多TCP实现对每个窗口的RTT仅进行一次测量。它们并不对每个报文段进行RTT测量。在一个长肥网络LFN上需要更好的RTT测量机制。
- TCP对每个字节数据使用一个32 bit无符号的序号来进行标识。如果在网络中有一个被延迟一段时间的报文段,它所在的连接已被释放,而一个新的连接在这两个主机之间又建立了,怎样才能防止这样的报文段再次出现呢?IP首部中的TTL为每个IP段规定了一个生存时间的上限—255跳或255秒,看哪一个上限先达到。我们定义了最大的报文段生存时间(MSL)作为一个实现的参数来阻止这种情况的发生。推荐的MSL的值为2分钟(给出一个240秒的2MSL)。
在长肥网络LFN上,TCP的序号会碰到一个不同的问题。由于序号空间是有限的,在已经传输了4294 967 296个字节以后序号会被重用。如果一个包含序号N字节数据的报文段在网络上被迟延并在连接仍然有效时又出现,会发生什么情况呢?这仅仅是一个相同序号N在MSL期间是否被重用的问题,也就是说,网络是否足够快以至于在不到一个MSL的时候序号就发生了回绕。在一个以太网上要发送如此多的数据通常需要60分钟左右,因此不会发生这种情况。但是在带宽增加时,这个时间将会减少:一个T3的电话线(45 Mb/s)在12分钟内会发生回绕,FDDI(100 Mb/s)为5分钟,而一个千兆比网络(1000 Mb/s)则为34秒。这时问题不再是带宽时延乘积,而在于带宽本身。
窗口扩大选项
窗口扩大选项使TCP的窗口定义从16 bit增加为32 bit。这并不是通过修改TCP首部来实现的,TCP首部仍然使用16 bit,而是通过定义一个选项实现对16 bit的扩大操作(scaling operation)来完成的。于是TCP在内部将实际的窗口大小维持为32 bit的值。
这个选项只能够出现在一个SYN报文段中,因此当连接建立起来后,在每个方向的扩大因子是固定的。为了使用窗口扩大,两端必须在它们的SYN报文段中发送这个选项。主动建立连接的一方在其SYN中发送这个选项,但是被动建立连接的一方只能够在收到带有这个选项的SYN之后才可以发送这个选项。每个方向上的扩大因子可以不同。
如果主动连接的一方发送一个非零的扩大因子,但是没有从另一端收到一个窗口扩大选项,它就将发送和接收的移位记数器置为0。这就允许较新的系统能够与较旧的、不理解新选项的系统进行互操作。
假定我们正在使用窗口扩大选项,发送移位记数为S,而接收移位记数则为R。于是我们从另一端收到的每一个16 bit的通告窗口将被左移R位以获得实际的通告窗口大小。每次当我们向对方发送一个窗口通告的时候,我们将实际的32 bit窗口大小右移S比特,然后用它来替换TCP首部中的16 bit的值。
TCP根据接收缓存的大小自动选择移位计数。这个大小是由系统设置的,但是通常向应用进程提供了修改途径。
时间戳选项
时间戳选项使发送方在每个报文段中放置一个时间戳值。接收方在确认中返回这个数值,从而允许发送方为每一个收到的ACK计算RTT(我们必须说“每一个收到的ACK”而不是“每一个报文段”,是因为TCP通常用一个ACK来确认多个报文段)。我们提到过目前许多实现为每一个窗口只计算一个RTT,对于包含8个报文段的窗口而言这是正确的。然而,较大的窗口大小则需要进行更好的RTT计算。
发送方在时间戳选项的第一个字段中防止一个32bit的值,接收方在应答字段中回显这个数值。包含这个选项的TCP首部长度将从正常的20字节增加为32字节。
时间戳是一个单调递增的值。由于接收方只需要回显收到的内容,因此不需要关注时间戳单元是什么。这个选项不需要在两个主机之间进行任何形式的时钟同步。RFC 1323推荐在1毫秒和1秒之间将时间戳的值加1。
在连接建立阶段,对这个选项的规定与前一节讲的窗口扩大选项类似。主动发起连接的一方在它的SYN中指定选项。只有在它从另一方的SYN中收到了这个选项之后,该选项才会在以后的报文段中进行设置。
TCP不需要对每个包含数据的报文段进行确认,许多实现每两个报文段发送一个ACK。如果接收方发送一个确认了两个报文段的ACK,那么哪一个收到的时间戳应当放入回显应答字段中来发回去呢?
为了减少任一端所维持的状态数量,对于每个连接只保持一个时间戳的数值。选择何时更新这个数值的算法非常简单:
- TCP跟踪下一个ACK中将要发送的时间戳的值(一个名为tsrecent的变量)以及最后发送的ACK中的确认序号(一个名为lastack的变量)。这个序号就是接收方期望的序号。
- 当一个包含有字节号lastack的报文段到达时,则该报文段中的时间戳被保存在tsrecent中。
- 无论何时发送一个时间戳选项,tsrecent就作为时间戳回显应答字段被发送,而序号字段被保存在lastack中。
这个算法能够处理下面两种情况:
- 如果ACK被接收方迟延,则作为回显值的时间戳值应该对应于最早被确认的报文段。例如,如果两个包含1~1024和1025~2048字节的报文段到达,每一个都带有一个时间戳选项,接收方产生一个ACK 2049来对它们进行确认。此时,ACK中的时间戳应该是包含字节1~1024的第1个报文段中的时间戳。这种处理是正确的,因为发送方在进行重传超时时间的计算时,必须将迟延的ACK也考虑在内。
- 如果一个收到的报文段虽然在窗口范围内但同时又是失序,这就表明前面的报文段已经丢失。当那个丢失的报文段到达时,它的时间戳(而不是失序的报文段的时间戳)将被回显。例如,假定有3个各包含1024字节数据的报文段,按如下顺序接收:包含字节1~1024的报文段1,包含字节2049~4072的报文段3和包含字节1025~2048的报文段2。返回的ACK应该是带有报文段1的时间戳的ACK 1025(一个正常的所期望的对数据的ACK)、带有报文段1的时间戳的ACK 1025(一个重复的、响应位于窗口内但却是失序的报文段的ACK),然后是带有报文段2的时间戳的ACK 3073(不是报文段3中的较后的时间戳)。这与当报文段丢失时的对RTT估计过高具有同样的效果,但这比估计过低要好些。而且,如果最后的ACK含有来自报文段3的时间戳,它可以包括重复的ACK返回和报文段2被重传所需要的时间,或者可以包括发送方的报文段2的重传超时定时器到期的时间。无论在哪一种情况下,回显报文段3的时间戳将引起发送方的RTT计算出现偏差。
尽管时间戳选项能够更好地计算RTT,它还为发送方提供了一种方法,以避免接收到旧的报文段,并认为它们是现在的数据的一部分。
PAWS:防止回绕的序号
32 bit的序号在高速连接中很容易发生回绕。
可以将时间戳视为序列号的一个32 bit的扩展。
PAWS算法不需要在发送方和接收方之间进行任何形式的时间同步。接收方所需要的就是时间戳的值应该单调递增,并且每个窗口至少增加1。
T/TCP:为事务用的TCP扩展
TCP提供的是一种虚电路方式的运输服务。一个连接的生存时间包括三个不同的阶段:建立、数据传输和终止。这种虚电路服务非常适合诸如远程注册和文件传输之类的应用。
但是,还有出现其他的应用进程被设计成使用事务服务。一个事务(transaction)就是符合下面这些特征的一个客户请求及其随后的服务器响应。
- 应该避免连接建立和连接终止的开销,在可能的时候,发送一个请求分组并接收一个应答分组。
- 等待时间应当减少到等于RTT与SPT之和。其中RTT(Round-Trip Time)为往返时间,而SPT(Server Processing Time)则是服务器处理请求的时间。
- 服务器应当能够检测出重复的请求,并且当收到一个重复的请求时不重新处理事务(避免重新处理意味着服务器不必再次处理请求,而是返回保存的、与该请求对应的应答)。
一个使用这种类型服务的应用就是域名服务,尽管DNS与服务器重新处理重复的请求无关。
如今一个应用程序设计人员面对的一种选择是使用TCP还是UDP。TCP提供了过多的事务特征,而UDP提供的则不够。通常应用程序使用UDP来构造(避免TCP连接的开销),而许多需要的特征(如动态超时和重传、拥塞避免等)被放置在应用层,一遍又一遍的重新设计和实现。
一个较好的解决方法是提供一个能够提供足够多的事务处理功能的运输层。
大多数的TCP需要使用7个报文段来打开和关闭一个连接。现在增加三个报文段:一个对应于请求,一个对应于应答和对请求的确认,第三个对应于对应答的确认。如果额外的控制比特被追加到报文段上—也就是,第1个报文段带有SYN、客户请求和一个FIN—客户仍然能够看到一个2倍的RTT与SPT之和的最小开销(与数据一起发送一个SYN和FIN是合法的;当前的TCP是否能够正确处理它们是另外一个问题)。
另一个与TCP有关的问题是TIME_WAIT状态和它需要的2MSL的等待时间。正如在习题18.14中看到的,这使两个主机之间的事务率降低到每秒268个。
TCP为处理事务而需要进行的两个改动是避免三次握手和缩短WAIT_TIME状态。T/TCP通过使用加速打开来避免三次握手:
- 它为打开的连接指定一个32 bit的连接计数CC(Connection Count),无论主动打开还是被动打开。一个主机的CC值从一个全局计数器中获得,该计数器每次被使用时加1。
- 在两个使用T/TCP的主机之间的每一个报文段都包括一个新的TCP选项CC。这个选项的长度为6个字节,包含发送方在该连接上的32 bit的CC值。
- 一个主机维持一个缓存,该缓存保留每个主机上一次的CC值,这些值从来自这个主机的一个可接受的SYN报文段中获得。
- 当在一个开始的SYN中收到一个CC选项的时候,接收方比较收到的值与为该发送方缓存的CC值。如果接收到的CC比缓存的大,则该SYN是新的,报文段中的任何数据被传递给接收应用进程(服务器)。这个连接被称为半同步。如果接收的CC比缓存的小,或者接收主机上没有对应这个客户的缓存CC,则执行正常的TCP三次握手过程。
- 为响应一个开始的SYN,带有SYN和ACK的报文段在另一个被称为CCECHO的选项中回显所接收到的CC值。
- 在一个非SYN报文段中的CC值检测和拒绝来自同一个连接的前一个替身的任何重复的报文段。
这种“加速打开”避免了使用三次握手的要求,除非客户或者服务器已经崩溃并重新启动。这样做的代价是服务器必须记住从每个客户接收的最近的CC值。
基于在两个主机之间测量RTT来动态计算TIME_WAIT的延时,可以缩短TIME_WAIT状态。TIME_WAIT时延被设置为8倍的重传超时值RTO。
通过使用这些特征,最小的事务序列是交换三个报文段:
- 由一个主动打开引起的客户到服务器:客户的SYN、客户的数据(请求)、客户的FIN以及客户的CC。当被动的服务器TCP接收到这个报文段的时候,如果客户的CC比为这个客户缓存的CC要大,则客户的数据被传送给服务器应用程序进行处理。
- 服务器到客户:服务器的SYN、服务器的数据(应答)、服务器的FIN、对客户的FIN的ACK、服务器的CC以及客户的CC的CCECHO。由于TCP的确认是累积的,这个对客户的FIN的ACK也对客户的SYN、数据及FIN进行了确认。
当客户TCP接收到这个报文段,就将其传送给客户应用进程。 - 客户到服务器:对服务器的FIN的ACK,它也确认了服务器的SYN、数据和FIN。客户对它的请求的响应时间为RTT与SPT的和。
在参考资料中有许多关于实现这个TCP选项的很好的地方。我们在这里将它们归纳如下:
- 服务器的SYN和ACK(第2个报文段)必须被迟延,从而允许应答与它一起捎带发送(通常对SYN的ACK是不迟延的)。但它也不能迟延得太多,否则客户将超时并引起重传。
- 请求可以需要多个报文段,但是服务器必须对它们可能失序达到的情况进行处理(通常当数据在SYN之前到达时,该数据被丢弃并产生一个复位。通过使用T/TCP,这些失序的数据将放入队列中处理)。
- API必须使服务器进程用一个单一的操作来发送数据和关闭连接,从而允许第二个报文段中的FIN与应答一起捎带发送(通常应用进程先写应答,从而引起发送一个数据报文段,然后关闭连接,引起发送FIN)。
- 在收到来自服务器的MSS通告之前,客户在第1个报文段中正在发送数据。为避免限制客户的MSS为536,一个给定主机的MSS应该与它的CC值一起缓存。
- 客户在没有接收到来自服务器的窗口通告之前也可以向服务器发送数据。T/TCP建议默认的窗口为4096,并且也为服务器缓存拥塞门限。
- 使用最小3个报文段交换,在每个方向上只能计算一个RTT。加上包括了服务器处理时间的客户测量RTT。这意味着被平滑的RTT及其方差的值也必须为服务器缓存起来。
T/TCP的特征中吸引人的地方在于它对现有协议进行了最小的修改,同时又兼容了现有的实现。它还利用了TCP中现有的工程特征(动态超时和重传、拥塞避免等),而不是迫使应用进程来处理这些问题。
一个可作为替换的事务协议是通用报文事务协议VMTP(Versatile Message Transaction Protocol),该协议在RFC 1045 [Cheriton 1988]中进行了描述。与T/TCP是现有协议的一个小的扩充不同,VMTP是使用IP的一个完整的运输层。VMTP处理差错检测、重传和重复压缩。它还支持多播通信。
TCP的性能
下面这些实际限制适用于任何的实际情况[Borman 1991]。
- 不能比最慢的链路运行得更快。
- 不能比最慢的机器的内存运行得更快。这假定实现是只使用一遍数据。如果不是这样(也就是说,实现使用一遍数据是将它从用户空间复制到内核中,而使用另一遍数据是计算TCP的检验和),那么将运行得更慢。[Dalton et al.1993]描述了将数据复制数目减少从而使一个标准伯克利源程序的性能得到改进。[Partridge and Pink 1993]将类似的“复制与检验和”的改变与其他性能改进措施一道应用于UDP,从而将UDP的性能提高了约30%。
- 不能够比由接收方提供的窗口大小除以往返时间所得结果运行得更快(这就是带宽时延乘积公式,使用窗口大小作为带宽时延乘积,并解出带宽)。