UDP 的构建模块
网络基础 101,第三章
用户数据报协议(UDP)由 Jon Postel 于 1980 年 8 月加入核心网络协议套件,这远在 TCP/IP 最初问世之后,却恰逢 TCP 与 IP 规范被拆分为两份独立 RFC 之际。这个时间点颇具深意——正如我们将看到的,UDP 的核心特性与吸引力不在于它引入了什么,而在于它刻意省略的所有功能。UDP 被俗称为”空协议”,而描述其运作的 RFC 768 确实简短到可以写在一张餐巾纸上。
数据报(Datagram)
一种自包含、独立的数据实体,携带足够信息以便从源节点路由至目标节点,无需依赖节点间先前的交换或传输网络。
“数据报”与”分组(packet)“常被混用,但存在微妙差别。虽然”分组”适用于任何格式化的数据块,但”数据报”通常专指通过不可靠服务传输的分组——无交付保证,无失败通知。因此,你会发现人们常用更具描述性的”不可靠(Unreliable)“替代 UDP 缩写中的官方术语”用户(User)“,戏称为”不可靠数据报协议”。这也是为什么 UDP 分组通常更准确地被称为数据报。
UDP 最知名的应用,也是每个浏览器和互联网应用都依赖的,当属域名系统(DNS):给定一个人类友好的主机名,我们需要在任何数据交换发生前解析其 IP 地址。然而,尽管浏览器本身依赖 UDP,历史上该协议从未被作为页面和浏览器内运行应用的一等传输层暴露。直到 WebRTC 的出现。
由 IETF 和 W3C 工作组联合制定的新一代 **Web 实时通信(WebRTC)**标准,正通过 UDP 在浏览器内原生实现实时通信——如语音视频通话及其他形式的点对点(P2P)通信。借助 WebRTC,UDP 如今已成为具备客户端 API 的一等浏览器传输层!我们将在后续章节深入探讨 WebRTC,但在此之前,让我们先剖析 UDP 协议的内部机制,以理解在何种场景、为何选择使用它。
空协议服务
要理解 UDP 及其”空协议”的别称,我们需先审视网际协议(IP)——它位于 TCP 和 UDP 的下一层。
IP 层的核心任务是根据地址将数据报从源主机交付至目标主机。为此,消息被封装在 IP 分组内(图 3-1),标识源地址、目标地址及其他路由参数。
再次强调,“数据报”这一称谓至关重要:IP 层不保证消息交付,也不通知失败,直接将底层网络的不可靠性暴露给上层。若路由节点因拥塞、高负载或其他原因丢弃 IP 分组,检测、恢复和重传数据的责任便落在 IP 之上的协议——当然,前提是这些行为是期望的!
图 3-1. IPv4 首部(20 字节)
UDP 协议将用户消息封装进自身的分组结构(图 3-2),仅增加四个字段:源端口、目标端口、分组长度和校验和。当 IP 将分组交付至目标主机,主机解封 UDP 分组,通过目标端口识别目标应用,继而交付消息。仅此而已,不多不少。
图 3-2. UDP 首部(8 字节)
事实上,UDP 数据报中的源端口和校验和字段均为可选。IP 分组自带首部校验和,应用可选择省略 UDP 校验和——这意味着所有差错检测与纠正均可委托给上层应用。UDP 的核心不过是基于 IP 提供”应用多路复用”:通过嵌入通信主机的源端口与目标应用端口。鉴于此,我们可总结 UDP 的所有”非服务”:
| UDP 不提供的服务 | 具体表现 |
|---|---|
| 无消息交付保证 | 无确认、无重传、无超时 |
| 无交付顺序保证 | 无分组序号、无重排序、无队头阻塞 |
| 无连接状态跟踪 | 无连接建立或拆除的状态机 |
| 无拥塞控制 | 无内置的客户端或网络反馈机制 |
TCP 是面向字节流的协议,能够传输跨越多个分组的应用消息,而分组内部无显式消息边界。为实现这一点,连接两端需分配连接状态,每个分组被编号、丢失时重传、并按序交付。UDP 数据报则具有明确边界:每个数据报承载于单个 IP 分组中,每次应用读取获得完整消息;数据报不可分片。
UDP 是一种简单、无状态的协议,适合作为其他应用协议的基石:几乎所有协议设计决策都留给上层应用。然而,在急于实现自己的协议以取代 TCP 之前,务必审慎考虑 UDP 与大量部署的中间层设备(如 NAT 穿透)的交互复杂性,以及通用网络协议设计的最佳实践。缺乏严谨的工程与规划,不乏以创新协议构想开始,却以蹩脚 TCP 复制品告终的案例。TCP 的算法与状态机历经数十年锤炼改进,已纳入数十种难以妥善复制的机制。
UDP 与网络地址转换器
遗憾的是,IPv4 地址仅 32 位,最多提供约 42.9 亿个唯一地址。随着 90 年代初互联网主机数量指数级增长,**IP 网络地址转换器(NAT)**规范于 1994 年中期(RFC 1631)作为临时方案被提出,以应对迫在眉睫的 IPv4 地址耗尽问题——我们无法期望为每台主机分配唯一 IP。
提出的 IP 复用方案是在网络边缘引入 NAT 设备,每台设备负责维护一张映射表,将本地 IP 与端口元组映射到一个或多个全局唯一(公网)IP 与端口元组(图 3-3)。翻译器背后的本地 IP 地址空间可在多个不同网络间复用,从而解决地址耗尽问题。
图 3-3. IP 网络地址转换器
然而,正如常言所说,没有什么比临时方案更永恒。NAT 设备不仅解决了燃眉之急,更迅速成为众多企业级和家庭级代理、路由器、安全设备、防火墙及其他软硬件设备的 ubiquitous 组件。NAT 中间层设备不再是临时方案,而已成为互联网基础设施的固化组成部分。
保留的私有网络地址段
互联网号码分配机构(IANA)——负责全球 IP 地址分配的实体,为通常位于 NAT 设备后的私有网络保留了三个知名地址段(表 3-1):
| IP 地址范围 | 地址数量 |
|---|---|
| 10.0.0.0–10.255.255.255 | 16,777,216 |
| 172.16.0.0–172.31.255.255 | 1,048,576 |
| 192.168.0.0–192.168.255.255 | 65,536 |
表 3-1. 保留 IP 地址段
这些地址段中至少有一个应让你感到熟悉。你的本地路由器很可能已为电脑分配了来自这些范围的 IP——那是你在内网的私有 IP 地址,由 NAT 设备在与外网通信时进行转换。
为避免路由错误和混淆,任何公网计算机均不得分配这些保留私有网络范围内的 IP 地址。
连接状态超时
就 UDP 而言,NAT 转换的问题恰恰在于它必须维护用于交付数据报的路由表。NAT 中间层设备依赖连接状态,而 UDP 恰恰无状态。这是根本性的错配,也是 UDP 数据报交付诸多问题的根源。此外,客户端如今位于多层 NAT 之后已非罕见,这更使问题复杂化。
每个 TCP 连接都有明确定义的协议状态机:以握手开始,继以应用数据传输,最后以规范的交换关闭连接。基于此流程,每个中间层设备可观察连接状态,按需创建和移除路由条目。UDP 则既无握手也无连接终止,故无连接状态机可供监控。
发送出站 UDP 流量无需额外工作,但路由回复要求翻译表中存在条目,以告知本地目标主机的 IP 和端口。因此,翻译器必须跟踪每个 UDP 流的状态——而 UDP 本身却是无状态的。
更糟的是,翻译器还需判断何时删除翻译记录,但由于 UDP 无连接终止序列,任一端都可能在无通知的情况下随时停止传输数据报。为此,UDP 路由记录采用定时器过期机制。多久?并无定论;超时时间取决于翻译器的厂商、型号、版本及配置。因此,UDP 长会话的事实最佳实践之一是引入双向保活分组,周期性重置路径上所有 NAT 设备中翻译记录的定时器。
TCP 超时与 NAT
技术上,NAT 设备无需为 TCP 设置额外超时。TCP 协议遵循规范的握手和终止序列,可指示何时添加或删除适当的翻译记录。
然而实践中,许多 NAT 设备对 TCP 和 UDP 会话应用类似的超时逻辑。因此,某些情况下 TCP 也需要双向保活分组。若你的 TCP 连接被断开,很可能是中间层 NAT 超时所致。
NAT 穿透
不可预测的连接状态处理是 NAT 造成的严重问题,但对许多应用而言,更大的问题是根本无法建立 UDP 连接。这对 P2P 应用(如 VoIP、游戏、文件共享)尤为突出——这些应用通常需要同时充当客户端和服务器,以实现节点间的双向直接通信。
首要问题是:存在 NAT 时,内部客户端不知晓其公网 IP。它知道自己的内网 IP 地址,NAT 设备会在每个 UDP 分组中重写源端口和地址,以及 IP 分组中的源 IP 地址。然而,若客户端将其私有 IP 地址作为应用数据的一部分与外部网络的对端通信,连接必然失败。因此,“透明”翻译的承诺不再成立,应用必须首先发现其公网 IP 地址,才能与私有网络外的对端共享。
然而,仅知晓公网 IP 也不足以成功进行 UDP 传输。任何到达 NAT 设备公网 IP 的分组,还必须具备目标端口及 NAT 表中的条目,才能将其翻译为内部目标主机的 IP 和端口元组。若该条目不存在——若有人试图从公网直接传输数据,这极可能是常态——分组将被直接丢弃(图 3-4)。NAT 设备充当简单的分组过滤器,因其无法自动确定内部路由,除非用户通过端口转发等机制显式配置。
图 3-4. 因缺少映射而被丢弃的入站分组
需注意,上述行为对从内部网络发起交互并在此过程中建立必要翻译记录的客户端应用并非问题。然而,在 NAT 存在的情况下处理入站连接(充当服务器)——如 VoIP、游戏主机、文件共享等 P2P 应用——我们立即会遭遇此问题。
为解决 UDP 与 NAT 的这一错配,必须使用各种穿透技术(TURN、STUN、ICE)以建立两端 UDP 节点间的端到端连通性。
STUN、TURN 与 ICE
NAT 会话穿透工具(STUN) 是一种协议(RFC 5389,更新版 RFC 8489),允许主机应用发现网络上网络地址转换器的存在,并在存在时获取当前连接被分配的公网 IP 和端口元组(图 3-5)。为此,协议需要位于公网的知名第三方 STUN 服务器协助。
图 3-5. STUN 查询公网 IP 与端口
假设已知 STUN 服务器 IP 地址(通过 DNS 发现或手动指定),应用首先向 STUN 服务器发送绑定请求。STUN 服务器继而回复包含客户端从公网可见的公网 IP 地址和端口的响应。这一简单流程解决了我们先前讨论的几个问题:
-
应用发现其公网 IP 和端口元组,随后可在与对端通信时将此信息作为应用数据的一部分使用。
-
向 STUN 服务器的出站绑定请求在路径上建立了 NAT 路由条目,使到达该公网 IP 和端口元组的入站分组得以路由回内网的主机应用。
-
STUN 协议定义了简单的保活 ping 机制,防止 NAT 路由条目超时。
机制就位后,每当两节点欲通过 UDP 通信,它们将首先向各自的 STUN 服务器发送绑定请求,待双方均成功响应后,即可使用已建立的公网 IP 和端口元组交换数据。
然而实践中,STUN 不足以应对所有 NAT 拓扑和网络配置。进一步说,某些情况下 UDP 可能被防火墙或其他网络设备完全阻断——这在许多企业网络中并不罕见。为解决此问题,当 STUN 失败时,我们可采用 NAT 中继穿透(TURN) 协议(RFC 5766,更新版 RFC 8656)作为后备,它可运行于 UDP,并在万不得已时切换至 TCP。
TURN 的关键词当然是 “中继” 。该协议依赖位于公网的中继服务器(图 3-6)的存在与可用性,以在节点间 shuttle 数据。
图 3-6. TURN 中继服务器
- 两客户端均向同一 TURN 服务器发送分配请求开始连接,随后进行权限协商。
- 协商完成后,两节点通过向 TURN 服务器发送数据通信,服务器继而中继至另一节点。
显然,此交换的明显弊端在于它不再是点对点通信!TURN 是在任何网络上为任意两节点提供连通性的最可靠方式,但运营 TURN 服务器的成本极高——至少中继必须具备足够容量以服务所有数据流。因此,TURN 最好作为直接连通失败时的最后手段后备。
STUN 与 TURN 实践
Google 的 libjingle 是一个开源 C++ 库,用于构建点对点应用,在底层处理 STUN、TURN 和 ICE 协商。该库曾用于驱动 Google Talk 聊天应用,其文档为真实世界中 STUN 与 TURN 的性能提供了宝贵参考:
- 92% 的情况下可直接建立连接(STUN)。
- 8% 的情况下需要中继(TURN)。
遗憾的是,即使使用 STUN,仍有相当比例的用户无法建立直接 P2P 隧道。为提供可靠服务,我们还需要 TURN 中继作为直接 P2P 通信不可行时的后备。
构建有效的 NAT 穿透解决方案绝非易事。所幸,我们可以借助 交互式连接建立(ICE) 协议(RFC 5245,更新版 RFC 8445)来协助完成此任务。ICE 是一种协议及方法集合,旨在建立参与者间最高效的隧道(图 3-7):尽可能直接连接,必要时利用 STUN 协商,万不得已则回退至 TURN。
图 3-7. ICE 尝试直接、STUN 及 TURN 连通性选项
实践中,若你正基于 UDP 构建 P2P 应用,你绝对应该利用现有的平台 API 或实现了 ICE、STUN 和 TURN 的第三方库。既然你已熟悉每种协议的功能,便可顺利导航所需的设置与配置!
UDP 优化实践
UDP 是一种简单且常用于引导新传输协议的协议。事实上,UDP 的核心特性在于它省略的所有功能:无连接状态、无握手、无重传、无重组、无重排序、无拥塞控制、无拥塞避免、无流量控制,甚至无可选差错检查。然而,这种极简面向消息的传输层所提供的灵活性,对实现者而言也是责任。你的应用很可能需要从零开始重新实现部分或全部这些功能,且每项设计都必须与网络上的其他节点和协议良好协作。
与内置流量控制、拥塞控制及拥塞避免的 TCP 不同,UDP 应用必须自行实现这些机制。对拥塞不敏感的 UDP 应用极易压垮网络,可能导致网络性能劣化,严重时甚至引发网络拥塞崩溃。
若欲将 UDP 用于自有应用,务必研读当前最佳实践与建议。RFC 5405(已更新为 RFC 8085)专门聚焦于单播 UDP 传输应用的设计指南。以下是部分建议摘要:
- 应用必须容忍广泛的互联网路径条件。
- 应用应控制传输速率。
- 应用应对所有流量执行拥塞控制。
- 应用应使用与 TCP 相似的带宽。
- 应用应在丢包后回退重传计数器。
- 应用不应发送超过路径 MTU 的数据报。
- 应用应处理数据报丢失、重复和乱序。
- 应用应能承受最长 2 分钟的交付延迟。
- 应用应启用 IPv4 UDP 校验和,必须启用 IPv6 校验和。
- 应用可在需要时使用保活机制(最小间隔 15 秒)。
设计新传输协议需要深思熟虑、周密规划和充分研究——务必做好功课。尽可能利用已考虑 NAT 穿透、并能与其他并发网络流量源实现一定程度公平性的现有库或框架。
好消息是:WebRTC 正是这样的框架!