TCP/IP详解--第二十章
第20章 TCP的成块数据流
20.1 引言
在第 15章我们看到 TFTP使用了停止等待协议。数据发送方在发送下一个数据块之前需要 等待接收对已发送数据的确认。本章我们将介绍 TCP所使用的被称为滑动窗口协议的另一种 形式的流量控制方法。该协议允许发送方在停止并等待确认前可以连续发送多个分组。由于 发送方不必每发一个分组就停下来等待确认,因此该协议可以加速数据的传输。
我们还将介绍 TCP的PUSH标志,该标志在前面的许多例子中都出现过。此外,我们还要 介绍慢启动, TCP使用该技术在一个连接上建立数据流,最后介绍成块数据流的吞吐量。
20.2 正常数据流
我们以从主机 svr4单向传输 8192个字节到主机 bsdi开始。在 bsdi上运行 sock程序作 为服务器:
bsdi % sock -i -s 7777
其中,标志 -i和-s指示程序作为一个“吸收( sink)”服务器运行(从网络上读取并丢弃 数据),服务器端口指明为 7777。相应的客户程序运行为:
svr4 % sock -i -n8 bsdi 7777
该命令指示客户向网络发送 8个1024字节的数据。图 20-1显示了这个过程的时间系列。我 们在输出的前 3个报文段中显示了每一端 MSS的值。
发送方首先传送 3个数据报文段( 4~6)。下一个报文段( 7)仅确认了前两个数据报文段, 这可以从其确认序号为 2048而不是 3073看出来。
报文段 7的ACK的序号之所以是 2048而不是 3073是由以下原因造成的:当一个分组到达时, 它首先被设备中断例程进行处理,然后放置到 IP的输入队列中。三个报文段 4、5和6依次到达 并按接收顺序放到 IP的输入队列。 IP将按同样顺序将它们交给 TCP 。当 TCP处理报文段 4时, 该连接被标记为产生一个经受时延的确认。 TCP处理下一报文段( 5),由于 TCP现在有两个未 完成的报文段需要确认,因此产生一个序号为 2048的ACK(报文段 7),并清除该连接产生经 受时延的确认标志。 TCP处理下一个报文段( 6),而连接又被标志为产生一个经受时延的确 认。在报文段9到来之前,由于时延定时器溢出,因此产生一个序号为 3073的ACK(报文段 8)。 报文段 8中的窗口大小为 3072,表明在 TCP的接收缓存中还有 1024个字节的数据等待被应用程 序读取。
报文段 11~16说明了通常使用的“隔一个报文段确认”的策略。报文段 11、12和13到达并 被放入 IP的接收队列。当报文段 11被处理时,连接被标记为产生一个经受时延的确认。当报 文段 12 被处理时,它们的 ACK(报文段 14 )被产生且连接的经受时延的确认标志被清除。报 文段 13使得连接再次被标记为产生经受时延。但在时延定时器溢出之前,报文段 15处理完毕, 因此该确认立刻被发送。
图20-1 从svr4 传输8192个字节到bsdi
注意到报文段 7、14和16中的 ACK确认了两个收到的报文段是很重要的。使用 TCP的滑动 窗口协议时,接收方不必确认每一个收到的分组。在 TCP 中, ACK是累积的 — 它们表示接 收方已经正确收到了一直到确认序号减1的所有字节。在本例中,三个确认的数据为 2048字节 而两个确认的数据为 1024字节(忽略了连接建立和终止中的确认)。
用tcpdump看到的是 TCP的动态活动情况。我们在线路上看到的分组顺序依赖于许多无 法控制的因素:发送方 TCP的实现、接收方 TCP的实现、接收进程读取数据(依赖于操作系统 的调度)和网络的动态性(如以太网的冲突和退避等)。对这两个 TCP而言,没有一种单一的、 正确的方法来交换给定数量的数据。
为显示情况可能怎样变化,图 20-2显示了在同样两个主机之间交换同样数据时的另一个 时间系列,它们是在图 20-1所示的几分钟之后截获的。
一些情况发生了变化。这一次接收方没有发送一个序号为 3073的ACK,而是等待并发送 序号为 4097的ACK。接收方仅发送了 4个ACK(报文段 7、10、12和15):三个确认了 2048字
节,另一个确认了 1024字节。最后 1024字节数据的 ACK出现在报文段 17中,它与 FIN的ACK
一道发送(比较该图中的报文段 17与图20-1中的报文段 16和18)。
图20-2 从svr4 到bsdi 的另外8192字节数据的传输过程
快的发送方和慢的接收方
图20-3显示了另外一个时间系列。这次是从一个快的发送方(一个 Sparc工作站)到一个 慢的接收方(配有慢速以太网卡的 80386机器)。它的动态活动情况又有所不同。
发送方发送 4个背靠背( back-to-back)的数据报文段去填充接收方的窗口,然后停下来 等待一个 ACK。接收方发送 ACK(报文段 8),但通告其窗口大小为 0,这说明接收方已收到 所有数据,但这些数据都在接收方的 TCP 缓冲区,因为应用程序还没有机会读取这些数据。 另一个 ACK(称为窗口更新)在 17.4 ms 后发送,表明接收方现在可以接收另外的 4096个字节 的数据。虽然这看起来像一个 ACK,但由于它并不确认任何新数据,只是用来增加窗口的右 边沿,因此被称为窗口更新。
发送方发送最后 4个报文段( 10~13),再次填充了接收方的窗口。注意到报文段 13中包括 两个比特标志:PUSH和FIN。随后从接收方传来另外两个 ACK,它们确认了最后的 4096字节 的数据(从 4097到8192字节)和 FIN(标号为 8192)。
图20-3 从一个快发送方发送8192字节的数据到一个慢接收方
20.3 滑动窗口
图20-4用可视化的方法显示了我们在前一节观察到的滑动窗口协议。
提供的窗口 由接收方通告
可用的窗口
发送并被确认
不能够发送,
发送,但未被确认 直至窗口移动 能够发送ASAP
图20-4 TCP滑动窗口的可视化表示
在这个图中,我们将字节从 1至11进行标号。接收方通告的窗口称为提出的窗口( offered window),它覆盖了从第 4字节到第 9字节的区域,表明接收方已经确认了包括第 3字节在内的 数据,且通告窗口大小为 6。回顾第 17章,我们知道窗口大小是与确认序号相对应的。发送方 计算它的可用窗口,该窗口表明多少数据可以立即被发送。
当接收方确认数据后,这个滑动窗口不时地向右移动。窗口两个边沿的相对运动增加或 减少了窗口的大小。我们使用三个术语来描述窗口左右边沿的运动:
1) 称窗口左边沿向右边沿靠近为窗口合拢。这种现象发生在数据被发送和确认时。
2) 当窗口右边沿向右移动时将允许发送更多的数据,我们称之为窗口张开。这种现象发 生在另一端的接收进程读取已经确认的数据并释放了 TCP的接收缓存时。
3) 当右边沿向左移动时,我们称之为窗口收缩。 Host Requirements RFC 强烈建议不要使 用这种方式。但 TCP必须能够在某一端产生这种情况时进行处理。第 22.3节给出了这样的一个 例子,一端希望向左移动右边沿来收缩窗口,但没能够这样做。
|
合拢 收缩 张开 窗 口
图20-5 窗口边沿的移动
如果左边沿到达右边沿,则称其为一个零窗口,此时发送方不能够发送任何数据。
一个例子
图20-6显示了在图 20-1所示的数据传输过程中滑动窗口协议的动态性。
由第2个报文段通告的窗口
在第4,5,6报文段中发送的数据
被第7报文段确认
由第7个报文段通告的窗口
被第8报 文段确认
由第8个报文段通告的窗口在第9报文段
中发送的数据 被第10报
文段确认
由第10个报文段通告的窗口
在第11,12,13报文段 中发送的数据
被第14报
文段确认
由第14个报文段 通告的窗口
在第15报文段
中发送的数据 被第16报
文段确认
图20-6 图20-1的滑动窗口协议
以该图为例可以总结如下几点:
1) 发送方不必发送一个全窗口大小的数据。
2) 来自接收方的一个报文段确认数据并把窗口向右边滑动。这是因为窗口的大小是相对 于确认序号的。
3) 正如从报文段 7到报文段 8中变化的那样,窗口的大小可以减小,但是窗口的右边沿却 不能够向左移动。
4) 接收方在发送一个 ACK前不必等待窗口被填满。在前面我们看到许多实现每收到两个
报文段就会发送一个 ACK。 下面我们可以看到更多的滑动窗口协议动态变化的例子。
20.4 窗口大小
由接收方提供的窗口的大小通常可以由接收进程控制,这将影响 TCP的性能。
4.2BSD默认设置发送和接受缓冲区的大小为 2048个字节。在4.3BSD中双方被增加 为 4 0 9 6 个字节。正如我们在本书中迄今为止所看到的例子一样, SunOS 4.1.3 、 BSD/386和SVR4仍然使用4096字节的默认大小。其他的系统,如Solaris 2.2、4.4BSD和 AIX3.2则使用更大的默认缓存大小,如8192或16384等。
插口 API允许进程设置发送和接收缓存的大小。接收缓存的大小是该连接上所能 够通告的最大窗口大小。有一些应用程序通过修改插口缓存大小来增加性能。
[Mogul 1993] 显示了在改变发送和接收缓存大小(在单向数据流的应用中,如文件传输, 只需改变发送方的发送缓存和接收方的接收缓存大小)的情况下,位于以太网上的两个工作站之间进行文件传输时的一些结果。它表明对以太网而言,默认的 4096字节并不是最理想的 大小,将两个缓存增加到 1 6 3 8 4 个字节可以增加约 4 0 % 左右的吞吐量。在 [ P a p a d o p o u l o s和 Parulkar 1993]中也有相似的结果。
在20.7节中,我们将看到在给定通信媒体带宽和两端往返时间的情况下,如何计算最小的 缓存大小。
一个例子 可以使用 sock程序来控制这些缓存的大小。我们以如下方式调用服务器程序: bsdi % sock -i -s -R6144 5555
该命令设置接收缓存为 6144个字节( -R选项)。接着我们在主机 sun上启动客户程序并使 之发送 8192个字节的数据:
sun % sock -i -n1 -w8192 bsdi 5555
图20-7显示了结果。
首先注意到的是在报文段 2中提供的窗口大小为 6144字节。由于这是一个较大的窗口,因 此客户立即连续发送了 6个报文段( 4~9),然后停止。报文段 10确认了所有的数据(从第 1到
6144字节),但提供的窗口大小却为 2048,这很可能是接收程序没有机会读取多于 2048字节的 数据。报文段 11和12完成了客户的数据传输,且最后一个报文段带有 FIN标志。
报文段 13 包含与报文段 10相同的确认序号,但通告了一个更大的窗口大小。报文段 14确
认了最后的 2048字节的数据和 FIN,报文段 15和16仅用于通告一个更大的窗口大小。报文段
17和18完成通常的关闭过程。
图20-7 接收方提供一个6144字节的接收窗口的情况下的数据传输
20.5 PUSH标志
在每一个 TCP例子中,我们都看到了 PUSH标志,但一直没有介绍它的用途。发送方使用 该标志通知接收方将所收到的数据全部提交给接收进程。这里的数据包括与 PUSH一起传送的 数据以及接收方 TCP已经为接收进程收到的其他数据。
在最初的 TCP规范中,一般假定编程接口允许发送进程告诉它的 TCP何时设置 PUSH标志。 例如,在一个交互程序中,当客户发送一个命令给服务器时,它设置 PUSH标志并停下来等待 服务器的响应(在习题 19.1中我们假定当发送 12字节的请求时客户设置 PUSH 标志)。通过允 许客户应用程序通知其TCP设置 PUSH标志,客户进程通知 TCP在向服务器发送一个报文段时 不要因等待额外数据而使已提交数据在缓存中滞留。类似地,当服务器的 TCP接收到一个设 置了 PUSH标志的报文段时,它需要立即将这些数据递交给服务器进程而不能等待判断是否还 会有额外的数据到达。
然而,目前大多数的 API没有向应用程序提供通知其 TCP设置 PUSH标志的方法。的确, 许多实现程序认为 PUSH标志已经过时,一个好的 TCP实现能够自行决定何时设置这个标志。
如果待发送数据将清空发送缓存,则大多数的源于伯克利的实现能够自动设置 PUSH标志。 这意味着我们能够观察到每个应用程序写的数据均被设置了 PUSH标志,因为数据在写的时候
就立即被发送。
代码中的注释表明该算法对那些只有在缓存被填满或收到一个 PUSH标志时才向应 用程序提交数据的TCP实现有效。
使用插口 API通知 TCP设置正在接收数据的 PUSH标志或得到该数据是否被设置
PUSH标志的信息是不可能的。
由于源于伯克利的实现一般从不将接收到的数据推迟交付给应用程序,因此它们忽略所接收的 PUSH标志。
举例
在图20-1中我们观察到所有 8个数据报文段( 4~6、9、11~13和15)的PUSH标志均被置 1, 这是因为客户进行了8次1024字节数据的写操作,并且每次写操作均清空了发送缓存。
再次观察图 20-7,我们预计报文段 12中的 PUSH标志被置 1,因为它是最后一个报文段。 为什么发送方知道有更多的数据需要发送还设置报文段 7中的 PUSH标志呢?这是因为虽然我 们指定写的是 8192个字节的数据,但发送方的发送缓存却是 4096个字节。
值得注意的另外一点是在图 20-7中的第 14、15 和16这三个连续的确认报文段。在图 20-3 中我们也观察到了两个连续的 ACK,但那是因为接收方已经通告其窗口为 0(使发送方停止)。 当窗口张开时,需要发送另一个窗口非 0的ACK来使发送方重新启动。可是,在图 20-7中,窗 口的大小从来没有达到过0。然而,当窗口大小增加了 2048个字节的时候,另一个 ACK(报文 段15和16) 被发送以通知对方窗口被更新(在报文段 15和16中,这两个窗口更新是不需要的, 因为已经收到了对方的 FIN,表明它不会再发送任何数据)。许多 TCP实现在窗口大小增加了 两个最大报文段长度(本例中为 2048字节,因为 MSS为1024字节)或者最大可能窗口的 50%
(本例中为 2048字节,因为最大窗口大小为 4096字节)时发送这个窗口更新。在第 22.3节详细 考察糊涂窗口综合症的时候,我们还会看到这种现象。
作为PUSH标志的另一个例子,再次回到图 20-3。我们之所以看到前 4个报文段( 4~7)的 标志被设置,是因为它们每一个均使 TCP产生了一个报文段并提交给 IP层。但是随后, TCP停 下来等待一个确认来移动4096字节的窗口。在此期间, TCP又得到了应用程序的最后 4096个 字节的数据。当窗口张开时(报文段 9),发送方 TCP知道它有 4个可立即发送的报文段,因此 它只设置了最后一个报文段( 13)的PUSH标志。
20.6 慢启动
迄今为止,在本章所有的例子中,发送方一开始便向网络发送多个报文段,直至达到接收方通告的窗口大小为止。当发送方和接收方处于同一个局域网时,这种方式是可以的。但 是如果在发送方和接收方之间存在多个路由器和速率较慢的链路时,就有可能出现一些问题。一些中间路由器必须缓存分组,并有可能耗尽存储器的空间。 [Jacobson 1988] 证明了这种连 接方式是如何严重降低了 TCP连接的吞吐量的。
现在, TCP需要支持一种被称为“慢启动 (slow start) ”的算法。该算法通过观察到新分组 进入网络的速率应该与另一端返回确认的速率相同而进行工作。
慢启动为发送方的 TCP增加了另一个窗口:拥塞窗口 (congestion window),记为 cwnd。当
与另一个网络的主机建立TCP连接时,拥塞窗口被初始化为1个报文段(即另一端通告的报文
段大小)。每收到一个 ACK,拥塞窗口就增加一个报文段( cwnd以字节为单位,但是慢启动 以报文段大小为单位进行增加)。发送方取拥塞窗口与通告窗口中的最小值作为发送上限。拥 塞窗口是发送方使用的流量控制,而通告窗口则是接收方使用的流量控制。
发送方开始时发送一个报文段,然后等待 ACK。当收到该 ACK时,拥塞窗口从 1增加为 2, 即可以发送两个报文段。当收到这两个报文段的 ACK时,拥塞窗口就增加为 4。这是一种指数 增加的关系。
在某些点上可能达到了互联网的容量,于是中间路由器开始丢弃分组。这就通知发送方 它的拥塞窗口开得过大。当我们在下一章讨论 TCP的超时和重传机制时,将会看到它们是怎 样对拥塞窗口起作用的。现在,我们来观察一个实际中的慢启动。
一个例子
图20-8表示的是将从主机sun发送到主机vangogh.cs.berkeley.edu的数据。这些数据 将通过一个慢的SLIP链路,该链路是TCP连接上的瓶颈(我们已经在时间系列上去掉了连接建立的过程)。
图20-8 慢启动的例子
我们观察到发送方发送一个长度为 512字节的报文段,然后等待 ACK。该ACK在716 ms后 收到。这个时间是一个往返时间的指示。于是拥塞窗口增加了2个报文段,且又发送了两个报 文段。当收到报文段 5的ACK后,拥塞窗口增加为 3。此时尽管可发送多达 3个报文段,可是在 下一个 ACK收到之前,只发送了 2个报文段。
在21.6节中我们将再次讨论慢启动,并介绍怎样采用另一种被称为“拥塞避免”的技术来
作为通常的实现。
20.7 成块数据的吞吐量
让我们看一看窗口大小、窗口流量控制以及慢启动对传输成块数据的 TCP连接的吞吐量 的相互作用。
图20-9显示了左边的发送方和右边的接收方之间的一个 TCP连接上的时间系列,共显示了
16个时间单元。为简单起见,本图只显示离散的时间单元。每个粗箭头线的上半部分显示的 是从左到右的携带数据的报文段,标记为 1, 2, 3, 等等。在粗线箭头下面表示的是反向传输的 ACK。我们把 ACK用细箭头线表示,并标注了被确认的报文段号。
发送方 发送方
发送方
接收方
接收方
接收方
接收方
接收方
接收方
发送方
发送方
图20-9 时间0~15的成块数据吞吐量举例
在时间 0,发送方发送了一个报文段。由于发送方处于慢启动中(其拥塞窗口为 1个报文 段),因此在继续发送以前它必须等待该数据段的确认。
在时间 1, 2 和3,报文段从左向右移动一个时间单元。在时间 4接收方读取这个报文段并产 生确认。经过时间 5、6和7,ACK移动到左边的发送方。我们有了一个 8个时间单元的往返时 间RTT(Round-Trip T ime)。
我们有意把 A C K 报文段画得比数据报文段小,这是因为它通常只有一个 I P 首部和一个 TCP首部。这里显示仅仅是一个单向的数据流动,并且假定 ACK的移动速率与数据报文段的 移动速率相等。实际上并不总是这样。
通常发送一个分组的时间取决于两个因素:传播时延(由光的有限速率、传输设
备的等待时间等引起)和一个取决于媒体速率(即媒体每秒可传输的比特数)的发送 时延。对于一个给定的两个接点之间的通路,传播时延一般是固定的,而发送时延则取决于分组的大小。在速率较慢的情况下发送时延起主要作用(例如,在习题 7.2中我 们甚至没有考虑传播时延),而在千兆比特速率下传播时延则占主要地位(见图24-6)。
当发送方收到ACK后,在时间8和9发送两个报文段(我们标记为2和3)。此时它的拥塞窗口为2个报文段。这两个报文段向右传送到接收方,在时间12和13接收方产生两个ACK。这两个返 回到发送方的 ACK之间的间隔与报文段之间的间隔一致,被称为 TCP的自计时 (self-clocking)行 为。由于接收方只有在数据到达时才产生 ACK,因此发送方接收到的 ACK之间的间隔与数据到 达接收方的间隔是一致的(然而在实际中,返回路径上的排队会改变ACK的到达率)。
图20-10表示的是后面 16个时间单位。 2个ACK的到达使得拥塞窗口从 2个报文段增加为 4 个,而这 4个报文段在时间 16~19时被发送。第 1个ACK在时间 23到达。 4个ACK的到达使得拥 塞窗口从 4个报文段增加为 8个,并在时间 24~31发送8个报文段。
发送方
发送方
发送方
发送方
发送方
发送方
发送方
发送方
发送方 发送方
发送方
接收方
发送方
接收方
接收方
接收方
发送方
接收方
接收方
接收方
接收方
发送方
接收方
接收方
接收方
接收方
发送方
接收方
接收方
发送方
接收方
发送方 接收方
发送方 接收方
图20-10 时间16~31的成块数据吞吐量举例
在时间 31及其后续时间,发送方和接收方之间的管道 (pipe)被填满。此时不论拥塞窗口和 通告窗口是多少,它都不能再容纳更多的数据。每当接收方在某一个时间单位从网络上移去 一个报文段,发送方就再发送一个报文段到网络上。但是不管有多少报文段填充了这个管道,返回路径上总是具有相同数目的 ACK。这就是连接的理想稳定状态。
20.7.1 带宽时延乘积
现在来回答窗口应该设置为多大的问题。在我们的例子中,作为最大的吞吐量,发送方 在任何时候有 8个已发送的报文段未被确认。接收方的通告窗口必须不小于这个数目,因为通 告窗口限制了发送方能够发送的段的数目。
可以计算通道的容量为:
capacity(bit) = bandwidth (b/s) × round-triptime (s) 一般称之为带宽时延乘积。这个值依赖于网络速度和两端的 RTT,可以有很大的变动。例如, 一条穿越美国( RTT约为60 ms)的T1的电话线路(1 544 000 b/s )的带宽时延乘积为 11 580字 节。对于 20.4节中讨论的缓存大小而言,这个结果是合理的。但是一条穿越美国的 T3电话线路
(45 000 000 b/s)的带宽时延乘积则为 337 500字节,这个数值超过了最大所允许的 TCP通告窗 口的大小(65535字节)。在24.4节我们将讨论能够避免当前 TCP限制的新的TCP窗口大小选项。
T1电话线的1 544 000 b/s是原始比特率。由于每193个bit使用1个作为帧同步,因此实际数据率为1 536 000b/s。一个T3电话线的原始比特率实际上是44 736 000 b/s,其数 据率可达到44 210 000 b/s。在讨论中我们使用1.544 Mb/s和45 Mb/s。
不论是带宽还是时延均会影响发送方和接收方之间通路的容量。在图 20-11中我们显示了 一个增加了一倍的 RTT会使通路容量也增加一倍。
在图20-11底下的说明部分,通过使用一个较长的 RTT,这个管道能够容纳 8个报文段而不 是4个。
类似地,图 20-12表示了增加一倍的带宽也可使该管道的容量增加一倍。
倍增的RTT
图20-11 RTT加倍可使管道容量增加一倍 图20-12 带宽加倍可使管道容量增加一倍
在图 20-12的下部,假定网络速率已经加倍,使得我们能够只使用上面一半的时间来发送
4个报文段。这样,该管道的容量再次加倍(假定该图的上半部分与下半部分中的报文段具有 同样大小,即具有相同的比特数)。
20.7.2 拥塞
当数据到达一个大的管道(如一个快速局域网)并向一个较小的管道(如一个较慢的广
域网)发送时便会发生拥塞。当多个输入流到达一个路由器,而路由器的输出流小于这些输
入流的总和时也会发生拥塞。
图20-13显示了一个典型的大管道向小管道发送报文的情况。之所以说它典型,是因为大 多数的主机都连接在局域网上,并通过一个路由器与速率相对较低的广域网相连(我们再次假定图中上半部分的报文段( 9~20)都是相同的,而图中下半部分的 ACK也都是相同的)。
发送方
发送方
瓶颈
路由器
R1
路由器
R3
路由 器R2
路由器
R4
接收方
接收方
图20-13 从较大管道向较小管道发送分组引起的拥塞
在该图中,我们已经标记路由器 R1 为“瓶颈”,因为它是拥塞发生的地方。它从左侧速 率较高的局域网接收数据并向右侧速率较低的广域网发送(通常 R 1 与R 3 是同样的路由器, 如同 R2与R4一样。但这并不是必需的,有时也会使用不对称的路径)。当路由器 R2将所接收 到的分组发送到右侧的局域网时,这些分组之间维持与其左侧广域网上同样的间隔,尽管局 域网具有更高的带宽。类似地,返回的确认之间的间隔也与其在路径中最慢的链路上的间隔一致。
在图 2 0 - 1 3中已经假定发送方不使用慢启动,它按照局域网的带宽尽可能快地发送编号为1~20 的报文段(假定接收方的通告窗口至少为 20个报文段)。正如我们看到的那样, ACK 之间的间隔与在最慢链路上的一致。假定瓶颈路由器具有足够的容纳这 2 0 个分组的缓存。 如果这个不能保证,就会引起路由器丢弃分组。在 21.6节讨论避免拥塞时会看到怎样避免这 种情况。
20.8 紧急方式
TCP提供了“紧急方式 (urgent mode) ”,它使一端可以告诉另一端有些具有某种方式的 “紧急数据”已经放置在普通的数据流中。另一端被通知这个紧急数据已被放置在普通数据流中,由接收方决定如何处理。
可以通过设置 TCP首部(图 17-2 )中的两个字段来发出这种从一端到另一端的紧急数据 已经被放置在数据流中的通知。 URG 比特被置 1,并且一个 16bit的紧急指针被置为一个正的 偏移量,该偏移量必须与 TCP首部中的序号字段相加,以便得出紧急数据的最后一个字节的 序号。
仍有许多关于紧急指针是指向紧急数据的最后一个字节还是指向紧急数据最后一 个字节的下一个字节的争论。最初的 TCP规范给出了两种解释,但 Host Requirements RFC确定指向最后一个字节是正确的。
然而,问题在于大多数的实现(包括源自伯克利的实现)继续使用错误的解释。
所有符合Host Requirements RFC的实现都是可兼容的,但很有可能无法与其他大多数 主机正确通信。
TCP必须通知接收进程,何时已接收到一个紧急数据指针以及何时某个紧急数据指针还不 在此连接上,或者紧急指针是否在数据流中向前移动。接着接收进程可以读取数据流,并必须能够被告知何时碰到了紧急数据指针。只要从接收方当前读取位置到紧急数据指针之间有 数据存在,就认为应用程序处于“紧急方式”。在紧急指针通过之后,应用程序便转回到正常 方式。
TCP本身对紧急数据知之甚少。没有办法指明紧急数据从数据流的何处开始。 TCP通过连 接传送的唯一信息就是紧急方式已经开始( TCP首部中的 URG比特)和指向紧急数据最后一 个字节的指针。其他的事情留给应用程序去处理。
不幸的是,许多实现不正确地称 TCP的紧急方式为带外数据 (out-of-band data) 。如果一个 应用程序确实需要一个独立的带外信道,第二个 T C P 连接是达到这个目的的最简单的方法
(许多运输层确实提供许多人认为的那种真正的带外数据:使用同一个连接的独立的逻辑数据 通道作为正常的数据通道。这是 TCP所没有提供的)。
TCP的紧急方式与带外数据之间的混淆,也是因为主要的编程接口(插口 API)将
TCP的紧急方式映射为称为带外数据的插口。
紧急方式有什么作用呢?两个最常见的例子是 Telnet和Rlogin。当交互用户键入中断键时, 我们在第 26章将看到使用紧急方式来完成这个功能的例子。另一个例子是 FTP,当交互用户 放弃一个文件的传输时,我们将在第 27章看到这样的一个例子。
Telnet和Rlogin从服务器到客户使用紧急方式是因为在这个方向上的数据流很可能要被客户 的TCP停止(也即,它通告了一个大小为 0的窗口)。但是如果服务器进程进入了紧急方式,尽管它不能够发送任何数据,服务器 TCP也会立即发送紧急指针和 URG标志。当客户 TCP接收到 这个通知时就会通知客户进程,于是客户可以从服务器读取其输入、打开窗口并使数据流动。
如果在接收方处理第一个紧急指针之前,发送方多次进入紧急方式会发生什么情况呢? 在数据流中的紧急指针会向前移动,而其在接收方的前一个位置将丢失。接收方只有一个紧急指针,每当对方有新的值到达时它将被覆盖。这意味着如果发送方进入紧急方式时所写的 内容对接收方非常重要,那么这些字节数据必须被发送方用某种方式特别标记。我们将看到Telnet通过在数据流中加入一个值为 255的字节作为前缀来标记它所有的命令。
一个例子
让我们观察一下即使是在接收方窗口关闭的情况下, TCP是如何发送紧急数据的。在主 机bsdi上启动 sock程序,并使之在连接建立后和从网络读取前暂停 10秒种(通过使用 -P选 项),这将使另一端填满发送窗口:
bsdi % sock -i -s -P10 5555
接着我们在主机 sun上启动客户,使之使用一个 8 1 9 2 字节的发送缓存(使用 -S选项) 并进行 6个向网络写 1 0 2 4字节数据的操作(使用 -n选项)。还指明 -U5选项,告知它向网络 写第 5 个缓存之前要写 1个字节的数据,并进入紧急数据方式。我们指明详细标志来观察写的顺序:
我们设置发送缓存为 8192个字节,以便让发送应用程序能够立即写所有的数据。图 20-14
显示了 tcpdump输出的这个交换过程的结果(删去了连接建立的过程)。第 1~5行表示发送方 用4个1024字节的报文段去填充接收方的窗口。然后由于接收方的窗口被填满(第 4行的 ACK 确认了数据,但并没有移动窗口的右边沿),所以发送方停止发送。
在写了第4个正常数据之后,应用进程写了1个字节并进入紧急方式。第6行是该应用进程写的结 果,紧急指针被设置为4098。尽管发送方不能发送任何数据,但紧急指针和URG标志一起被发送。
5个这样的 ACK在13 ms 内被发送(第 6~10行)。第 1个ACK在应用进程写 1个字节并进入 紧急方式时被发送,后面两个在应用进程写最后两个 1024字节的数据时被发送(尽管 TCP不 能发送这 2048个字节的数据,可每次当应用程序执行写操作的时候, TCP的输出功能被调用。 当 T C P 看到正处于紧急方式时,它会发送其他的紧急通知 )。第 4 个 A C K在应用进程关闭其 TCP连接时被发送( TCP的输出功能再次被调用)。发送应用程序在启动几毫秒后终止— 在 接收方应用进程已经发出其第一个写操作之前。 TCP将所有的数据进行排队,并在可能时发 送出去(这就是为何指明发送缓存为 8192字节的原因,因此只有这样才能够把所有的数据都 放置在缓存中)。第 5个ACK很可能是在接收第 4行的 ACK时产生的。发送 TCP很可能在这个 ACK 到达前便已将其第 4个报文段放入队列以便输出(第 5行)。另一端接收到这个 ACK也会 引起TCP输出例程被调用。
图20-14 tcpdump 对TCP紧急方式的输出结果
接着,接收方确认最后的 1024字节的数据(第 11行),但同时通告窗口为 0。发送方用一 个包含紧急通知的报文段进行了响应。
在第 13行,当应用进程被唤醒、并从接收缓存读取一些数据时,接收方通告窗口为 2048 字节。于是后面又发送了两个 1024字节的报文段(第 14和15行)。其中,由于紧急指针在第 1 个报文段的范围内,因此这个报文段被设置了紧急通知标志,而第 2 个报文段则关闭了该标 志。
当接收方再次打开窗口(第 16行)时,发送方传输最后的数据(序号为 6145)并发起正 常的连接关闭。
图20-15显示了发送的 6145个字节数据的序号。可以看到当进入紧急方式时所发送的字节 的序号是 4097,但在图 20-14中紧急指针指向 4098,这证明了该实现( SunOS 4.1.3)将紧急指 针设置为紧急数据最后字节的下一个字节。
写 写 写 写
紧急 写 写
报文段 报文段 报文段 报文段 报文段 报文段
图20-15 紧急方式例子中,应用进程的写操作和TCP的一些报文段
该图还可以让我们观察 TCP 是如何对应用进程写的数据进行重新分组化的。当进入紧急 方式时待输出的 1个字节是与在缓存中的后面 1023个字节一同发送的。下一个报文段也包含
1024字节的数据,而最后一个报文段则只包含一个字节。
20.9 小结
正如我们在本章一开始时讲的那样,没有一种单一的方法可以使用 TCP进行成块数据的 交换。这是一个依赖于许多因素的动态处理过程,有些因素我们可以控制(如发送和接收缓 存的大小),而另一些我们则没有办法控制(如网络拥塞、与实现有关的特性等)。在本章, 我们已经考察了许多 TCP的传输过程,介绍了所有我们能够看到的特点和算法。
进行成块数据有效传输的最重要的方法是 TCP的滑动窗口协议。我们考察了 TCP为使发送 方和接收方之间的管道充满来获得最可能快的传输速度而采用的方法。我们用带宽时延乘积 衡量管道的容量,并分析了该乘积与窗口大小之间的关系。在 24.8节介绍 TCP性能的时候将再 次涉及这个概念。
我们还介绍了 TCP的PUSH标志,因为在跟踪结果中总是观察到它,但我们无法对它的设 置与否进行控制。本章最后一个主题是 TCP的紧急数据,人们常常错误地称其为“带外数据”。 TCP 的紧急方式只是一个从发送方到接收方的通知,该通知告诉接收方紧急数据已被发送,并提供该数据最后一个字节的序号。应用程序使用的有关紧急数据部分的编程接口常常都不 是最佳的,从而导致更多的混乱。
习题
20.1 在图 20-6中,我们可以看到一个序号为 0的字节和一个序号为 8193的字节,试问这两个
字节的含义是什么?
20.2 提前观察图 22-1,并解释主机 bsdi设置PUSH标志的含义。
20.3 在一个Usenet记录中,有人抱怨说美国和日本之间的一个128 ms时延、速率为256 000b/s的链 路吞吐量为120 000b/s(利用率为47%),而当链路通过卫星时其吞吐量则为33 000b/s(利用 率为13%)。试问在这两种情况下窗口大小各为多少(假定卫星链路的时延为500 ms)?卫星 链路的窗口大小应该如何调整?
20.4 如果API提供一种方法,使得发送方可以告诉其 TCP打开PUSH标志,而接收方可以查询 一个接收的报文段是否被设置了 PUSH标志,试问该标志能否被用作一个记录标记?
20.5 在图20-3中为什么没有合并报文段 15和16?
20.6 在图 20-13中,我们假定对应数据报文段之间的间隔,返回的 ACK之间的间隔被分隔得 很好。如果在链路某处进行缓存并使许多 ACK同时到达发送方,试问会发生什么情况?