---
UDP 的构成
---

#### 目录

1. 前言
2. 无协议服务
3. UDP 与网络地址转换器
   * 连接状态超时
   * NAT 穿透
   * STUN、TURN 与 ICE
4. 针对 UDP 的优化建议

#### 前言

1980 年 8月，紧随 TCP/IP 之后，UDP（用户数据报协议）被 John Postel 加入了核心网络协议套件。UDP 的主要功能和亮点不在于它引入了什么特性，而在于它忽略了哪些特性。UDP 经常被称为无（Null）协议，RFC 768 描述了其运作机制，全文完全可以写在一张餐巾纸上。

> 数据报
>
> 一个完整、独立的数据实体，携带着从源节点到目的地节点的足够信息，对这些节点间之前的数据交换和传输网络没有任何依赖。

数据报（datagram）和分组（packet）是两个经常被人混用的词，实际上它们还是有区别的。分组可以用来指代任何格式化的数据块，而数据报则通常只用来描述那些通过不可靠的服务传输的分组，既不保证送达，也不发送失败通知。正因为如此，很多场合下人们都把 UDP 中 User 的 U 改成 Unreliable（不可靠）的 U，于是 UDP 就成了 “不可靠数据报协议”。这也是为什么把 UDP 分组称为数据报更为恰当的原因。

关于 UDP 的应用，最广为人知同时也是所有浏览器和因特网应用都赖以运作的，就是 DNS。DNS 负责把主机名转换成 IP 地址。可是，尽管浏览器有赖于 UDP，但这个协议以前从未被看成网页和应用的关键传输机制。HTTP 并未规定要使用 TCP，但现实中所有 HTTP 实现都是用 TCP。

不过，这都是过去的事了。IETF 和 W3C 工作组共同制定了一套新 API --- WebRTC（Web 实时通信）。WebRTC 着眼于在浏览器中通过 UDP 实现原生的语音和视频实时通信，以及其他形式的 P2P（端到端）通信。正是因为 WebRTC 的出现，UDP 作为浏览器中重要传输机制的地位才得以突显，而且还有了浏览器 API！

#### 无协议服务

要理解为什么 UDP 被人称作 “无协议”，必须从作为 TCP 和 UDP 下一层的 IP 协议说起。

IP 层的主要任务就是按照地址从源主机向目标主机发送数据报。为此，消息会被封装在一个 IP 分组内，其中载明了源地址和目标地址，以及其他一些路由参数。注意，数据报这个词暗示了一个重要的信息：IP 层不保证消息可靠的交互，也不发送失败通知，实际上是把底层网络的不可靠性直接暴露给了上一层。如果某个路由节点因为网络阻塞、负载过高或其他原因而删除了 IP 分组，那么在必要的情况下，IP 的上一层协议要负责检测、恢复和重发数据。

下面看一下 IPv4 首部（20 字节）：

![](https://i.loli.net/2019/06/24/5d103b3bccf2e69399.png)

UDP 协议会用自己的分组结构（如下图）封装用户消息，它只增加了 4 个字段：源端口、目标端口、分组长度和效验和。这样，当 IP 把分组送达目标主机时，主机能够拆开 UDP 分组，根据目标端口找到目标应用程序，然后再把消息发送过去。仅此而已。

下面就是 UDP 首部（8 字节）：

![](https://i.loli.net/2019/06/24/5d103c4d2b2fb14310.png)

事实上，UDP 数据报中的源端口和效验和字段都是可选的。IP 分组的首部也有效验和，应用程序可以忽略 UDP 效验和。也就是说，所有错误检测和错误纠正工作都可以委托给上层的应用程序。说到底，UDP 仅仅是在 IP 层之上通过嵌入应用程序的源端口和目标端口，提供了一个 “应用程序多路复用” 机制。明白了这一点，就可以总结一下 UDP 的无服务是怎么回事了。

* 不保证消息交互

  不确认，不重传，无超时。

* 不保证交互顺序

  不设置包序号，不重拍，不会发生队首阻塞。

* 不跟踪连接状态

  不必建立连接或重启状态机。

* 不需要拥塞控制

  不内置客户端或网络反馈机制。

TCP 是一个面向字节流的协议，能够以多个分组形式发送应用程序消息，且对分组中的消息范围没有任何明确限制。因此，连接的两端存在一个连接状态，每个分组都有序号，丢失还要重发，并且要按顺序交互。相对来说，UDP 数据报有明确的限制：数据报必须封装在 IP 分组中，应用程序必须读取完整的消息。换句话说，数据报不能分片。

UDP 是一个简单、无状态的协议，适合作为其他上层应用协议的辅助。实际上，这个协议的所有决定都需要由上层的应用程序作出。

#### UDP 与网络地址转换器

令人遗憾的是，IPv4 地址只有 32 位长，因而最多只能提供 42.9 亿个唯一 IP 地址。1990 年代初，互联网上的主机数量呈指数级增长，但不可能所有主机都分配一个唯一的 IP 地址。1994 年，作为解决 IPv4 地址即将耗尽的一个临时性方案，IP 网络地址转换器（NAT）规范出台了。

建议的 IP 重用方案就是在网络边缘加入 NAT 设备，每个 NAT 设备负责维护一个表，表中包含本地 IP 和端口到全球唯一（外网）IP 和端口的映射。这样，NAT 设备背后的 IP 地址空间就可以在各种不同的网络中得到重用，从而解决地址耗尽问题。

IP 地址网络转换器：

![](https://i.loli.net/2019/06/24/5d104178654ce82237.png)

然而，这个临时性的方案居然就那么一直沿用了下来。新增的 NAT 设备不仅立竿见影的解决了地址耗尽问题，而且还迅速成为很多公司及家庭代理和路由器、安全装置、防火墙，以及其他很多硬件和软件设备中的内置组件。NAT 不再是个临时性方案，它已经成了因特网基础设施的一个组成部分。

作为监管全球 IP 地址分配的机构，IANA 为私有网络保留了三段 IP 地址，这些 IP 地址经常可以在 NAT 设备后面的内网中看到。

保留的 IP 地址范围：

| 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     |

其中一段 IP 地址是不是很眼熟？在你的局域网中，路由器给你的计算机分配的 IP 地址很可能位于其中一段。这个地址就是你在内网中的私有地址。在需要与外网通信时，NAT 设备会将它们转换成外网地址。

为防止路由器错误和引起不必要的麻烦，不允许给外网计算机分配这些保留的私有地址。

##### 连接状态超时

NAT 转换的问题（至少对于 UDP 而言）在于必须维护一份精确的路由表才能保证数据转发。NAT 设备依赖连接状态，而 UDP 没有状态。这种根本上的错配是很多 UDP 数据报传输问题的总根源。况且，客户端前面有很多个 NAT 设备的情况也不鲜见，问题由此进一步恶化了。

每个 TCP 连接都有一个设计周密的协议状态机，从握手开始，然后传输应用数据，最后通过明确的信号确认关闭连接。在这种设计下，路由设备可以监控连接状态，根据情况创建或删除路由表中的条目。而 UDP 呢，没有握手，没有连接终止，实际根本没有可监控的连接状态机。

发送出站的 UDP 不费事，但路由响应却需要转换表中有一个条目能告诉我们本地目标主机的 IP 和端口。因此，转换器必须保存每个 UDP 流的状态，而 UDP 自身却没有状态。

更糟糕的是，NAT 设备还被赋予了删除转换记录的责任，但由于 UDP 没有连接终止确认环节，任何一端随时都可以停止传输数据报，而不必发送通告。为解决这个问题，UDP 路由记录会定时过期。定时多长？没有规定，完全取决于转换器的制造商、型号、版本和配置。因此，对于较长时间的 UDP 通信，有一个事实上的最佳做法，即引入一个双向 keep-alive 分组，周期性的重置传输路径上所有 NAT 设备中转换记录的计时器。

##### NAT 穿透

不可预测的连接状态处理是 NAT 设备带来的一个严重问题，但更为严重的则是很多应用程序根本不能建立 UDP 连接。尤其是 P2P 应用程序，涉及 VoIP、游戏和文件共享等，它们客户端与服务器经常需要角色互换，以实现端到端的双向通信。

NAT 带来的第一个问题，就是内部客户端不知道外网 IP 地址，制只知道内网 IP 地址。NAT 负责重写每个 UDP 分组中的源端口、地址，以及 IP 分组中的源 IP 地址。如果客户端在应用数据中以其内网 IP 地址与外网主机通信，必然连接失败。所谓的 “透明” 转换因此也就成了一句空话，如果应用程序想与私有网络外部的主机通信，那么它首先必须知道自己的外网 IP 地址。

然鹅，知道外网 IP 地址还不是实现 UDP 传输的充分条件。任何到达 NAT 设备外网 IP 的分组还必须有一个目标端口，而且 NAT 转换表中也要有一个条目可以将其转换为内部主机的 IP 地址和端口号。如果没有这个条目（通常是从外网传数据进来），那到达地分组就会被删除。此时的 NAT 设备就像一个分组过滤器，除非用户通过端口转发或类似机制配置过，否则它无法确定将分组发送给哪台内部主机。

由于没有映射规则，入站分组直接被删除：

![](https://i.loli.net/2019/06/24/5d1062efd7abb51936.png)

需要注意的是，上述行为对客户端应用程序不是问题。客户端应用程序基于内部网络实现交互，会在交互期间建立必要的转换记录。不过，如果隔着 NAT 设备，那客户端（作为服务器）处理来自 P2P 应用程序（VoIP、游戏、文件共享）的入站连接时，就必须面对 NAT 穿透问题。

为解决 UDP 与 NAT 的这种不搭配，人们发明了很多穿透技术（TURN、STUN、ICE），用于在 UDP 主机之间建立端对端的连接。

##### STUN、TURN 与 ICE

STUN 是一个协议，可以让应用程序发现网络中的地址转换器，发现之后进一步取得为当前连接分配的外网 IP 地址和端口。为此，这个协议需要一个已知的第三方 STUN 服务器支持，该服务器必须架设在公网上。

STUN 查询外网 IP 地址和端口：

![](https://i.loli.net/2019/06/24/5d10680a54b8347982.png)

假设 STUN 服务器的 IP 地址已知（通过 DNS 查找或手工指定），应用程序首先向 STUN 服务器发送一个绑定请求。然后，STUN 服务器返回一个响应，其中包含在外网中代表客户端的 IP 地址和端口号。这种简单的方式解决了前面讨论的一些问题：

* 应用程序可以获得外网 IP 和端口，并利用这些信息与对端通信
* 发送到 STUN 服务器的出站绑定请求将在通信要经过的 NAT 中建立路由条目，使得到达该 IP 和端口的入站分组可以找到内网中的应用程序。
* STUN 协议定义了一个简单的 keep-alive 探测机制，可以保证 NAT 路由条目不超时

有了这个机制，两台主机端需要通过 UDP 通信时，它们首先都会向各自的 STUN 服务器发送绑定请求，然后分别使用响应中的外网 IP 地址和端口号交换数据。

但在实际应用中，STUN 并不能适应所有类型的 NAT 和网络配置。不仅如此，某些情况下 UDP 还会被防火墙或其他网络设备完全屏蔽。这种情况在很多企业网很常见。为解决这个问题，在 STUN 失败的情况下，我们还可以使用 TURN 协议作为后备。TURN 可以在最坏的情况下跳过 UDP 而切换到 TCP。

TURN 中的关键词当然是中继。这个协议依赖于外网中继设备在两端间传递数据。

TURN 中继服务器：

![](https://i.loli.net/2019/06/24/5d106ad8c7f7369476.png)

* 两端都要向同一台 TURN 服务器发送分配请求未建立连接，然后在进行权限协商
* 协商完毕，两端都把数据发送到 TURN 服务器，再有 TURN 服务器转发，从而实现通信

很明显，这就不再是端对端的数据交换了！TURN 是在任何网络中为两端提供连接的最可靠方式，但运维 TURN 服务器的投入也很大。至少，为满足传输数据的需要，中继设备的容量必须足够大。因此，最好在其他直连手段都失败的情况下，在使用 TURN。

##### ICE

构建高效的 NAT 穿透方案可不容易。好在，我们还有 ICE 协议。ICE 规定了一套方法，致力于在通信各端之间建立一条最有效的通信：能直连就直连，必要时 STUN 协商，再不行使用 TURN。

ICE 先后尝试直连、STUN 和 TURN：

![](https://i.loli.net/2019/06/24/5d106e15a20d534505.png)

#### 针对 UDP 的优化建议

UDP 是一个简单常用的协议，经常用于引导其他传输协议。事实上，UDP 的特色在于它所省略的那些功能：连接状态、握手、重发、重组、重排、拥塞控制、拥塞预防、流量控制，甚至可选的错误检测，统统没有。这个面向消息的最简单的传输层在提供灵活性的同时，也给实现者带来了麻烦。你的应用程序很可能需要从头实现上诉几个或大部分功能，而且每项功能都必须保证与网络中的其他主机和协议和谐共存。

与内置流量和拥塞控制以及拥塞预防的 TCP 不同，UDP 应用程序必须自己实现这些机制。拥塞处理做的不到位的 UDP 应用程序很容易堵塞网络，造成网络性能下降，严重时还会导致网络拥塞崩溃。如果你想在自己的应用程序中使用 UDP，务必要认真研究和学习当下的最佳实践和建议。RFC 5405 就是这么一份文档，它对设计单播 UDP 应用程序给出了很多设计建议，简述如下：

* 应用程序必须容忍各种因特网路径条件
* 应用程序应该控制传输速度
* 应用程序应该对所有流量进行拥塞控制
* 应用程序应该使用与 TCP 相近的带宽
* 应用程序应该准备基于丢包的重发计数器
* 应用程序应该不发送大于路径 MTU 的数据报
* 应用程序应该处理数据报丢失、重复和重排
* 应用程序应该足够稳定以支持两分钟以上的交付延迟
* 应用程序应该支持 IPv4 UDP 校验和，必须支持 IPv6 校验和
* 应用程序可以在需要时使用 keep-alive（最小间隔 15 秒）

很高兴的告诉大家：WebRTC 就是符合这些要求的框架！