第 2 章中,应用进程通过 socket 使用 TCP 或 UDP 的服务(见 Chapter 2.1 Application Layer 1 §2.1.2、§2.1.5)。本章打开 socket 下面的运输层,看这些服务怎样在端系统中实现。本篇介绍运输层的基本任务、复用与分用,以及最简单的运输协议 UDP;可靠传输原理见 Chapter 3.2 Transport Layer 2。

第 3 章分两条线展开:一条是运输层的一般原理,一条是 Internet 中的具体协议。原理与协议的对应关系如下:

原理解决的问题在 Internet 协议中的体现
复用与分用一台主机上的多个进程如何共用网络UDP、TCP 都提供
可靠数据传输下层会出错、丢包时,如何保证数据完整、有序TCP
流量控制发送方不要压垮接收方TCP
拥塞控制发送方不要压垮网络TCP

3.1 运输层服务

第 1 章把运输层描述为“进程到进程的数据传输”(Chapter 1 Introduction §1.6.2)。这一节说明这句话的含义,以及它与网络层的分工。

3.1.1 进程之间的逻辑通信

两个应用进程可能相隔许多路由器和链路,但从应用看来,它们好像直接相连,只需把报文交给运输层,对方的运输层就会把它交给目标进程。

定义:逻辑通信(Logical Communication)

从某一层的视角看,通信双方好像直接连接在一起,只需关心本层的消息交换,不必关心下层经过哪些设备和链路。运输层为运行在不同主机上的应用进程提供逻辑通信。

运输协议只在端系统中运行:

  • 发送方:把应用报文划分成若干运输层报文段(segment),加上运输层首部后交给网络层。
  • 接收方:从网络层取得报文段,重组为报文,交给应用层。
  • 路由器只处理到网络层(Chapter 1 Introduction §1.6.3),不检查运输层报文段的内容。

Internet 向应用提供不止一种运输协议,主要是 TCP 和 UDP。

3.1.2 运输层与网络层

运输层建立在网络层之上,两者的区别在于通信的端点不同:

层提供的逻辑通信端点
网络层主机之间主机(IP 地址)
运输层进程之间进程(socket)

运输层依赖网络层的服务,也可以增强它:

  • 网络层不能保证的时延和带宽,运输层也无法凭空提供。
  • 网络层会丢失、损坏或乱序交付数据时,运输层可以通过重传、确认等机制,在端系统中提供可靠、有序的交付(§3.4 起展开)。

3.1.3 Internet 的运输层协议

Internet 的网络层协议 IP 只提供尽力而为(best-effort)的服务:尽量把数据报送到目的主机,但不保证送达、不保证顺序,也不保证数据完整。TCP 和 UDP 在此基础上提供不同程度的增强:

协议交付服务附加机制
TCP可靠、按序交付连接建立、流量控制、拥塞控制
UDP不可靠、不保证顺序只在 IP 之上增加进程间交付和差错检测,没有其他额外功能

两者都不提供时延保证和带宽保证。各类应用如何根据这些服务选择协议,见 Chapter 2.1 Application Layer 1 §2.1.5。

3.2 复用与分用

网络层只负责把数据送到主机,但主机上同时运行着浏览器、邮件客户端等多个进程,各自打开一个或多个 socket。运输层首先要解决的问题是:发送时如何让多个 socket 共用网络,接收时如何把报文段交给正确的 socket。

3.2.1 复用与分用的含义

定义:复用(Multiplexing)与分用(Demultiplexing)

  • 复用:发送主机的运输层从多个 socket 收集数据,为每块数据加上首部(其中包含之后分用要用的信息),形成报文段交给网络层。
  • 分用:接收主机的运输层根据报文段首部的信息,把收到的报文段交付给正确的 socket。

下图中,host 2 的运输层要把收到的报文段分别交给进程 P1 和 P2 的 socket(分用),同时把 P1、P2 发出的数据一起交给网络层(复用)。

3.2.2 分用依据:IP 地址与端口号

分用需要报文段携带“交给谁”的信息。主机收到 IP 数据报后,可以使用以下信息:

  • 每个 IP 数据报有源 IP 地址和目的 IP 地址,并携带一个运输层报文段。
  • 每个报文段的首部有源端口号和目的端口号,各占 16 bit。

TCP 和 UDP 报文段首部的前 32 bit 都是源端口号和目的端口号,其余首部字段两者不同,之后是应用数据。主机综合使用 IP 地址和端口号,把报文段导向相应的 socket。UDP 和 TCP 使用这些信息的方式不同,下面分别说明。

3.2.3 无连接分用(UDP)

UDP 不建立连接,接收方只看目的地址就能决定交给哪个 socket。

定义:UDP socket 的标识

UDP socket 由二元组标识:

主机收到 UDP 报文段后,检查其中的目的端口号,把报文段交给绑定该端口号的 socket。

因此,只要目的 IP 地址和目的端口号相同,即使源 IP 地址或源端口号不同,报文段也会交给同一个 socket。

源端口号仍然有用:它充当“回信地址”。接收方要回复时,把收到的源端口号作为回复报文段的目的端口号。

下图中,客户端 A 和 B 的进程都向服务器 C 的 6428 端口发送数据,两者到达服务器上的同一个 socket(P3);服务器回复时,分别以 9157 和 5775 作为目的端口号。

3.2.4 面向连接分用(TCP)

TCP 为每个连接维护单独的状态,所以接收方需要区分来自不同客户端的报文段,即使它们发往同一个端口。

定义:TCP socket 的标识

TCP socket 由四元组标识:

接收主机使用全部四个值,把报文段交给相应的 socket。

一台服务器主机可以同时维护许多 TCP socket,每个 socket 由自己的四元组标识。Web 服务器为每个连接的客户端使用不同的 socket;使用非持续 HTTP 时,每个请求都使用新的连接,因而对应不同的 socket(见 Chapter 2.1 Application Layer 1 §2.2.2)。

下图中,三个报文段的目的 IP 地址都是 C,目的端口号都是 80:

  • A 的报文段与 B 的一个报文段源端口号都是 9157,但源 IP 地址不同(A 与 B)。
  • B 的两个报文段源 IP 地址相同,但源端口号不同(9157 与 5775)。

三者的四元组互不相同,所以分别交给三个 socket。

socket 与进程不一定一一对应。多线程 Web 服务器只运行一个进程(P4),为每个连接创建一个新的 socket,交给不同的线程处理:

3.3 无连接运输:UDP

有了分用,最简单的运输协议只需在 IP 之上做到“把数据交给正确的进程”,再加上基本的差错检测。UDP 就是这样的协议,定义于 RFC 768。

3.3.1 UDP 提供的服务

UDP 是“不加修饰”(no frills)的 Internet 运输协议,提供尽力而为的服务:

  • 报文段可能丢失,也可能乱序交付给应用。
  • 无连接:发送方和接收方之间没有握手,每个 UDP 报文段都独立处理,与其他报文段无关。

3.3.2 为什么需要 UDP

既然 TCP 提供可靠传输,仍然保留 UDP,是因为它“少做的事”对某些应用反而是优点:

UDP 的特点带来的好处
无需建立连接省去建立连接带来的时延
不维护连接状态发送方和接收方实现简单,服务器可以支持更多客户端
首部小每个报文段的额外开销少
没有拥塞控制应用可以按自己需要的速率发送,不被运输层压低

UDP 常用于流媒体等多媒体应用:这类应用能容忍少量丢失(loss tolerant),但对速率敏感(rate sensitive)。其他使用 UDP 的应用包括 DNS、SNMP(网络管理)和多人在线游戏。

应用需要可靠性但又想使用 UDP 时,可以在应用层自行加入确认、重传等机制(如 Chapter 2.2 Application Layer 2 §2.2.7 中的 QUIC)。

3.3.3 UDP 报文段格式

UDP 首部只有 4 个字段,每个 16 bit,共 8 byte:

字段含义
源端口号发送进程的端口,供对方回复时使用
目的端口号用于在接收主机上分用
长度(length)整个 UDP 报文段的字节数,包括首部
校验和(checksum)用于检测报文段中的差错

3.3.4 UDP 校验和

数据在传输中可能发生比特翻转。UDP 不负责纠正错误,但提供校验和,让接收方能发现错误。

定义:Internet 校验和(Checksum)

发送方:

  1. 把报文段内容看作一串 16 bit 整数。
  2. 用反码求和(1’s complement sum)把它们相加:最高位产生的进位要回卷(wraparound),加到结果的最低位。
  3. 将和取反(每位 0、1 互换),得到校验和,填入 UDP 首部的校验和字段。

接收方:对收到的报文段重新计算,与校验和字段比较。

  • 不相等:检测到差错。
  • 相等:没有检测到差错,但仍可能存在未被发现的差错。

等价的检查方法是:接收方把所有 16 bit 字连同校验和字段一起做反码求和,结果全为 1 时没有检测到差错,出现 0 位就说明有差错。

校验和只能提供较弱的保护:若两个字在同一位置上的比特同时反向翻转,和保持不变,差错无法被发现。

链路层可能已经做了差错检测,但并非每段链路都检测,数据在路由器内存中也可能出错。运输层在端系统之间再检查一次,才能覆盖整条路径。