第5章 运输层¶
第 5 章 运输层¶
本章先概括介绍运输层协议的特点、进程之间的通信和端口等重要概念,然后讲述比较简单的 UDP 协议。其余的篇幅都是讨论较为复杂但非常重要的 TCP 协议 \(^{①}\) 和可靠传输的工作原理,包括停止等待协议和 ARQ 协议。在详细讲述 TCP 报文段的首部格式之后,讨论 TCP 的三个重要问题:滑动窗口、流量控制和拥塞控制机制。最后,介绍 TCP 的连接管理。
运输层是整个网络体系结构中的关键层次之一。一定要弄清以下一些重要概念:
5.1 运输层协议概述¶
5.1.1 进程之间的通信¶
从通信和信息处理的角度看,运输层向它上面的应用层提供通信服务,它属于面向通信部分的最高层,同时也是用户功能中的最低层。当网络边缘部分的两台主机使用网络核心部分的功能进行端到端的通信时,都要使用协议栈中的运输层,而网络核心部分中的路由器在转发分组时只用到下三层的功能。
下面通过图 5-1 的示意图来说明运输层的作用。设局域网 LAN \(_{1}\) 上的主机 A 和局域网 LAN \(_{2}\) 上的主机 B 通过互连的广域网 WAN 进行通信。我们知道,IP 协议能够把源主机 A 发送出的分组,按照首部中的目的地址,送交到目的主机 B,那么,为什么还需要运输层呢?
从 IP 层来说,通信的两端是两台主机。IP 数据报的首部明确地标志了这两台主机的 IP 地址。但 “两台主机之间的通信” 这种说法还不够明确。真正进行通信的实体是在主机中的哪个构件呢?是主机中的应用进程,是一台主机中的应用进程和另一台主机中的应用进程在交换数据(即通信)。因此严格地讲,两台主机进行通信就是两台主机中的应用进程互相通信。IP 协议虽然能把分组送到目的主机,但是这个分组还停留在主机的网络层而没有交付主机中的应用进程。通信的两端应当是两个主机中的应用进程。也就是说,端到端的通信是应用进程之间的通信。在一台主机中经常有多个应用进程同时分别和另一台主机中的多个应用进程通信。例如,某用户在使用浏览器查找某网站的信息时,其主机的应用层运行浏览器客户进程。如果在浏览网页的同时,还要用电子邮件给网站发送反馈意见,那么主机的应用层就还要运行电子邮件的客户进程。在图 5-1 中,主机 A 的应用进程 AP \(_{1}\) 和主机 B 的应用进程 AP \(_{3}\) 通信,而与此同时,应用进程 AP \(_{2}\) 也和对方的应用进程 AP \(_{4}\) 通信。这表明运输层有一个很重要的功能 —— 复用 (multiplexing) 和分用 (demultiplexing)。这里的 “复用” 是指在发送方不同的应用进程都可以使用同一个运输层协议传送数据(当然需要加上适当的首部),而 “分用” 是指接收方的运输层在剥去报文的首部后能够把这些数据正确交付目的应用进程①。图 5-1 中两个运输层之间有一个深色双向粗箭头,写明 “运输层提供应用进程间的逻辑通信”。“逻辑通信” 的意思是:从应用层来看,只要把应用层报文交给下面的运输层,运输层就可以把这报文传送到对方的运输层(哪怕双方相距很远,例如几千公里),好像这种通信就是沿水平方向直接传送数据。但事实上这两个运输层之间并没有一条水平方向的物理连接。数据的传送是沿着图中的虚线方向(经过多个层次)传送的。“逻辑通信” 的意思是 “好像是这样通信,但事实上并非真的这样通信”。

从这里可以看出网络层和运输层有明显的区别。网络层为主机之间的通信提供服务,而运输层则在网络层的基础上,为应用进程之间的通信提供服务。然而正如后面还要讨论的,运输层还具有网络层无法代替的许多其他重要功能。
运输层还要对收到的报文进行差错检测。大家应当还记得,在网络层,IP 数据报首部中的检验和字段,只检验首部是否出现差错而不检查数据部分。
根据应用程序的不同需求,运输层需要有两种不同的运输协议,即面向连接的 TCP 和无连接的 UDP,这两种协议就是本章要讨论的主要内容。
我们还应指出,运输层向高层用户屏蔽了下面网络核心的细节(如网络拓扑、所采用的路由选择协议等),它使应用进程看见的就是好像在两个运输层实体之间有一条端到端的逻辑通信信道,但这条逻辑通信信道对上层的表现却因运输层使用的不同协议而有很大的差别。当运输层采用面向连接的 TCP 协议时,尽管下面的网络是不可靠的(只提供尽最大努力服务),但这种逻辑通信信道就相当于一条全双工的可靠信道。但当运输层采用无连接的 UDP 协议时,这种逻辑通信信道仍然是一条不可靠信道。
5.1.2 运输层的两个主要协议¶
TCP/IP 运输层的两个主要协议都是互联网的正式标准,即:
图 5-2 给出了这两种协议在协议栈中的位置。
图 5-2 TCP/IP 体系中的运输层协议
| 应用层 | |
| UDP | TCP |
| IP | |
| 与各种网络接口 | |
按照 OSI 的术语,两个对等运输实体在通信时传送的数据单位叫作运输协议数据单元 TPDU (Transport Protocol Data Unit)。但在 TCP/IP 体系中,则根据所使用的协议是 TCP 或 UDP,分别称之为 TCP 报文段 (segment) 或 UDP 用户数据报。
UDP 在传送数据之前不需要先建立连接。远地主机的运输层在收到 UDP 报文后,不需要给出任何确认。虽然 UDP 不提供可靠交付,但由于 UDP 非常简单,在某些情况下 UDP 是一种最有效的工作方式。
TCP 则提供面向连接的服务。在传送数据之前必须先建立连接,数据传送结束后要释放连接。TCP 不提供广播或多播服务。由于 TCP 要提供可靠的、面向连接的运输服务,因此不可避免地增加了许多的开销,如确认、流量控制、计时器以及连接管理等。这不仅使协议数据单元的首部增大很多,还要占用许多的处理机资源。
表 5-1 给出了一些应用和应用层协议主要使用的运输层协议(UDP 或 TCP)。
表 5-1 使用 UDP 或 TCP 协议的各种应用和应用层协议
| 应用 | 应用层协议 | 运输层协议 |
| 名字转换 | DNS(域名系统) | UDP |
| 文件传送 | TFTP(简单文件传送协议) | UDP |
| 路由选择协议 | RIP(路由信息协议) | UDP |
| IP地址配置 | DHCP(动态主机配置协议) | UDP |
| 网络管理 | SNMP(简单网络管理协议) | UDP |
| 远程文件服务器 | NFS(网络文件系统) | UDP |
| IP电话 | 专用协议 | UDP |
| 流式多媒体通信 | 专用协议 | UDP |
| 多播 | IGMP(网际组管理协议) | UDP |
| 电子邮件 | SMTP(简单邮件传送协议) | TCP |
| 远程终端接入 | TELNET(远程终端协议) | TCP |
| 万维网 | HTTP(超文本传送协议) | TCP |
| 文件传送 | FTP(文件传送协议) | TCP |
5.1.3 运输层的端口¶
前面已经提到过运输层的复用和分用功能。其实在日常生活中也有很多复用和分用的例子。假定一个机构的所有部门向外单位发出的公文都由收发室负责寄出,这相当于各部门都 “复用” 这个收发室。当收发室收到从外单位寄来的公文时,则要完成 “分用” 功能,即按照信封上写明的本机构的部门地址把公文正确进行交付。
运输层的复用和分用功能也是类似的。应用层所有的应用进程都可以通过运输层再传送到 IP 层(网络层),这就是复用。运输层从 IP 层收到发送给各应用进程的数据后,必须分别交付指明的各应用进程,这就是分用。显然,给应用层的每个应用进程赋予一个非常明确的标志是至关重要的。
我们知道,在单个计算机中的进程是用进程标识符(一个不大的整数)来标志的。但是在互联网环境下,用计算机操作系统所指派的这种进程标识符来标志运行在应用层的各种应用进程则是不行的。这是因为在互联网上使用的计算机的操作系统种类很多,而不同的操作系统又使用不同格式的进程标识符。为了使运行不同操作系统的计算机的应用进程能够互相通信,就必须用统一的方法(而这种方法必须与特定操作系统无关)对 TCP/IP 体系的应用进程进行标志。
但是,把一个特定机器上运行的特定进程,指明为互联网上通信的最后终点是不可行的。这是因为进程的创建和撤销都是动态的,通信的一方几乎无法知道和识别对方机器上的进程。另外,我们往往需要利用目的主机提供的功能来识别终点,而不需要知道具体实现这个功能的进程是哪一个(例如,要和互联网上的某个邮件服务器联系,但并不一定要知道这个服务器功能是由目的主机上的哪个进程实现的)。
解决这个问题的方法很巧妙,就是在应用层和运输层之间的界面上,设置一个特殊的抽象的 “门”。应用层中的应用进程要通过运输层发送到互联网,必须要通过这个门。而别的主机上的应用进程要寻找本主机中的某个应用进程,也必须通过这个门。这样,我们就可以把应用层和运输层的界面上这些 “门”,设为通信的抽象终点。这些抽象终点的正式名称就是协议端口 (protocol port),一般就简称为端口 (port)。每一个端口用一个称为端口号 (port number) 的正整数来标志。主机的操作系统提供了接口机制,使得进程能够通过这种机制找到所要找的端口。
请注意,这种在协议栈层间的抽象的协议端口是软件端口,和路由器或交换机上的硬件端口是完全不同的概念。硬件端口是不同硬件设备进行交互的接口,而软件端口是应用层的各种协议进程与运输实体进行层间交互的地点。不同的系统具体实现端口的方法可以是不同的(取决于系统使用的操作系统)。
当应用层要发送数据时,应用进程就把数据发送到适当的端口,然后运输层从该端口读取数据,进行后续的处理(把数据发送到目的主机)。当运输层收到对方主机发来的数据时,就把数据发送到适当的端口,然后应用进程就从该端口读取这些数据。显然,端口必须有一定容量的缓存来暂时存放数据。
在后面将讲到的 UDP 和 TCP 的首部格式中(图 5-5 和图 5-14),都有源端口和目的端口这两个重要字段。这两个端口就是运输层和应用层进行交互的地点。
TCP/IP 的运输层用一个 16 位端口号来标志一个端口。但请注意,端口号只具有本地意义,它只是为了标志本计算机应用层中的各个进程在和运输层交互时的层间接口。在互联网不同计算机中,相同的端口号是没有关联的。16 位的端口号可允许有 65535 个不同的端口
号,这个数目对一个计算机来说是足够用的。
由此可见,两个计算机中的进程要互相通信,不仅必须知道对方的 IP 地址(为了找到对方的计算机),而且要知道对方的端口号(为了找到对方计算机中的应用进程)。这和我们寄信的过程类似。当我们要给某人写信时,就必须在信封上写明他的通信地址(这是为了找到他的住所,相当于 IP 地址),并且还要写上收件人的姓名(这是因为在同一住所中可能有好几个人,这相当于端口号)。在信封上还应写明自己的地址和姓名。当收信人回信时,很容易在信封上找到发信人的地址和姓名。互联网上的计算机通信是采用客户 - 服务器方式。客户在发起通信请求时,必须先知道对方服务器的 IP 地址(用来找到目的主机)和端口号(用来找到目的进程)。因此运输层的端口号分为下面的两大类。
(1) 服务器端使用的端口号 这里又分为两类,最重要的一类叫作熟知端口号 (well-known port number) 或全球通用端口号,数值为 0\~1023。这些熟知端口号最初公布在文档中 [RFC 1700, STD2],但后来因为互联网发展太快,这种标准文档无法随时更新,因此在 RFC 3232 中就把 RFC 1700 列为陈旧的,而当前最新的熟知端口号可在网址 www.iana.org 上查到。IANA 把这些熟知端口号指派给了 TCP/IP 最重要的一些应用程序,让所有的用户都知道。当一种新的应用程序出现后,IANA 必须为它指派一个熟知端口,否则互联网上的其他应用进程就无法和它进行通信。和电话通信对比,熟知端口号相当于所有人都应知晓的重要电话号码,如报警电话 110,急救电话 120 等。表 5-2 给出了一些常用的熟知端口号。
表 5-2 常用的熟知端口号
| 应用程序 | FTP | TELNET | SMTP | DNS | TFTP | HTTP | SNMP | SNMP (trap) | HTTPS |
| 熟知端口号 | 21 | 23 | 25 | 53 | 69 | 80 | 161 | 162 | 443 |
另一类叫作登记端口号,数值为 1024\~49151。这类端口号是为没有熟知端口号的应用程序使用的。要使用这类端口号必须在 IANA 按照规定的手续登记,以防止重复。
(2) 客户端使用的端口号 数值为 49152\~65535。由于这类端口号仅在客户进程运行时才动态选择,因此又叫作短暂端口号 \(^{①}\) 。这类端口号就是临时端口号,留给客户进程选择临时使用。当服务器进程收到客户进程的报文时,就知道了客户进程所使用的端口号,因而可以把数据发送给客户进程。通信结束后,刚才已使用过的客户端口号就被系统收回,以便给其他客户进程使用。
下面将分别讨论 UDP 和 TCP。UDP 比较简单,本章主要讨论 TCP。
5.2 用户数据报协议 UDP¶
5.2.1 UDP 概述¶
用户数据报协议 UDP 只在 IP 的数据报服务之上增加了很少一点的功能,这就是复用和分用的功能以及差错检测的功能。UDP 的主要特点是:

(4) UDP 没有拥塞控制,因此网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用是很重要的。很多的实时应用(如 IP 电话、实时视频会议等)要求源主机以恒定的速率发送数据,并且允许在网络发生拥塞时丢失一些数据,但却不允许数据有太大的时延。UDP 正好适合这种要求。
(5) UDP 支持一对一、一对多、多对一和多对多的交互通信。
(6) UDP 的首部开销小,只有 8 个字节,比 TCP 的 20 个字节的首部要短。
下面举例说明 UDP 的通信和端口号的关系(如图 5-4 所示)。主机 \(H_{1}\) 中有三个应用进程分别要和主机 \(H_{2}\) 中的两个应用进程进行通信。通信双方的关系是: \(P_{1} \rightarrow P_{4}, P_{2} \rightarrow P_{4}, P_{3} \rightarrow P_{5}\) 。主机 \(H_{1}\) 的操作系统为这三个进程分别指派了端口,其端口号分别为 a, b 和 c。图中位于应用层和运输层之间的小方框代表端口。在端口小方框中间还画有队列,表示端口具有缓存的功能,可以把收到的数据暂时存储一下。有时也可以把队列画成双向的,即分别表示存放来自应用层或运输层的数据。现在假定主机 \(H_{1}\) 中的进程已经知道了对方进程 \(P_{4}\) 和 \(P_{5}\) 的端口号分别为 x 和 y,于是在主机 \(H_{1}\) 的运输层就可以组装成需要发送的 UDP 用户数据报,其中最重要的地址信息 (源端口,目的端口) 分别是 (a, x),(b, x) 和 (c, y)。在图 5-4 中把运输层以下的都省略了。因此,进程之间的通信现在可以看成是两个端口之间的通信。
图中在两个运输层之间有一条虚线,表示在两个运输层之间可以进行通信,而不是一条连接。但这种通信是不可靠的通信,即所发送的报文在传输过程中有可能丢失,同时也不保证报文都能按照发送的先后顺序到达终点。这正是 UDP 通信的特点:简单方便,但不可靠。如果想要得到可靠的运输层通信,那就要使用后面要介绍的 TCP 进行通信。请注意,在两个运输层的 UDP 之间并没有建立连接。

图 5-4 的例子画出了多对一的通信( \(a \rightarrow x, b \rightarrow x\) )。如果改成 \(a \rightarrow x, a \rightarrow y\) ,则是一对多的情况了。
主机 \(H_{1}\) 中的 3 个应用进程,把用户数据通过各自的端口传送到了运输层后,就共用一个网络层协议,把收到的 UDP 用户数据报组装成不同的 IP 数据报,发送到互联网。这就是 UDP 的复用功能。主机 \(H_{2}\) 的网络层收到 3 个 IP 数据报后,提取出数据部分(即 UDP 用户数据报),然后根据其首部中的目的端口号,分别传送到相应的端口,以便上层的应用进程到端口读取数据。这就是 UDP 的分用功能。
虽然某些实时应用需要使用没有拥塞控制的 UDP,但当很多的源主机同时都向网络发送高速率的实时视频流时,网络就有可能发生拥塞,结果大家都无法正常接收。因此,不使用拥塞控制功能的 UDP 有可能会引起网络产生严重的拥塞问题。
还有一些使用 UDP 的实时应用,需要对 UDP 的不可靠的传输进行适当的改进,以减少数据的丢失。在这种情况下,应用进程本身可以在不影响应用的实时性的前提下,增加一些提高可靠性的措施,如采用前向纠错或重传已丢失的报文。
5.2.2 UDP 的首部格式¶
用户数据报 UDP 有两个字段:数据字段和首部字段。首部字段很简单,只有 8 个字节(如图 5-5 所示),由 4 个字段组成,每个字段的长度都是 2 字节。各字段意义如下:

如果接收方 UDP 发现收到的报文中的目的端口号不正确(即不存在对应于该端口号的应用进程),就丢弃该报文,并由网际控制报文协议 ICMP 发送 “端口不可达” 差错报文给发送方。我们在上一章 4.4.2 节 “ICMP 的应用举例” 讨论 traceroute 时,就是让发送的 UDP 用户数据报故意使用一个非法的 UDP 端口,结果 ICMP 就返回 “端口不可达” 差错报文,因而达到了测试的目的。
UDP 用户数据报首部中检验和的计算方法有些特殊。在计算检验和时,要在 UDP 用户数据报之前增加 12 个字节的伪首部。所谓 “伪首部” 是因为这种伪首部并不是 UDP 用户数据报真正的首部。只是在计算检验和时,临时添加在 UDP 用户数据报前面,得到一个临时的 UDP 用户数据报。检验和就是按照这个临时的 UDP 用户数据报来计算的。伪首部既不向下传送也不向上递交,而仅仅是为了计算检验和。图 5-5 的最上面给出了伪首部各字段的内容。
UDP 计算检验和的方法和计算 IP 数据报首部检验和的方法相似。但不同的是:IP 数据报的检验和只检验 IP 数据报的首部,但 UDP 的检验和是把首部和数据部分一起都检验。在发送方,首先是先把全零放入检验和字段。再把伪首部以及 UDP 用户数据报看成是由许多 16 位的字串接起来的。若 UDP 用户数据报的数据部分不是偶数个字节,则要填入一个全零字节(但此字节不发送)。然后按二进制反码计算出这些 16 位字的和。将此和的二进制反码写入检验和字段后,就发送这样的 UDP 用户数据报。在接收方,把收到的 UDP 用户数据报连同伪首部(以及可能的填充全零字节)一起,按二进制反码求这些 16 位字的和。当无差错时其结果应为全 1。否则就表明有差错出现,接收方就应丢弃这个 UDP 用户数据报(也可以上交给应用层,但附上出现了差错的警告)。图 5-6 给出了一个计算 UDP 检验和的例子。这里假定用户数据报的长度是 15 字节,因此要添加一个全 0 的字节。读者可以自己检验一下在接收端是怎样对检验和进行检验的。不难看出,这种简单的差错检验方法的检错能力并不强,但它的好处是简单,处理起来较快。

如图 5-6 所示,伪首部的第 3 字段是全零;第 4 字段是 IP 首部中的协议字段的值。以前曾讲过,对于 UDP,此协议字段值为 17;第 5 字段是 UDP 用户数据报的长度。因此,这样的检验和,既检查了 UDP 用户数据报的源端口号和目的端口号以及 UDP 用户数据报的数据部分,又检查了 IP 数据报的源 IP 地址和目的地址。
5.3 传输控制协议 TCP 概述¶
由于 TCP 协议比较复杂,因此本节先对 TCP 协议进行一般的介绍,然后再逐步深入讨论 TCP 的可靠传输、流量控制和拥塞控制等问题。

5.3.1 TCP 最主要的特点¶
TCP 是 TCP/IP 体系中非常复杂的一个协议。下面介绍 TCP 最主要的特点。

为了突出示意图的要点,我们只画出了一个方向的数据流。但请注意,在实际的网络中,一个 TCP 报文段包含上千个字节是很常见的,而图中的各部分都只画出了几个字节,这仅仅是为了更方便地说明 “面向字节流” 的概念。另一点很重要的是:图 5-7 中的 TCP 连接是一条虚连接(也就是逻辑连接),而不是一条真正的物理连接。TCP 报文段先要传送到 IP 层,加上 IP 首部后,再传送到数据链路层;再加上数据链路层的首部和尾部后,才离开主机发送到物理链路。
从图 5-7 可看出,TCP 和 UDP 在发送报文时所采用的方式完全不同。TCP 并不关心应用进程一次把多长的报文发送到 TCP 的缓存中,而是根据对方给出的窗口值和当前网络拥塞的程度(后面还将深入讨论),来决定一个报文段应包含多少个字节(UDP 发送的报文长度是应用进程给出的)。如果应用进程传送到 TCP 缓存的数据块太长,TCP 就可以把它划分为短一些的数据块再传送。如果应用进程一次只发来一个字节,TCP 也可以等待积累足够多的字节后再构成报文段发送出去。关于 TCP 报文段的长度问题,在后面还要进行讨论。
5.3.2 TCP 的连接¶
TCP 把连接作为最基本的抽象。TCP 的许多特性都与 TCP 是面向连接的这个基本特性有关。因此我们对 TCP 连接需要有更清楚的了解。
前面已经讲过,每一条 TCP 连接有两个端点。那么,TCP 连接的端点是什么呢?不是主机,不是主机的 IP 地址,不是应用进程,也不是运输层的协议端口。TCP 连接的端点叫作套接字 (socket) 或插口。根据 RFC 793 的定义:端口号拼接到 (concatenated with) IP 地址即构成了套接字。因此,套接字的表示方法是在点分十进制的 IP 地址后面写上端口号,中间用冒号或逗号隔开。例如,若 IP 地址是 192.3.4.5 而端口号是 80,那么得到的套接字就是 (192.3.4.5: 80)。总之,我们有
每一条 TCP 连接唯一地被通信两端的两个端点(即套接字对 socket pair)所确定。即:
这里 \(IP_{1}\) 和 \(IP_{2}\) 分别是两个端点主机的 IP 地址,而 \(port_{1}\) 和 \(port_{2}\) 分别是两个端点主机中的端口号。因此,TCP 连接就是两个套接字 \(socket_{1}\) 和 \(socket_{2}\) 之间的连接。套接字 socket 是个很抽象的概念,在下一章的 6.8 节还要对套接字进行更多的介绍。
总之,TCP 连接就是由协议软件所提供的一种抽象。虽然有时为了方便,我们也可以说,在一个应用进程和另一个应用进程之间建立了一条 TCP 连接,但一定要记住:TCP 连接的端点是个很抽象的套接字,即(IP 地址:端口号)。也还应记住:同一个 IP 地址可以有多个不同的 TCP 连接,而同一个端口号也可以出现在多个不同的 TCP 连接中。
本来 socket 的意思就是插座(或插口)。选用 socket 这个名词是相当准确的。其实一条 TCP 连接就像一条电缆线,其两端都各带有一个插头。把每一端的插头插入位于主机的应用层和运输层之间的插座 (socket) 后,两个主机之间的进程就可以通过这条电缆线进行可靠通信了。但插座这个名词很容易让人想起来是个硬件,而 socket 是个软件名词,这样 “套接字” 就成为 socket 的标准译名了。
请注意,socket 这个名词有时容易使人把一些概念弄混淆,因为随着互联网的不断发展
以及网络技术的进步,同一个名词 socket 却可表示多种不同的意思。例如:
上面的这些 socket 的意思都和本章所引用的 RFC 793 定义的 socket(指端口号拼接到 IP 地址)不同。请读者加以注意。
5.4 可靠传输的工作原理¶
我们知道,TCP 发送的报文段是交给 IP 层传送的。但 IP 层只能提供尽最大努力服务,也就是说,TCP 下面的网络所提供的是不可靠的传输。因此,TCP 必须采用适当的措施才能使得两个运输层之间的通信变得可靠。
理想的传输条件有以下两个特点:
在这样的理想传输条件下,不需要采取任何措施就能够实现可靠传输。
然而实际的网络都不具备以上两个理想条件。但我们可以使用一些可靠传输协议,当出现差错时让发送方重传出现差错的数据,同时在接收方来不及处理收到的数据时,及时告诉发送方适当降低发送数据的速率。这样一来,本来不可靠的传输信道就能够实现可靠传输了。下面从最简单的停止等待协议 \(^{①}\) 讲起。
5.4.1 停止等待协议¶
全双工通信的双方既是发送方也是接收方。下面为了讨论问题的方便,我们仅考虑 A 发送数据而 B 接收数据并发送确认。因此 A 叫作发送方,而 B 叫作接收方。因为这里是讨论可靠传输的原理,因此把传送的数据单元都称为分组,而并不考虑数据是在哪一个层次上传送的 \(^{②}\) 。“停止等待” 就是每发送完一个分组就停止发送,等待对方的确认。在收到确认后再发送下一个分组。
1. 无差错情况¶
停止等待协议可用图 5-8 来说明。图 5-8 (a) 是最简单的无差错情况。A 发送分组 \(M_{1}\) ,发完就暂停发送,等待 B 的确认。B 收到了 \(M_{1}\) 就向 A 发送确认。A 在收到了对 \(M_{1}\) 的确认后,就再发送下一个分组 \(\mathbf{M}_2\) 。同样,在收到 B 对 \(\mathbf{M}_2\) 的确认后,再发送 \(\mathbf{M}_3\) 。


2. 出现差错¶
图 5-8 (b) 是分组在传输过程中出现差错的情况。B 接收 \(M_{1}\) 时检测出了差错,就丢弃 \(M_{1}\) ,其他什么也不做(不通知 A 收到有差错的分组) \(^{①}\) 。也可能是 \(M_{1}\) 在传输过程中丢失了,这时 B 当然什么都不知道。在这两种情况下,B 都不会发送任何信息。可靠传输协议是这样设计的:A 只要超过了一段时间仍然没有收到确认,就认为刚才发送的分组丢失了,因而重传前面发送过的分组。这就叫作超时重传。要实现超时重传,就要在每发送完一个分组时设置一个超时计时器。如果在超时计时器到期之前收到了对方的确认,就撤销已设置的超时计时器。其实在图 5-8 (a) 中,A 为每一个已发送的分组都设置了一个超时计时器。但 A 只要在超时计时器到期之前收到了相应的确认,就撤销该超时计时器。为简单起见,这些细节在图 5-8 (a) 中都省略了。
这里应注意以下三点。
第一,A 在发送完一个分组后,必须暂时保留已发送的分组的副本(在发生超时重传时使用)。只有在收到相应的确认后才能清除暂时保留的分组副本。
第二,分组和确认分组都必须进行编号 \(^{②}\) 。这样才能明确是哪一个发送出去的分组收到了确认,而哪一个分组还没有收到确认。
第三,超时计时器设置的重传时间应当比数据在分组传输的平均往返时间更长一些。图 5-8 (b) 中的一段虚线表示如果 \(M_{1}\) 正确到达 B 同时 A 也正确收到确认的过程。可见重传时间应设定为比平均往返时间更长一些。显然,如果重传时间设定得很长,那么通信的效率就会很低。但如果重传时间设定得太短,以致产生不必要的重传,就浪费了网络资源。然而,在运输层重传时间的准确设定是非常复杂的,这是因为已发送出的分组到底会经过哪些网络,以及这些网络将会产生多大的时延(这取决于这些网络当时的拥塞情况),这些都是不确定因素。图 5-9 中把往返时间当作固定的(这显然不符合网络的实际情况),只是为了讲述原理的方便。关于重传时间应如何选择,在后面的 5.6.3 节还要进一步讨论。
3. 确认丢失和确认迟到¶
图 5-9 (a) 说明的是另一种情况。B 所发送的对 \(M_{1}\) 的确认丢失了。A 在设定的超时重传时间内没有收到确认,并无法知道是自己发送的分组出错、丢失,或者是 B 发送的确认丢失了。因此 A 在超时计时器到期后就要重传 \(M_{1}\) 。现在应注意 B 的动作。假定 B 又收到了重传的分组 \(M_{1}\) 。这时应采取两个行动。
第一,丢弃这个重复的分组 \(M_{1}\) ,不向上层重复交付。
第二,向 A 发送确认。不能认为已经发送过确认就不再发送,因为 A 之所以重传 \(M_{1}\) 就表示 A 没有收到对 \(M_{1}\) 的确认。


图 5-9 (b) 也是一种可能出现的情况。传输过程中没有出现差错,但 B 对分组 \(M_{1}\) 的确认迟到了。A 会收到重复的确认。对重复的确认的处理很简单:收下后就丢弃,但什么也不做。B 仍然会收到重复的 \(M_{1}\) ,并且同样要丢弃重复的 \(M_{1}\) ,并重传确认分组。
通常 A 最终总是可以收到对所有发出的分组的确认。如果 A 不断重传分组但总是收不到确认,就说明通信线路太差,不能进行通信。
使用上述的确认和重传机制,我们就可以在不可靠的传输网络上实现可靠的通信。
像上述的这种可靠传输协议常称为自动重传请求 ARQ (Automatic Repeat reQuest)。意思是重传的请求是自动进行的,因此也可见到自动请求重传这样的译名。接收方不需要请求发送方重传某个出错的分组。
4. 信道利用率¶
停止等待协议的优点是简单,但缺点是信道利用率太低。我们可以用图 5-10 来说明这个问题。为简单起见,假定在 A 和 B 之间有一条直通的信道来传送分组。

假定 A 发送分组需要的时间是 \(T_{D}\) 。显然, \(T_{D}\) 等于分组长度除以数据率。再假定分组正确到达 B 后,B 处理分组的时间可以忽略不计,同时立即发回确认。假定 B 发送确认分组需要时间 \(T_{A}\) 。如果 A 处理确认分组的时间也可以忽略不计,那么 A 在经过时间 \((T_{\mathrm{D}} + \mathrm{RTT} + T_{\mathrm{A}})\) 后就可以再发送下一个分组,这里的 RTT 是往返时间。因为仅仅是在时间 \(T_{D}\) 内才用来传送有用的数据(包括分组的首部),因此信道的利用率 U 可用下式计算:
请注意,更细致的计算还可以在上式分子的时间 \(T_{D}\) 内扣除传送控制信息(如首部)所花费的时间。但在进行粗略计算时,用近似的式 (5-3) 就可以了。
我们知道,式 (5-3) 中的往返时间 RTT 取决于所使用的信道。例如,假定 1200 km 的信道的往返时间 RTT = 20 ms,分组长度是 1200 bit,发送速率是 1 Mbit/s。若忽略处理时间和 \(T_{A}\) ( \(T_{A}\) 一般都远小于 \(T_{D}\) ),则可算出信道的利用率 U = 5.66%。但若把发送速率提高到 10 Mbit/s,则 \(U = 5.96 \times 10^{-3}\) 。信道在绝大多数时间内都是空闲的。
从图 5-10 还可看出,当往返时间 RTT 远大于分组发送时间 \(T_{D}\) 时,信道的利用率就会非常低。还应注意的是,图 5-10 并没有考虑出现差错后的分组重传。若出现重传,则对传送有用的数据信息来说,信道的利用率就还要降低。
为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是采用流水线传输(如图 5-11 所示)。流水线传输就是发送方可连续发送多个分组,不必每发完一个分组就停顿下来等待对方的确认。这样可使信道上一直有数据在不间断地传送。显然,这种传输方式可以获得很高的信道利用率。

当使用流水线传输时,就要使用下面介绍的连续 ARQ 协议和滑动窗口协议。
5.4.2 连续 ARQ 协议¶
滑动窗口协议比较复杂,是 TCP 协议的精髓所在。这里先给出连续 ARQ 协议最基本的概念,但不涉及许多细节问题。详细的滑动窗口协议将在后面的 5.6 节中讨论。
图 5-12 (a) 表示发送方维持的发送窗口,它的意义是:位于发送窗口内的 5 个分组都可
连续发送出去,而不需要等待对方的确认。这样,信道利用率就提高了。
在讨论滑动窗口时,我们应当注意到,图中还有一个时间坐标(但以后往往省略这样的时间坐标)。按照习惯,“向前” 是指 “向着时间增大的方向”,而 “向后” 则是 “向着时间减少的方向”。分组发送是按照分组序号从小到大发送的。

连续 ARQ 协议规定,发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置。图 5-12 (b) 表示发送方收到了对第 1 个分组的确认,于是把发送窗口向前移动一个分组的位置。如果原来已经发送了前 5 个分组,那么现在就可以发送窗口内的第 6 个分组了。
接收方一般都是采用累积确认的方式。这就是说,接收方不必对收到的分组逐个发送确认,而是在收到几个分组后,对按序到达的最后一个分组发送确认,这就表示:到这个分组为止的所有分组都已正确收到了。
累积确认有优点也有缺点。优点是容易实现,即使确认丢失也不必重传;但缺点是不能向发送方及时反映接收方已经正确收到所有分组的信息。
例如,如果发送方发送了前 5 个分组,而中间的第 3 个分组丢失了。这时接收方只能对前两个分组发出确认。发送方无法知道后面三个分组的下落,而只好把后面的三个分组都再重传一次。这就叫作 Go-back-N(回退 N),表示需要再退回来重传已发送过的 N 个分组。可见当通信线路质量不好时,连续 ARQ 协议会带来负面的影响。
在深入讨论 TCP 的可靠传输问题之前,必须先了解 TCP 的报文段首部的格式。
5.5 TCP 报文段的首部格式¶
TCP 虽然是面向字节流的,但 TCP 传送的数据单元却是报文段。一个 TCP 报文段分为首部和数据两部分,而 TCP 的全部功能都体现在它首部中各字段的作用。因此,只有弄清 TCP 首部各字段的作用才能掌握 TCP 的工作原理。下面讨论 TCP 报文段的首部格式。
TCP 报文段首部的前 20 个字节是固定的(如图 5-13 所示),后面有 4n 字节是根据需要而增加的选项(n 是整数)。因此 TCP 首部的最小长度是 20 字节。
首部固定部分各字段的意义如下:
(1) 源端口和目的端口 各占 2 个字节,分别写入源端口号和目的端口号。和前面图 5-5 所示的 UDP 的分用相似,TCP 的分用功能也是通过端口实现的。
(2) 序号 占 4 字节。序号范围是 \([0, 2^{32} - 1]\) ,共 \(2^{32}\) (即 4294967296)个序号。序号增加到 \(2^{32} - 1\) 后,下一个序号就又回到 0。也就是说,序号使用 mod \(2^{32}\) 运算。TCP 是面向字节流的。在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号。整个要传送的字节流的起始序号必须在连接建立时设置。首部中的序号字段值则指的是本报文段所发送的数据的第一个字节的序号。例如,一报文段的序号字段值是 301,而携带的数据共有 100 字节。这就表明:本报文段的数据的第一个字节的序号是 301,最后一个字节的序号是 400。显然,下一个报文段(如果还有的话)的数据序号应当从 401 开始,即下一个报文段的序号字段值应为 401。这个字段的名称也叫作 “报文段序号”。

(3) 确认号 占 4 字节,是期望收到对方下一个报文段的第一个数据字节的序号。例如,B 正确收到了 A 发送过来的一个报文段,其序号字段值是 501, 而数据长度是 200 字节 (序号 \(501 \sim 700\)), 这表明 B 正确收到了 A 发送的到序号 700 为止的数据。因此,B 期望收到 A 的下一个数据序号是 701, 于是 B 在发送给 A 的确认报文段中把确认号置为 701。请注意,现在的确认号不是 501, 也不是 700, 而是 701。
总之,应当记住:
若确认号 = N,则表明:到序号 N-1 为止的所有数据都已正确收到。
由于序号字段有 32 位长,可对 4 GB(即 4 千兆字节)的数据进行编号。在一般情况下可保证当序号重复使用时,旧序号的数据早已通过网络到达终点了。
(4) 数据偏移 占 4 位,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。这个字段实际上是指出 TCP 报文段的首部长度。由于首部中还有长度不确定的选项字段,因此数据偏移字段是必要的。但应注意,“数据偏移” 的单位是 32 位字(即以 4 字节长的字为计算单位)。由于 4 位二进制数能够表示的最大十进制数字是 15,因此数据偏移的最大值是 60 字节,这也是 TCP 首部的最大长度(即选项长度不能超过 40 字节)。
(5) 保留 占 6 位,保留为今后使用,但目前应置为 0。
下面有 6 个控制位,用来说明本报文段的性质,它们的意义见下面的 (6)\~(11)。
(6) 紧急 URG (URGent) 当 URG = 1 时,表明紧急指针字段有效。它告诉系统此报
文段中有紧急数据,应尽快传送(相当于高优先级的数据),而不要按原来的排队顺序传送。例如,已经发送了很长的一个程序要在远地的主机上运行。但后来发现了一些问题,需要取消该程序的运行。因此用户从键盘发出中断命令(Control-C)。如果不使用紧急数据,那么这两个字符将存储在接收 TCP 的缓存末尾。只有在所有的数据被处理完毕后这两个字符才被交付接收方的应用进程。这样做就浪费了许多时间。
当 URG 置 1 时,发送应用进程就告诉发送方的 TCP 有紧急数据要传送。于是发送方 TCP 就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据。这时要与首部中紧急指针 (Urgent Pointer) 字段配合使用。
然而在紧急指针字段的具体实现上,由于过去的有些文档有错误或有不太明确的地方,这就导致人们对有关的 RFC 文档产生了不同的理解。于是,2011 年公布的建议标准 RFC 6093,对紧急指针字段的使用方法做出了更加明确的解释,并更新了几个重要的 RFC 文档,如 RFC 793, RFC 1011, RFC 1122 等。
(7) 确认 ACK (ACKnowledgment) 仅当 ACK = 1 时确认号字段才有效。当 ACK = 0 时,确认号无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置为 1。
(8) 推送 PSH (PuSH) 当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下,TCP 就可以使用推送 (push) 操作。这时,发送方 TCP 把 PSH 置 1,并立即创建一个报文段发送出去。接收方 TCP 收到 PSH = 1 的报文段,就尽快地(即 “推送” 向前)交付接收应用进程,而不再等到整个缓存都填满了后再向上交付。
虽然应用程序可以选择推送操作,但推送操作很少使用。
依据。
例如,发送了一个报文段,其确认号是 701,窗口字段是 1000。这就是告诉对方:“从 701 号算起,我(即发送此报文段的一方)的接收缓存空间还可接收 1000 个字节数据(字节序号是 701~1700),你在给我发送数据时,必须考虑到我的接收缓存容量。”
总之,应当记住:
窗口字段明确指出了现在允许对方发送的数据量。窗口值经常在动态变化着。
(13) 检验和 占 2 字节。检验和字段检验的范围包括首部和数据这两部分。和 UDP 用户数据报一样,在计算检验和时,要在 TCP 报文段的前面加上 12 字节的伪首部。伪首部的格式与图 5-5 中 UDP 用户数据报的伪首部一样。但应把伪首部第 4 个字段中的 17 改为 6(TCP 的协议号是 6),把第 5 字段中的 UDP 长度改为 TCP 长度。接收方收到此报文段后,仍要加上这个伪首部来计算检验和。若使用 IPv6,则相应的伪首部也要改变。
(14) 紧急指针 占 2 字节。紧急指针仅在 URG = 1 时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此,紧急指针指出了紧急数据的末尾在报文段中的位置。当所有紧急数据都处理完时,TCP 就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为零时也可发送紧急数据。
(15) 选项 长度可变,最长可达 40 字节。当没有使用 “选项” 时,TCP 的首部长度是 20 字节。最后的填充字段仅仅是为了使整个 TCP 首部长度是 4 字节的整数倍。
TCP 最初只规定了一种选项,即最大报文段长度 MSS (Maximum Segment Size) [RFC 6691]。请注意 MSS 这个名词的含义。MSS 是每一个 TCP 报文段中的数据字段的最大长度。数据字段加上 TCP 首部才等于整个的 TCP 报文段。所以 MSS 并不是整个 TCP 报文段的最大长度,而是 “TCP 报文段长度减去 TCP 首部长度”。
为什么要规定一个最大报文段长度 MSS 呢?这并不是考虑接收方的接收缓存可能放不下 TCP 报文段中的数据。实际上,MSS 与接收窗口值没有关系。我们知道,TCP 报文段的数据部分,至少要加上 40 字节的首部(TCP 首部 20 字节和 IP 首部 20 字节,这里都还没有考虑首部中的选项部分),才能组装成一个 IP 数据报。若选择较小的 MSS 长度,网络的利用率就降低了。设想在极端的情况下,当 TCP 报文段只含有 1 字节的数据时,在 IP 层传输的数据报的开销至少有 40 字节(包括 TCP 报文段的首部和 IP 数据报的首部)。这样,对网络的利用率就不会超过 1/41。到了数据链路层还要加上一些开销。但反过来,若 TCP 报文段非常长,那么在 IP 层传输时就有可能要分解成多个短数据报片。在终点要把收到的各个短数据报片装配成原来的 TCP 报文段。当传输出错时还要进行重传。这些也都会使开销增大。
因此,从提高网络传输效率考虑,MSS 应尽可能大些,只要在 IP 层传输时不需要再分片就行。由于 IP 数据报所经历的路径是动态变化的,因此在某条路径上确定的不需要分片的 MSS,如果改走另一条路径就可能需要进行分片。因此最佳的 MSS 实际上是很难确定的。在连接建立的过程中,双方都把自己能够支持的 MSS 写入这一字段,以后就按照这个数值传送数据,两个传送方向可以有不同的 MSS 值 \(^{①}\) 。若主机未填写这一项,则 MSS 的默认值是 536 字节(这个数值来自 576 字节的 IP 数据报总长度减去 TCP 和 IP 的固定首部)。因此,所有互联网上的主机都应能接受的报文段长度是 \(536 + 20\) (固定首部长度)=556 字节。
随着互联网的发展,又陆续增加了几个选项,如窗口扩大选项、时间戳选项等(见建议标准 RFC 7323)。以后又增加了有关选择确认 (SACK) 选项(见建议标准 RFC 2018)。这些选项的位置都在图 5-13 所示的选项字段中。
窗口扩大选项是为了扩大窗口。我们知道,TCP 首部中窗口字段长度是 16 位,因此最大的窗口大小为 64 KB(见下一节)。虽然这对早期的网络是足够用的,但对于包含卫星信道的网络 \(^{①}\) ,其传播时延和带宽都很大,要获得高吞吐率需要更大的窗口大小。
窗口扩大选项占 3 字节,其中有一个字节表示移位值 S。新的窗口值等于 TCP 首部中的窗口位数从 16 增大到 \((16 + S)\) 。移位值允许使用的最大值是 14,相当于窗口最大值增大到 \(2^{(16 + 14)} - 1 = 2^{30} - 1\) 。
窗口扩大选项可以在双方初始建立 TCP 连接时进行协商。如果连接的某一端实现了窗口扩大,当它不再需要扩大其窗口时,可发送 S = 0 的选项,使窗口大小回到 16。
时间戳选项占 10 字节,其中最主要的字段是时间戳值字段(4 字节)和时间戳回送回答字段(4 字节)。时间戳选项有以下两个功能:
第一,用来计算往返时间 RTT(见后面的 5.6.2 节)。发送方在发送报文段时把当前时钟的时间值放入时间戳字段,接收方在确认该报文段时把时间戳字段值复制到时间戳回送回答字段。因此,发送方在收到确认报文后,可以准确地计算出 RTT。
第二,用于处理 TCP 序号超过 \(2^{32}\) 的情况,这又称为防止序号绕回 PAWS (Protect Against Wrapped Sequence numbers)。我们知道,TCP 报文段的序号只有 32 位,而每增加 \(2^{32}\) 个序号就会重复使用原来用过的序号。当使用高速网络时,在一次 TCP 连接的数据传送中序号很可能会被重复使用。例如,当使用 1.5 Mbit/s 的速率发送报文段时,序号重复要 6 小时以上。但若用 2.5 Gbit/s 的速率发送报文段,则不到 14 秒序号就会重复。为了使接收方能够把新的报文段和迟到很久的报文段区分开,可以在报文段中加上这种时间戳。
我们将在后面的 5.6.3 节介绍选择确认选项。
5.6 TCP 可靠传输的实现¶
本节讨论 TCP 可靠传输的实现。
我们首先介绍以字节为单位的滑动窗口。为了讲述可靠传输原理的方便,我们假定数据传输只在一个方向进行,即 A 发送数据,B 给出确认。这样的好处是使讨论限于两个窗口,即发送方 A 的发送窗口和接收方 B 的接收窗口。如果再考虑 B 也向 A 发送数据,那么还要增加 A 的接收窗口和 B 的发送窗口,这样总共有 4 个都不断在变化大小的窗口。这对讲述可靠传输的原理并没有多少帮助,反而会使问题变得更加烦琐。
5.6.1 以字节为单位的滑动窗口¶
TCP 的滑动窗口是以字节为单位的。为了便于说明滑动窗口的工作原理,我们故意把后面图 5-14 至图 5-17 中的字节编号都取得很小(实际的窗口大小多为数千字节)。现假定 A 收到了 B 发来的确认报文段,其中窗口是
20 字节,而确认号是 31(这表明 B 期望收到的下一个字节序号是 31(请注意,这里不是分组的序号),而到序号 30 为止的数据已经收到了)。根据这两个数据,A 就构造出自己的发送窗口,如图 5-14 所示。

我们先讨论发送方 A 的发送窗口。发送窗口表示:在没有收到 B 的确认的情况下,A 可以连续把窗口内的数据都发送出去。凡是已经发送过的数据,在未收到确认之前都必须暂时保留,以便在超时重传时使用。
发送窗口里面的序号表示允许发送的序号。显然,窗口越大,发送方就可以在收到对方确认之前连续发送更多的数据,因而可能获得更高的传输效率。在上面的 5.5 节我们已经讲过,接收方会把自己的接收窗口数值放在窗口字段中发送给对方。因此,A 的发送窗口一定不能超过 B 的接收窗口数值。在后面的 5.8 节我们将要讨论,发送方的发送窗口大小还要受到当时网络拥塞程度的制约。但在目前,我们暂不考虑网络拥塞的影响。
发送窗口后沿的后面部分表示已发送且已收到了确认。这些数据显然不需要再保留了。而发送窗口前沿的前面部分表示不允许发送,因为接收方没有为这部分数据保留临时存放的缓存空间。
发送窗口的位置由窗口前沿和后沿的位置共同确定。发送窗口后沿的变化情况有两种可能,即不动(没有收到新的确认)和前移(收到了新的确认)。发送窗口后沿不可能向后移动,因为不能撤销掉已收到的确认。发送窗口前沿通常是不断向前移动的,但也有可能不动。这对应两种情况:一是没有收到新的确认,对方通知的窗口大小也不变;二是收到了新的确认但对方通知的窗口缩小了,使得发送窗口前沿正好不动。
发送窗口前沿也有可能向后收缩。这发生在对方通知的窗口缩小了。但 TCP 的标准强烈不赞成这样做。因为很可能发送方在收到这个通知以前已经发送了窗口中的许多数据,现在又要收缩窗口,不让发送这些数据,这样就会产生一些错误。
现在假定 A 发送了序号为 31~41 的数据。这时,发送窗口位置并未改变(如图 5-15 所示),但发送窗口内靠后面有 11 个字节(灰色方框表示)表示已发送但未收到确认。而发送窗口内靠前面的 9 个字节(序号 42~50)是允许发送但尚未发送的。

从以上所述可以看出,要描述一个发送窗口的状态需要三个指针: \(P_{1}\) , \(P_{2}\) 和 \(P_{3}\) (如图 5-15 所示)。指针都指向字节的序号。A 的发送窗口中三个指针指向的几个部分的意义如下:
\(P_{1}\) 之前的数据(序号 < 31)是已发送并已收到确认的部分。
\(P_{3}\) 之后的数据(序号 > 50)是不允许发送的部分。
\(P_{3}-P_{1}=A\) 的发送窗口 =20(序号 \(31\sim50\) )。
\(P_{2}-P_{1}=\) 已发送但尚未收到确认的字节数(序号 31\~41)。
\(P_{3}-P_{2}=\) 允许发送但当前尚未发送的字节数(序号 42~50)(又称为可用窗口或有效窗口)。
再看一下 B 的接收窗口。设 B 的接收窗口大小是 20。在接收窗口外面,到序号为 30 的数据是已经发送过确认,并且已经交付主机了。因此在 B 可以不再保留这些数据。接收窗口内的数据(序号 31~50)是允许接收的。在图 5-15 中,B 收到了序号为 32 和 33 的数据,但序号为 31 的数据没有收到(也许丢失了,也许滞留在网络中的某处)。请注意,B 只能对按序收到的数据中的最高序号给出确认,因此 B 发送的确认报文段中的确认号仍然是 31(即期望收到的序号)。
现在假定 B 收到了序号为 31 的数据,把序号为 31~33 的数据交付主机,删除这些数据。接着把接收窗口向前移动 3 个序号(如图 5-16 所示),同时给 A 发送确认,其中窗口值仍为 20,但确认号是 34。这表明 B 已经收到了到序号 33 为止的数据。我们注意到,B 还收到了序号为 37, 38 和 40 的数据,但这些数据都没有按序到达,只能先暂存在接收窗口中。A 收到 B 的确认后,就可以把发送窗口向前滑动 3 个序号,但指针 P₂ 不动。可以看出,现在 A 的可用窗口增大了些,可发送的序号范围是 42~53。

A 在继续发送完序号 42 \~ 53 的数据后,指针 \(P_{2}\) 向前移动和 \(P_{3}\) 重合。发送窗口内的序号都已用完,但还没有再收到确认(如图 5-17 所示)。由于 A 的发送窗口已满,可用窗口已减小到零,因此必须停止发送。请注意,存在下面这种可能性,就是发送窗口内所有的数据都已正确到达 B,B 也早已发出了确认。但不幸的是,所有这些确认都滞留在网络中。在没有收到 B 的确认时,为了保证可靠传输,A 只能认为 B 还没有收到这些数据。于是,A 在经过一段时间后(由超时计时器控制)就重传这部分数据,重新设置超时计时器,直到收到 B 的确认为止。如果 A 按序收到落在发送窗口内的确认号,那么 A 就可以使发送窗口继续向前滑动,并发送新的数据。

我们在前面的图 5-7 中曾给出了这样的概念:发送方的应用进程把字节流写入 TCP 的发送缓存,接收方的应用进程从 TCP 的接收缓存中读取字节流。下面我们就进一步讨论前面讲的窗口和缓存的关系。图 5-18 画出了发送方维持的发送缓存和发送窗口,以及接收方维持的接收缓存和接收窗口。这里首先要明确两点:


第一,缓存空间和序号空间都是有限的,并且都是循环使用的。最好是把它们画成圆环状的。但这里为了画图的方便,我们还是把它们画成了长条状的。
第二,由于缓存或窗口中实际的字节数可能很大,因此图 5-18 仅仅是个示意图,没有标出具体的数值。但用这样的图来说明缓存和发送窗口以及接收窗口的关系是很清楚的。
我们先看一下图 5-18 (a) 所示的发送方的情况。
发送缓存用来暂时存放:
发送窗口通常只是发送缓存的一部分。已被确认的数据应当从发送缓存中删除,因此发送缓存和发送窗口的后沿是重合的。发送应用程序最后写入发送缓存的字节减去最后被确认的字节,就是还保留在发送缓存中的被写入的字节数。发送应用程序必须控制写入缓存的速率,不能太快,否则发送缓存就会没有存放数据的空间。
再看一下图 5-18 (b) 所示的接收方的情况。
接收缓存用来暂时存放:
如果收到的分组被检测出有差错,则要丢弃。如果接收应用程序来不及读取收到的数据,接收缓存最终就会被填满,使接收窗口减小到零。反之,如果接收应用程序能够及时从接收缓存中读取收到的数据,接收窗口就可以增大,但最大不能超过接收缓存的大小。
图 5-18 (b) 中还指出了下一个期望收到的字节号。这个字节号也就是接收方给发送方的报文段的首部中的确认号。
根据以上所讨论的,我们还要再强调以下三点。
第一,虽然 A 的发送窗口是根据 B 的接收窗口设置的,但在同一时刻,A 的发送窗口并不总是和 B 的接收窗口一样大。这是因为通过网络传送窗口值需要经历一定的时间滞后(这个时间是不确定的)。另外,正如后面 5.7 节将要讲到的,发送方 A 还可能根据网络当时的拥塞情况适当减小自己的发送窗口数值。
第二,对于不按序到达的数据应如何处理,TCP 标准并无明确规定。如果接收方把不按序到达的数据一律丢弃,那么接收窗口的管理将会比较简单,但这样做对网络资源的利用不利(因为发送方会重复传送较多的数据)。因此 TCP 通常是把不按序到达的数据先临时存放在接收窗口中,等到字节流中所缺少的字节收到后,再按序交付上层的应用进程。
第三,TCP 要求接收方必须有累积确认的功能,这样可以减小传输开销。接收方可以在合适的时候发送确认,也可以在自己有数据要发送时把确认信息顺便捎带上。但请注意两点。一是接收方不应过分推迟发送确认,否则会导致发送方不必要的重传,这反而浪费了网络的资源。TCP 标准规定,确认推迟的时间不应超过 0.5 秒。若收到一连串具有最大长度的报文段,则必须每隔一个报文段就发送一个确认 [RFC 1122, STD3]。二是捎带确认实际上并不经常发生,因为大多数应用程序很少同时在两个方向上发送数据。
最后再强调一下,TCP 的通信是全双工通信。通信中的每一方都在发送和接收报文段。因此,每一方都有自己的发送窗口和接收窗口。在谈到这些窗口时,一定要弄清是哪一方的窗口。
5.6.2 超时重传时间的选择¶
上面已经讲到,TCP 的发送方在规定的时间内没有收到确认就要重传已发送的报文段。这种重传的概念是很简单的,但重传时间的选择却是 TCP 最复杂的问题之一。
由于 TCP 的下层是互联网环境,发送的报文段可能只经过一个高速率的局域网,也可能经过多个低速率的网络,并且每个 IP 数据报所选择的路由还可能不同。如果把超时重传时间设置得太短,就会引起很多报文段的不必要的重传,使网络负荷增大。但若把超时重传时间设置得过长,则又使网络的空闲时间增大,降低了传输效率。
那么,运输层的超时计时器的超时重传时间究竟应设置为多大呢?
TCP 采用了一种自适应算法,它记录一个报文段发出的时间,以及收到相应的确认的时间。这两个时间之差就是报文段的往返时间 RTT。TCP 保留了 RTT 的一个加权平均往返时间 RTTs(这又称为平滑的往返时间,S 表示 Smoothed。因为进行的是加权平均,因此得出的结果更加平滑)。每当第一次测量到 RTT 样本时,RTTs 值就取为所测量到的 RTT 样本值。但以后每测量到一个新的 RTT 样本,就按下式重新计算一次 RTTs:
在上式中, \(0 \leqslant \alpha < 1\) 。若 \(\alpha\) 很接近于零,表示新的 \(\mathrm{RTT}_{\mathrm{S}}\) 值和旧的 \(\mathrm{RTT}_{\mathrm{S}}\) 值相比变化不大,而对新的 RTT 样本影响不大(RTT 值更新较慢)。若选择 \(\alpha\) 接近于 1,则表示新的 \(\mathrm{RTT}_{\mathrm{S}}\) 值受新的 RTT 样本的影响较大(RTT 值更新较快)。已成为建议标准的 RFC 6298 推荐的 \(\alpha\) 值为 \(1/8\) ,即 0.125。用这种方法得出的加权平均往返时间 \(\mathrm{RTT}_{\mathrm{S}}\) 就比测量出的 RTT 值
更加平滑。
显然,超时计时器设置的超时重传时间 RTO (RetransmissionTime-Out) 应略大于上面得出的加权平均往返时间 \(\mathrm{RTT}_{\mathrm{S}}\) 。RFC 6298 建议使用下式计算 RTO:
而 \(\mathrm{RTT}_{\mathrm{D}}\) 是 RTT 的偏差的加权平均值,它与 \(\mathrm{RTT}_{\mathrm{S}}\) 和新的 RTT 样本之差有关。RFC6298 建议这样计算 \(\mathrm{RTT}_{\mathrm{D}}\) 。当第一次测量时, \(\mathrm{RTT}_{\mathrm{D}}\) 值取为测量到的 RTT 样本值的一半。在以后的测量中,则使用下式计算加权平均的 \(\mathrm{RTT}_{\mathrm{D}}\) :
这里 \(\beta\) 是个小于 1 的系数,它的推荐值是 1/4,即 0.25。
上面所说的往返时间的测量,实现起来相当复杂。试看下面的例子。
如图 5-19 所示,发送出一个报文段,设定的重传时间到了,还没有收到确认,于是重传报文段。经过了一段时间后,收到了确认报文段。现在的问题是:如何判定此确认报文段是对先发送的报文段的确认,还是对后来重传的报文段的确认?由于重传的报文段和原来的报文段完全一样,因此源主机在收到确认后,就无法做出正确的判断,而正确的判断对确定加权平均 RTT \(_{S}\) 的值关系很大。
若收到的确认是对重传报文段的确认,但却被源主机当成是对原来的报文段的确认,则这样计算出的 \(RTT_{s}\) 和超时重传时间 RTO 就会偏大。若后面再发送的报文段又是经过重传后才收到确认报文段,则按此方法得出的超时重传时间 RTO 就越来越长。

同样,若收到的确认是对原来的报文段的确认,但被当成是对重传报文段的确认,则由此计算出的 \(RTT_{S}\) 和 RTO 都会偏小。这就必然导致报文段过多地重传。这样就有可能使 RTO 越来越短。
根据以上所述,Karn 提出了一个算法:在计算加权平均 RTTs 时,只要报文段重传了,就不采用其往返时间样本。这样得出的加权平均 RTTs 和 RTO 就较准确。
但是,这又引起新的问题。设想出现这样的情况:报文段的时延突然增大了很多。因此在原来得出的重传时间内不会收到确认报文段,于是就重传报文段。但根据 Karn 算法,不考虑重传的报文段的往返时间样本。这样,超时重传时间就无法更新。
因此要对 Karn 算法进行修正。方法是:报文段每重传一次,就把超时重传时间 RTO 增大一些。典型的做法是取新的重传时间为旧的重传时间的 2 倍。当不再发生报文段的重传时,才根据上面给出的式 (5-5) 计算超时重传时间。实践证明,这种策略较为合理。
总之,Karn 算法能够使运输层区分开有效的和无效的往返时间样本,从而改进了往返时间的估测,使计算结果更加合理。
5.6.3 选择确认 SACK¶
现在还有一个问题没有讨论。这就是若收到的报文段无差错,只是未按序号,中间还缺少一些序号的数据,那么能否设法只传送缺少的数据而不重传已经正确到达接收方的数据?答案是可以的。选择确认 (Selective ACK)[RFC 2018,建议标准] 就是一种可行的处理方法。
我们用一个例子来说明选择确认的工作原理。TCP 的接收方在接收对方发送过来的数据字节流的序号不连续,结果就形成了一些不连续的字节块(如图 5-20 所示)。可以看出,序号 1~1000 收到了,但序号 1001~1500 没有收到。接下来的字节流又收到了,可是又缺少了 3001~3500。再后面从序号 4501 起又没有收到。也就是说,接收方收到了和前面的字节流不连续的两个字节块。如果这些字节的序号都在接收窗口之内,那么接收方就先收下这些数据,但要把这些信息准确地告诉发送方,使发送方不要再重复发送这些已收到的数据。

从图 5-20 可看出,和前后字节不连续的每一个字节块都有两个边界:左边界和右边界,因此在图中用四个指针标记这些边界。请注意,第一个字节块的左边界 \(L_{1} = 1501\) ,但右边界 \(R_{1} = 3001\) 而不是 3000。这就是说,左边界指出字节块的第一个字节的序号,但右边界减 1 才是字节块的最后一个序号。同理,第二个字节块的左边界 \(L_{2} = 3501\) ,而右边界 \(R_{2} = 4501\) 。
我们知道,TCP 的首部没有哪个字段能够提供上述这些字节块的边界信息。RFC 2018 规定,如果要使用选择确认 SACK,那么在建立 TCP 连接时,就要在 TCP 首部的选项中加上 “允许 SACK” 的选项,而双方必须都事先商定好。如果使用选择确认,那么原来首部中的 “确认号字段” 的用法仍然不变。只是以后在 TCP 报文段的首部中都增加了 SACK 选项,以便报告收到的不连续的字节块的边界。由于首部选项的长度最多只有 40 字节,而指明一个边界就要用掉 4 字节(因为序号有 32 位,需要使用 4 个字节表示),因此在选项中最多只能指明 4 个字节块的边界信息。这是因为 4 个字节块共有 8 个边界,因而需要用 32 个字节来描述。另外还需要两个字节,一个字节用来指明是 SACK 选项,另一个字节指明这个选项要占用多少字节。如果要报告 5 个字节块的边界信息,那么至少需要 42 个字节。这就超过了选项长度 40 字节的上限。互联网建议标准 RFC 2018 还对报告这些边界信息的格式都做出了非常明确的规定,这里从略。
然而,SACK 文档并没有指明发送方应当怎样响应 SACK。因此大多数的实现还是重传所有未被确认的数据块。
5.7 TCP 的流量控制¶
5.7.1 利用滑动窗口实现流量控制¶
一般说来,我们总是希望数据传输得更快一些。但如果发送方把数据发送得过快,接收方就可能来不及接收,这就会造成数据的丢失。所谓流量控制 (flow control) 就是让发送方的发送速率不要太快,要让接收方来得及接收。
利用滑动窗口机制可以很方便地在 TCP 连接上实现对发送方的流量控制。
下面通过图 5-21 的例子说明如何利用滑动窗口机制进行流量控制。

设 A 向 B 发送数据。在连接建立时,B 告诉了 A:“我的接收窗口 rwnd = 400。”(这里 rwnd 表示 receiver window。)因此,发送方的发送窗口不能超过接收方给出的接收窗口 \(^{①}\) 的数值。请注意,TCP 的窗口单位是字节,不是报文段。TCP 连接建立时的窗口协商过程在图中没有显示出来。再设每一个报文段为 100 字节长,而数据报文段序号的初始值设为 1(见图中第一个箭头上面的序号 seq = 1。图中右边的注释可帮助理解整个过程)。请注意,图中箭头上面大写 ACK 表示首部中的确认位 ACK,小写 ack 表示确认字段的值。
我们应注意到,接收方的主机 B 进行了三次流量控制。第一次把窗口减小到 rwnd = 300,第二次又减到 rwnd = 100,最后减到 rwnd = 0,即不允许发送方再发送数据了。这种使发送方暂停发送的状态将持续到主机 B 重新发出一个新的窗口值为止。我们还应注意到,B 向 A 发送的三个报文段都设置了 ACK = 1,只有在 ACK = 1 时确认号字段才有意义。
现在我们考虑一种情况。在图 5-21 中,B 向 A 发送了零窗口的报文段后不久,B 的接收缓存又有了一些存储空间。于是 B 向 A 发送了 rwnd = 400 的报文段。然而这个报文段在传送过程中丢失了。A 一直等待收到 B 发送的非零窗口的通知,而 B 也一直等待 A 发送的数据。如果没有其他措施,这种互相等待的死锁局面将一直延续下去。
为了解决这个问题,TCP 为每一个连接设有一个持续计时器 (persistence timer)。只要
TCP 连接的一方收到对方的零窗口通知,就启动持续计时器。若持续计时器设置的时间到期,就发送一个零窗口探测报文段(仅携带 1 字节的数据) \(^{①}\) ,而对方就在确认这个探测报文段时给出了现在的窗口值。如果窗口仍然是零,那么收到这个报文段的一方就重新设置持续计时器。如果窗口不是零,那么死锁的僵局就可以打破了。
5.7.2 TCP 的传输效率¶
前面已经讲过,应用进程把数据传送到 TCP 的发送缓存后,剩下的发送任务就由 TCP 来控制了。可以用不同的机制来控制 TCP 报文段的发送时机。例如,第一种机制是 TCP 维持一个变量,它等于最大报文段长度 MSS。
只要缓存中存放的数据达到 MSS 字节时,就组装成一个 TCP 报文段发送出去。第二种机制是由发送方的应用进程指明要求发送报文段,即 TCP 支持的推送 (push) 操作。第三种机制是发送方的一个计时器期限到了,这时就把当前已有的缓存数据装入报文段(但长度不能超过 MSS)发送出去。
但是,如何控制 TCP 发送报文段的时机仍然是一个较为复杂的问题 [RFC 1122]。
例如,一个交互式用户使用一条 TELNET 连接(运输层为 TCP 协议)。假设用户只发 1 个字符,加上 20 字节的首部后,得到 21 字节长的 TCP 报文段。再加上 20 字节的 IP 首部,形成 41 字节长的 IP 数据报。在接收方 TCP 立即发出确认,构成的数据报是 40 字节长(假定没有数据发送)。若用户要求远地主机回送这一字符,则又要发回 41 字节长的 IP 数据报和 40 字节长的确认 IP 数据报。这样,用户仅发 1 个字符时,线路上就需传送总长度为 162 字节共 4 个报文段。当线路带宽并不富裕时,这种传送方法的效率的确不高。因此应适当推迟发回确认报文,并尽量使用捎带确认的方法。
在 TCP 的实现中广泛使用 Nagle 算法。算法如下:若发送应用进程把要发送的数据逐个字节地送到 TCP 的发送缓存,则发送方就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来。当发送方收到对第一个数据字符的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存。只有在收到对前一个报文段的确认后才继续发送下一个报文段。当数据到达较快而网络速率较慢时,用这样的方法可明显地减少所用的网络带宽。Nagle 算法还规定,当到达的数据已达到发送窗口大小的一半或已达到报文段的最大长度时,就立即发送一个报文段。这样做,就可以有效地提高网络的吞吐量。
另一个问题叫作糊涂窗口综合征 (silly window syndrome),有时也会使 TCP 的性能变坏。设想一种情况:TCP 接收方的缓存已满,而交互式的应用进程一次只从接收缓存中读取 1 个字节(这样就使接收缓存空间仅腾出 1 个字节),然后向发送方发送确认,并把窗口设置为 1 个字节(但发送的数据报是 40 字节长)。接着,发送方又发来 1 个字节的数据(请注意,发送方发送的 IP 数据报是 41 字节长)。接收方发回确认,仍然将窗口设置为 1 个字节。这样进行下去,使网络的效率很低。
要解决这个问题,可以让接收方等待一段时间,使得或者接收缓存已有足够空间容纳一个最长的报文段,或者等到接收缓存已有一半空闲的空间。只要出现这两种情况之一,接收方就发出确认报文,并向发送方通知当前的窗口大小。此外,发送方也不要发送太小的报文段,而是把数据积累成足够大的报文段,或达到接收方缓存的空间的一半大小。
上述两种方法可配合使用。使得在发送方不发送很小的报文段的同时,接收方也不要在缓存刚刚有了一点小的空间就急忙把这个很小的窗口大小信息通知给发送方。
5.8 TCP 的拥塞控制¶
5.8.1 拥塞控制的一般原理¶
在计算机网络中的链路容量(即带宽)、交换节点中的缓存和处理机等,都是网络的资源。在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫作拥塞 (congestion)。可以把出现网络拥塞的条件写成如下的关系式:
若网络中有许多资源同时呈现供应不足,网络的性能就要明显变坏,整个网络的吞吐量将随输入负荷的增大而下降。
有人可能会说:“只要任意增加一些资源,例如,把节点缓存的存储空间扩大,或把链路更换为更高速率的链路,或把节点处理机的运算速度提高,就可以解决网络拥塞的问题。” 其实不然。这是因为网络拥塞是一个非常复杂的问题。简单地采用上述做法,在许多情况下,不但不能解决拥塞问题,而且还可能使网络的性能更坏。
网络拥塞往往是由很多因素引起的。例如,当某个节点缓存的容量太小时,到达该节点的分组因无存储空间暂存而不得不被丢弃。现在设想将该节点缓存的容量扩展到非常大,于是凡到达该节点的分组均可在节点的缓存队列中排队,不受任何限制。由于输出链路的容量和处理机的处理速度并未提高,因此在这队列中的绝大多数分组的排队等待时间将会大大增加,结果上层软件只好把它们进行重传(因为早就超时了)。由此可见,简单地扩大缓存的存储空间同样会造成网络资源的严重浪费,因而解决不了网络拥塞的问题。
又如,处理机处理的速率太低可能引起网络的拥塞。简单地将处理机的速率提高,可能会使上述情况缓解一些,但往往又会将瓶颈转移到其他地方。问题的实质往往是整个系统的各个部分不匹配。只有所有的部分都平衡了,问题才会得到解决。
拥塞常常趋于恶化。如果一个路由器没有足够的缓存空间,它就会丢弃一些新到的分组。但当分组被丢弃时,发送这一分组的源点就会重传这一分组,甚至可能还要重传多次。这样会引起更多的分组流入网络和被网络中的路由器丢弃。可见拥塞引起的重传并不会缓解网络的拥塞,反而会加剧网络的拥塞。
拥塞控制与流量控制的关系密切,它们之间也存在着一些差别。所谓拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不至于过载。拥塞控制所要做的都有一个前提,就是网络能够承受现有的网络负荷。拥塞控制是一个全局性的过程,涉及所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。但 TCP 连接的端点只要迟迟不能收到对方的确认信息,就猜想在当前网络中的某处很可能发生了拥塞,但这时却无法知道拥塞到底发生在网络的何处,也无法知道发生拥塞的具体原因。(是访问某个服务器的通信量过大?还是在某个地区出现自然灾害?)
相反,流量控制往往是指点对点通信量的控制,是个端到端的问题(接收端控制发送
端)。流量控制所要做的就是抑制发送端发送数据的速率,以便接收端来得及接收。
可以用一个简单例子说明这种区别。设某个光纤网络的链路传输速率为 1000 Gbit/s,有一台巨型计算机向一台个人电脑以 1 Gbit/s 的速率传送文件。显然,网络本身的带宽是足够大的,因而不存在产生拥塞的问题。但流量控制却是必需的,因为巨型计算机必须经常停下来,以便个人电脑来得及接收。
但如果有另一个网络,其链路传输速率为 1 Mbit/s,而有 1000 台大型计算机连接在这个网络上。假定其中的 500 台计算机分别向其余的 500 台计算机以 100 kbit/s 的速率发送文件。那么现在的问题已不是接收端的大型计算机是否来得及接收,而是整个网络的输入负载是否超过网络所能承受的。
拥塞控制和流量控制之所以常常被弄混,是因为某些拥塞控制算法是向发送端发送控制报文,并告诉发送端,网络已出现麻烦,必须放慢发送速率。这点又和流量控制是很相似的。
流量控制和拥塞控制的区别可以用图 5-22 的简单比喻来说明。图中表示一水龙头通过管道向一个水桶放水。图 5-22 (a) 表示水桶太小,来不及接收注入水桶的水。这时只好请求管水龙头的人把水龙头拧小些,以减缓放水的速率。这就相当于流量控制。图 5-22 (b) 表示虽然水桶足够大,但管道中有很狭窄的地方,使得管道不通畅,水流被堵塞。这种情况被反馈到管水龙头的人,请求把水龙头拧小些,以减缓放水的速率,为的是减缓水管的堵塞状态。这就相当于拥塞控制。请注意,同样是把水龙头拧小些,但目的是很不一样的。

进行拥塞控制需要付出代价。这首先需要获得网络内部流量分布的信息。在实施拥塞控制时,还需要在节点之间交换信息和各种命令,以便选择控制的策略和实施控制。这样就产生了额外开销。拥塞控制有时需要将一些资源(如缓存、带宽等)分配给个别用户(或一些类别的用户)单独使用,这样就使得网络资源不能更好地实现共享。十分明显,在设计拥塞控制策略时,必须全面衡量得失。
在图 5-23 中的横坐标是提供的负载 (offered load),代表单位时间内输入给网络的分组数目。因此提供的负载也称为输入负载或网络负载。纵坐标是吞吐量 (throughput),代表单位时间内从网络输出的分组数目。具有理想拥塞控制的网络,在吞吐量饱和之前,网络吞吐量应等于提供的负载,故吞吐量曲线是 \(45^{\circ}\) 的斜线。但当提供的负载超过某一限度时,由于网络资源受限,吞吐量不再增长而保持为水平线,即吞吐量达到饱和。这就表明提供的负载中有一部分损失掉了(例如,输入到网络的某些分组被某个节点丢弃了)。虽然如此,在这种理想的拥塞控制作用下,网络的吞吐量仍然维持在其所能达到的最大值。
但是,实际网络的情况就很不相同了。从图 5-23 可看出,随着提供的负载的增大,网络吞吐量的增长速率逐渐减小。也就是说,在网络吞吐量还未达到饱和时,就已经有一部分的输入分组被丢弃了。当网络的吞吐量明显地小于理想的吞吐量时,网络就进入了轻度拥塞的状态。更值得注意的是,当提供的负载达到某一数值时,网络的吞吐量反而随提供的负载的增大而下降,这时网络就进入了拥塞状态。当提供的负载继续增大到某一数值时,网络的吞吐量就下降到零,网络已无法工作,这就是所谓的死锁 (deadlock)。
从原理上讲,寻找拥塞控制的方案无非是寻找使不等式 (5-7) 不再成立的条件。这或者是增大网络的某些可用资源(如业务繁忙时增加一些链路,增大链路的带宽,或使额外的通信量从另外的通路分流),或减少一些用户对某些资源的需求(如拒绝接受新的建立连接的请求,或要求用户减轻其负荷,这属于降低服务质量)。但正如上面所讲过的,在采用某种措施时,还必须考虑到该措施所带来的其他影响。
实践证明,拥塞控制是很难设计的,因为它是一个动态的(而不是静态的)问题。当前网络正朝着高速化的方向发展,这很容易出现缓存不够大而导致分组的丢失。但分组的丢失是网络发生拥塞的征兆而不是原因。在许多情况下,甚至正是拥塞控制机制本身成为引起网络性能恶化甚至发生死锁的原因。这点应特别引起重视。
由于计算机网络是一个很复杂的系统,因此可以从控制理论的角度来看拥塞控制这个问题。这样,从大的方面看,可以分为开环控制和闭环控制两种方法。开环控制就是在设计网络时事先将发生拥塞的有关因素考虑周到,力求网络在工作时不产生拥塞。但一旦整个系统运行起来,就不再中途进行改正了。
闭环控制是基于反馈环路的概念,主要有以下几种措施:
有很多的方法可用来监测网络的拥塞。主要的一些指标是:由于缺少缓存空间而被丢弃的分组的百分数、平均队列长度、超时重传的分组数、平均分组时延、分组时延的标准差,等等。上述这些指标的上升都标志着拥塞发生的可能性增加。
一般在监测到拥塞发生时,要将拥塞发生的信息传送到产生分组的源站。当然,通知拥塞发生的分组同样会使网络更加拥塞。
另一种方法是在路由器转发的分组中保留一个比特或字段,用该比特或字段的值表示网络没有拥塞或产生了拥塞。也可以由一些主机或路由器周期性地发出探测分组,以询问拥塞是否发生。
此外,过于频繁地采取行动以缓和网络的拥塞,会使系统产生不稳定的振荡。但过于迟缓地采取行动又不具有任何实用价值。因此,要采用某种折中的方法,但选择正确的时间常数是相当困难的。
下面就来介绍更加具体的防止网络拥塞的方法。
5.8.2 TCP 的拥塞控制方法¶
TCP 进行拥塞控制的算法有四种,即慢开始 (slow-start)、拥塞避免 (congestion avoidance)、快重传 (fast retransmit) 和快恢复 (fast recovery)(见草案标准 RFC 5681)。下面就介绍这些算法的原理。为了集中精力讨论拥塞控制,我们假定:
1. 慢开始和拥塞避免¶
下面讨论的拥塞控制也叫作基于窗口的拥塞控制。为此,发送方维持一个叫作拥塞窗口 cwnd (congestion window) 的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且是动态变化着的。发送方让自己的发送窗口等于拥塞窗口。根据假定,对方的接收窗口足够大,发送方在发送数据时,只需考虑发送方的拥塞窗口。
发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就可以再增大一些,以便把更多的分组发送出去,这样就可以提高网络的利用率。但只要网络出现拥塞或有可能出现拥塞,就必须把拥塞窗口减小一些,以减少注入到网络中的分组数,以便缓解网络出现的拥塞。
发送方又如何知道网络发生了拥塞呢?我们知道,当网络发生拥塞时,路由器就要把来不及处理而排不上队的分组丢弃。因此只要发送方没有按时收到对方的确认报文,也就是说,只要出现了超时,就可以估计可能在网络某处出现了拥塞。现在通信线路的传输质量一般都很好,因传输出差错而丢弃分组的概率是很小的(远小于 1%)。因此,发送方在超时重传计时器启动时,就判断网络出现了拥塞。
下面将讨论拥塞窗口 cwnd 的大小是怎样变化的。我们从 “慢开始算法” 讲起。
慢开始算法的思路是这样的:当主机在已建立的 TCP 连接上开始发送数据时,并不清楚网络当前的负荷情况。如果立即把大量数据字节注入到网络,那么就有可能引起网络发生拥塞。经验证明,较好的方法是先探测一下,即由小到大逐渐增大注入到网络中的数据字节,也就是说,由小到大逐渐增大拥塞窗口数值。
旧的规定是这样的:在刚刚开始发送报文段时,先把初始拥塞窗口 cwnd 设置为 1 至 2 个发送方的最大报文段 SMSS (Sender Maximum Segment Size) 的数值,但新的 RFC 5681(草案标准)把初始拥塞窗口 cwnd 设置为不超过 2 至 4 个 SMSS 的数值。具体的规定如下:
若 SMSS > 2190 字节,
则设置初始拥塞窗口 cwnd = 2 × SMSS 字节,且不得超过 2 个报文段。
若(SMSS>1095 字节)且(SMSS ≤ 2190 字节),
则设置初始拥塞窗口 cwnd = 3 × SMSS 字节,且不得超过 3 个报文段。
若 SMSS ≤ 1095 字节,
则设置初始拥塞窗口 cwnd = 4 × SMSS 字节,且不得超过 4 个报文段。
可见这个规定就是限制初始拥塞窗口的字节数。
慢开始规定,在每收到一个对新的报文段的确认后,可以把拥塞窗口增加最多一个 SMSS 的数值。更具体些,就是
其中 N 是原先未被确认的、但现在被刚收到的确认报文段所确认的字节数。不难看出,当 N < SMSS 时,拥塞窗口每次的增加量要小于 SMSS。
用这样的方法逐步增大发送方的拥塞窗口 cwnd,可以使分组注入到网络的速率更加合理。
下面用例子说明慢开始算法的原理。请注意,虽然实际上 TCP 用字节数作为窗口大小的单位。但为叙述方便起见,我们用报文段的个数作为窗口大小的单位,这样可以使用较小的数字来阐明拥塞控制的原理。
在一开始发送方先设置 cwnd = 1,发送第一个报文段,接收方收到后就发送确认。慢开始算法规定,发送方每收到一个对新报文段的确认(对重传的确认不算在内),就把发送方的拥塞窗口加 1。因此,经过一个往返时延 RTT 后,发送方就增大拥塞窗口,使 cwnd = 2,即发送方现在可连续发送两个报文段。接收方收到这两个报文段后,先后发回两个确认。现在发送方收到两个确认,根据慢开始算法,拥塞窗口就应当加 2,使拥塞窗口从 cwnd = 2 增加到 cwnd = 4,即可连续发送 4 个报文段。发送方收到这 4 个确认后,就可以把拥塞窗口再加 4,使 cwnd = 8(如图 5-24 所示)。显然,发送方并不是要在所有的确认都收齐了之后才调整其拥塞窗口,而是收到一个确认就调整一下拥塞窗口,抓紧时间发送报文段。但这样的细节不是我们现在所要研究的,我们想知道的只是拥塞窗口的大致增长趋势。

由此可见,慢开始的 “慢” 并不是指 cwnd 的增长速率慢,而是指在 TCP 开始发送报文段时,只发送一个报文段,即设置 cwnd = 1,目的是试探一下网络的拥塞情况,然后视情况再逐渐增大 cwnd。这当然比一开始设置大的 cwnd 值,一下子把许多报文段迅速注入到网络要 “慢得多”。这对防止出现网络拥塞是一个非常好的方法。
为了防止拥塞窗口 cwnd 增长过大引起网络拥塞,还需要设置一个慢开始门限 ssthresh 状态变量(可以把门限 ssthresh 的数值设置大些,例如达到发送窗口的最大容许值)。慢开
始门限 ssthresh 的用法如下:
当 cwnd < ssthresh 时,使用上述的慢开始算法。
当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞避免算法。
拥塞避免算法的目的是让拥塞窗口 cwnd 缓慢地增大(具体算法见 [RFC 5681])。执行算法后的结果大约是这样的:每经过一个往返时间 RTT,发送方的拥塞窗口 cwnd 的大小就加 1,而不是像慢开始阶段那样加倍增长。因此在拥塞避免阶段就称为 “加法增大” AI (Additive Increase),表明在拥塞避免阶段,拥塞窗口 cwnd 按线性规律缓慢增长,比慢开始算法的拥塞窗口增长速率缓慢得多。
可以用曲线来说明 TCP 的拥塞窗口 cwnd 是怎样随时间变化的(如图 5-25 所示)。但这里请特别注意横坐标采用的单位是往返时延 RTT。在实际的互联网中,TCP 发送的每一个报文段的往返时延 RTT 都是不一样的(不会像图 5-24 中所画出的那样很理想的情况)。但在这里我们是讲解拥塞控制的原理,因此应当把图中的 RTT 理解为一个大致的时间,在这样的时间之内,发送方发出了一批报文段,并且都收到了接收方的确认。图 5-25 中的数字①至⑤是特别要注意的几个点。现假定 TCP 的发送窗口等于拥塞窗口。
当 TCP 连接已建立后,把拥塞窗口 cwnd 置为 1。在本例中,慢开始门限的初始值设置为 16 个报文段,即 ssthresh = 16。在执行慢开始算法阶段,每经过一个往返时间 RTT,拥塞窗口 cwnd 就加倍。当拥塞窗口 cwnd 增长到慢开始门限值 ssthresh 时(图中的点①,此时拥塞窗口 cwnd = 16),就改为执行拥塞避免算法,拥塞窗口按线性规律增长。但请注意,“拥塞避免” 并非完全避免拥塞,而是让拥塞窗口增长得缓慢些,使网络不容易出现拥塞。
当拥塞窗口 cwnd = 24 时,网络出现了超时(图中的点②),这就是网络发生拥塞的标志。于是调整门限值 ssthresh = cwnd / 2 = 12,同时设置拥塞窗口 cwnd = 1,执行慢开始算法。
按照慢开始算法,发送方每收到一个对新报文段的确认 ACK,就把拥塞窗口值加 1。当拥塞窗口 cwnd = ssthresh = 12 时(图中的点③,这是 ssthresh 第 1 次调整后的数值),改为执行拥塞避免算法,拥塞窗口按线性规律增大。
当拥塞窗口 cwnd = 16 时(图中的点④),出现了一个新的情况,就是发送方一连收到 3 个对同一个报文段的重复确认(图中记为 3-ACK)。关于这个问题要解释如下。
有时,个别报文段会在网络中意外丢失,但实际上网络并未发生拥塞。如果发送方迟迟收不到确认,就会产生超时,并误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口 cwnd 又设置为 1,因而不必要地降低了传输效率。
采用快重传算法可以让发送方尽早知道发生了个别报文段的丢失。快重传算法首先要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认,即使收到了失序的报文段也要立即发出对已收到的报文段的重复确认。如图 5-26 所示,接收方收到了 \(M_{1}\) 和 \(M_{2}\) 后都分别及时发出了确认。现假定接收方没有收到 \(M_{3}\) 但却收到了 \(M_{4}\) 。本来接收方可以什么都不做。但按照快重传算法,接收方必须立即发送对 \(M_{2}\) 的重复确认,以便让发送方及早知道接收方没有收到报文段 \(M_{3}\) 。发送方接着发送 \(M_{5}\) 和 \(M_{6}\) 。接收方收到后也仍要再次分别发出对 \(M_{2}\) 的重复确认。这样,发送方共收到了接收方的 4 个对 \(M_{2}\) 的确认,其中后 3 个都是重复确认。快重传算法规定,发送方只要一连收到 3 个重复确认,就可知道现在并未出现网络拥塞,而只是接收方少收到一个报文段 \(M_{3}\) ,因而立即进行重传 \(M_{3}\) (即 “快重传”)。使用快重传可以使整个网络的吞吐量提高约 20%。

因此,在图 5-25 中的点④,发送方知道现在只是丢失了个别的报文段。于是不启动慢开始,而是执行快恢复算法。这时,发送方第 2 次调整门限值,使 ssthresh = cwnd / 2 = 8,同时设置拥塞窗口 cwnd = ssthresh = 8(见图 5-25 中的点⑤),并开始执行拥塞避免算法。
在图 5-25 中还标注有 “TCP Reno 版本”,表示区别于老的 TCP Tahao 版本。
请注意,也有的快恢复实现是把快恢复开始时的拥塞窗口 cwnd 值再增大一些(增大 3 个报文段的长度),即等于新的 ssthresh + 3 × MSS。这样做的理由是:既然发送方收到 3 个重复的确认,就表明有 3 个分组已经离开了网络。这 3 个分组不再消耗网络的资源而是停留在接收方的缓存中(接收方发送出 3 个重复的确认就证明了这个事实)。可见现在网络中并不是堆积了分组而是减少了 3 个分组。因此可以适当把拥塞窗口扩大些。
从图 5-25 可以看出,在拥塞避免阶段,拥塞窗口是按照线性规律增大的,这就是前面提到过的加法增大 AI。而一旦出现超时或 3 个重复的确认,就要把门限值设置为当前拥塞窗口值的一半,并大大减小拥塞窗口的数值。这常称为 “乘法减小” MD (Multiplicative Decrease)。二者合在一起就是所谓的 AIMD 算法。
采用这样的拥塞控制方法使得 TCP 的性能有明显的改进 [STEV94][RFC 5681]。
根据以上所述,TCP 的拥塞控制可以归纳为图 5-27 的流程图。这个流程图就比图 5-25 所示的特例要更加全面些。例如,图 5-25 没有说明在慢开始阶段如果出现了超时(即出现了网络拥塞)或出现 3-ACK,发送方应采取什么措施。但从图 5-27 的流程图就可以很明确地知道发送方应采取的措施。

在这一节的开始我们就假定了接收方总是有足够大的缓存空间,因而发送窗口的大小由网络的拥塞程度来决定。但实际上接收方的缓存空间总是有限的。接收方根据自己的接收能力设定了接收方窗口 rwnd,并把这个窗口值写入 TCP 首部中的窗口字段,传送给发送方。因此,接收方窗口又称为通知窗口 (advertised window)。因此,从接收方对发送方的流量控制的角度考虑,发送方的发送窗口一定不能超过对方给出的接收方窗口值 rwnd。
如果把本节所讨论的拥塞控制和接收方对发送方的流量控制一起考虑,那么很显然,发送方的窗口的上限值应当取为接收方窗口 rwnd 和拥塞窗口 cwnd 这两个变量中较小的一个,也就是说:
式 (5-9) 指出:
当 rwnd < cwnd 时,是接收方的接收能力限制发送方窗口的最大值。
反之,当 cwnd < rwnd 时,则是网络的拥塞程度限制发送方窗口的最大值。
也就是说,rwnd 和 cwnd 中数值较小的一个,控制了发送方发送数据的速率。
5.8.3 主动队列管理 AQM¶
上一节讨论的 TCP 拥塞控制并没有和网络层采取的策略联系起来。其实,它们之间有着密切的关系。
例如,假定一个路由器对某些分组的处理时间特别长,那么这就可能使这些分组中的数据部分(即 TCP 报文段)经过很长时间才能到达终点,结果引起发送方对这些报文段的重传。根据前面所讲的,重传会使 TCP 连接的发送端认为在网络中发生了拥塞。于是在 TCP 的发送端就采取了拥塞控制措施,但实际上网络并没有发生拥塞。
网络层的策略对 TCP 拥塞控制影响最大的就是路由器的分组丢弃策略。在最简单的情况下,路由器的队列通常都按照 “先进先出” FIFO (First In First Out) 的规则处理到来的分组。由于队列长度总是有限的,因此当队列已满时,以后再到达的所有分组(如果能够继续排队,这些分组都将排在队列的尾部)将都被丢弃。这就叫作尾部丢弃策略 (tail-drop policy)。
路由器的尾部丢弃往往会导致一连串分组的丢失,这就使发送方出现超时重传,使 TCP 进入拥塞控制的慢开始状态,结果使 TCP 连接的发送方突然把数据的发送速率降低到很小的数值。更为严重的是,在网络中通常有很多的 TCP 连接(它们有不同的源点和终点),这些连接中的报文段通常是复用在网络层的 IP 数据报中传送的。在这种情况下,若发生了路由器中的尾部丢弃,就可能会同时影响到很多条 TCP 连接,结果使这许多 TCP 连接在同一时间突然都进入到慢开始状态。这在 TCP 的术语中称为全局同步 (global synchronization)。全局同步使得全网的通信量突然下降了很多,而在网络恢复正常后,其通信量又突然增大很多。
为了避免发生网络中的全局同步现象,在 1998 年提出了主动队列管理 AQM (Active Queue Management)。所谓 “主动” 就是不要等到路由器的队列长度已经达到最大值时才不得不丢弃后面到达的分组。这样就太被动了。应当在队列长度达到某个值得警惕的数值时(即当网络拥塞有了某些拥塞征兆时),就主动丢弃到达的分组。这样就提醒了发送方放慢发送的速率,因而有可能使网络拥塞的程度减轻,甚至不出现网络拥塞。AQM 可以有不同实现方法,其中曾流行多年的就是随机早期检测 RED (Random Early Detection)。RED 还有几个不同的名称,如 Random Early Drop 或 Random Early Discard(随机早期丢弃)。
实现 RED 时需要使路由器维持两个参数,即队列长度最小门限和最大门限。当每一个分组到达时,RED 就按照规定的算法先计算当前的平均队列长度。
由此可见,RED 不是等到已经发生网络拥塞后才把所有在队列尾部的分组全部丢弃,而是在检测到网络拥塞的早期征兆时(即路由器的平均队列长度达到一定数值时),就以概率 \(p\) 丢弃个别的分组,让拥塞控制只在个别的 TCP 连接上进行,因而避免发生全局性的拥塞控制。
在 RED 的操作中,最难处理的就是丢弃概率 \(p\) 的选择,因为 \(p\) 并不是个常数。对每一个到达的分组,都必须计算丢弃概率 \(p\) 的数值。IETF 曾经推荐在互联网中的路由器使用 RED 机制 [RFC 2309],但多年的实践证明,RED 的使用效果并不太理想。因此,在 2015 年公布的 RFC 7567 已经把过去的 RFC 2309 列为 “陈旧的”,并且不再推荐使用 RED。对路由器进行主动队列管理 AQM 仍是必要的。AQM 实际上就是对路由器中的分组排队进行智能管理,而不是简单地把队列的尾部丢弃。现在已经有几种不同的算法来代替旧的 RED,但都还在实验阶段。目前还没有一种算法能够成为 IETF 的标准,读者可注意这方面的进展。
5.9 TCP 的运输连接管理¶
TCP 是面向连接的协议。运输连接是用来传送 TCP 报文的。TCP 运输连接的建立和释放是每一次面向连接的通信中必不可少的过程。因此,运输连接就有三个阶段,即:连接建立、数据传送和连接释放。运输连接的管理就是使运输连接的建立和释放都能正常地进行。
在 TCP 连接建立过程中要解决以下三个问题:
TCP 连接的建立采用客户服务器方式。主动发起连接建立的应用进程叫作客户 (client),而被动等待连接建立的应用进程叫作服务器 (server)。
5.9.1 TCP 的连接建立¶
TCP 建立连接的过程叫作握手,握手需要在客户和服务器之间交换三个 TCP 报文段。图 5-28 画出了三报文握手 \(^{①}\) 建立 TCP 连接的过程。

假定主机 A 运行的是 TCP 客户程序,而 B 运行 TCP 服务器程序。最初两端的 TCP 进程都处于 CLOSED(关闭)状态。图中在主机下面的方框分别是 TCP 进程所处的状态。请注意,在本例中,A 主动打开连接,而 B 被动打开连接。
一开始,B 的 TCP 服务器进程先创建传输控制块 TCB \(^{②}\) ,准备接受客户进程的连接请求。然后服务器进程就处于 LISTEN(收听)状态,等待客户的连接请求。如有,即做出响应。
A 的 TCP 客户进程也是首先创建传输控制块 TCB。然后,在打算建立 TCP 连接时,向 B 发出连接请求报文段,这时首部中的同步位 SYN = 1,同时选择一个初始序号 seq = x。TCP 规定,SYN 报文段(即 SYN = 1 的报文段)不能携带数据,但要消耗掉一个序号。这时,TCP 客户进程进入 SYN-SENT(同步已发送)状态。
B 收到连接请求报文段后,如同意建立连接,则向 A 发送确认。在确认报文段中应把 SYN 位和 ACK 位都置 1,确认号是 ack = x + 1,同时也为自己选择一个初始序号 seq = y。请注意,这个报文段也不能携带数据,但同样要消耗掉一个序号。这时 TCP 服务器进程进入 SYN-RCVD(同步收到)状态。
TCP 客户进程收到 B 的确认后,还要向 B 给出确认。确认报文段的 ACK 置 1,确认号 ack = y + 1,而自己的序号 seq = x + 1。TCP 的标准规定,ACK 报文段可以携带数据。但如果不携带数据则不消耗序号,在这种情况下,下一个数据报文段的序号仍是 seq = x + 1。这时,TCP 连接已经建立,A 进入 ESTABLISHED(已建立连接)状态。
当 B 收到 A 的确认后,也进入 ESTABLISHED 状态。
上面给出的连接建立过程叫作三报文握手。请注意,在图 5-28 中 B 发送给 A 的报文段,也可拆成两个报文段。可以先发送一个确认报文段(ACK = 1, ack = x + 1),然后再发送一个同步报文段(SYN = 1, seq = y)。这样的过程就变成了四报文握手,但效果是一样的。
为什么 A 最后还要发送一次确认呢?这主要是为了防止已失效的连接请求报文段突然又传送到了 B,因而产生错误。
所谓 “已失效的连接请求报文段” 是这样产生的。考虑一种正常情况,A 发出连接请求,但因连接请求报文丢失而未收到确认。于是 A 再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。A 共发送了两个连接请求报文段,其中第一个丢失,第二个到达了 B,没有 “已失效的连接请求报文段”。
现假定出现一种异常情况,即 A 发出的第一个连接请求报文段并没有丢失,而是在某些网络节点长时间滞留了,以致延误到连接释放以后的某个时间才到达 B。本来这是一个早已失效的报文段。但 B 收到此失效的连接请求报文段后,就误认为是 A 又发出一次新的连接请求。于是就向 A 发出确认报文段,同意建立连接。假定不采用报文握手,那么只要 B 发出确认,新的连接就建立了。
由于现在 A 并没有发出建立连接的请求,因此不会理睬 B 的确认,也不会向 B 发送数据。但 B 却以为新的运输连接已经建立了,并一直等待 A 发来数据。B 的许多资源就这样白白浪费了。
采用三报文握手的办法,可以防止上述现象的发生。例如在刚才的异常情况下,A 不会向 B 的确认发出确认。B 由于收不到确认,就知道 A 并没有要求建立连接。
5.9.2 TCP 的连接释放¶
TCP 连接释放过程比较复杂,我们仍结合双方状态的改变来阐明连接释放的过程。
数据传输结束后,通信的双方都可释放连接。现在 A 和 B 都处于 ESTABLISHED 状态(如图 5-29 所示)。A 的应用进程先向其 TCP 发出连
接释放报文段,并停止再发送数据,主动关闭 TCP 连接。A 把连接释放报文段首部的终止控制位 FIN 置 1,其序号 seq = u,它等于前面已传送过的数据的最后一个字节的序号加 1。这时 A 进入 FIN-WAIT-1(终止等待 1)状态,等待 B 的确认。请注意,TCP 规定,FIN 报文段即使不携带数据,它也消耗掉一个序号。

B 收到连接释放报文段后即发出确认,确认号是 ack = u + 1,而这个报文段自己的序号是 v,等于 B 前面已传送过的数据的最后一个字节的序号加 1。然后 B 就进入 CLOSE-WAIT(关闭等待)状态。TCP 服务器进程这时应通知高层应用进程,因而从 A 到 B 这个方向的连接就释放了,这时的 TCP 连接处于半关闭 (half-close) 状态,即 A 已经没有数据要发送了,但 B 若发送数据,A 仍要接收。也就是说,从 B 到 A 这个方向的连接并未关闭,这个状态可能会持续一段时间。
A 收到来自 B 的确认后,就进入 FIN-WAIT-2(终止等待 2)状态,等待 B 发出的连接释放报文段。
若 B 已经没有要向 A 发送的数据,其应用进程就通知 TCP 释放连接。这时 B 发出的连接释放报文段必须使 FIN = 1。现假定 B 的序号为 w(在半关闭状态 B 可能又发送了一些数据)。B 还必须重复上次已发送过的确认号 ack = u + 1。这时 B 就进入 LAST-ACK(最后确认)状态,等待 A 的确认。
A 在收到 B 的连接释放报文段后,必须对此发出确认。在确认报文段中把 ACK 置 1,确认号 ack = w + 1,而自己的序号是 seq = u + 1(根据 TCP 标准,前面发送过的 FIN 报文段要消耗一个序号)。然后进入到 TIME-WAIT(时间等待)状态。请注意,现在 TCP 连接还没有释放掉。必须经过时间等待计时器 (TIME-WAIT timer) 设置的时间 2MSL 后,A 才进入到 CLOSED 状态。时间 MSL 叫作最长报文段寿命 (Maximum Segment Lifetime),RFC 793 建议设为 2 分钟。但这完全是从工程上来考虑的,对于现在的网络,MSL = 2 分钟可能太长了一些。因此 TCP 允许不同的实现可根据具体情况使用更小的 MSL 值。因此,从 A 进入到 TIME-WAIT 状态后,要经过 4 分钟才能进入到 CLOSED 状态,才能开始建立下一个新的连接。当 A 撤销相应的传输控制块 TCB 后,就结束了这次的 TCP 连接。
为什么 A 在 TIME-WAIT 状态必须等待 2MSL 的时间呢?这有两个理由。
第一,为了保证 A 发送的最后一个 ACK 报文段能够到达 B。这个 ACK 报文段有可能丢失,因而使处在 LAST-ACK 状态的 B 收不到对已发送的 FIN + ACK 报文段的确认。B 会超时重传这个 FIN + ACK 报文段,而 A 就能在 2MSL 时间内收到这个重传的 FIN + ACK 报文段。接着 A 重传一次确认,重新启动 2MSL 计时器。最后,A 和 B 都正常进入到 CLOSED 状态。如果 A 在 TIME-WAIT 状态不等待一段时间,而是在发送完 ACK 报文段后立即释放连接,那么就无法收到 B 重传的 FIN + ACK 报文段,因而也不会再发送一次确认报文段。这样,B 就无法按照正常步骤进入 CLOSED 状态。
第二,防止上一节提到的 “已失效的连接请求报文段” 出现在本连接中。A 在发送完最后一个 ACK 报文段后,再经过时间 2MSL,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。
B 只要收到了 A 发出的确认,就进入 CLOSED 状态。同样,B 在撤销相应的传输控制块 TCB 后,就结束了这次的 TCP 连接。我们注意到,B 结束 TCP 连接的时间要比 A 早一些。
上述的 TCP 连接释放过程是四报文握手。
除时间等待计时器外,TCP 还设有一个保活计时器 (keepalive timer)。设想有这样的情况:客户已主动与服务器建立了 TCP 连接。但后来客户端的主机突然出故障。显然,服务器以后就不能再收到客户发来的数据。因此,应当有措施使服务器不要再白白等待下去。这就是使用保活计时器。服务器每收到一次客户的数据,就重新设置保活计时器,时间的设置通常是两小时。若两小时没有收到客户的数据,服务器就发送一个探测报文段,以后则每隔 75 秒钟发送一次。若一连发送 10 个探测报文段后仍无客户的响应,服务器就认为客户端出了故障,接着就关闭这个连接。
5.9.3 TCP 的有限状态机¶
为了更清晰地看出 TCP 连接的各种状态之间的关系,图 5-30 给出了 TCP 的有限状态机。图中每一个方框是 TCP 可能具有的状态。每个方框中的大写英文字符串是 TCP 标准所使用的 TCP 连接状态名。状态之间的箭头表示可能发生的状态变迁。箭头旁边的字,表明引起这种变迁的原因,或表明发生状态变迁后又出现什么动作。请注意图中有三种不同的箭头。粗实线箭头表示对客户进程的正常变迁。粗虚线箭头表示对服务器进程的正常变迁。另一种细线箭头表示异常变迁。

我们可以把图 5-30 和前面的图 5-28、图 5-29 对照起来看。在图 5-28 和图 5-29 中左边客户进程从上到下的状态变迁,就是图 5-30 中粗实线箭头所指的状态变迁。而在图 5-28 和 5-29 右边服务器进程从上到下的状态变迁,就是图 5-30 中粗虚线箭头所指的状态变迁。
还有一些状态变迁,例如连接建立过程中的从 LISTEN 到 SYN-SENT 和从 SYN-SENT 到 SYN-RCVD。读者可分析在什么情况下会出现这样的变迁(见习题 5-43)。

本章的重要概念¶
- 运输层提供应用进程间的逻辑通信,也就是说,运输层之间的通信并不是真正在两个运输层之间直接传送数据。运输层向应用层屏蔽了下面网络的细节(如网络拓扑、所采用的路由选择协议等),它使应用进程看见的就是好像在两个运输层实体之间有一条端到端的逻辑通信信道。
- 网络层为主机之间提供逻辑通信,而运输层为应用进程之间提供端到端的逻辑通信。
- 运输层有两个主要的协议:TCP 和 UDP。它们都有复用和分用,以及检错的功能。当运输层采用面向连接的 TCP 协议时,尽管下面的网络是不可靠的(只提供尽最大努力服务),但这种逻辑通信信道就相当于一条全双工通信的可靠信道。当运输层采用无连接的 UDP 协议时,这种逻辑通信信道仍然是一条不可靠信道。
- 运输层用一个 16 位端口号来标志一个端口。端口号只具有本地意义,它只是为了标志本计算机应用层中的各个进程在和运输层交互时的层间接口。在互联网的不同计算机中,相同的端口号是没有关联的。
- 两台计算机中的进程要互相通信,不仅要知道对方的 IP 地址(为了找到对方的计算机),而且还要知道对方的端口号(为了找到对方计算机中的应用进程)。
- 运输层的端口号分为服务器端使用的端口号(0~1023 指派给熟知端口,1024~49151 是登记端口号)和客户端暂时使用的端口号(49152~65535)。
- UDP 的主要特点是:(1) 无连接;(2) 尽最大努力交付;(3) 面向报文;(4) 无拥塞控制;(5) 支持一对一、一对多、多对一和多对多的交互通信;(6) 首部开销小(只有四个字段:源端口、目的端口、长度和检验和)。
- TCP 的主要特点是:(1) 面向连接;(2) 每一条 TCP 连接只能是点对点的(一对一);(3) 提供可靠交付的服务;(4) 提供全双工通信;(5) 面向字节流。
- TCP 用主机的 IP 地址加上主机上的端口号作为 TCP 连接的端点。这样的端点就叫作套接字 (socket) 或插口。套接字用(IP 地址:端口号)来表示。
- 停止等待协议能够在不可靠的传输网络上实现可靠的通信。每发送完一个分组就停止发送,等待对方的确认。在收到确认后再发送下一个分组。分组需要进行编号。
- 超时重传是指只要超过了一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为自动重传请求 ARQ。
- 在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认。
- 连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组都可连续发送出去,而不需要等待对方的确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已正确收到了。
- TCP 报文段首部的前 20 个字节是固定的,后面有 4N 字节是根据需要而增加的选项(N 是整数)。在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号。首部中的序号字段值则指的是本报文段所发送的数据的第一个字节的序号。
- TCP 首部中的确认号是期望收到对方下一个报文段的第一个数据字节的序号。若确认号为 N,则表明:到序号 N-1 为止的所有数据都已正确收到。
- TCP 首部中的窗口字段指出了现在允许对方发送的数据量。窗口值是经常动态变化着的。
- TCP 使用滑动窗口机制。发送窗口里面的序号表示允许发送的序号。发送窗口后沿的后面部分表示已发送且已收到了确认,而发送窗口前沿的前面部分表示不允许发送。发送窗口后沿的变化情况有两种可能,即不动(没有收到新的确认)和前移(收到了新的确认)。发送窗口前沿通常是不断向前移动的。
- 流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。
- 在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫作拥塞。拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不至于过载。
- 流量控制是一个端到端的问题,是接收端抑制发送端发送数据的速率,以便使接收端来得及接收。拥塞控制是一个全局性的过程,涉及所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。
- 为了进行拥塞控制,TCP 的发送方要维持一个拥塞窗口 cwnd 的状态变量。拥塞窗
口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口取为拥塞窗口和接收方的接收窗口中较小的一个。
- TCP 的拥塞控制采用了四种算法,即慢开始、拥塞避免、快重传和快恢复。在网络层,也可以使路由器采用适当的分组丢弃策略(如主动队列管理 AQM),以减少网络拥塞的发生。
- 运输连接有三个阶段,即:连接建立、数据传送和连接释放。
- 主动发起 TCP 连接建立的应用进程叫作客户,而被动等待连接建立的应用进程叫作服务器。TCP 的连接建立采用三报文握手机制。服务器要确认客户的连接请求,然后客户要对服务器的确认进行确认。
- TCP 的连接释放采用四报文握手机制。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后就进入半关闭状态。当另一方也没有数据再发送时,则发送连接释放通知,对方确认后就完全关闭了 TCP 连接。
习题¶
5-01 试说明运输层在协议栈中的地位和作用。运输层的通信和网络层的通信有什么重要的区别?为什么运输层是必不可少的?
5-02 网络层提供数据报或虚电路服务对上面的运输层有何影响?
5-03 当应用程序使用面向连接的 TCP 和无连接的 IP 时,这种传输是面向连接的还是无连接的?
5-04 试画图解释运输层的复用。画图说明许多个运输用户复用到一条运输连接上,而这条运输连接又复用到 IP 数据报上。
5-05 试举例说明有些应用程序愿意采用不可靠的 UDP,而不愿意采用可靠的 TCP。
5-06 接收方收到有差错的 UDP 用户数据报时应如何处理?
5-07 如果应用程序愿意使用 UDP 完成可靠传输,这可能吗?请说明理由。
5-08 为什么说 UDP 是面向报文的,而 TCP 是面向字节流的?
5-09 端口的作用是什么?为什么端口号要划分为三种?
5-10 试说明运输层中伪首部的作用。
5-11 某个应用进程使用运输层的用户数据报 UDP,然后继续向下交给 IP 层后,又封装成 IP 数据报。既然都是数据报,是否可以跳过 UDP 而直接交给 IP 层?哪些功能 UDP 提供了但 IP 没有提供?
5-12 一个应用程序用 UDP,到了 IP 层把数据报再划分为 4 个数据报片发送出去。结果前两个数据报片丢失,后两个到达目的站。过了一段时间应用程序重传 UDP,而 IP 层仍然划分为 4 个数据报片来传送。结果这次前两个到达目的站而后两个丢失。试问:在目的站能否将这两次传输的 4 个数据报片组装成为完整的数据报?假定目的站第一次收到的后两个数据报片仍然保存在目的站的缓存中。
5-13 一个 UDP 用户数据报的数据字段为 8192 字节。在链路层要使用以太网来传送。试问应当划分为几个 IP 数据报片?说明每一个 IP 数据报片的数据字段长度和片偏移字段的值。
5-14 一个 UDP 用户数据报的首部的十六进制表示是:06 32 00 45 00 1C E2 17。试求源端口、目的端口、用户数据报的总长度、数据部分长度。这个用户数据报是从客户发送给服务器还是从服务器发送给客户?使用 UDP 的这个服务器程序是什么?
5-15 使用 TCP 对实时话音数据的传输会有什么问题?使用 UDP 在传送数据文件时会有什么问题?
5-16 在停止等待协议中如果不使用编号是否可行?为什么?
5-17 在停止等待协议中,如果收到重复的报文段时不予理睬(即悄悄地丢弃它而其他什么也不做)是否可行?试举出具体例子说明理由。
5-18 假定在运输层使用停止等待协议。发送方发送报文段 \(M_{0}\) 后在设定的时间内未收到确认,于是重传 \(M_{0}\) ,但 \(M_{0}\) 又迟迟不能到达接收方。不久,发送方收到了迟到的对 \(M_{0}\) 的确认,于是发送下一个报文段 \(M_{1}\) ,不久就收到了对 \(M_{1}\) 的确认。接着发送方发送新的报文段 \(M_{0}\) ,但这个新的 \(M_{0}\) 在传送过程中丢失了。正巧,一开始就滞留在网络中的 \(M_{0}\) 现在到达接收方。接收方无法分辨 \(M_{0}\) 是旧的。于是收下 \(M_{0}\) ,并发送确认。显然,接收方后来收到的 \(M_{0}\) 是重复的,协议失败了。 试画出类似于图 5-9 所示的双方交换报文段的过程。
5-19 试证明:当用 n 比特进行分组的编号时,若接收窗口等于 1(即只能按序接收分组),则仅在发送窗口不超过 \(2^{n}-1\) 时,连续 ARQ 协议才能正确运行。窗口单位是分组。
5-20 在连续 ARQ 协议中,若发送窗口等于 7,则发送端在开始时可连续发送 7 个分组。因此,在每一分组发出后,都要置一个超时计时器。现在计算机里只有一个硬时钟。设这 7 个分组发出的时间分别为 \(t_{0}, t_{1}, \ldots, t_{6}\) ,且 \(t_{out}\) 都一样大。试问如何实现这 7 个超时计时器(这叫软时钟法)?
5-21 假定使用连续 ARQ 协议,发送窗口大小是 3,而序号范围是 [0, 15],而传输媒体保证在接收方能够按序收到分组。在某一时刻,在接收方,下一个期望收到的序号是 5。试问:
(1) 在发送方的发送窗口中可能出现的序号组合有哪些?
(2) 接收方已经发送出的、但在网络中(即还未到达发送方)的确认分组可能有哪些?说明这些确认分组是用来确认哪些序号的分组。
5-22 主机 A 向主机 B 发送一个很长的文件,其长度为 L 字节。假定 TCP 使用的 MSS 为 1460 字节。
(1) 在 TCP 的序号不重复使用的条件下,L 的最大值是多少?
(2) 假定使用上面计算出的文件长度,而运输层、网络层和数据链路层所用的首部开销共 66 字节,链路的数据率为 10 Mbit/s,试求这个文件所需的最短发送时间。
5-23 主机 A 向主机 B 连续发送了两个 TCP 报文段,其序号分别是 70 和 100。试问:
(1) 第一个报文段携带了多少字节的数据?
(2) 主机 B 收到第一个报文段后发回的确认中的确认号应当是多少?
(3) 如果 B 收到第二个报文段后发回的确认中的确认号是 180,试问 A 发送的第二个报文段中的数据有多少字节?
(4) 如果 A 发送的第一个报文段丢失了,但第二个报文段到达了 B。B 在第二个报文段到达后向 A 发送确认。试问这个确认号应为多少?
5-24 一个 TCP 连接下面使用 256 kbit/s 的链路,其端到端时延为 128 ms。经测试,发现吞吐量只有 120 kbit/s。试问发送窗口 W 是多少?(提示:可以有两种答案,取决于接收端发出确认的时机。)
5-25 为什么在 TCP 首部中要把 TCP 的端口号放入最开始的 4 个字节?
5-26 为什么在 TCP 首部中有一个首部长度字段,而 UDP 的首部中就没有这个字段?
5-27 一个 TCP 报文段的数据部分最多为多少个字节?为什么?如果用户要传送的数据的字节长度超过 TCP 报文段中的序号字段可能编出的最大序号,问还能否用 TCP 来传送?
5-28 主机 A 向主机 B 发送 TCP 报文段,首部中的源端口是 m 而目的端口是 n。当 B 向 A 发送回信时,其 TCP 报文段的首部中的源端口和目的端口分别是什么?
5-29 在使用 TCP 传送数据时,如果有一个确认报文段丢失了,也不一定会引起与该确认报文段对应的数据的重传。试说明理由。
5-30 设 TCP 使用的最大窗口为 65535 字节,而传输信道不产生差错,带宽也不受限制。若报文段的平均往返时间为 20 ms,问所能得到的最大吞吐量是多少?
5-31 通信信道带宽为 1 Gbit/s,端到端传播时延为 10 ms。TCP 的发送窗口为 65535 字节。试问:可能达到的最大吞吐量是多少?信道的利用率是多少?
5-32 什么是 Karn 算法?在 TCP 的重传机制中,若不采用 Karn 算法,而是在收到确认时都认为是对重传报文段的确认,那么由此得出的往返时间样本和重传时间都会偏小。试问:重传时间最后会减小到什么程度?
5-33 假定 TCP 在开始建立连接时,发送方设定超时重传时间 RTO = 6 秒。
5-34 已知第一次测得 TCP 的往返时间 RTT 是 30 ms。接着收到了三个确认报文段,用它们测量出的往返时间样本 RTT 分别是:26 ms, 32 ms 和 24 ms。设 \(\alpha = 0.1\) 。试计算每一次新的加权平均往返时间值 \(RTT_{S}\) 。讨论所得出的结果。
5-35 用 TCP 通过速率为 1 Gbit/s 的链路传送一个 10 MB 的文件。假定链路的往返时延 RTT = 50 ms。TCP 选用了窗口扩大选项,使窗口达到可选用的最大值。在接收端,TCP 的接收窗口为 1 MB(保持不变),而发送端采用拥塞控制算法,从慢开始传送。假定拥塞窗口以分组为单位计算,在一开始发送 1 个分组,而每个分组长度都是 1 KB。假定网络不会发生拥塞和分组丢失,并且发送端发送数据的速率足够快,因此发送时延可以忽略不计,而接收端每一次收完一批分组后就立即发送确认 ACK 分组。
5-36 假定 TCP 采用一种仅使用线性增大和乘法减小的简单拥塞控制算法,而不使用慢开始。发送窗口不采用字节为计算单位,而是使用分组 pkt 为计算单位。在一开始时发送窗口为 1 pkt。假定分组的发送时延非常小,可以忽略不计。所有产生的时延就是传播时延。假定发送窗口总是小于接收窗口。接收端每收到一组分组后,就立即发回确认 ACK。假定分组的编号为 i,在一开始发送的是 i = 1 的分组。以后当 i = 9, 25,
30, 38 和 50 时,发生了分组的丢失。再假定分组的超时重传时间正好是下一个 RTT 开始的时间。试画出拥塞窗口(也就是发送窗口)与 RTT 的关系曲线,画到发送第 51 个分组为止。
5-37 在 TCP 的拥塞控制中,什么是慢开始、拥塞避免、快重传和快恢复算法?这里每一种算法各起什么作用?“乘法减小” 和 “加法增大” 各用在什么情况下?
5-38 设 TCP 的 ssthresh 的初始值为 8(单位为报文段)。当拥塞窗口上升到 12 时网络发生了超时,TCP 使用慢开始和拥塞避免。试分别求出 RTT = 1 到 RTT = 15 的各拥塞窗口大小。你能说明拥塞窗口每一次变化的原因吗?
5-39 TCP 的拥塞窗口 cwnd 大小与 RTT 的关系如下所示:
| cwnd | 1 | 2 | 4 | 8 | 16 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
| RTT | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| cwnd | 40 | 41 | 42 | 21 | 22 | 23 | 24 | 25 | 26 | 1 | 2 | 4 | 8 |
| RTT | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
(1) 试画出如图 5-25 所示的拥塞窗口与 RTT 的关系曲线。
(2) 指明 TCP 工作在慢开始阶段的时间间隔。
(3) 指明 TCP 工作在拥塞避免阶段的时间间隔。
(4) 在 RTT = 16 和 RTT = 22 之后发送方是通过收到三个重复的确认还是通过超时检测到丢失了报文段?
(5) 在 RTT = 1、RTT = 18 和 RTT = 24 时,门限 ssthresh 分别被设置为多大?
(6) 在 RTT 等于多少时发送出第 70 个报文段?
(7) 假定在 RTT = 26 之后收到了三个重复的确认,因而检测出了报文段的丢失,那么拥塞窗口 cwnd 和门限 ssthresh 应设置为多大?
5-40 TCP 在进行流量控制时,以分组的丢失作为产生拥塞的标志。有没有不是因拥塞而引起分组丢失的情况?如有,请举出三种情况。
5-41 用 TCP 传送 512 字节的数据。设窗口为 100 字节,而 TCP 报文段每次也是传送 100 字节的数据。再设发送方和接收方的起始序号分别选为 100 和 200,试画出类似于图 5-28 的工作示意图。从连接建立阶段到连接释放都要画上(可不考虑传播时延)。
5-42 在图 5-29 中所示的连接释放过程中,在 ESTABLISHED 状态下,服务器进程能否先不发送 ack = u + 1 的确认?(因为后面要发送的连接释放报文段中仍有 ack = u + 1 这一信息。)
5-43 在图 5-30 中,在什么情况下会发生从状态 SYN-SENT 到状态 SYN-RCVD 的变迁?
5-44 试以具体例子说明为什么一个运输连接可以有多种方式释放。可以设两个互相通信的用户分别连接在网络的两节点上。
5-45 解释为什么突然释放运输连接就可能会丢失用户数据,而使用 TCP 的连接释放方法就可保证不丢失数据。
5-46 试用具体例子说明为什么在运输连接建立时要使用三报文握手。说明如不这样做可能会出现什么情况。
5-47 一客户向服务器请求建立 TCP 连接。客户在 TCP 连接建立的三报文握手中的最后一个报文段中捎带上一些数据,请求服务器发送一个长度为 L 字节的文件。假定:
试证明,从客户开始发起连接建立到接收服务器发送的整个文件所需的时间 T 是:
当 \(nM > R \, (\text{RTT}) + M\)
或 \(T=2\mathrm{RTT}+L/R+(K-1)[M/R+\mathrm{RTT}-nM/R]\)
当 \(nM < R(\mathrm{RTT}) + M\)
其中, \(K=\left[L/nM\right]\) ,符号 \(\left[x\right]\) 表示若 x 不是整数,则把 x 的整数部分加 1。
(提示:求证的第一个等式发生在发送窗口较大的情况,可以连续把文件发送完。求证的第二个等式发生在发送窗口较小的情况,发送几个报文段后就必须停顿下来,等收到确认后再继续发送。建议先画出双方交互的时间图,然后再进行推导。)
5-48 网络允许的最大报文段长度为 128 字节,序号用 8 位表示,报文段在网络中的寿命为 30 秒。求发送报文段的一方所能达到的最高数据率。
5-49 下面是以十六进制格式存储的一个 UDP 首部:
CB84000D001C001C¶
试问:
5-50 把图 5-6 计算 UDP 检验和的例子自己具体演算一下,看是否能够得出书上的计算结果。
5-51 在以下几种情况下,UDP 的检验和在发送时的数值分别是多少?
5-52 UDP 和 IP 的不可靠程度是否相同?请加以解释。
5-62 TCP 连接处于 ESTABLISHED 状态。以下的事件相继发生:
在每一个事件之后,连接的状态是什么?在每一个事件之后发生的动作是什么?
5-63 TCP 连接处于 SYN-RCVD 状态。以下的事件相继发生:
在以上的每一个事件之后,连接的状态是什么?在每一个事件之后发生的动作是什么?
5-64 TCP 连接处于 FIN-WAIT-1 状态。以下的事件相继发生:
收到 ACK 报文段。
收到 FIN 报文段。
发生了超时。
在以上的每一个事件之后,连接的状态是什么?在每一个事件之后发生的动作是什么?
这会不会出现问题?
5-69 现在假定使用类似 TCP 的协议(即使用滑动窗口可靠传送字节流),数据传输速率是 1 Gbit/s,而网络的往返时间 RTT = 140 ms。假定报文段的最大生存时间是 60 秒。如果要尽可能快地传送数据,在我们的通信协议的首部中,发送窗口和序号字段至少各应当设为多大?
5-70 假定用 TCP 协议在 40 Gbit/s 的线路上传送数据。
5-71 在 5.5 节中指出:例如,若用 2.5 Gbit/s 的速率发送报文段,则不到 14 秒钟序号就会重复。请计算验证这句话。
5-72 已知 TCP 的接收窗口大小是 600(单位是字节,为简单起见以后就省略了单位),已经确认了的序号是 300。试问,在不断地接收报文段和发送确认报文段的过程中,接收窗口也可能会发生变化(增大或缩小)。请用具体例子(指出接收方发送的确认报文段中的重要信息)来说明哪些情况是可能发生的,而哪些情况是不允许发生的。
5-73 在上题中,如果接收方突然因某种原因不能够再接收数据了,可以立即向发送方发送把接收窗口置为零的报文段(即 rwnd = 0)。这时会导致接收窗口的前沿后退。试问这种情况是否允许?
5-74 流量控制和拥塞控制的最主要的区别是什么?发送窗口的大小取决于流量控制还是拥塞控制?