运输层位于应用层和网络层之间,是分层的网络体系结构的重要部分。该层为运行在不同主机上的应用进程提供直接的通信服务 。通常特别关注因特网协议,即TCP 和 UDP 运输层协议。
讨论运输层和网络层的关析,为研究运输层第一个关键功能:将网络层的在两个端系统之间的交付任务扩展到运行在两个不同端系统上的应用层进程之间的交付任务。 讨论因特网的无连接运输协议 UDP 时阐述这个功能。
两个实体怎样才能在一种会丢失或损坏数据的媒体上可靠的通信。通过一系列复杂性不断增加的场景,建立起一套被运输协议用来解决的技术,体现在因特网面向连接的运输协议 TCP 中。
讨论第二个基础性问题:控制运输层实体的传输速率以避免网络中的拥塞 ,或从拥塞中恢复过来。将考虑拥塞的原因和后果,以及常用的拥塞技术。将研究 TCP 应对拥塞控制的方法。
3.1 概述运输层服务
运输层协议为运行在不同主机上的应用进程 之间提供了逻辑通信 ( logic communication) 功能。从应用程序的角度看,通过逻辑通信,运行不同进程的主机好像直接相连一样 ;实际上,这些主机也许位于地球的两侧,通过很多路由器及多种不同类型的链路相连。应用进程使用运输层提供的逻辑通信功能彼此发送报文,而无须考虑承载这些报文的物理基础设施的细节。图3-1 图示了逻辑通信的概念。
如图 3-1 所示,运输层协议是在端系统 中而不是在路由器中实现的。在发送端,运输层将从发送应用程序进程接收到的报文转换成运输层分组 ,用因特网术语来讲该分组称为运输层报文段 (segment)。实现的方法(可能)是将应用报文划分为较小的块 ,并为每块加上一个运输层首部 以生成运输层报文段。然后,在发送端系统中,运输层将这些报文段传递给网络层 ,网路层将其封装成网络层分组(即数据报)并向目的地发送 。注意到下列事实是重要的:网络路由器仅作用于该数据报的网络层字段;即它们不检查封装在该数据报的运输层报文段的字段。在接收端 ,网络层从数据报中提取运输层报文段 ,并将该报文段向上交给运输层 。运输层则处理接收到的报文段,使该报文段中的数据为接收应用进程使用。
网络应用程序可以使用多种的运输层协议。例如,因特网有两种协议,即 TCP 和 UDP。每种协议都能为调用的应用程序提供一组不同的运输层服务。

3.1.1 运输层和网络层的关系
在协议栈中,运输层位于网络层之上。网络层提供了主机之间的逻辑通信 ,而传输层为运行在不同主机上的进程提供了逻辑通信。
考虑两个家庭,分别位于东海岸和西海岸,每家有12个孩子。这两家的孩子是堂兄弟姐妹,喜欢彼此通信,没人每星期要互相写一封信,用单独的信封通过传统的邮政服务传送。因此,每个家庭向另一家发送 144 封信。每个家庭有个孩子负责收发邮件,分别是Ann 和 Bill。通过邮政服务的邮车将信件传输。
在这个例子中,邮政服务为两个家庭间提供逻辑通信 ,邮政服务将信件从一家送往另一家 (而不是一个人送往另一个人)。另一方面,Ann 和 Bill 为堂兄弟姐妹之间提供了逻辑通信,Ann 和 Bill 从兄弟姐妹那里收取信件或到兄弟姐妹那里交付信件。从兄弟姐妹的角度,Ann 和 Bill 就是邮件服务,尽管它们只是端到端交付过程的一部分(即端系统),解释运输层和网络层之间的关系

Ann 和 Bill 都是在各自家中进行工作的;例如,它们并没有参与任何一个中间邮件中心对邮件进行分拣,或者将邮件从一个邮件中心送到另一个邮件中心之类的工作。类似的,运输层协议只工作在端系统 。在端系统中,运输层协议将来自应用进程的报文移动到网络边缘 (网络层 ),反过来一样,但对有关这些报文在网络核心如何移动并不做任何规定。事实上,如图3-1所示,中间路由器既不处理也不识别运输层加在应用层报文的任何信息。
此外,计算机网络中可以安排多种运输层协议,每种协议为应用程序提供不同的服务模型。 并且,**运输协议能够提供的服务常常受制于底层网络层协议的服务模型。**如果网络层协议无法为主机之间发送的运输层报文段提供时延或带宽保证的话,运输层协议也就无法为进程之间发送的应用程序报文提供时延或带宽保证。
然而,即使底层网络协议不能在网络层提供相应的服务 ,运输层协议也能提供某些服务。例如,即使底层网络协议是不可靠的,也就是说网络层协议会使分组丢失、篡改和冗余,运输协议也能为应用程序提供可靠的数据传输服务 。另一个例子是,即使网络层不能保证运输层报文段的机密性,运输协议也能使用加密来确保应用程序报文不被入侵者读取。
3.1.2 因特网运输层概述
因特网为应用层提供了两种截然不同的可用运输层协议。一种是UDP(用户数据报协议) ,为调用它的应用程序提供了一种不可靠、无连接的服务 。另一种是TCP(传输控制协议) ,为调用它的应用程序提供了一种可靠的、面向连接的服务。
统一将TCP 和 UDP 的分组通常为报文段 ,而将数据报 名称保留给网络层分组。
因特网的网络层:
因特网网络层协议 有一个名字---IP ,即网际协议 。IP 为主机之间提供了逻辑通 信。IP 的服务模型是尽力而为交付服务 。意味着 IP 尽最大努力在通信的主机之间交付报文段,但不做任何确保。它不确保报文段的交付,不保证报文段的按序交付,不保证报文段中数据的完整性。由于这些原因,IP 被称为 不可靠服务。还要指出,每台主机至少有一个网络层地址,即 IP 地址。
UDP 和 TCP 的服务模型:
基本责任:将两个端系统间 IP 的交付服务扩展为运行在端系统上的两个进程之间的交付任务。将主机间交付扩展到进程间交付 被称为运输层的多路复用 与 多路分解。 UDP 和 TCP 还可以通过在其报文段首部中包括差错检查字段而提供完整性检查。进程到进程的数据交付 和差错检查是两种最低限度的运输层服务,也是UDP 所能提供的仅有的两种服务。特别是,与 IP 一样, UDP 也是一种不可靠的服务,即不能保证一个进程所发送的数据能够完整无缺地到达目的进程。
另一方面,TCP 为应用程序提供了几种附加服务。首先,提供可靠数据传输 。通过使用流量控制、序号、确认和计时器,TCP 确保正确地、按序地将数据从发送进程交付给接收进程。这样,TCP 就将两个端系统间的不可靠 IP 服务转换成了一种进程间的可靠数据传输服务 。TCP 还提供拥塞控制 。防止任何一条 TCP 连接用过多流量来淹没通信主机之间的链路和交换设备。TCP 力求为每个通过一条拥塞网络链路的连接平等的共享网络链路带宽 。这可以通过调节 TCP 连接的发送端发送进网络的流量速率做到。在另一方面,UDP 流量是不可调节的。使用 UDP 传输的应用程序可以根据其需要以其愿意的任何速率发送数据。
3.2 多路复用与多路分解
运输层的多路复用 与多路分解 ,将由网络层提供的主机到主机交付服务延伸到为运行在主机上的应用程序提供进程到进程的交付服务。将在因特网环境中讨论这种基本的运输层服务。
在目的主机,运输层 从紧邻其下的网络层接收报文段 。运输层负责将这些报文段中的数据交付给在主机上运行的适当应用程序进程 。来看一个例子,假定你正坐在计算机前下载 Web 页面,同时还在运行一个 FTP 会话和两个Telnet 会话。这样你就有4 个网络应用进程 在运行,即两个Telnet 进程,一个 FTP 进程和一个 HTTP 进程。当你的计算机中的运输层从底层的网络层接收数据时,它需要将所接收到的数据定向到这 4 个进程中的一个。现在我们来研究这是怎样完成的。
首先回想 2.7 节的内容,一个进程 (作为网络应用的一部分)有一个或多个套接字
(socket),它相当于从网络向进程传递数据 和从进程向网络传递数据 的门户。因此,如图3-2 示,在接收主机中的运输层 实际上并没有直接将数据交付给进程 , 而是将数据交给了一个中间的套接字。 由于在任一时刻,在接收主机上可能有不止一个套接字 ,所以每个套接字都有唯一的标识符。标识符的格式取决于它是 UDP 还是 TCP 套接字。

接收主机 怎样将一个到达的运输层报文段 定向到适当的套接字 。为此目的,每个运输层报文段 中具有几个字段 。在接收端,运输层检查这些字段,标识出接收套接字 ,进而将报文段定向到该套接字。将运输层报文段中的数据交付到正确的套接字 的工作称为多路分解 (demulti plexing) 。在源主机从不同套接字中收集数据块 ,并为每个数据块封装上首部信息 (这将在以后用于分解)从而生成报文段,然后将报文段传递到网络层 ,所有这些工作称为多路复用 (multiplexing)。值得注意的是,图 3-2 中的中间那台主机的运输层必须将从其下的网络层收到的报文段分解后交给其上的 P1 或 P2 进程 ;这一过程是通过将到达的报文段数据定向到对应进程的套接字来完成的。中间主机中的运输层也必须收集从这些套接字输出的数据,形成运输层报文段, 然后将其向下传递给网络层。尽管在因特网运输层协议的环境下引入了多路复用和多路分解;它们在与某层(运输层或别处)的单一协议何时被位于接下来的高层的多个协议有关。
运输层多路复用要求:① 套接字有唯一标识符 ;② 每个报文段有特殊字段来指示该报文段所要交付到的套接字 。如图 3-3 所示,这些特殊字段是源端口号字段 和目的端口号 字段。端口号是一个 16 比特的数,其大小在 0~65535 之间。0~1023 范围的端口号称为周知端口号,是受限制的,这是指它们保留给诸如 HTTP(端口号 80)和 FTP(端口号21)之类的周知应用层协议来使用。当开发一个新的应用程序时,必须为其分配一个端口号。

运输层实现分解服务:在主机上的每个套接字能够分配一个端口号 ,当报文段到达主机时,运输层检查报文段中的目的端口号,并将其定向到相应的套接字 。然后报文段中的数据通过套接字进入其所连接的进程。正如看到的那样,UDP 大体上是这样做的。
1. 无连接的多路复用与多路分解
在主机上运行的 Python 程序使用下面一行代码创建了一个 UDP 套接字:
clientSocket = socket(AF_INET , SOCK_DGRAM)
当用这种方式创建一个 UDP 套接字时,运输层自动地为该套接字分配一个端口号 。特别是,运输层从范围 1024~65535 内分配一个端口号,该端口号是当前未被该主机中任何其他 UDP 端口使用的号。另一种方法是,在创建一个套接字后,能够在 Python 中增加一行代码,通过套接字 bind() 方法为 UDP 套接字关联一个特定的端口号。
clientSocket.bind((' ',19157))
如果应用程序开发者所编写的代码实现的是一个"周知协议"的服务器端,那么开发者就必须为其分配一个相应的周知端口号。通常,应用程序的客户端让运输层自动地(并且是透明地)分配端口号,而服务器端则分配一个特定的端口号。
通过为 UDP 套接字分配端口号,来描述 UDP 的复用与分解。假定在主机 A 中的一个进程具有 UDP 端口 19157 ,它要发送一个应用程序数据块给位于主机 B 的另一进程 ,该进程具有 UDP 端口 46428 。① 主机 A 中的运输层创建一个运输层报文段 ,其中包括应用程序数据、源端口号(19157),目的端口号(46428)和两个其他值。然后,运输层将得到的报文段传递到网络层 。网络层将该报文段封装到一个 IP 数据报 中,并尽力而为地将报文段交付给接收主机。
② 如果该报文段到达接收主机B ,接收主机运输层 就检查 该报文段中的目的端口号 (46428 )
并将该报文段交付给端口号 46428 所标识的套接字 。值得注意的是,主机B 能够运行多个进程,每个进程有自己的套接字及相应的端口号。当 UDP 报文段从网络到达时, 主机B 通过检查该报文段中的目的端口号,将每个报文段定向(分解)到相应的套接字。
注意到下述事实是重要的:一个 UDP 套接字是由一个二元组全面标识的 ,该二元组包含一个目的 lP 地址和一个目的端口号 。因此,如果两个 UDP 报文段有不同的源 IP 地址和/或源端口号 ,但具有相同的目的 IP 地址和目的端口号 ,那么这两个报文段将通过相同的目的套接字被定向到相同的目的进程。
源端口号的用途是什么呢?如图 3-4 所示,在 A 到 B 的报文段中,源端口号用作 " 返回地址 " 的一部分 ,即当 B 需要回发一个报文段给 A 时,B 到 A 的报文段中的目的端口号便从 A 到 B 的报文段中的源端口号中取值。(完整的返回地址是 A 的IP 地址和源端口号)。问想2.7节学习过的那个 UDP 服务器程序。UDPServer. py 中,服务器使用 recvfrom( )方法从其自客户接收到的报文段中提取出客户端 (源 )端口号,然后,它将所提取的源端口号作为目的端口号,向客户发送一个新的报文段。

2.面向连接的多路复用与多路分解
为了理解 TCP 多路分解,必须更为仔细地研究 TCP 套接字和 TCP 连接创建。TCP 套接字和UDP套接字之间的一个细微差别是,TCP 套接字是由一个四元组(源 IP 地址、源端口号、目的 IP 地址、目的端口号)来表示的。 因此,当一个 TCP 报文段从网络到达一台主机时,该主机使用全部 4 个值来将报文段定向(分解)到相应的套接字 。特别与 UDP 不同的是,两个具有不同源 IP 地址 或 源端口号的到达 TCP 报文段将被定向到两个不同的套接字,除非 TCP 报文段携带了初始创建连接的请求。
考虑 2.7.2 节中的 TCP客户-服务器编程的例子:
• TCP 服务器应用程序有一个"欢迎套接字",它在 12000 端口上等待来自TCP客户 (见图 2-27) 的连接建立请求
• TCP 客户 使用下面的代码创建一个套接字并发送一个连接建立请求报文段 :
clientSocket=socket (AF_INET, SOCK_ STREAM)
ciientSocket. connect({serverName, 12000)}
• 一条连接建立请求只不过是一个目的端口号为 12000,TCP 首部的特定"连接建立位"置位的 TCP 报文段。这个报文段也包含一个由客户选择的源端口号。
• 当运行服务器进程的计算机的主机操作系统接收到具有目的端口 12000 的入连接请求报文 段后,它就定位服务器进程,该进程正在端口号 12000 等待接受连接。该服务器进程则创建一个新的套接字:
connectionSocket, addr = serverSocket.accept ()
• 该服务器的运输层还注意到连接请求报文段中的下列 4 个值:① 该报文段中的源端口号;② 源主机 IP 地址;③ 该报文段中的目的端口号;④ 自身的 lP 地址。新创建的连接套接字通过这 4 个值来标识。所有后续到达的报文段,如果它们的源端口号、源主机 IP 地址、目的端口号和目的 IP 地址都与这 4 个值匹配,则被分解到这个套接字。
随着 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. Web 服务器与 TCP
考虑一台运行 Web 服务器的主机,例如在端口 80 上运行一个 Apache Web 服务器。当客户(如浏览器)向该服务器发送报文段时,所有报文段的目的端口都将为 80。特别是,初始连接建立报文段和承载 HTTP 请求的报文段都有 80 的目的端口。该服务器能够根据源 IP 地址和源端口号来区分来自不同客户的报文段。
图 3-5 显示了一台 Web 服务器为每条连接生成一个新进程 。如图 3-5,每个这样的进程都有自己的连接套接字 ,通过这些套接字 可以收到 HTTP 请求 和发送 HTTP 响应 。 然而,连接套接字与进程之间并非总是有着一一对应的关系。事实上,当今的高性能 Web 服务器通常只使用一个进程,但是为每个新的客户连接创建一个具有新连接套接字的新线程 。(线程被看作是一个轻量级的子进程)对于这样一台服务器,在任意给定的时间内都可能有(具有不同标识的)许多连接套接字连接到相同的进程。
如果客户与服务器使用持续 HTTP ,则在整条连续持续期间,客户与服务器之间经由同一服务器套接字交换 HTTP 报文 。然而,如果客户与服务器使用非持续 HTTP ,则对每一对请求/响应都创建一个新的 TCP 连接并在随后关闭,因此对每一对请求/响应创建一个新的套接字并在随后关闭。这种套接字的频繁创建和关闭会严重影响一个繁忙的 Web 服务器的性能。
3.3 无连接运输:UDP
如果应用程序开发人员选择 UDP 而不是 TCP,则该应用程序差不多就是直接与 IP 打交道。UDP 从应用进程得到数据 ,附加上用于多路复用/分解服务的源和目的端口号字段 ,以及两个其他的小字段,然后将形成的报文段交给网络层 。网络层将该运输层报文段封装到一个 IP 数据报 中,然后尽力而为地尝试将此报文段交付给接收主机 。如果报文段到达接收主机,UDP 使用目的端口号 将报文段中的数据交付给正确的应用进程。值得注意的是,使用 UDP 时,在发送报文段之前,发送方和接收方的运输层实体之间没有握手 ,正因如此,UDP被称为是 无连接的。
DNS 是一个通常使用 UDP 的应用层协议的例子。当一台主机中的 DNS 应用程序想要进行一次查询时,它构造了一个DNS 查询报文并将其交给 UDP 。无须执行任何与运行在目的端系统中的 UDP 实体之间的握手,主机端的 UDP 为此报文添加首部字段 ,然后将形成的报文段交给网络层 。网络层将此UDP 报文段封装进一个 lP 数据报中,然后将其发送给一个名字服务器。在查询主机中的 DNS 应用程序则等待对该查询的响应。如果它没有收到响应(可能是由于底层网络丢失了查询或响应) ,则要么试图向另一个名字服务器发送该查询,要么通知调用的应用程序它不能获得响应
许多应用情况下更适合用 UDP 而不是 TCP,原因有如下:
① 关于发送什么数据以及合适发送的应用层控制更为精细 。采用 UDP 时,只要应用进程将数据传递给 UDP,UDP 就会将此数据打包进 UDP 报文段并立即传递给网络层 。在另一方面,TCP 拥有一个阻塞控制机制 ,以便当源和目的主机间的一条或多条链路变得极度拥塞时来遏制运输层 TCP 发送方。TCP 仍然继续重新发送数据报文段直到目的主机收到此报文并加以确认,而不管可靠交付需要多长时间。因为实时应用通常要求最小的发送速率,不希望过分地延迟报文段的传送,且能容忍一些数据丢失,TCP 服务模型不是特别适合这些应用的需要。
② 无须连接建立 。TCP 在开始传输数据之前都要经过三次握手。UDP 却不需要任何准备即可进行数据传输。因此,UDP 不会引入建立连接的时延。这可能是 DNS 运行在 UDP 之上而不是运行在 TCP 之上的原因(如果运行在 TCP 上,则 DNS 会慢很多)。HTTP 使用 TCP 而不是 UDP,因为对于具有文本数据的 Web 网页来说,可靠性是至关重要的。
③ 无连接状态 。TCP 需要在端系统中维护连接状态。此连接状态包括接收和发送缓存、拥塞控制参数以及序号与确认号的参数。要实现 TCP 的可靠数据传输服务并提供拥塞控制,这些状态是必要的。另一方面,UDP 不维护连接状态,也不跟踪参数。因此,某些专门用于某种特定应用的服务器当应用程序运行在 UDP 之上而不是运行在TCP 上时,一般都能支持更多的活跃用户。
**④ 分组首部开销小。**每个 TCP 报文段有 20 字节的首部开销,而 UDP 仅有 8 字节的开销。
图 3-6 列出了流行的因特网应用及其所使用的运输协议。如期望的那样,电子邮件、远程终端访问、Web 及文件传输 都运行在 TCP 上。因为所有这些应用都需要TCP 的可靠数据传输服务 。无论如何,有很多重要的应用是运行在 UDP 上而不是 TCP 上。例如,UDP 用于承载网络管理数据(SNMP)。这种场合下,UDP 要优于 TCP,因为网络管理应用程序通常必须在该网络处于重压状态时运行,而正是这个时候可靠的、拥塞受控的数据传输难以实现。此外,DNS 运行在 UDP 之上,从而避免了 TCP 的连接创建时延。

UDP 没有拥塞控制 ,而有时需要拥塞控制来预防网络进入一种拥塞状态,在拥塞状态中做的有用工作非常少 。如果每个人都启用流式高比特率视频而不使用拥塞控制,就会使路由器中有大量分组溢出 ,以至于非常少的 UDP 分组能成功地通过源到目的的路径传输 。另外,无控制的 UDP 发送方引入的高丢包率将引起 TCP 发送方大大地减少速率。UDP 中缺乏拥塞控制呢能够导致 UDP 发送方和接收方之间的高丢包率,并挤垮了 TCP 会话。
3.3.1 UDP 报文结构
UDP 报文段结构如图 3-7 所示。应用层数据占用 UDP 报文段的数据字段 ,例如,对于 DNS 应用,数据字段要么包含一个查询报文,要么包含一个响应报文。对于流式音频应用,音频抽样数据填充到数据字段。UDP 首部只有 4 个字段,每个字段由两个字节组成 。通过端口号可以使目的主机将应用数据交给运行在目的端系统中的相应进程 (执行分解功能)。长度字段指示了 UDP 报文段中的字节数(首部+数据)。 数据字段的长度在一个 UDP 段中不同于在另一个段中,需要一个明确的长度。接收方使用检验和 来检查在该报文段中是否出现了差错。实际上,计算检验和时,除了 UDP 报文段以外还包括了 IP 首部一些字段。

3.3.2 UDP 检验和
UDP 检验和提供了差错检测 功能。这就是说,检验和用于确定当 UDP 报文段从源到达目的地移动时,其中的比特是否发生了改变 (例如,由于链路中的噪声干扰或者存储在路由器中时引入问题)。发送方的 UDP 对报文段中的所有16 比特字的和进行反码运算 ,求和时遇到的任何溢出都被回卷。得到的结果被放在 UDP 报文段中的检验和字段。下面给出一个计算检验和的简单例子。
举例来说,有下面 3 个 16 比特的字:

注意到最后一次加法有溢出,它要被回卷(溢出为加到后面) 。反码运算就是将所有的 0 换成 1,所有的 1转换成 0 。因此,该和 0100101011000010 的反码运算结果是 1011010100111101 ,这就变为了检验和。在接收方,全部的 16 比特字(包括检验和)加在一起,如果该分组中没有引入差错,则显然在接收方处该和将是 1111111111111111。如果这些比特之一是 0, 那么我们就知道该分组中已经出现了差错。
UDP 提供了检验和,原因是不能保证源和目的之间的所有链路都提供差错检测。此外,即使报文段经链路正确地传输,当报文段存储在某台路由器的内存中时,也可能引入比特差错。在既无法确保证链路的可靠性 ,又无法确保内存中的差错检测的情况 下,如果端到端数据传输服务要提供差错检测,UDP 就必须在端到端基础上再运输层提供差错检测。这是一个在系统设计中被称为的端到端原则的例子。
因为假定 IP 是可以运行在任何第二层协议之上的,运输层提供差错检测作为保险措施非常有效。虽然 UDP 提供差错检测,但对差错恢复无能为力 。UDP 的某种实现只是丢弃受损的报文段 ;其他实现是将受损的报文段交给应用程序并给出警告。
3.4 可靠数据传输原理
可靠数据传输的实现问题不仅在运输层出现,也会在链路层和应用层出现。
图3-8 图示说明了可靠数据传输的框架 。为上层实体提供的服务抽象是:数据可以通过一条可靠的信道进行传输 。借助于可靠信道,传输数据比特就不会受到损坏 (由 0 变为 1,或者相反)或丢失,而且所有数据都是按照其发送顺序进行交付。这恰好就是 TCP 向调用它的因特网应用所提供的服务模型。

实现这种服务抽象是可靠数据传输协议 的责任。由于可靠数据传输协议的下层协议也许是不可靠的,因此这是一项困难的任务。例如,TCP 是不可靠的(IP)端到端网络层 之上实现的可靠数据传输协议 。更一般的情况,两个可靠通信端点的下层可能是由一条物理链路 (如在链路级数据传输协议的场合下)组成或是由一个全球互联网络 (如在运输级协议的场合下)组成。然而,我们可将较低层直接视为不可靠的点对点信道。
本节中,底层信道模型越来越复杂,不断地开发一个可靠数据传输协议 的发送方 一侧和接收方 一侧。例如,将考虑当底层信道能够损坏比特或丢失整个分组时,需要什么样的协议机制。这里始终的一个假设是分组将以它们发送的次序进行交付 ,某些分组可能会丢失 ;底层信道将不会对分组重排序。图3-8b 图示说明了用于数据传输协议的接口 。通过调用 rdt_send()函数,上层可以调用数据传输协议的发送方 。将要发送的数据交付给位于接受方的较高层。(rdt 表示可靠数据传输 协议,_send 指示rdt 的发送端正在被调用 。)在接收端,当分组从信道的接收端到达时,将调用rdt_rcv() 。当 rdt 协议想要向较高层交付数据时,通过调用 deliver_data() 来完成。后面,使用"分组"而不是运输层的" 报文段 "。
本节中,仅考虑 单向数据传输 的情况,即数据传输从发送端到接收端的 。还有可靠的双向数据传输(即全双工数据传输)。协议需要在发送端和接收端两个方向上传输分组,如图 3-8 所示,除了交换含有待传送的分组之外,rdt 的发送端和接收端还需往返交换控制分组 。rdt 的发送端和接收端都要调用 udt_send()发送分组给对方(udt 指不可靠数据传输)
3.4.1 构造可靠数据传输协议
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 都只有一个状态,因此变迁必定是从一个状态返回到自身;很快将看到更复杂的状态图。)引起变迁的事件 显示在表示变迁的横线上方 ,事件发生时所采取的动作 显式在横线下方。如果对一个事件没有动作或没有事件而发生了一个动作,将在横线上方或下方使用符号 ∧,以明确表示缺少动作或事件。FSM 的初始状态用虚线表示。尽管 图 3-9 只有一个初始状态,标识每个 FSM 的初始状态是非常重要的。

rdt 的发送端只通过 rdt_send(data)事件接受来自较高层的数据 ,产生一个包含该数据的分组(经由 make_pkt(data)动作),并将分组发送到信道中。实际上,rdt_send(data)事件是由较高层应用的过程调用产生的(rdt_send())。
在接收端,rdt 通过 rdt_rcv(packet) 事件从底层信道接收一个分组 ,从分组中取出数据 (经由 extract(packet,data) 动作),将数据上传给较高层(通过 deliver_data(data) 动作) 。实际上,rdt_rcv(packet) 事件是由较低层协议的过程调用产生的(例如,rdt_rcv())
在这个简单的协议中,一个单元数据与一个分组没差别。而且,所有分组是从发送方流向接收方;有了完全可靠的信道,接收端就不需要提供任何反馈信息给发送方 ,因为不必担心出现差错!注意到我们也已经假定了接收方接收数据的速率能够与发送方发送数据的速率一样快。因此,接收方没有必要请求发送方慢一点!
2. 经具有比特差错信道的可靠数据传输:rdt2.0
底层信道更为实际的模型是分组中的比特可能受损的模型。 在分组的传输、传播或缓存的过程中,这种比特差错通常会出现在网络的物理部件中。我们还将继续假定所有发送的分组 (虽然有些比特可能受损)将按其发送的顺序被接收。
研发经这种信道进行可靠通信的协议前,首先考虑人们如何处理这类情形。考虑怎样通过电话口述一条长报文的 。通常情况下,报文接收者在听到、理解并记下每句话后可能会说" OK "。如果报文接收者听到一句含糊不清的话时,可能要求你重复那句容易误解的话。这种口述报文协议使用了肯定确认(OK)与 否定确认("请重复一遍") 。这些控制报文使得接收方可以让发送方知道哪些内容被正确接收或有误需要重复。在计算机网络中,基于这种重传机制的可靠传输协议称为自动重传请求(ARQ)协议。
ARQ 协议还需要另外三种协议功能来处理比特差错的情况:
① 差错检测 。首先,需要一种机制以使接收方检测到何时出现了比特差错 。前一节讲到, UDP 使用因特网检验和字段正是为了这个目的。在第 5 章中,我们将更详细地学习差错检测和纠错技术。这些技术使接收方可以检测并可能纠正分组中的比特差错 。此刻,只需知道这些技术要求有额外的比特(除了待发送的初始数据比特之外的比特)从发送方发送到接收方;这些比特将被汇集在 rdt2 .0 数据分组的分组检验和字段中。
② 接收方反馈 。因为发送方和接收方通常在不同端系统上执行,可能相隔数千英里,**发送方要了解接收方情况(此时为分组是否被正确接收)的唯一途径就是让接收方提供明确的反馈信息给发送方。**在口述报文情况下回答的"肯定确认" (ACK) 和"否定确认" (NAK) 就是这种反馈的例子。类似地,rdt2 .0 协议将从接收方向发送方回送 ACK 和 NAK 分组。理论上,这些分组只需要一个比特长:如用 0 表示 NAK ,用 1 表示 ACK。
③ 重传。接收方收到有差错的分组时,发送方将重传该分组文。
图 3-10 说明了表示 rdt2.0 的 FSM,该数据传输协议采用了差错检测、肯定确认与否定确认

rdt2.0 的发送端有两个状态。在最左边的状态中,发送端协议正等待来自上传传下来的数据。当 rdt_send(data) 事件出现时,发送方将一个包含待发送数据的分组,带有检验和,然后经由 udt_send(sndpkt) 操作发送该分组。在最右边的状态 ,发送方协议等待来自接收方的 ACK 或 NAK 分组。如果收到一个 ACK 分组 (图3-10 中符号 rdt_rcv(rcvpkt) && isACK(rcvpkt) 对应该事件),则发送方知道最近发送的分组已被正确接收,因此协议返回到等待来自上层的数据的状态 。如果收到一个 NAK 分组, 该协议重传一个分组并等待接收方为响应重传分组而回送的 ACK 和 NAK 。下列事实很重要:当发送方处于等待 ACK 或 NAK 的状态时,它不能从上层获得更多的数据 ;就是说,rdt_send() 事件不可能出现 ;仅当接收到 ACK 并离开该状态时才能发生这样的事件 。因此,发送方将不会发送一块新数据,除非发送方确信接收方已正确接收当前分组。由于这种行为,rdt2.0 这样的协议被称为 停等协议。
rdt2.0 接收方的 FSM 仍然只有单一状态。当分组到达时,接收方要么回答一个 ACK,要么回答一个 NAK,这取决于收到的分组是否受损。在图 3-10 中,符号 rdt_rcv(rcpkt) && corrupt(rcvpkt) 对应于收到一个分组并发现有错的事件。
rdt2.0 协议看似可行了。但遗憾的是,它存在一个致命的缺陷。尤其是没有考虑到 ACK 和 NAK 分组受损的可能性 。至少,需要在 ACK/NAK 分组中添加检验和比特以检测这样的差错 。更难的问题是协议应该怎样纠正 ACK 或 NAK 分组中的差错 。难点在于,如果一个 ACK 或 NAK 分组受损,发送方无法知道接收方是否接受了上一块发送的数据。
考虑处理受损 ACK 和 NAK 时的三种可能性:
① 考虑在口述报文情况下人可能的做法。如果说话者不理解来自接收方回答的 " OK "或 " 请重复一遍 ",说话者可能问 " 你说什么?" (因此在协议中引入了一种新型发送方到接收方的分组)接收方则将复述其回答。但如果说话者的 " 你说什么 "产生了差错,情况又会怎样呢?接收者不明白那句混淆的话是口述内容的一部分,还是一个要求重复上次回答的请求,很可能回一句 "你说什么?"。于是回答可能含糊不清了,走上一条苦难重重之路。
② 增加足够的检验和比特,使发送方不仅可以检测差错,还可以恢复差错。对于会产生差错但不丢失分组的信道,可以直接解决问题。
③ 当发送方收到含糊不清的 ACK 或 NAK 分组时,只需重传当前数据分组即可。 然而,这种方法在发送方到接收方的信道中引入了冗余分组 。冗余分组的根本困难在于接收方不知道它上次所发送的 ACK 或 NAK 是否被发送方正确地收 到。因此,它无法事先知道接收到的分组是新的还是重传。
解决这个新问题的一个简单方法(几乎所有现有的数据传输协议中,包括 TCP ,都采用了这种方法)是在数据分组中添加一新字段 ,让发送方对其数据分组编号 , 即将发送数据分组的序号 (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 所示。rt2.1和 rdt2.2 之间的细微变化在于,接收方此时必须包括由一个 ACK 报文所确认的分组序号 (这可以通过在接收方 FSM 中,在 make_pkt () 包括参数 ACK 0 或ACK 1 来实现),发送方此时必须检验接收到的 ACK 报文中被确认的分组序号(这可通过在发送方 FSM ,在 isACK() 中包括参数 0 或 1 来实现)。
3. 经具有比特差错的丢包信道的可靠数据传输:rdt3.0
现在假定除了比特受损 外 ,底层信道还会丢包 ,这在今天的计算机网络(包括因特网)中并不罕见。协议现在必须处理另外两个关注的问题: 怎样检测丢包 以及**发生丢包后该做些什么,**rdt2.2 中已经研发的技术,如使用检验和、序号、ACK 分组和重传等。为解决第一个关注的问题,还需增加一种新的协议机制。
有很多可能的方法用于解决丢包问题,这里,让发送方负责检测和恢复丢包工作 。假定发送方传输一个数据分组,该分组 或者接收方对该分组 的 ACK 发生了丢失。这两种情况下,发送方都收不到应当到来的接收方响应。如果发送方 愿意等待足够长的时间以便确定分组已经丢失 ,则它只需重传该数据分组即可。
但是发送方需要等待多久才能确定已丢失了某些东西呢?发送方至少需要等待这样长的时间:即发送方与接收方之间的一个往返时延 (可能会包括在中间路由器的缓冲时延)加上接收方处理一个分组 所需的时间。在很多网络中,最坏情况下的最大时延 是很难估算 的,确定的因素非常少。此外,理想的协议应尽可能快地从丢包中恢复出来;等待一个最坏情况的时延可能意味着要等待一段较长的时间,直到启动差错恢复为止。因此实践中采取的方法是发送方 明智地选择一个时间值,以判定可能发生了丢包 (尽管不能确保)。如果在这个时间内没有收到 ACK,则重传该分组 .注意到如果一个分组经历了一个特别大的时延,发送方可能会重传该分组,即使该数据分组及其 ACK 都没有丢失 。这就在发送方到接收方的信道中引入了冗余数据分组 的可能性。幸运的是,rdt2.2 协议 已经有足够的功能**(序号)处理冗余分组**的情况。
从发送方的观点来看,重传是一种万能灵药。发送方不知道是一个数据分组丢失 ,还是一个 ACK 丢失 ,或者只是该分组或 ACK 过度延时 。在所有这些情况下,动作是同样的:重传 。为了实现基于时间的重传机制,需要一个倒计数定时器 (countdown timer) ,在一个给定的时间量过期后,可中断发送方 。因此,发送方需要能做到:①每次发送一个分组 (包括第一次分组和重传分组)时,便启动一个定时器 。②响应定时器中断 (采取适当的动作)。③终止定时器。
图 3-15 给出了 rdt3.0 的发送方 FSM,这是一个在可能出错和丢包的信道上可靠传输数据的协议。

图 3-16 显示了在没有丢包和延迟分组情况下协作运作的情况,以及如何处理数据分组丢失的。在图 3-16 中,时间从图的顶部朝底部移动;注意到一个分组的接收时间必定迟于一个分组的发送时间 ,这是因为发送时延与传播时延之故 。在图 3-16b~d 中,发送方括号部分表明了定时器的设置时刻以及随后的超时。因为分组序号在 0 和 1 之间交替, 因此时rdt3.0 有时被称为比特交替协议( alternating- bìt protocol)。
