第三章 运输层
运输层位于应用层和网络层之间, 是分层的网络体系结构的重要部分。 该层为运行在不同主机上的应用进程提供直接的通信服务起着至关重要的作用 。 我们在本章采用的教学方法是 , 交替地讨论运输层的原理和这些原理在现有的协议中是如何实现的 。与往常一样 , 我们将特别关注因特网协议, 即 TCP 和 UDP 运输层协议 。
3.1 概述和运输层服务
运输层负责端到端的通信,既是七层模型中负责数据通信的最高层,又是面向网络通信的低三层和面向信息处理的最高三层之间的中间层。运输层位于网络层之上、会话层之下,它利用网络层子系统提供给它的服务去开发本层的功能,并实现本层对会话层的服务。【来源:百度百科】
运输层协议为运行在不同主机上的应用进程之间提供了逻辑通信 ( logic communication) 功能 。从应用程序的角度看,通过逻辑通信 ,运行不同进程的主机好像直接相连一样 ; 实际上 ,这些主机也许位于地球的两侧 ,通过很多路由器及多种不同类型的链路相连 。
如图 3-1所示,运输层协议是在端系统中而不是在路由器中实现的 。 在发送端,运输层将从发送应用程序进程接收到的报文转换成运输层分组,用因特网术语来讲该分组称为运输层报文段 ( segment)。
网络应用程序可以使用多种的运输层协议 。例如 , 因特网有两种协议, 即 TCP 和UDP 。 每种协议都能为调用的应用程序提供一组不同的运输层服务。
3.1.1 运输层和网络层的关系
在协议栈中 ,运输层刚好位于网络层之上。 网络层提供了主机之间的逻辑通信 ,而运输层为运行在不同主机上的进程之间提供了逻辑通信。
我们用一个家庭类比来帮助分析上述差别。
考虑有两个家庭 , 一家位于美国东海岸 , 一家位于美国西海岸 , 每家有 12 个孩子 。东海岸家庭的孩子们是西海岸家庭孩子们的堂兄弟姐妹 。 这两个家庭的孩子们喜欢彼此通信 , 每个人每星期要互相写一封信 , 每封信都用单独的信封通过传统的邮政服务传送 。 因此 , 每个家庭每星期向另一家发送 144 封信。 ( 如果他们有电子邮件的话,这些孩子可以省不少钱!) 每一个家庭有个孩子负责收发邮件 ,西海岸家庭是 Ann 而东海岸家庭是 Bill 。每星期 Ann 去她的所有兄弟姐妹那里收集信件 , 并将这些信件交到每天到家门口来的邮政服务的邮车上。 当信件到达西海岸家庭时 , Ann 也负责将信件分发到她的兄弟姐妹手上。在东海岸家庭中的 Bill 也负责类似的工作。
在这个例子中 ,邮政服务为两个家庭间提供逻辑通信 ,邮政服务将信件从一家送往另一家 ,而不是从一个人送往另一个人。 在另一方面, Ann 和 Bill 为堂兄弟姐妹之间提供了逻辑通信 , Arm 和 Bill 从兄弟姐妹那里收取信件或到兄弟姐妹那里交付信件。 注意到从堂兄弟姐妹们的角度来看, Ann 和 Bill 就是邮件服务 , 尽管他们只是端到端交付过程的一部分 ( 即端系统部分 ) 。 在解释运输层和网络层之间的关系时 ,这个家庭的例子是一个非常好的类比 。
- 应用层报文 = 信封上的字符
- 进程 = 堂兄弟姐妹
- 主机 ( 又称为端系统) = 家庭
- 运输层协议 = Ann 和 Bill
- 网络层协议 = 邮政服务 ( 包括邮车)
我们继续观察这个类比 。值得注意的是 , Ann 和 Bill 都是在各自家里进行工作的; 例如 , 他们并没有参与任何一个中间邮件中心对邮件进行分拣 , 或者将邮件从一个邮件中心送到另一个邮件中心之类的工作。 类似地 ,运输层协议只工作在端系统中。 在端系统中 ,运输层协议将来自应用进程的报文移动到网络边缘( 即网络层 ), 反过来也是一样 , 但对有关这些报文在网络核心如何移动并不作任何规定 。事实上 , 如图 3-1 所示, 中间路由器既不处理也不识别运输层加在应用层报文的任何信息 。
我们还是继续讨论这两家的情况。 现在假定 Ann 和 Bill 外出度假 , 另外一对堂兄妹( 如 Susan 和 Harvey) 接替他们的工作 , 在家庭内部进行信件的收集和交付工作。不幸的是 , Susan 和 Harvey 的收集和交付工作与 Ann 和 Bill 所做的并不完全一样 。 由于年龄更小 , Susan 和 Harvey 收发邮件的次数更少 ,而且偶尔还会丢失邮件 ( 有时是被家里的狗咬坏了 ) 。 因此 , Susan 和 Harvey 这对堂兄妹并没有提供与 Ann 和 Bill 一样的服务集合 ( 即相同的服务模型 ) 。与此类似,计算机网络中可以安排多种运输层协议,每种协议为应用程序提供不同的服务模型 。
运输协议能够提供的服务常常受制于底层网络层协议的服务模型 。 如果网络层协议无法为主机之间发送的运输层 报文段提供时延或带宽保证的话,运输层协议也就无法为进程之间发送的应用程序报文提供时延或带宽保证 。
然而, 即使底层网络协议不能在网络层提供相应的服务 ,运输层协议也能提供某些服务。例如 , 如我们将在本章所见, 即使底层网络协议是不可靠的, 也就是说网络层协议会使分组丢失 、 篡改和冗余 ,运输协议也能为应用程序提供可靠的数据传输服务。
3.1.2 因特网运输层概述
因特网为应用层提供了两种截然不同的可用运输层协议 。
- 一种是UDP (用户数据报协议), 它为调用它的应用程序提供了一种不可靠 、 无连接的服务。
- 另一种是 TCP ( 传输控制协议), 它为调用它的应用程序提供了一种可靠的 、 面向连接的服务。
应用程序开发人员在生成套接字时必须指定是选择 UDP 还是选择 TCP 。
为了简化术语, 我们将运输层分组称为报文段 ( segment) 。 然而, 因特网文献(如RFC 文档)常将 UDP 的分组称为数据报 ( data-gram ) 。 而这类因特网文献也将网络层分组称为数据报 ! 在这里, 我们认为将 TCP 和 UDP 的分组统称为报文段 ,而将数据报名称保留给网络层分组不容易混淆 。
在对 UDP 和 TCP 进行简要介绍之前 ,简单介绍一下因特网的网络层。 因特网网络层协议有一个名字叫 IP, 即网际协议 。 IP为主机之间提供了逻辑通信。 IP 的服务模型是尽力而为交付服务( best- effort delivery service) 。 这意味着 IP 尽它 “ 最大的努力” 在通信的主机之间交付报文段 , 但它并不做任何确保。 特别是 , 它不确保报文段的交付 , 不保证报文段的按序交付 , 不保证报文段中数据的完整性 。 由于这些原因 , IP 被称为不可靠服务 ( unreliable service) 。 在此还要指出的是,每台主机至少有一个网络层地址 , 即所谓的 IP 地址 ,每台主机有一个 IP 地址 。
UDP 和 TCP 最基本的责任是 , 将两个端系统间 IP 的交付服务扩展为运行在端系统上的两个进程之间的交付服务。 将主机间交付扩展到进程间交付被称为运输层的多路复用(transport-layer multiplexing) 与多路分解(demultiplexing) 。
UDP 和 TCP 还可以通过在其报文段首部中包括差错检查字段而提供完整性检查 。
进程到进程的数据交付和差错检查是两种最低限度的运输层服务 , 也是UDP 所能提供的仅有的两种服务。
TCP 为应用程序提供了几种附加服务:
- 可靠数据传输(reliable data transfer)。 通过使用流量控制、 序号、 确认和定时器,TCP 确保正确地 、 按序地将数据从发送进程交付给接收进程 。
- 拥塞控制 ( comgestion control ) 。TCP 拥塞控制防止任何一条 TCP 连接用过多流量来淹没通信主机之间的链路和交换设备 。 TCP 力求为每个通过一条拥塞网络链路的连接平等地共享网络链路带宽 。 这可以通过调节 TCP 连接的发送端发送进网络的流量速率来做到。
3.2 多路复用与多路分解
运输层的多路复用与多路分解是将由网络层提供的主机到主机交付服务延伸到为运行在主机上的应用程序提供进程到进程的交付服务。需要强调的是 , 多路复用与多路分解服务是所有计算机网络都需要的 。
在目的主机 ,运输层从紧邻其下的网络层接收报文段 。 运输层负责将这些报文段中的数据交付给在主机上运行的适当应用程序进程 。 我们来看一个例子 。假定你正坐在计算机前下载 Web 页面, 同时还在运行一个 FTP 会话和两个 Telnet 会话 。 这样你就有 4 个网络应用进程在运行, 即两个 Telnet 进程, 一个 FTP 进程和一个 HTTP 进程 。 当你的计算机中的运输层从底层的网络层接收数据时 , 它需要将所接收到的数据定向到这 4 个进程中的一 个。 现在我们来研究这是怎样完成的 。
首先回想 第2章的内容 , 一个进程(作为网络应用的一部分)有一个或多个套接字(socket), 它相当于从网络向进程传递数据和从进程向网络传递数据的门户 。 因此 , 如图 3-2 所示, 在接收主机中的运输层实际上并没有直接将数据交付给进程,而是将数据交给了一个中间的套接字 。 由于在任一时刻 , 在接收主机上可能有不止一个套接字 , 所以每个套接字都有唯一的标识符 。 标识符的格式取决于它是 UDP 还是 TCP 套接字 , 我们将很快对它们进行讨论 。
现在我们考虑接收主机怎样将一个到达的运输层报文段定向到适当的套接字 。为此目的, 每个运输层报文段中具有几个字段 。 在接收端,运输层检查这些字段 , 标识出接收套接字 ,进而将报文段定向到该套接字 。 将运输层报文段中的数据交付到正确的套接字的工作称为多路分解 ( demultiplexing) 。 在源主机从不同套接字中收集数据块 , 并为每个数据块封装上首部信息(这将在以后用于分解)从而生成报文段 , 然后将报文段传递到网络层 , 所有这些工作称为多路复用 ( multiplexing) 。
通过上述讨论, 我们知道运输层多路复用要求 : ①套接字有唯一标识符;②每个报文段有特殊字段来指示该报文段所要交付到的套接字 。
如图3-3所示,这些特殊字段是源端口号字段 ( source port number field ) 和目的端口号字段 ( destination port number field) 。
端口号是一个 16 比特的数 , 其大小在 0 ~ 65535 之间 。 0 ~ 1023 范围的端口号称为周知端口号( well-known port number),是受限制的,这是指它们保留给诸如 HTTP (它使用端口号 80) 和 FTP (它使用端口号 21) 之类的周知应用层协议来使用 。
现在应该清楚运输层是怎样能够实现分解服务的了 : 在主机上的每个套接字能够分配一个端口号 , 当报文段到达主机时 ,运输层检査报文段中的目的端口号 , 并将其定向到相应的套接字 。 然后报文段中的数据通过套接字进入其所连接的进程 。 如我们将看到的那样 , UDP 大体上是这样做的 。 然而, 也将如我们所见, TCP 中的多路复用与多路分解更为复杂 。
-
无连接的多路复用与多路分解
假定在主机 A 中的一个进程具有 UDP 端口 19157, 它要发送一个应用程序数据块给位于主机B 中的另一进程,该进程具有 UDP 端口 46428。主机 A 中的运输层创建一个运输层报文段 , 其中包括应用程序数据 、 源端口号 ( 19157 ) 、 目的端口号 ( 46428 ) 和两个其他值。 然后 ,运输层将得到的报文段传递到网络层 。 网络层将该报文段封装到一个 IP数据报中 , 并尽力而为地将报文段交付给接收主机 。如果该报文段到达接收主机 B, 接收主机运输层就检查该报文段中的目的端口号 ( 46428 )并将该报文段交付给端口号46428 所标识的套接字 。值得注意的是 , 主机 B 可能运行多个进程, 每个进程都具有其自己的 UDP 套接字和相联系的端口号。 当 UDP 报文段从网络到达时 , 主机 B 通过检查该报文段中的目的端口号 , 将每个报文段定向 ( 分解) 到相应的套接字 。
一个 UDP 套接字是由一个二元组全面标识的,该二元组包含一个目的IP 地址和一个目的端口号。 因此,如果两个 UDP 报文段有不同的源 IP 地址和/ 或源端口号 , 但具有相同的目的 IP 地址和目的端口号 ,那么这两个报文段将通过相同的目的套接字被定向到相同的目的进程 。
源端口号的用途是什么呢 ? 如图 3-4 所示, 在 A 到 B 的报文段中,源端口号用作“ 返回地址”的一部分 , 即当 B 需要回发一个报文段给 A 时 , B 到 A 的报文段中的目的端口号便从 A 到 B 的报文段中的源端口号中取值。 ( 完整的返回地址是 A 的 IP 地址和源端口号。 )
-
而向连接的多路复用与多路分解
TCP 套接字是由一个四元组( 源 IP 地址,源端口号 ,目的 IP 地址 ,目的端口号 ) 来标识的 。 因此 , 当一个 TCP 报文段从网络到达一台主机时 ,该主机使用全部 4 个值来将报文段定向 ( 分解) 到相应的套接字 。 特别与UDP 不同的是 , 两个具有不同源 IP 地址或源端口号的到达 TCP 报文段将被定向到两个不同的套接字 ,除非 TCP 报文段携带了初始创建连接的请求 。
服务器主机可以支持很多并行的 TCP 套接字 , 每个套接字与一个进程相联系, 并由其四元组来标识每个套接字 。 当一个 TCP 报文段到达主机时 , 所有 4 个字段 ( 源 IP 地址,源端口 ,目的 IP 地址 ,目的端口 )被用来将报文段定向 ( 分解) 到相应的套接字 。
图 3-5 图示了这种情况 , 图中主机 C 向服务器 B 发起了两个 HTTP 会话, 主机 A 向服务器 B 发起了一个 HTTP 会话 。主机 A 与主机 C 及服务器 B 都有自己唯一的 IP 地址 , 它们分别是 A 、 C 、 B 。 主机 C 为其两个 HTTP 连接分配了两个不同的源端口号 ( 26145 和7532 ) 。 因为主机 A 选择源端口号时与主机 C 互不相干 , 因此它也可以将源端口号 26145分配给其 HTTP 连接 。但这不是问题, 即服务器 B 仍然能够正确地分解这两个具有相同源端口号的连接 , 因为这两条连接有不同的源 IP 地址 。
3.3 无连接运输
UDP 从应用进程得到数据 ,附加上用于多路复用 /分解服务的源和目的端口号字段 , 以及两个其他的小字段, 然后将形成的报文段交给网络层 。 网络层将该运输层报文段封装到一个 IP 数据报中 , 然后尽力而为地尝试将此报文段交付给接收主机 。 如果该报文段到达接收主机 , UDP 使用目的端口号将报文段中的数据交付给正确的应用进程 。值得注意的是 , 使用 UDP 时 , 在发送报文段之前 , 发送方和接收方的运输层实体之间没有握手 。 正因为如此 , UDP 被称为是无连接的 。
DNS 是一个通常使用 UDP 的应用层协议的例子 。 当一台主机中的 DNS 应用程序想要进行一次查询时 , 它构造了一个 DNS 查询报文并将其交给 UDP 。 无须执行任何与运行在目的端系统中的 UDP 实体之间的握手 , 主机端的 UDP 为此报文添加首部字段 , 然后将形成的报文段交给网络层。 网络层将此 UDP 报文段封装进一个 IP 数据报中 , 然后将其发送给一个名字服务器。 在查询主机中的 DNS 应用程序则等待对该查询的响应 。 如果它没有 收到响应 ( 可能是由于底层网络丢失了查询或响应 ), 则要么试图向另一个名字服务器发送该査询,要么通知调用的应用程序它不能获得响应 。
UDP相比于TCP的优点
- 关于发送什么数据以及何时发送的应用层控制更为精细 。
- 无须连接建立 。
- 无连接状态 。
- 分组首部开销小 。
3.3.1 UDP报文段结构
UDP 报文段结构如图 3-7 所示, 它由 RFC 768 定义。 应用层数据占用 UDP 报文段的数据字段 。
UDP 首部只有 4 个字段 , 每个字段由两个字节组成 。 如前一节所讨论的,通过端口号可以使目的主机将应用数据交给运行在目的端系统中的相应进程( 即执行分解功能) 。 长度字段指示了在 UDP 报文段中的字节数 (首部加数据 ) 。 因为数据字段的长度在一个 UDP 段中不同于在另一个段中 , 故需要一个明确的长度 。接收方使用检验和来检查在该报文段中是否出现了差错 。 实际上 ,计算检验和时 ,除了 UDP 报文段以外还包括了 IP 首部的一些字段 。
3.3.2 UDP检验和
UDP 检验和提供了差错检测功能 。 这就是说, 检验和用于确定当 UDP 报文段从源到达目的地移动时 , 其中的比特是否发生了改变 。发送方的 UDP 对报文段中的所有 16 比特字的和进行反码运算,求和时遇到的任何溢出都被回卷。 得到的结果被放在 UDP 报文段中的检验和字段 。
举例来说, 假定我们有下面 3个 16 比特的字 :
0110011001100000
0101010101010101
1000111100001100
这些 16 比特字的前两个之和是 :
0110011001100000
0101010101010101
1011101110110101
再将上面的和与第三个字相加 , 得出 :
1011101110110101
1000111100001100
0100101011000010
注意到最后一次加法有溢出 , 它要被回卷。反码运算就是将所有的 0 换成 1, 所有的1 转换成 0 。 因此,该和 0100101011000010 的反码运算结果是 1011010100111101, 这就变为了检验和。 在接收方 , 全部的 4 个 16 比特字 ( 包括检验和 ) 加在一起 。 如果该分组中没有引入差错, 则显然在接收方处该和将是1111111111111111 。如果这些比特之一是 0,那么我们就知道该分组中已经出现了差错 。
你可能想知道为什么 UDP 首先提供了检验和 , 就像许多链路层协议( 包括流行的以太网协议) 也提供了差错检测那样 。其原因是不能保证源和目的之间的所有链路都提供差错检测 ;这就是说, 也许这些链路中的一条可能使用没有差错检测的协议 。 此外 , 即使报文段经链路正确地传输, 当报文段存储在某台路由器的内存中时 , 也可能引入比特差错 。在既无法确保逐链路的可靠性 , 又无法确保内存中的差错检测的情况下 , 如果端到端数据传输服务要提供差错检测 , UDP 就必须在端到端基础上在运输层提供差错检测 。 这是一个在系统设计中被称颂的端到端原则 ( end-encl principle) 的例子,该原则表述为因为某种功能( 在此时为差错检测 ) 必须基于端到端实现: “与在较高级别提供这些功能的代价相比 ,在较低级别上设置的功能可能是冗余的或几乎没有价值的 。”
3.4 可靠数据传输原理
图 3-8 图示说明了我们学习可靠数据传输的框架 。为上层实体提供的服务抽象是 : 数据可以通过一条可靠的信道进行传输 。借助于可靠信道, 传输数据比特就不会受到损坏(由 0 变为 1, 或者相反 ) 或丢失 ,而且所有数据都是按照其发送顺序进行交付。 这恰好就是 TCP 向调用它的因特网应用所提供的服务模型 。
实现这种服务抽象是可靠数据传输协议( reliable data transfer protocol )的责任。 由于可靠数据传输协议的下层协议也许是不可靠的, 因此这是一项困难的任务。例如 , TCP 是在不可靠的 ( IP) 端到端网络层之上实现的可靠数据传输协议 。 更一般的情况是 , 两个可靠通信端点的下层可能是由一条物理链路( 如在链路级数据传输协议的场合下 )组成或是由一个全球互联网络( 如在运输级协议的场合下 )组成 。 然而, 就我们的目的而言, 我们可将较低层直接视为不可靠的点对点信道 。
在本节中 ,考虑到底层信道模型越来越复杂 , 我们将不断地开发一个可靠数据传输协议的发送方一侧和接收方一侧。例如 , 我们将考虑当底层信道能够损坏比特或丢失整个分组时 ,需要什么样的协议机制。 这里贯穿我们讨论始终的一个假设是分组将以它们发送的次序进行交付 , 某些分组可能会丢失 ;这就是说, 底层信道将不会对分组重排序 。 图 3-8b说明了用于数据传输协议的接口。 通过调用rdt_send ()
函数 , 上层可以调用数据传输
协议的发送方 。 它将要发送的数据交付给位于接收方的较高层 。(这里 rdt 表示可靠数据传输协议, _send 指示 rdt 的发送端正在被调用 。 )在接收端, 当分组从信道的接收端到达时 , 将调用 rdt_rcv()
。当 rdt 协议想要向较高层交付数据时 , 将通过调用deliver_data()
来完成 。
在本节中 , 我们仅考虑单向数据传输 (unidirectional data transfer) 的情况 , 即数据传输是从发送端到接收端的 。可靠的双向数据传输 (bidirectional data transfer) (即全双工数据传输)情况从概念上讲不会更难, 但解释起来更为单调乏味。 虽然我们只考虑单向数据传输, 注意到下列事实是重要的, 我们的协议也需要在发送端和接收端两个方向上传输分组, 如图 3-8 所示 。 我们很快会看到 ,除了交换含有待传送的数据的分组之外 , rdt 的发送端和接收端还需往返交换控制分组 。 rdt 的发送端和接收端都要通过调用 udt_send()
发送分组给对方(其中 udt 表示不可靠数据传输)。
3.4.1 构造可靠数据传输协议
-
经完全可靠信道的可靠数据传输: rdt1. 0
首先 , 我们考虑最简单的情况 , 即底层信道是完全可靠的 。 我们称该协议为 rdt1.0,该协议本身是简单的 。 图 3-9 显示了 rdt1.0 发送方和接收方的有限状态机 (Finite-State Machine , FSM) 的定义。图 3-9a 中的 FSM定义了发送方的操作 , 图 3-9b 中的 FSM 定义了接收方的操作。 注意到下列问题是重要的, 发送方和接收方有各自的 FSM 。 图 3-9中发送方和接收方的 FSM 每个都只有一个状态 。 FSM 描述图中的箭头指示了协议从一个状态变迁到另一个状态 。因为图 3-9 中的每个 FSM 都只有一个状态 , 因此变迁必定是从一个状态返回到自身。引起变迁的事件显示在表示变迁的横线上方 , 事件发生时所采取的动作显示在横线下方 。 如果对一个事件没有动作,或没有就事件发生而采取了一个动作 , 我们将在横线上方或下方使用符号$\Lambda$,以分别明确地表示缺少动作或事件。FSM 的初始状态用虚线表示 。 尽管图3-9中的 FSM 只有一个状态 , 但马上我们就将看到多状态的 FSM, 因此标识每个 FSM 的初始状态是非常重要的 。
rdt 的发送端只通过
rdt_send(data)
事件接受来自较高层的数据 , 产生一个包含该数据的分组(经由make-pkt (data)
动作, 并将分组发送到信道中。 实际上 ,rdt_send(data)
事件是由较高层应用的过程调用产生的 ( 例如 ,rdcsend()
) 。在接收端,rdt通过
rdt_rcv(packet)
事件从底层信道接收一个分组, 从分组中取岀数据(经由extract(packet,data)
动作), 并将数据上传给较高层(通过deliver_data( data )
动作)。实际上 ,rdt_rcv( packet)
事件是由较低层协议的过程调用产生的(例如 ,rdt_rcv()
) 。在这个简单的协议中 , 一个单元数据与一个分组没差别。 而且 , 所有分组是从发送方流向接收方 ; 有了完全可靠的信道, 接收端就不需要提供任何反馈信息给发送方 , 因为不必担心出现差错!
-
经具有比特差错信道的可靠数据传输: rdt2.0
底层信道更为实际的模型是分组中的比特可能受损的模型 。 在分组的传输 、传播或缓存的过程中 ,这种比特差错通常会岀现在网络的物理部件中。 我们眼下还将继续假定所有发送的分组(虽然有些比特可能受损)将按其发送的顺序被接收 。
考虑一下你自己是怎样通过电话口述一条长报文的 。 在通常情况下 , 报文接收者在听到、 理解并记下每句话后可能会说 “0K” 。 如果报文接收者听到一句含糊不清的话时,他可能要求你重复那句容易误解的话 。 这种口述报文协议使用了肯定确认 ( positive acknowledgment )( “OK” ) 与否定确认 ( negative acknowledgment)( “请重复一遍)。 这些控制报文使得接收方可以让发送方知道哪些内容被正确接收 , 哪些内容接收有误并因此需要重复 。 在计算机网络环境中 , 基于这样重传机制的可靠数据传输协议称为自动重传请求( Automatic Repeat Request , ARQ) 协议 。
重要的是 , ARQ 协议中还需要另外三种协议功能来处理存在比特差错的情况 :
- 差错检测 。首先 ,需要一种机制以使接收方检测到何时出现了比特差错 。
- 接收方反馈 。我们的rdt2.0 协议将从接收方向发送方回送 ACK 与 NAK 分组 。 理论上 ,这些分组只需要一个比特长;如用 0 表示 NAK,,用 1 表示 ACK。
- 重传。接收方收到有差错的分组时 , 发送方将重传该分组文 。
图 3-10 说明了表示 rdt2.0 的 FSM, 该数据传输协议采用了差错检测 、 肯定确认与否定确认 。
rdt2.0 的发送端有两个状态 。 在最左边的状态中 , 发送端协议正等待来自上层传下来的数据 。 当
rdt_send(data)
事件岀现时 , 发送方将产生一个包含待发送数据的分组 ( sndpk),带有检验和(例如 , 就像在 3.3.2 节讨论的对 UDP 报文段使用的方法) , 然后经由udt_send(sndpkt)
操作发送该分组 。 在最右边的状态中 , 发送方协议等待来自接收方的ACK 或 NAK 分组 。 如果收到一个 ACK 分组(图 3-10 中符号rdt_rcv( rcvpkt) && isACK(rcvpkt)
对应该事件) , 则发送方知道最近发送的分组已被正确接收 ,因此协议返回到等待来自上层的数据的状态 。 如果收到一个 NAK 分组,该协议重传上一个分组并等待接收方为响应重传分组而回送的 ACK 和 NAKO 注意到下列事实很重要: 当发送方处于等待ACK 或 NAK 的状态时 ,它不能从上层获得更多的数据 ;这就是说,rdt_send ()
事件不可能岀现; 仅当接收到 ACK 并离开该状态时才能发生这样的事件。 因此 , 发送方将不会发送一块新数据 ,除非发送方确信接收方已正确接收当前分组 。由于这种行为 , rdt2.0 这样的协议被称为停等 ( stop-and-wait) 协议 。rdt2.0 接收方的 FSM 仍然只有单一状态 。 当分组到达时 , 接收方要么回答一个 ACK,要么回答 一个NAK, 这取决于收到的分组是否受损 。 在图 3-10 中 ,符号
rdt_rcv ( rcvpkt ) &&corrupt ( rcvpkt)
对应于收到一个分组并发现有错的事件。rdt2.0 协议存在一个致命的缺陷 :没有考虑到 ACK 或 NAK 分组受损的可能性!这里的难点在于 , 如果一个 ACK 或 NAK 分组受损 , 发送方无法知道接收方是否正确接收了上一块发送的数据 。
考虑处理受损 ACK 和 NAK 时的 3 种可能性:
- 对于第一种可能性 ,考虑在口述报文情况下人可能的做法 。重复询问。
- 第二种可能性是增加足够的检验和比特, 使发送方不仅可以检测差错,还可恢复差错 。 对于会产生差错但不丢失分组的信道,这就可以直接解决问题 。
- 第三种方法是 , 当发送方收到含糊不清的 ACK 或 NAK 分组时 , 只需重传当前数据分组即可。 然而,这种方法在发送方到接收方的信道中引入了冗余分组( duplicate packet )。冗余分组的根本困难在于接收方不知道它上次所发送的 ACK 或 NAK是否被发送方正确地收到。
解决这个新问题的一个简单方法是在数据分组中添加一新字段 ,让发送方对其数据分组编号 , 即将发送数 据分组的序号 ( sequence number) 放在该字段 。于是 , 接收方只需要检查序号即可确定收到的分组是否一次重传。 对于停等协议这种简单情况 , 1 比特序号就足够了 , 因为它可让接收方知道发送方是否正在重传前一个发送分组( 接收到的分组序号与最近收到的分组序号相同 ), 或是一个新分组( 序号变化了 ,用模 2 运算 “前向” 移动 ) 。 因为目前我们假定信道不丢分组, ACK 和 NAK 分组本身不需要指明它们要确认的分组序号。发送方知道所接收到的 ACK 和 NAK 分组( 无论是否是含糊不清的) 是为响应其最近发送的数据分组而生成的 。
图 3 -11 和图 3-12 给出了对 rdt2.1 的 FSM 描述,这是rdt2.0的修订版 。 rdt2.1的发送方和接收方 FSM 的状态数都是以前的两倍。 这是因为协议状态此时必须反映出目前 (由发送方 ) 正发送的分组或 ( 在接收方 ) 希望接收的分组的序号是 0 还是 1 。值得注意的是 , 发送或期望接收 0 号分组的状态中的动作与发送或期望接收 1 号分组的状态中的动作是相似的; 唯一的不同是序号处理的方法不同。
协议 rdt2.1 使用了从接收方到发送方的肯定确认和否定确认 。 当接收到失序的分组时 , 接收方对所接收的分组发送一个肯定确认 。 如果收到受损的分组, 则接收方将发送一个否定确认 。 如果不发送 NAK, 而是对上次正确接收的分组发送一个 ACK, 我们也能实现与 NAK — 样的效果 。发送方接收到对同一个分组的两个 ACK ( 即接收冗余 ACK(duplicate ACK)) 后 , 就知道接收方没有正确接收到跟在被确认两次的分组后面的分组 。rdt2.2 是在有比特差错信道上实现的一个无 NAK 的可靠数据传输协议,如图 3-13 和图 3-14 所示 。 rdt2. 1 和 rdt2.2 之间的细微变化在于 , 接收方此时必须包括由一个 ACK 报文所确认的分组序号(这可以通过在接收方 FSM 中 , 在
make_pkt ()
中包括参数 ACK 0 或 ACK 1 来实现) , 发送方此时必须检查接收到的ACK 报文中被确认的分组序号(这可通过在发送方 FSM 中 , 在isACK()
中包括参数 0 或 1 来实现)。
-
经具有比特差错的丢包信道的可靠数据传输: rdt3.0
现在假定除了比特受损外 , 底层信道还会丢包 。协议现在必须处理另外两个关注的问题: 怎样检测丢包以及发生丢包后该做些什么。 在 rdt2.2 中已经研发的技术 , 如使用检验和、 序号、 ACK 分组和重传等, 使我们能给出后一个问题的答案 。为解决第一个关注的问题,还需增加一种新的协议机制。
这里, 我们让发送方负责检测和恢复丢包工作。假定发送方传输一个数据分组,该分组或者接收方对该分组的 ACK 发生了丢失 。 在这两种情况下 , 发送方都收不到应当到来的接收方的响应 。 如果发送方愿意等待足够长的时间以便确定分组已丢失 , 则它只需重传该数据分组即可。、
但是发送方需要等待多久才能确定已丢失了某些东西呢 ? 很明显发送方至少需要等待这样长的时间: 即发送方与接收方之间的一个往返时延 ( 可能会包括在中间路由器的缓冲时延 ) 加上接收方处理一个分组所需的时间 。 在很多网络中 , 最坏情况下的最大时延是很难估算的,确定的因素非常少 。 此外 ,理想的协议应尽可能快地从丢包中恢复出来 ;等待一个最坏情况的时延可能意味着要等待一段较长的时间,直到启动差错恢复为止 。 因此实践中采取的方法是发送方明智地选择一个时间值 , 以判定可能发生了丢包 ( 尽管不能确保 ) 。 如果在这个时间内没有收到 ACK,则重传该分组 。 注意到如果一个分组经历了一个特别大的时延 , 发送方可能会重传该分组, 即使该数据分组及其 ACK 都没有丢失 。 这就在发送方到接收方的信道中引入了冗余数据分组( duplicate data packet ) 的可能性 。 幸运的是 , rdt2.2 协议已经有足够的功能( 即序号 ) 来处理冗余分组情况。
发送方不知道是一个数据分组丢失 ,还是一个 ACK 丢失 , 或者只是该分组或 ACK 过度延时 。 在所有这些情况下 , 动作是同样的:重传。为了实现基于时间的重传机制 ,需要一个倒计数定时器 ( countdown timer) , 在一个给定的时间量过期后 , 可中断发送方 。 因此 , 发送方需要能做到 : ①每次发送一个分组( 包括第一次分组和重传分组) 时 , 便启动一个定时器。②响应定时器中断 (采取适当的动作 ) 。③终止定时器。
图 3-15 给出了 rdt3. 0 的发送方 FSM,这是一个在可能出错和丢包的信道上可靠传输数据的协议。图 3-16 显示了在没有丢包和延迟分组情况下协议运作的情况 , 以及它是如何处理数据分组丢失的 。 在图 3-16 中 , 时间从图的顶部朝底部移动 ; 注意到一个分组的接收时间必定迟于一个分组的发送时间,这是因为发送时延与传播时延之故 。 在图 3-16b ~ d 中 , 发送方括号部分表明了定时器的设置时刻以及随后的超时 。 。 因为分组序号在 0 和 1 之间交替 , 因此 rdt3.0 有时被称为比特交替协议 ( alternating- bit protocol) 。
3.4.2 流水线可靠数据传输协议
rdt3.0 是一个功能正确的协议, 但并非人人都对它的性能满意 ,特别是在今天的高速网络中更是如此 。 rdt3.0 性能问题的核心在于它是一个停等协议 。
为了评价该停等行为对性能的影响 , 可考虑一种具有两台主机的理想化场合 , 一台主机位于美国西海岸 , 另一台位于美国东海岸 , 如图 3-17 所示 。 在这两个端系统之间的光速往返传播时延 RTT 大约为 30 毫秒 。假定彼此通过一条发送速率 R 为 1Gbps ( 每秒$10^{9}$比特)的信道相连 。包括首部字段和数据的分组长$L$为 1000 字节( 8000 比特), 发送一个分组进入 1Gbps 链路实际所需时间是 :
\[t_{trans}=\frac{L}{R}=\frac{8000bit/pkt}{10^{9}bit/s}=8\mu s/pkt\]图 3-18a 显示了对于该停等协议, 如果发送方在$t=0$时刻开始发送分组, 则在 $t=L/R=8\mu s$ 后 , 最后 1 比特数据进入了发送端信道 。 该分组经过 15ms 的穿越国家的旅途后到达接收端,该分组的最后 1 比特在时刻 $t=RTT/2+L/R=15.008\mu s$ 时到达接收方 。为了简化起见, 假设 ACK 分组很小 ( 以便我们可以忽略其发送时间), 接收方一旦收到一个数据分组的最后 1 比特后立即发送 ACK,ACK 在时刻 $t=RTT/2+L/R=30.008\mu s$ 时在发送方出现 。 此时 , 发送方可以发送下一个报文 。 因此 , 30.008ms 内 , 发送方的发送只用了0.008 ms。 如果我们定义发送方 ( 或信道)的利用率 ( utilization) 为:发送方实际忙于将发送比特送进信道的那部分时间与发送时间之比 , 图 3-18a 中的分析表明了停等协议有着非常低的发送方利用率 $U_{sender}$:
\[U_{sender}=\frac{L/R}{RTT+L/R}=\frac{0.008}{30.008}=0.00027\]这就是说, 发送方只有万分之 2. 7 时间是忙的 。从其他角度来看, 发送方在 30. 008ms内只能发送 1000 字节, 有效的吞吐量仅为 267kbps,即使有 1Gbps 的链路可用也是如此!
这种特殊的性能问题的一个简单解决方法是 : 不以停等方式运行, 允许发送方发送多个分组而无须等待确认, 如在图 3-17b 图示的那样 。 图 3-18b 显示了如果发送方可以在等待确认之前发送 3 个报文 , 其利用率也基本上提高 3 倍。因为许多从发送方向接收方输送的分组可以被看成是填充到一条流水线中 , 故这种技术被称为流水线 ( pipelining) 。 流水线技术对可靠数据传输协议可带来如下影响:
- 必须增加序号范围 , 因为每个输送中的分组( 不计算重传的) 必须有一个唯一的序号 ,而且也许有多个在输送中的未确认报文 。
- 协议的发送方和接收方两端也许不得不缓存多个分组 。发送方最低限度应当能缓冲那些已发送但没有确认的分组 。 如下面讨论的那样 , 接收方或许也需要缓存那些已正确接收的分组 。
- 所需序号范围和对缓冲的要求取决于数据传输协议如何处理丢失 、 损坏及延时过大的分组 。 解决流水线的差错恢复有两种基本方法是 : 回退 N 步 ( Go-Back-N,GBN) 和选择重传( Selective Repeat , SR) 。
3.4.3 回退N步
在回退N步( GBN) 协议中 , 允许发送方发送多个分组( 当有多个分组可用时 )而不需等待确认, 但它也受限于在流水线中未确认的分组数不能超过某个最大允许数N。
图 3-19 显示了发送方看到的 GBN 协议的序号范围 。 如果我们将基序号 ( base) 定义为最早未确认分组的序号 , 将下一个序号 ( nextseqnum ) 定义为最小的未使用序号 ( 即下一个待发分组的序号 ), 则可将序号范围分割成 4 段 。 在 [0, base - 1] 段内的序号对应于已经发送并被确认的分组 。 [base, nextseqnum - 1 ]段内对应已经发送但未被确认的分组 。 [ nextseqnum , base + N - 1 ] 段内的序号能用于那些要被立即发送的分组, 如果有数据来自上层的话 。最后 , 大于或等于 base + N 的序号是不能使用的,直到当前流水线中未被确认的分组(特别是序号为 base 的分组) 已得到确认为止 。
如图 3-19 所提示的那样 ,那些已被发送但还未被确认的分组的许可序号范围可以被看成是一个在序号范围内长度为”的窗口。 随着协议的运行,该窗口在序号空间向前滑动。 因此 ,N常被称为窗口长度( window size ), GBN 协议也常被称为滑动窗口协议( sliding-window protocol) 。
在实践中 , 一个分组的序号承载在分组首部的一个固定长度的字段中。 如果分组序号字段的比特数是$k$ ,则该序号范围是 $[0, 2^{k}-1]$在一个有限的序号范围内 , 所有涉及序号的运算必须使用模 $2^{k}$运算 。 ( 即序号空间可被看作是一个长度为$2^{k}$ 的环, 其中序号$2^{k}-1$ 紧接着序号 0 。 )
图 3-20 和图 3-21 给出了一个基于 ACK 、 无 NAK 的 GBN 协议的发送方和接收方这两端的扩展 FSM 描述 。 我们称该 FSM 描述为扩展 FSM,是因为我们已经增加了变量 base 和 nextseqnum,还增加了对这些变量的操作以及与这些变量有关的条件动作。
GBN 发送方必须响应三种类型的事件 :
- 上层的调用 。当上层调用
rdt_send()
时 , 发送方首先检查发送窗口是否已满 , 即是否有 N 个已发送但未被确认的分组 。 如果窗口未满 , 则产生一个分组并将其发送,并相应地更新变量 。 如果窗口已满 , 发送方只需将数据返回给上层 ,隐式地指示上层该窗口已满 。 然后上层可能会过一会儿再试 。 在实际实现中 , 发送方更可能缓存 ( 并不立刻发送)这些数据 , 或者使用同步机制 ( 如一个信号量或标志 ) 允许上层在仅当窗口不满时才调用rdt_send()
。 - 收到一个 ACK 。在 GBN 协议中 , 对序号为几的分组的确认采取累积确认( cumulative acknowledgment ) 的方式 ,表明接收方已正确接收到序号为 n 的以前且包括 n在内的所有分组 。
- 超时事件。协议的名字 “ 回退 N 步 ” 来源于出现丢失和时延过长分组时发送方的行为。 就像在停等协议中那样 , 定时器将再次用于恢复数据或确认分组的丢失 。如果出现超时 , 发送方重传所有已发送但还未被确认过的分组 。
在 GBN 中 , 接收方的动作也很简单。 如果一个序号为$n$的分组被正确接收到 , 并且按序 ( 即上次交付给上层的数据是序号为$n-1$的分组), 则接收方为分组 $n$发送一个ACK, 并将该分组中的数据部分交付到上层 。 在所有其他情况下 , 接收方丢弃该分组, 并为最近按序接收的分组重新发送 ACK 。 注意到因为一次交付给上层一个分组, 如果分组$k$已接收并交付 , 则所有序号比$k$小的分组也已经交付。 因此 , 使用累积确认是 GBN —个自然的选择 。
图 3-22 给岀了窗口长度为 4 个分组的 GBN 协议的运行情况。 因为该窗口长度的限制,发送方发送分组 0~3,然后在继续发送之前 , 必须等待直到一个或多个分组被确认 。 当接收到每一个连续的 ACK ( 例如 ACK 0 和 ACK 1 ) 时,该窗口便向前滑动 , 发送方便可以发送新的分组( 分别是分组 4 和分组 5 ) 。 在接收方 , 分组 2 丢失 , 因此分组 3 、 4和 5 被发现是失序分组并被丢弃 。
3.4.4 选择重传
在图 3-17 中 , GBN 协议潜在地允许发送方用多个分组 “ 填充流水线 ” , 因此避免了停等协议中所提到的信道利用率问题 。 然而, GBN 本身也有一些情况存在着性能问题 。 尤其是当窗口长度和带宽时延积都很大时 , 在流水线中会有很多分组更是如此 。单个分组的差错就能够引起 GBN 重传大量分组,许多分组根本没有必要重传。 随着信道差错率的增加,流水线可能会被这些不必要重传的分组所充斥 。 想象一下 , 在我们口述消息的例子中 , 如果每次有一个单词含糊不清 , 其前后 1000 个单词( 例如 ,窗口长度为 1000 个单词) 不得不被重传的情况。 此次口述会由于这些反复述说的单词而变慢 。
顾名思义 ,选择重传 (SR) 协议通过让发送方仅重传那些它怀疑在接收方出错( 即丢失或受损 )的分组而避免了不必要的重传。 这种个别的 、 按需的重传要求接收方逐个地确认正确接收的分组 。再次用窗口长度 N来限制流水线中未完成 、 未被确认的分组数 。 然而, 与 GBN 不同的是 , 发送方已经收到了对窗口中某些分组的 ACK 。 图 3-23 显示了 SR发送方看到的序号空间 。 图 3-24 详细描述了 SR 发送方所采取的动作。
SR 接收方将确认一个正确接收的分组而不管其是否按序 。 失序的分组将被缓存直到所有丢失分组( 即序号更小的分组)皆被收到为止 ,这时才可以将一批分组按序交付给上层 。 图 3-25 详细列出了 SR 接收方所采用的各种动作。 图 3-26 给出了一个例子以说明出现丢包时 SR 的操作。值得注意的是 , 在图 3 ・ 26 中接收方初始时缓存了分组 3 、 4 、 5,并在最终收到分组 2 时.才将它们一并交付给上层 。
注意到图 3-25 中的第二步很重要, 接收方重新确认(而不是忽略) 已收到过的那些序号小于当前窗口基序号的分组 。你应该理解这种重新确认确实是需要的 。例如 ,给定在图 3-23 中所示的发送方和接收方的序号空间, 如果分组 send_base 的 ACK 没有从接收方传播回发送方 , 则发送方最终将重传分组 send_base, 即使显然 ( 对我们而不是对发送方来说! ) 接收方已经收到了该分组 。 如果接收方不确认该分组, 则发送方窗口将永远不能向 前滑动 !这个例子说明了 SR 协议( 和很多其他协议一样 )的一个重要方面 。 对于哪些分组已经被正确接收 , 哪些没有 , 发送方和接收方并不总是能看到相同的结果 。 对 SR 协议而言,这就意味着发送方和接收方的窗口并不总是一致 。
当我们面对有限序号范围的现实时 , 发送方和接收方窗口间缺乏同步会产生严重的后果 。 考虑下面例子中可能发生的情况 ,该例有包括 4 个分组序号 0 、 1 、 2 、 3 的有限序号范围且窗口长度为 3 。假定发送了分组 0 至 2, 并在接收方被正确接收且确认了。 此时,接收方窗口落在第 4 、 5 、 6 个分组上 , 其序号分别为 3 、 0 、 1 。 现在考虑两种情况。 在第一种情况下 , 如图 3-27a 所示, 对前 3 个分组的 ACK 丢失 , 因此发送方重传这些分组 。 因此 , 接收方下一步要接收序号为 0 的分组, 即第一个发送分组的副本 。
在第二种情况下 , 如图 3-27b 所示, 对前 3 个分组的 ACK 都被正确交付。 因此发送方向前移动窗口并发送第 4 、 5 、 6 个分组, 其序号分别为 3 、 0 、 10 序号为 3 的分组丢失 ,但序号为 0 的分组到达( 一个包含新数据的分组) 。
现在考虑一下图 3-27 中接收方的观点 , 在发送方和接收方之间有一个假想的帘子,因为接收方不能 “ 看见 ”发送方采取的动作。 接收方所能观察到的是它从信道中收到的以及它向信道中发出报文序列。 就其所关注的而言, 图 3-27 中的两种情况是等同的 。 没有办法区分是第 1 个分组的重传还是第 5 个分组的初次传输 。 显然 ,窗口长度比序号空间小1 时协议无法工作。但窗口必须多小呢 ? 对于 SR 协议而言,窗口长度必须小于或等于序号空间大小的一半。
至此我们结束了对可靠数据传输协议的讨论 。 我们已涵盖许多基础知识, 并介绍了多种机制 ,这些机制可一起提供可靠数据传输 。 表 3-1 总结这些机制。
3.5 面向连接的运输: TCP
TCP 是因特网运输层的面向连接的可靠的运输协议 。 我们在本节中将看到 , 为了提供可靠数据传输, TCP 依赖于前一节所讨论的许多基本原理, 其中包括差错检测 、 重传、 累积确认 、 定时器以及用于序号和确认号的首部字段 。 TCP 定义在 RFC 793 、 RFC 1122 、 RFC 1323 、RFC 2018 以及 RFC 2581 中。
3.5.1 TCP 连接
TCP 被称为是面向连接的( connection-oriented ),这是因为在一个应用进程可以开始向另一个应用进程发送数据之前 ,这两个进程必须先相互“ 握手 ” , 即它们必须相互发送某些预备报文段 , 以建立确保数据传输的参数 。作为 TCP 连接建立的一部分 ,连接的双方都将初始化与 TCP 连接相关的许多 TCP 状态变量。
这种 TCP “ 连接 ”不是一条像在电路交换网络中的端到端 TDM 或 FDM 电路 。 相反,该 “ 连接 ” 是一条逻辑连接 , 其共同状态仅保留在两个通信端系统的 TCP 程序中。前面讲过,由于 TCP 协议只在端系统中运行,而不在中间的网络元素(路由器和链路层交换机 ) 中运行, 所以中间的网络元素不会维持 TCP 连接状态 。事实上 , 中间路由器对 TCP连接完全视而不见, 它们看到的是数据包,而不是连接。
TCP 连接总是点对点 ( point-to-point) 的, 即在单个发送方与单个接收方之间的连接 。 所谓 “ 多播 ” , 即在一次发送操作中 , 从一个发送方将数据传送给多个接收方 ,这种情况对 TCP 来说是不可能的 。
我们现在来看看 TCP 连接是怎样建立的 。假设运行在某台主机上的一个进程想与另一台主机上的一个进程建立一条连接 。前面讲过, 发起连接的这个进程被称为客户进程,而另一个进程被称为服务器进程 。 该客户应用进程首先要通知客户运输层 , 它想与服务器上的一个进程建立一条连接 。
客户首先发送一个特殊的 TCP 报文段 , 服务器用另一个特殊的 TCP 报文段来响应 , 最后 , 客户再用第三个特殊报文段作为响应 。前两个报文段不承载 “ 有效载荷 ” , 也就是不包含应用层数据 ;而第三个报文段可以承载有效载荷 。由于在这两台主机之间发送了 3 个报文段 , 所以这种连接建立过程常被称为三次握手( three- way handshake ) 。
一旦建立起一条 TCP 连接 , 两个应用进程之间就可以相互发送数据了。客户进程通过套接字 (该进程之门) 传递数据流 。 数据一旦通过该门, 它就由客户中运行的 TCP 控制了。如图3-28所示, TCP 将这些数据引导到该连接的发送缓存 (send buffer)里, 发送缓存是发起三次握手期间设置的缓存之一。 接下来 TCP 就会不时从发送缓存里取出一块数据 ,并将数据传递到网络层 。TCP 可从缓存中取出并放入报文段中的数据数量受限于最大报文段长度 ( Maximum Segment Size,MSS)。 MSS 通常根据最初确定的由本地发送主机发送的最大链路层帧长度 ( 即所谓的最大传输单元 ( Maximum Transmission Unit,MTU)) 来设置 。 设置该 MSS 要保证一个 TCP报文段 ( 当封装在一个 IP 数据报中 )加上 TCP/IP 首部长度 (通常 40 字节)将适合单个链路层帧 。以太网和 PPP 链路层协议都具有 1500 字节的 MTU,因此 MSS 的典型值为 1460字节 。TCP 为每块客户数据配上一个 TCP 首部, 从而形成多个 TCP 报文段 ( TCP segment ) 。这些报文段被下传给网络层 ,网络层将其分别封装在网络层 IP 数据报中。 然后这些 IP 数据报被发送到网络中。 当 TCP 在另一端接收到一个报文段后 ,该报文段的数据就被放入该TCP 连接的接收缓存中 , 如图 3-28 中所示 。 应用程序从此缓存中读取数据流 。 该连接的每一端都有各自的发送缓存和接收缓存 。
从以上讨论中我们可以看出 , TCP 连接的组成包括 : 一台主机上的缓存 、变量和与进程连接的套接字 , 以及另一台主机上的另一组缓存 、变量和与进程连接的套接字 。 如前面讲过的那样 , 在这两台主机之间的网络元素(路由器、交换机和中继器)中没有为该连接分配任何缓存和变量 。
3.5.2 TCP 报文段结构
TCP 报文段由首部字段和一个数据字段组成 。 数据字段包含一块应用数据 。 如前所述, MSS 限制了报文段数据字段的最大长度 。 当 TCP 发送一个大文件 , 例如某 Web 页面上的一个图像时 , TCP 通常是将该文件划分成长度为 MSS 的若干块(最后一块除外 , 它通常小于 MSS)。然而,交互式应用通常传送长度小于 MSS 的数据块 。
图 3 ・ 29 显示了 TCP 报文段的结构 。与UDP — 样 ,首部包括源端口号和目的端口号 , 它被用于多路复用 /分解来自或送到上层应用的数据 。另外 , 同 UDP — 样 , TCP 首部也包括检验和字段 ( checksum field ) 。TCP 报文段首部还包含下列字段:
- 32 比特的序号字段 (sequence number field) 和 32 比特的确认号字段 (acknowledgment number field) 。 这些字段被 TCP 发送方和接收方用来实现可靠数据传输服务 。
- 16 比特的接收窗口字段 ( receive window field ), 该字段用于流量控制。 我们很快就会看到 ,该字段用于指示接收方愿意接受的字节数量 。
- 4 比特的首部长度字段 ( header length field ), 该字段指示了以 32 比特的字为单位 的 TCP 首部长度 。 由于 TCP 选项字段的原因 , TCP 首部的长度是可变的 。(通常, 选项字段为空, 所以 TCP 首部的典型长度是 20 字节 。)
- 可选与变长的选项字段 ( options field ), 该字段用于发送方与接收方协商最大报文 段长度 ( MSS) 时 , 或在高速网络环境下用作窗口调节因子时使用 。 首部字段中 还定义了一个时间戳选项 。
- 6 比特的标志字段 ( flag field ) 。ACK 比特用于指示确认字段中的值是有效的, 即 该报文段包括一个对已被成功接收报文段的确认 。 RST 、 SYN 和 FIN 比特用于连 接建立和拆除, 我们将在本节后面讨论该问题 。
- 序号和确认号
TCP 报文段首部中两个最重要的字段是序号字段和确认号字段 。 这两个字段是 TCP 可靠传输服务的关键部分。但是在讨论这两个字段是如何用于提供可靠数据传输之前 , 我们首先来解释一下 TCP 在这两个字段中究竟放置了什么。
TCP 把数据看成一个无结构的 、 有序的字节流 。 我们从 TCP 对序号的使用上可以看出这一点 , 因为序号是建立在传送的字节流之上 ,而不是建立在传送的报文段的序列之上。一个报文段的序号 ( sequence number for a segment) 因此是该报文段首字节的字节流编号。
举例来说, 假设主机 A 上的一个进程想通过一条 TCP 连接向主机 B 上的一个进程发送一个数据流 。主机 A 中的 TCP 将隐式地对数据流中的每一个字节编号。假定数据流由一个包含 500 000 字节的文件组成 , 其 MSS 为 1000字节, 数据流的首字节编号是 0 。 如图 3-30所示,该 TCP 将为该数据流构建 500 个报文段 。 给第一个报文段分配序号 0,第二个报文段分配序号 1000, 第三个报文段分配序号 2000, 以此类推 。 每一个序号被填入到相应 TCP报文段首部的序号字段中。
现在我们考虑一下确认号。 确认号要比序号难处理一些。前面讲过, TCP 是全双工的,因此主机 A 在向主机 B 发送数据的同时 , 也许也接收来自主机 B 的数据(都是同一条 TCP连接的一部分)。从主机 B 到达的每个报文段中都有一个序号用于从 B 流向 A 的数据 。主机A 填充进报文段的确认号是主机 A 期望从主机 B 收到的下一字节的序号。 看一些例子有助于理解实际发生的事情 。假设主机 A 已收到了来自主机 B 的编号为 0 ~535 的所有字节, 同时假设它打算发送一个报文段给主机 B 。 主机 A 等待主机 B 的数据流中字节 536 及之后的所有字节 。所以主机 A 就会在它发往主机 B 的报文段的确认号字段中填上 536 。
再举一个例子 , 假设主机 A 已收到一个来自主机 B 的包含字节 0 ~535 的报文段 , 以及另一个包含字节 900 〜 1000 的报文段 。 由于某种原因 , 主机 A 还没有收到字节 536 - 899 的报文段 。 在这个例子中 , 主机 A 为了重新构建主机 B 的数据流 , 仍在等待字节 536 ( 和其后的字节) 。 因此 , A 到 B 的下一个报文段将在确认号字段中包含 536 。 因为 TCP 只确认该流中至第一个丢失字节为止的字节, 所以 TCP 被称为提供累积确认( cumulative acknowledgment ) 。
最后一个例子也会引发一个重要而微妙的问题 。主机 A 在收到第二个报文段 ( 字节536 ~899 ) 之前收到第三个报文段 ( 字节 900 ~ 1000 ) 。 因此 ,第三个报文段失序到达 。 该微妙的问题是 : 当主机在一条 TCP 连接中收到失序报文段时该怎么办 ? 有趣的是 ,TCP的RFC 并没有为此明确规定任何规则 ,而是把这一问题留给实现 TCP 的编程人员去处理 。他们有两个基本的选择 : ①接收方立即丢弃失序报文段 ( 如前所述,这可以简化接收方的设计); ②接收方保留失序的字节, 并等待缺少的字节以填补该间隔 。 显然 , 后一种选择对网络带宽而言更为有效 , 是实践中采用的方法 。
在图 3-30 中 , 我们假设初始序号为 0 。事实上 , 一条 TCP 连接的双方均可随机地选择 初始序号。 这样做可以减少将那些仍在网络中存在的来自两台主机之间先前已终止的连接的报文段 ,误认为是后来这两台主机之间新建连接所产生的有效报文段的可能性 ( 它碰巧与旧连接使用了相同的端口号 )。
- Telnet : 序号和确认号的一个学习案例
详情请查阅书P156。
3.5.3 往返时间的估计与超时
TCP 如同前面 3. 4 节所讲的 rdt 协议一样 , 它采用超时 / 重传机制来处理报文段的丢失问题 。 尽管这在概念上简单 , 但是当在如 TCP 这样的实际协议中实现超时 / 重传机制时是会产生许多微妙的问题 。也许最明显的一个问题就是超时间隔长度的设置 。 显然 ,超间隔必须大于该连接的往返时间 ( RTT), 即从一个报文段发出到它被确认的时间 。否则会造成不必要的重传。但是这个时间间隔到底应该是多大呢 ? 刚开始时应如何估计往返时间呢 ? 是否应该为所有未确认的报文段各设一个定时器 ? 我们将在本节进行讨论。
- 估计往返时间
我们开始学习 TCP 定时器的管理问题,要考虑一下 TCP 是如何估计发送方与接收方之间往返时间的 。 这是通过如下方法完成的 。 报文段的样本 RTT (表示为 SampleRTT) 就是从某报文段被发出 ( 即交给 IP) 到对该报文段的确认被收到之间的时间量 。 大多数 TCP的实现仅在某个时刻做一次 SampleRTT 测量,而不是为每个发送的报文段测量一个 SampleRTT。 这就是说, 在任意时刻 , 仅为一个已发送的但目前尚未被确认的报文段估计 SampleRTT, 从而产生一个接近每个 RTT 的新 SampleRTT 值。另外 , TCP 决不为已被重传的报文段计算 SampleRTT ; 它仅为传输一次的报文段测量 。
显然 ,由于路由器的拥塞和端系统负载的变化 ,这些报文段的 SampleRTT 值会随之波 动。 由于这种波动 , 任何给定的 SampleRTT 值也许都是非典型的 。 因此 , 为了估计一个典型的 RTT, 自然要采取某种对 SampleRTT 取平均的办法 。 TCP 维持一个 SampleRTT 均值(称为 EstimatedRTT)。 一旦获得一个新 SampleRTT 时 , TCP 就会根据下列公式来更新 Estimated RTT :
\[EstimatedRTT=(1-\alpha)\cdot EstimatedRTT+\alpha\cdot SampleRTT\]EstimatedRTT 的新值是由以前的 EstimatedRTT 值与 SampleRTT 新值加权组合而成的 。在 [ RFC 6298 ] 中给岀的$\alpha$推荐值是$\alpha=0.125$ ( 即 1/8 ) , 这时上面的公式变为 :
\[EstimatedRTT=0.875\cdot EstimatedRTT+0.125\cdot SampleRTT\]值得注意的是 , EstimatedRTT 是一个 SampleRTT 值的加权平均值,这个加权平均对最近的样本赋予的权值要大于对旧样本赋予的权值。这是很自然的, 因为越近的样本越能更好地反映网络的当前拥塞情况。从统计学观点讲,这种平均被称为指数加权移动平均 ( Exponential Weighted Moving Average , EWMA) 。
除了估算 RTT 外 , 测量 RTT 的变化也是有价值的 。 [RFC 6298] 定义了 RTT 偏差DevRTT ,用于估算 SampleRTT 一般会偏离 EstimatedRTT 的程度 :
\[DevRTT=(1-\beta)\cdot DevRTT+\beta \cdot |SampleRTT-EstimatedRTT|\]注意到 DevRTT 是一个 SampleRTT 与 EstimatedRTT 之间差值的 EWMA 。 如果 Samp- leRTT 值波动较小 ,那么 DevRTT 的值就会很小 ; 另一方面, 如果波动很大 ,那DevRTT 的值就会很大 。 $\beta$的推荐值为 0. 25 。
- 设置和管理重传超时间隔
假设已经给岀了 EstimatedRTT 值和 DevRTT 值 ,那么 TCP 超时间隔应该用什么值呢? 很明显 ,超时间隔应该大于等于 EstimatedR^IT, 否则 , 将造成不必要的重传。但是超时间隔也不应该比 EstimatedRTT 大太多 , 否则当报文段丢失时 , TCP 不能很快地重传该报文段 , 导致数据传输时延大 。 因此要求将超时间隔设为 EstimatedRTT 加上一定余量 。 当SampleRTT 值波动较大时 ,这个余量应该大些 ; 当波动较小时 ,这个余量应该小些。 因此 , DevRTT 值应该在这里发挥作用了。 在 TCP 的确定重传超时间隔的方法中 , 所有这些因素都考虑到了:
\[TimeoutInterval=EstimatedRTT+4\cdot DevRTT\]推荐的初始 Timeoutinterval 值为 ]秒 [RFC 6298] 0 同时 , 当出现超时后, Timeoutlnterval 值将加倍 , 以免即将被确认的后继报文段过早出现超时 。 然而, 只要收到报文段并更新 EstimatedRTT, 就使用上述公式再次计算 TimeoutInterval。
3.5.4 可靠数据传输
TCP 在 IP 不可靠的尽力而为服务之上创建了一种可靠数据传输服务( reliable data transfer service)。TCP 的可靠数据传输服务确保一个进程从其接收缓存中读出的数据流是无损坏 、无间隙 、 非冗余和按序的数据流 ; 即该字节流与连接的另一方端系统发送出的字节流是完全相同。
我们将以两个递增的步骤来讨论 TCP 是如何提供可靠数据传输的 。 我们先给出一个 TCP发送方的高度简化的描述,该发送方只用超时来恢复报文段的丢失 ; 然后再给岀一个更全面的描述,该描述中除了使用超时机制外 ,还使用冗余确认技术 。 在接下来的讨论中 , 我们假定数据仅向一个方向发送, 即从主机 A 到主机 B, 且主机 A 在发送一个大文件。
图 3-33 给岀了一个 TCP 发送方高度简化的描述 。 我们看到在 TCP 发送方有 3 个与发送和重传有关的主要事件 : 从上层应用程序接收数据 ; 定时器超时和收到 ACK 。 一旦第一个主要事件发生, TCP 从应用程序接收数据 , 将数据封装在一个报文段中 , 并把该报文段交给 IP 。 注意到每一个报文段都包含一个序号 , 如 3.5.2 节所讲的那样 ,这个序号就是该报文段第一个数据字节的字节流编号。 还要注意到如果定时器还没有为某些其他报文段而运行, 则当报文段被传给 IP 时 , TCP 就启动该定时器。 ( 将定时器想象为与最早的未被确认的报文段相关联是有帮助的 。 )该定时器的过期间隔是 Timeoutinterval , 它是由 3.5.3 节中所描述的 EstimatedRTT 和 DevRTT 计算得出的 。
第二个主要事件是超时 。 TCP 通过重传引起超时的报文段来响应超时事件。 然后 TCP重启定时器。
TCP 发送方必须处理的第三个主要事件是 , 到达一个来自接收方的确认报文段 ( ACK)( 更确切地说, 是一个包含了有效 ACK 字段值的报文段 ) 。 当该事件发生时 , TCP 将 ACK 的值 y 与它的变量 SendBase 进行比较 。 TCP 状态变量 SendBase 是最早未被确认的字节的序号。( 因此 SendBase - 1 是指接收方已正确按序接收到的数据的最后一个字节的序号。 ) 如前面指出的那样 , TCP 采用累积确认, 所以 y 确认了字节编号在 y 之前的所有字节都已经收到。 如果 y > SendBase , 则该 ACK 是在确认一个或多个先前未被确认的报文段 。 因此发送方更新它的 SendBase 变量; 如果当前有未被确认的报文段 , TCP 还要重新启动定时器。
- 一些有趣的情况
我们刚刚描述了一个关于 TCP 如何提供可靠数据传输的高度简化的版本 。但即使这种高度简化的版本 , 仍然存在着许多微妙之处 。为了较好地感受该协议的工作过程, 我们来看几种简单情况。 图 3-34 描述了第一种情况 , 主机 A 向主机 B 发送一个报文段 。假设该报文段的序号是 92, 而且包含 8 字节数据 。 在发岀该报文段之后 , 主机 A 等待一个来自主机 B 的确认号为 100 的报文段 。 虽然 A 发出的报文段在主机 B 上被收到 , 但从主机 B发往主机 A 的确认报文丢失了。 在这种情况下 ,超时事件就会发生, 主机 A 会重传相同的报文段 。 当然 ,当主机 B 收到该重传的报文段时 , 它将通过序号发现该报文段包含了早已收到的数据 。 因此 , 主机 B 中的 TCP 将丢弃该重传的报文段中的这些字节 。 在第二种情况中 , 如图 3-35 所示, 主机 A 连续发回了两个报文段 。 第一个报文段序号是 92, 包含 8 字节数据 ;第二个报文段序号是 100, 包含 20 字节数据 。假设两个报文段都完好无损地到达主机 B, 并且主机 B 为每一个报文段分别发送一个确认 。 第一个确认报文的确认号是 100, 第二个确认报文的确认号是 120 。 现在假设在超时之前这两个报文段中没有一个确认报文到达主机 A 。 当超时事件发生时 , 主机 A 重传序号 92 的第一个报文段 , 并重启定时器。只要第二个报文段的 ACK 在新的超时发生以前到达, 则第二个报文段将不会被重传。
在第三种也是最后一种情况中 , 假设主机 A 与在第二种情况中完全一样 , 发送两个报文段 。 第一个报文段的确认报文在网络丢失 , 但在超时事件发生之前主机 A 收到一个确认 号为 120 的确认报文 。主机 A 因而知道主机 B 已经收到了序号为 119 及之前的所有字节; 所以主机 A 不会重传这两个报文段中的任何一个。 这种情况在图 3-36 中进行了图示 。
- 超时间隔加倍
我们现在讨论一下在大多数 TCP 实现中所做的一些修改 。 首先关注的是在定时器时限过期后超时间隔的长度 。 在这种修改中 每当超时事件发生时 , 如前所述, TCP 重传具有最小序号的还未被确认的报文段 。只是每次 TCP 重传时都会将下一次的超时间隔为先前值的两倍 ,而不是用从 EstimatedRTT和 DevRTT 推算出的值 ( 如在 3.5.3 节中所描述的) 。例如 , 假设当定时器第一次过期时 , 与最早的未被确认的报文段相关联的TimeoutInterval 是 0. 75 秒 。TCP 就会重传该报文段 , 并把新的过期时间设置为 1.5 秒 。如果 1.5 秒后定时器又过期了 , 则 TCP 将再次重传该报文段 , 并把过期时间设置为 3.0秒 。 因此 ,超时间隔在每次重传后会呈指数型增长 。 然而, 每当定时器在另两个事件 ( 即收到上层应用的数据和收到 ACK) 中的任意一个启动时 , TimeoutInterval 由最近的 EstimatedRTT 值与 DevRTT 值推算得到。 这种修改提供了一个形式受限的拥塞控制。 定时器过期很可能是由网络拥塞引起的, 即太多的分组到达源与目的地之间路径上的一台 ( 或多台 )路由器的队列中 ,造成分组丢失或长时间的排队时延 。 在拥塞的时候 , 如果源持续重传分组, 会使拥塞更加严重 。 相反 , TCP 使用更文雅的方式 , 每个发送方的重传都是经过越来越长的时间间隔后进行的 。
- 快速重传
超时触发重传存在的问题之一是超时周期可能相对较长 。 当一个报文段丢失时,这种长超时周期迫使发送方延迟重传丢失的分组, 因而增加了端到端时延 。 幸运的是 , 发送方通常可在超时事件发生之前通过注意所谓冗余 ACK 来较好地检测到丢包情况。冗余 ACK (duplicate ACK) 就是再次确认某个报文段的 ACK , 而发送方先前已经收到对该报文段的确认 。 要理解发送方对冗余 ACK 的响应 , 我们必须首先看一下接收方为什么会发送冗余 ACK 。 表 3-2 总结了 TCP 接收方的 ACK 生成策略 [RFC5681 ]。 当 TCP 接收方收到一个具有这样序号的报文段时 , 即其序号大于下一个所期望的 、 按序的报文段 , 它检测到了数据流中的一个间隔,这就是说有报文段丢失 。 这个间隔可能是由于在网络中报文段丢失或重新排序造成的 。 因为 TCP 不使用否定确认, 所以接收方不能向发送方发回一个显式的否定确认 。 相反 , 它只是对已经接收到的最后一个按序字节数据进行重复确认( 即产生一个冗余 ACK) 即可。( 注意到在表 3-2 中允许接收方不丢弃失序报文段 。 )
因为发送方经常一个接一个地发送大量的报文段 , 如果一个报文段丢失 , 就很可能引起许多一个接一个的冗余 ACK 。 如果 TCP 发送方接收到对相同数据的 3 个冗余 ACK, 它把这当作一种指示,说明跟在这个已被确认过 3 次的报文段之后的报文段已经丢失 。 ( 在课后习题中 , 我们将考虑为什么发送方等待 3 个冗余 ACK, 而不是仅仅等待一个冗余ACK 。) 一旦收到 3 个冗余 ACK, TCP 就执行快速重传 ( fast retransmit), 即在该报文段的定时器过期之前重传丢失的报文段 。 对于采用快速重传的 TCP, 可用下列代码片段代替图 3-33 中的 ACK 收到事件 :
事件 : 收到 ACK, 具有 ACK 字段值 y
if (y > SendBase) {
SendBase=y
if (当前仍无任何应答报文段)
启动定时器
}
else {/ 快对已经确认的报文段的一个冗余 ACK */
对 y 收到的冗余 AC K 数加 1
if (对 y==3 收到的冗余 ACK 数)
/ *TCP 快速重传 */
重新发送具有序号 y 的报文段
}
break;
- 是回退 N 步还是选择重传
考虑下面这个问题来结束有关 TCP 差错恢复机制的学习 : TCP 是一个 GBN 协议还是一个 SR 协议? 前面讲过, TCP 确认是累积式的, 正确接收但失序的报文段是不会被接收方逐个确认的 。 因此 , 如图 3-33 所示( 也可参见图 3-19), TCP 发送方仅需维持已发送过但未被确认的字节的最小序号 ( SendBase ) 和下一个要发送的字节的序号(NextSeqNum)。在这种意义下 , TCP 看起来更像一个 GBN 风格的协议 。但是 TCP 和 GBN协议之间有着一些显著的区别。 许多 TCP 实现会将正确接收但失序的报文段缓存起来。另外考虑一下 , 当发送方发送的一组报文段 1, 2,… , N,并且所有的报文段都按序无差错地到达接收方时会发生的情况。 进一步假设对分组 n<N 的确认报文丢失 , 但是其余 N-1 个确认报文在分别超时以前到达发送端,这时又会发生的情况。 在该例中 , GBN 不仅会重传分组n,还会重传所有后继的分组 n + 1, n+2, … , N 。在另一方面, TCP 将重传至多一个报文段 , 即报文段几。 此外 , 如果对报文段 n+1 的确认报文在报文段 n 超时之前到达,TCP 甚至不会重传报文段 n。
对 TCP 提岀的一种修改意见是所谓的选择确认 ( selective acknowledgment ), 它允许 TCP 接收方有选择地确认失序报文段 ,而不是累积地确认最后一个正确接收的有序报文段 。 当将该机制与选择重传机制结合起来使用时(即跳过重传那些已被接收方选择性地确认过的报文段) , TCP 看起来就很像我们通常的 SR 协议 。 因此 , TCP 的差错恢复机制也许最好被分类为 GBN 协议与 SR 协议的混合体。
3.5.5 流量控制
前面讲过, 一条 TCP 连接的每一侧主机都为该连接设置了接收缓存 。 当该 TCP连接收到正确 、 按序的字节后 , 它就将数据放入接收缓存 。 相关联的应用进程会从该缓存中读取数据 , 但不必是数据刚一到达就立即读取。事实上 , 接收方应用也许正忙于其他任务 ,甚至要过很长时间后才去读取该数据 。 如果某应用程序读取数据时相对缓慢 ,而发送方发送得太多 、 太快 , 发送的数据就会很容易地使该连接的接收缓存溢出。
TCP 为它的应用程序提供了流量控制服务( flow control service ) 以消除发送方使接收方缓存溢岀的可能性 。 流量控制因此是一个速度匹配服务, 即发送方的发送速率与接收方应用程序的读取速率相匹配 。前面提到过, TCP 发送方也可能因为 IP 网络的拥塞而被遏制 ;这种形式的发送方的控制被称为拥塞控制( congestion control ),我们将在 3.6 节和3.7 节详细地讨论这个主题 。即使流量控制和拥塞控制采取的动作非常相似(对发送方的遏制),但是它们显然是针对完全不同的原因而采取的措施 。 现在我们来讨论 TCP 如何提供流量控制服务的 。为了能从整体上看问题, 我们在本节都假设 TCP 是这样实现的, 即 TCP接收方丢弃失序的报文段 。
TCP 通过让发送方维护一个称为接收窗口 ( receive window ) 的变量来提供流量控制。通俗地说, 接收窗口用于给发送方一个指示一 一 该接收方还有多少可用的缓存空间 。 因为TCP 是全双工通信 , 在连接两端的发送方都各自维护一个接收窗口。 我们在文件传输的情况下研究接收窗口。假设主机 A 通过一条 TCP 连接向主机 B 发送一个大文件。主机 B 为该连接分配了一个接收缓存 , 并用 RcvBuffer
来表示其大小 。主机 B 上的应用进程不时地从该缓存中读取数据 。 我们定义以下变量:
LastByteRead
:主机 B 上的应用进程从缓存读出的数据流的最后一个字节的编号。LastByteRcvd
:从网络中到达的并且已放入主机 B 接收缓存中的数据流的最后一个字节的编号。
由于 TCP 不允许已分配的缓存溢岀 , 下式必须成立:
\[LastByteRcvd-LastByteRead \le RcvBuffer\]接收窗口用 rwnd 表示, 根据缓存可用空间的数量来设置:
\[rwnd=RcvBuffer-[LastByteRcvd-LastByteRead]\]由于该空间是随着时间变化的, 所以 rwnd
是动态的 。 图 3-38 对变量 rwnd
进行了图示。
连接是如何使用变量 rwnd
来提供流量控制服务的呢 ? 主机 B 通过把当前的rwnd
值放入它发给主机 A 的报文段接收窗口字段中 ,通知主机 A 它在该连接的存中还有多少可用空间 。 开始时 , 主机 B设定 rwnd = RcvBuffer
。 注意到为了实现这一点 , 主机 B 必须跟踪几个与连接有关的变量。
主机 A 轮流跟踪两个变量, LastByteSent
和 LastByteAcked
,这两个变量的意义很明显 。注意到这两个变量之间的差 LastByteSent - LastByteAcked
, 就是主机 A 发送到连接中但未被确认的数据量 。 通过将未确认的数据量控制在值 rwnd
以内 , 就可以保证主机 A 不使主机 B 的接收缓存溢出。 因此 , 主机 A 在该连接的整个生命周期须保证:
对于这个方案还存在一个小小的技术问题 。为了理解这一点 , 假设主机 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 将在一个有限大小的缓存中加上报文段 ,该缓存在相应套接字 (进程的门户 ) “之前”。 进程每次从缓存中读取一个完整的报文段 。 如果进程从缓存中读取报文段的速度不够快 ,那么缓存将会溢出 , 并且将丢失报文段 。
3.5.6 TCP连接管理
在本小节中 , 我们更为仔细地观察如何建立和拆除一条 TCP 连接 。 尽管这个主题并不特别令人兴奋 , 但是它很重要, 因为 TCP 连接的建立会显著地增加人们感受到的时延( 如在 Web 上冲浪时 ) 。 此外 ,许多常见的网络攻击 ( 包括极为流行的 SYN 洪泛攻击 ) 利用了 TCP 连接管理中的弱点 。 现在我们观察一下一条 TCP 连接是如何建立的 。假设运行在一台主机 ( 客户 ) 上的一个进程想与另一台主机 ( 服务器 ) 上的一个进程建立一条连接 。 客户应用进程首先通知客户 TCP,它想建立一个与服务器上某个进程之间的连接 。 客户中的 TCP 会用以下方式与服务器中的 TCP 建立一条 TCP 连接:
- 第一步:客户端的 TCP 首先向服务器端的 TCP 发送一个特殊的 TCP 报文段 。 该报文段中不包含应用层数据 。但是在报文段的首部( 参见图 3-29) 中的一个标志位( 即 SYN 比特)被置为 1 。 因此 ,这个特殊报文段被称为 SYN 报文段 。另外 , 客 户会随机地选择一个初始序号 ( client_isn ) ,,并将此编号放置于该起始的 TCP SYN报文段的序号字段中。 该报文段会被封装在一个 IP 数据报中 , 并发送给服务器。
- 第二步:一旦包含 TCP SYN 报文段的 IP 数据报到达服务器主机 , 服务器会从该数据报中提取出 TCP SYN 报文段 , 为该 TCP 连接分配 TCP 缓存和变量, 并向该客户 TCP 发送允许连接的报文段 。 这个允许连接的报文段也不包含应用层数据 。但是 ,在报文段的首部却包含 3 个重要的信息 。 首先 , SYN 比特被置为 1 。其次 ,该 TCP 报文段首部的确认号字段被置为 client_isn + 1。 最后 , 服务器选择自己的初始序号( server_isn ) ,并将其放置到 TCP 报文段首部的序号字段中。 这个允许连接的报文段实际上表明了 : “ 我收到了你发起建立连接的 SYN 分组,该分组带有初始序号client_isn 我同意建立该连接 。 我自己的初始序号是 server_isn 该允许连接的报文段被称为 SYNACK 报文段 ( SYNACK segment) 。
- 第三步:在收到 SYNACK 报文段后 , 客户也要给该连接分配缓存和变量 。 客户主机则向服务器发送另外一个报文段 ;这最后一个报文段对服务器的允许连接的报文段进行了确认(该客户通过将值 server_isn + 1 放置到 TCP 报文段首部的确认字段中来完成此项工作 ) 。因为连接已经建立了 , 所以该 SYN 比特被置为 0 。 该三次握手的第三个阶段可以在报文段负载中携带客户到服务器的数据 。
一旦完成这 3 个步骤, 客户和服务器主机就可以相互发送包括数据的报文段了。 在以后每一个报文段中 , SYN 比特都将被置为 0 。 注意到为了创建该连接 , 在两台主机之间发送了 3 个分组, 如图 3- 39 所示。 由于这个原因 ,这种连接创建过程通常被称为 3次握手 ( three-way handshake) 。 注意到这样一件事是很有趣的, 一个攀岩者和一个保护者( 他位于攀岩者的下面, 他的任务是处理好攀岩者的安全绳索) 就使用了与 TCP 相同的 3 次握手通信协议, 以确保在攀岩者开始攀爬前双方都已经准备好了。
当连接结束后,服务器主机中的 “ 资源 ” ( 即缓存和变量) 将被释放 。举一个例子 , 假设某客户打算关闭连接 , 如图 3-40 所示 。 客户应用进程发出一个关闭连接命令。 这会引起客户 TCP 向服务器进程发送一个特殊的 TCP 报文段 。这个特殊的报文段让其首部中的一个标志位即 FIN 比特( 参见图 3-29) 被设置为1。当服务器接收到该报文段后 , 就向发送方回送一个确认报文段 。 然后 , 服务器发送它自己的终止报文段 , 其 FIN 比特被置为 1 。 最后 , 该客户对这个服务器的终止报文段进行确认 。 此时 , 在两台主机上用于该连接的所有资源都被释放了。
在一个 TCP 连接的生命周期内 ,运行在每台主机中的 TCP 协议在各种 TCP 状态 ( TCP state) 之间变迁 。 图 3-41 说明了客户 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 分钟 。 经过等待后 ,连接就正式关闭, 客户端所有资源 ( 包括端口号 ) 将被释放 。
图 3-42 图示了服务器端的 TCP 通常要经历的一系列状态 , 其中假设客户开始连接拆除 。 这些状态变迁是自解释的 。 在这两个状态变迁图中 , 我们只给出了 TCP 连接是如何正常地被建立和拆除的 。 我们没有描述在某些不正常的情况下 ( 例如当连接的双方同时都要发起或终止一条连接时 ) 发生的事情 。
我们上面的讨论假定了客户和服务器都准备通信 , 即服务器正在监听客户发送其 SYN报文段的端口。 我们来考虑当一台主机接收到一个 TCP 报文段 , 其端口号或源 IP 地址与该主机上进行中的套接字都不匹配的情况。例如 , 假如一台主机接收了具有目的端口 80的一个 TCP SYN 分组, 但该主机在端口 80 不接受连接 ( 即它不在端口 80 上运行 Web 服务器 ) 。则该主机将向源发送一个特殊重置报文段 。 该 TCP 报文段将 RST 标志位 ( 参见3.5.2 节)置为 1 。 因此 , 当主机发送一个重置报文段时 , 它告诉该源 “ 我没有那个报文段的套接字 。 请不要再发送该报文段了”。 当一台主机接收一个 UDP 分组, 它的目的端口与进行中的 UDP 套接字不匹配,该主机发送一个特殊的 ICMP 数据报 ,这将在第 4 章中讨论 。
3.6 拥塞控制原理
在前面几节中 , 我们已经分析了面临分组丢失时用于提供可靠数据传输服务的基本原理及特定的 TCP 机制。 我们以前讲过, 在实践中 ,这种丢包一般是当网络变得拥塞时由于路由器缓存溢岀引起的 。分组重传因此作为网络拥塞的征兆 ( 某个特定的运输层报文段的丢失 ) 来对待 , 但是却无法处理导致网络拥塞的原因 , 因为有太多的源想以过高的速率发送数据 。为了处理网络拥塞原因 ,需要一些机制以在面临网络拥塞时遏制发送方 。
在本节中 , 我们考虑一般情况下的拥塞控制问题,试图理解为什么网络拥塞是一件坏事情 ,网络拥塞是如何在上层应用得到的服务性能中明确地显露出来的? 如何可用各种方法来避免网络拥塞或对它做出反应 ?下面一节详细研究 TCP 的拥塞控制算法 。
3.6.1 拥塞原因与代价
我们通过分析 3 个复杂性越来越高的发生拥塞的情况 , 开始对拥塞控制的一般性研究 。 在每种情况下 , 我们首先将看看出现拥塞的原因以及拥塞的代价 ( 根据资源未被充分利用以及端系统得到的低劣服务性能来评价 ) 。 我们暂不关注如何对拥塞做出反应或避免拥塞 ,而是重点理解一个较为简单的问题, 即随着主机增加其发送速率并使网络变得拥这时会发生的情况。
- 情况1:两个发送方和一台具有无穷大缓存的路由器
我们先考虑也许是最简单的拥塞情况 : 两台主机 ( A 和 B) 都有一条连接 , 且这两条连接共享源与目的地之间的单跳路由, 如图 3-43 所示 。
我们假设主机 A 中的应用程序以$\lambda _{in}$ 字节 / 秒的平均速率将数据发送到连接中 ( 例如 ,通过一个套接字将数据传递给运输层协议) 。 这些数据是初始数据 ,这意味着每个数据单元仅向套接字中发送一次 。下面的运输层协议是一个简单的协议 。 数据被封装并发送;不 执行差错恢复 ( 如重传 ) 、 流量控制或拥塞控制。 忽略由于添加运输层和较低层首部信息产生的额外开销, 在第一种情况下 , 主机 A 向路由器提供流量的速率是$\lambda _{in}$ 字节 / 秒 。主机B 也以同样的方式运行, 为了简化问题, 我们假设它也是以速率$\lambda _{in}$ 字节 / 秒发送数据 。 来自主机 A 和主机 B 的分组通过一台路由器 , 在一段容量为 R 的共享式输出链路上传输 。该路由器带有缓存 , 可用于当分组到达速率超过该输出链路的容量时存储“入分组 ”。 在此第一种情况下 , 我们将假设路由器有无限大的缓存空间 。
图 3-44 描绘出了第一种情况下主机 A 的连接性能 。 左边的图形描绘了每连接的吞吐量 ( per-connection throughput) ( 接收方每秒接收的字节数 ) 与该连接发送速率之间的函数关系 。 当发送速率在 0 〜 R/2 之间时 , 接收方的吞吐量等于发送方的发送速率, 即发送方发送的所有数据经有限时延后到达接收方 。 然而当发送速率超过 R/2 时 , 它的吞吐量只能达R/2 。 这个吞吐量上限是由两条连接之间共享链路容量造成的 。 链路完全不能以超过R/2 的稳定状态速率向接收方交付分组 。 无论主机 A 和主机 B 将其发送速率设置为多高,它们都不会看到超过 R/2 的吞吐量 。
取得每连接 R/2 的吞吐量实际上看起来可能是件好事 , 因为在将分组交付到目的地的过程中链路被充分利用了。但是 , 图 3-44b 的图形却显示了以接近链路容量的速率运行时产生的后果 。 当发送速率接近 R/2 时 ( 从左至右 ), 平均时延就会越来越大 。 当发送速率超过 R/2 时 ,路由器中的平均排队分组数就会无限增长, 源与目的地之间的平均时延也会变成无穷大 ( 假设这些连接以此发送速率运行无限长时间并且有无限量的缓存可用) 。 因此 ,虽然从吞吐量角度看,运行在总吞吐量接近 R 的状态也许是一个理想状态 , 但从时延角度看, 却远不是一个理想状态 。 甚至在这种( 极端)理想化的情况中 , 我们已经发现了拥塞网络的一种代价 , 即当分组的到达速率接近链路容量时 , 分组经历巨大的排队时延 。
- 情况 2 : 两个发送方和一台具有有限缓存的路由器
现在我们从下列两个方面对情况 1 稍微做一些修改 ( 参见图 3-45 )。首先 , 假定路由器缓存的容量是有限的 。 这种现实世界的假设的结果是 , 当分组到达一个已满的缓存时会 被丢弃 。其次 , 我们假定每条连接都是可靠的 。 如果一个包含有运输层报文段的分组在路由器中被丢弃 ,那么它终将被发送方重传。 由于分组可以被重传 , 所以我们现在必须更小心地使用发送速率这个术语 。 特別是我们再次以$\lambda_{in}$字节 / 秒表示应用程序将初始数据发送到套接字中的速率 。 运输层向网络中发送报文段 ( 含有初始数据或重传数据 )的速率用$\lambda_{in}^{‘}$ 字节 / 秒表示 。$\lambda_{in}^{‘}$ 有时被称为网络的供给载荷( offered load ) 。
在情况 2 下实现的性能强烈地依赖于重传的方式 。 首先 ,考虑一种不真实的情况 , 即主机 A 能够以某种方式 ( 不可思议地 !)确定路由器中的缓存是否空闲, 因而仅当缓存空闲时才发送一个分组 。 在这种情况下, 将不会产生丢包 , $\lambda_{in}$ 与$\lambda_{in}^{‘}$相等, 并且连接的吞吐量就等于$\lambda_{in}$ 。 图 3-46a 中描述了这种情况。从吞吐量的角度看, 性能是理想的, 即发送的每个分组都被接收到。 注意到在这种情况下 , 平均主机发送速率不能超过R/2, 因为假定不会发生分组丢失 。
接下来考虑一种更为真实的情况 , 发送方仅当在确定了一个分组已经丢失时才重传。( 同样 , 所做的假设有一些弹性 。 然而, 发送主机有可能将超时时间设置得足够长, 以无 形中使其确信一个还没有被确认的分组已经丢失 。) 在这种情况下 , 性能就可能与图 3-46b所示的情况相似。为了理解这时发生的情况 ,考虑一下供给载荷入幕 ( 初始数据传输加上重传的总速率)等于R/2 的情况。 根据图 3-46b, 在这一供给载荷值时 , 数据被交付给接收方应用程序的速率是R/3 。 因此 , 在所发送的 0.5R 单位数据当中 , 从平均的角度说,0.333R字节/ 秒是初始数据,而 0. 1667R字节/秒是重传数据 。 我们在此看到了另一种网络拥塞的代价 , 即发送方必须执行重传以补偿因为缓存溢出而丢弃 ( 丢失 )的分组 。
最后 , 我们考虑下面一种情况 : 发送方也许会提前发生超时并重传在队列中已被推迟但还未丢失的分组 。 在这种情况下 , 初始数据分组和重传分组都可能到达接收方 。 当然,接收方只需要一份这样的分组副本就行了 ,重传分组将被丢弃 。 在这种情况下 ,路由器转发重传的初始分组副本是在做无用功 , 因为接收方已收到了该分组的初始版本 。 而路由器本可以利用链路的传输能力去发送另一个分组 。 这里, 我们又看到了网络拥塞的另一种代价 , 即发送方在遇到大时延时所进行的不必要重传会引起路由器利用其链路带宽来转发不必要的分组副本 。 图 3-46c 显示了当假定每个分组被路由器转发 ( 平均 ) 两次时 , 吞吐量与供给载荷的对比情况。 由于每个分组被转发两次 , 当其供给载荷接近 R/2 时 , 其吞吐量将渐近 R/4 。
- 情况 3 : 4 个发送方和具有有限缓存的多台路由器及多跳路径
在最后一种拥塞情况中 , 有 4 台主机发送分组, 每台都通过交叠的两跳路径传输, 如图 3-47 所示 。 我们再次假设每台主机都采用超时 / 重传机制来实现可靠数据传输服务 , 所有的主机都有相同的爲值 , 所有路由器的链路容量都是R字节/秒 。
我们考虑从主机 A 到主机 C 的连接 ,该连接经过路由器 R1 和 R2 。 A-C 连接与 D-B 连接共享路由器 R1,并与 B-D 连接共享路由器 R2 。 对极小的$\lambda_{in}$值 ,路由器缓存的溢出是很少见的( 与拥塞情况 1 、 拥塞情况 2 中的一样 ), 吞吐量大致接近供给载荷 。 对稍大的$\lambda_{in}$值 , 对应的吞吐量也更大 , 因为有更多的初始数据被发送到网络中并交付到目的地 , 溢出仍然很少 。 因此 , 对于较小的$\lambda_{in}$,$\lambda_{in}$的增大会导致$\lambda_{out}$的增大 。
在考虑了流量很小的情况后 , 下面分析当$\lambda_{in}$( 因此$\lambda_{in}^{‘}$)很大时的情况。 考虑路由器R2 。 不管$\lambda_{in}$ 的值是多大 , 到达路由器 R2 的 A-C 流量( 在经过路由器 R1 转发后到达路由器 R2) 的到达速率至多是R , 也就是从 R1 到 R2 的链路容量 。 如果$\lambda_{in}^{‘}$对于所有连接 ( 包括B-D 连接 ) 来说是极大的值 ,那么在 R2 上 , B-D 流量的到达速率可能会比 A-C流量的到达速率大得多 。 因为 A-C 流量与 B-D 流量在路由器 R2 上必须为有限缓存空间而竞争 , 所以当来自 B-D 连接的供给载荷越来越大时 , A-C 连接上成功通过 R2 ( 即由于缓存溢出而未被丢失 )的流量会越来越小 。 在极限情况下 , 当供给载荷趋近于无穷大时 , R2 的空闲缓存会立即被 B-D 连接的分组占满 , 因而 A-C连接在 R2 上的吞吐量趋近于 0 。 这又一次说明在重载的极限情况下 , A-C 端到端呑吐量将趋近于 0 。 这些考虑引发了供给载荷与吞吐量之间的权衡, 如图 3-48 所示 。
当考虑由网络所做的浪费掉的工作量时,随着供给载荷的增加而使吞吐量最终减少的原 因是明显的 。 在上面提到的大流量的情况中,每当有一个分组在第二跳路由器上被丢弃时 ,第一跳路由器所做的将分组转发到第二跳路由器的工作就是 “劳而无功” 的 。 如果第一跳路由器只是丢弃该分组并保持空闲, 则网络中的情况是幸运的( 更准确地说是糟糕的) 。 需要指岀的是 ,第一跳路由器所使用的将分组转发到第二跳路由器的传输容量用来传送不同的分组可能更有效益 。 ( 例如 , 当选择一个分组发送时 ,路由器最好优先考虑那些已经历过一定数量的上游路由器的分组 。 ) 所以 , 我们在此又看到了由于拥塞而丢弃分组的另一种代价,即当一个分组沿一条路径被丢弃时 , 每个上游路由器用于转发该分组到丢弃该分组而使用的传输容量最终被浪费掉了 。
3.6.2 拥塞控制方法
在最为宽泛的级别上 , 我们可根据网络层是否为运输层拥塞控制提供了显式帮助 , 来区分拥塞控制方法 。
- 端到端拥寒控制。在端到端拥塞控制方法中 ,网络层没有为运输层拥塞控制提供显式支持 。即使网络中存在拥塞 ,端系统也必须通过对网络行为的观察 ( 如分组丢失与时延 ) 来推断之。 我们将在 3.7.1 节中将看到 , TCP 采用端到端的方法解决拥塞控制 , 因为 IP 层不会向端系统提供有关网络拥塞的反馈信息 。 TCP 报文段的丢失 (通过超时或 3 次冗余确认而得知)被认为是网络拥塞的一个迹象, TCP会相应地减小其窗口长度 。 我们还将看到关于 TCP 拥塞控制的一些最新建议, 即使用增加的往返时延值作为网络拥塞程度增加的指示 。
- 网络辅助的拥塞控制。在网络辅助的拥塞控制中 ,路由器向发送方提供关于网络 中拥塞状态的显式反馈信息 。 这种反馈可以简单地用一个比特来指示链路中的拥 塞情况。 更复杂的网络反馈也是可能的 。例如 , 在 ATM 可用比特率 ( Available Bite Rate , ABR) 拥塞控制中 ,路由器显式地通知发送方它 (路由器 )能在输出链路上支持的最大主机发送速率 。 如上面所提到的,默认因特网版本的 IP 和 TCP 采用端到端拥塞控制方法 。 然而, 我们在 3.7.2 节中将看到 , 最近 IP 和 TCP 也能够选择性地实现网络辅助拥塞控制。
对于网络辅助的拥塞控制 , 拥塞信息从网络反馈到发送方通常有两种方式 , 如图 3-49所示 。 直接反馈信息可以由网络路由器发给发送方 。 这种方式的通知通常采用了一种阻塞分组 ( choke packet ) 的形式 ( 主要是说: “ 我拥塞了 ! ” ) 。 更为通用的第二种形式的通知是 ,路由器标记或更新从发送方流向接收方的分组中的某个字段来指示拥塞的产生口 一旦收到一个标记的分组后 , 接收方就会向发送方通知该网络拥塞指示 。 注意到后一种形式的通知至少要经过一个完整的往返时间 。
3.7 TCP 拥塞控制
详情请阅读书P176。