You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
173 lines
14 KiB
173 lines
14 KiB
6 years ago
|
---
|
||
|
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 就是符合这些要求的框架!
|