🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
第 3 章 UDP的构成 1980 年 8 月,紧随 TCP/IP 之后, UDP( User Datagram Protocol,用户数据报协议)被 John Postel 加入了核心网络协议套件 。当时,正值 TCP 和 IP 规范分立为两个单独的 RFC。这个时间点非常重要,稍后我们会看到, UDP 的主要功能和亮点并不在于它引入了什么特性,而在于它忽略的那些特性。 UDP 经常被称为无( Null)协议, RFC 768 描述了其运作机制,全文完全可以写在一张餐巾纸上。 * 数据报 一个完整、独立的数据实体,携带着从源节点到目的地节点的足够信息,对这些节点间之前的数据交换和传输网络没有任何依赖。 数据报( datagram)和分组( packet)是两个经常被人混用的词,实际上它们还是有区别的。分组可以用来指代任何格式化的数据块,而数据报则通常只用来描述那些通过不可靠的服务传输的分组,既不保证送达,也不发送失败通知。正因为如此,很多场合下人们都把 UDP 中 User(用户)的 U,改成 Unreliable(不可靠)的 U,于是 UDP 就成了“不可靠数据报协议”( Unreliable Datagram Protocol)。这也是为什么把UDP 分组称为数据报更为恰当的原因。 关于 UDP 的应用,最广为人知同时也是所有浏览器和因特网应用都赖以运作的,就是 DNS( Domain Name System,域名系统)。 DNS 负责把对人类友好的主机名转换成 IP 地址。可是,尽管浏览器有赖于 UDP,但这个协议以前从未被看成网页和应用的关键传输机制。 HTTP 并未规定要使用 TCP,但现实中所有 HTTP 实现(以及构建于其上的所有服务)都使用 TCP。 不 过, 这 都 是 过 去 的 事 了。 IETF 和 W3C 工 作 组 共 同 制 定 了 一 套 新 API——WebRTC( Web Real-Time Communication, Web 实时通信)。 WebRTC 着眼于在浏览器中通过 UDP 实现原生的语音和视频实时通信,以及其他形式的 P2P( Peer-to-Peer,端到端)通信。正是因为 WebRTC 的出现, UDP 作为浏览器中重要传输机制的地位才得以突显,而且还有了浏览器 API !本书将在第 18 章再探讨 WebRTC,本章我们先来介绍一下 UDP 协议的工作原理,搞清楚为什么以及什么时候会用到它。 ### 3.1 无协议服务 要理解为什么 UDP 被人称作“无协议”,必须从作为 TCP 和 UDP 下一层的 IP 协议说起。 IP 层的主要任务就是按照地址从源主机向目标主机发送数据报。为此,消息会被封装在一个 IP 分组内(图 3-1),其中载明了源地址和目标地址,以及其他一些路由参数。注意,数据报这个词暗示了一个重要的信息: IP 层不保证消息可靠的交付,也不发送失败通知,实际上是把底层网络的不可靠性直接暴露给了上一层。如果某个路由节点因为网络拥塞、负载过高或其他原因而删除了 IP 分组,那么在必要的情况下, IP 的上一层协议要负责检测、恢复和重发数据。 ![图 3-1: IPv4 首部( 20 字节)](https://box.kancloud.cn/c686dced25a63b98ee5f0f5cf533c70a_697x267.png) 图 3-1: IPv4 首部( 20 字节) UDP 协议会用自己的分组结构(图 3-2)封装用户消息,它只增加了 4 个字段:源 端口、目标端口、分组长度和校验和。这样,当 IP 把分组送达目标主机时,该主机 能够拆开 UDP 分组,根据目标端口找到目标应用程序,然后再把消息发送过去。仅 此而已。 UDP的构成 | 33 ࿋ ᇸ܋੨ ࢅ ၯᄓ܈׊ ࢁ৫ ణՔ܋੨ 图 3-2: UDP 首部( 8 字节) 事实上, UDP 数据报中的源端口和校验和字段都是可选的。 IP 分组的首部也有校验 和,应用程序可以忽略 UDP 校验和。也就是说,所有错误检测和错误纠正工作都可 以委托给上层的应用程序。说到底, UDP 仅仅是在 IP 层之上通过嵌入应用程序的 源端口和目标端口,提供了一个“应用程序多路复用”机制。明白了这一点,就可 以总结一下 UDP 的无服务是怎么回事了。 • 不保证消息交付 不确认,不重传,无超时。 • 不保证交付顺序 不设置包序号,不重排,不会发生队首阻塞。 • 不跟踪连接状态 不必建立连接或重启状态机。 • 不需要拥塞控制 不内置客户端或网络反馈机制。 TCP 是一个面向字节流的协议,能够以多个分组形式发送应用程序消息,且对分组 中的消息范围没有任何明确限制。因此,连接的两端存在一个连接状态,每个分组 都有序号,丢失还要重发,并且要按顺序交付。相对来说, UDP 数据报有明确的限 制:数据报必须封装在 IP 分组中,应用程序必须读取完整的消息。换句话说,数据 报不能分片。 UDP 是一个简单、无状态的协议,适合作为其他上层应用协议的辅助。实际上,这 个协议的所有决定都需要由上层的应用程序作出。不过,在急着去实现一个协议来 扮演 TCP 的角色之前,你还应该认真想一想这里涉及的复杂细节,比如 UDP 要与 很多中间设备打交道( NAT 穿透),再想一想设计网络协议的那些最佳实践。如果 没有周密的设计和规划,一流的构想也可能沦为二流的 TCP 实现。 TCP 中的算法和 状态机已经经过了几十年的磨合与改进,而且吸收几十种并不那么容易重新实现的 机制。 34 | 第 3 章 3.2 UDP与网络地址转换器 令人遗憾的是, IPv4 地址只有 32 位长,因而最多只能提供 42.9 亿个唯一 IP 地址。 1990 年代初,互联网上的主机数量呈指数级增长,但不可能所有主机都分配一个唯 一的 IP 地址。 1994 年,作为解决 IPv4 地址即将耗尽的一个临时性方案, IP 网络地 址转换器( NAT, Network Address Translator) 规范出台了,这就是 RFC 1631。 建议的 IP 重用方案就是在网络边缘加入 NAT 设备,每个 NAT 设备负责维护一个 表,表中包含本地 IP 和端口到全球唯一(外网) IP 和端口的映射(图 3-3)。这样, NAT 设备背后的 IP 地址空间就可以在各种不同的网络中得到重用,从而解决地址 耗尽问题。 ՗࣑ገ ాྪ ੨܋ ྔྪ ੨܋ 图 3-3: IP 网络地址转换器 然而,这个临时性的方案居然就那么一直沿用了下来(这种现象倒也不鲜见)。新增 的 NAT 设备不仅立杆见影地解决了地址耗尽问题,而且迅速成为很多公司及家庭 代理和路由器、安全装置、防火墙,以及其他许多硬件和软件设备中的内置组件。 NAT 不再是个临时性方案,它已经成了因特网基础设施的一个组成部分。 保留的私有网络地址范围 作为监管全球 IP 地址分配的机构, IANA( Internet Assigned Numbers Authority, 因特网号码分配机构)为私有网络保留了三段 IP 地址,这些 IP 地址经常可以在 NAT 设备后面的内网中看到。 UDP的构成 | 35 表3-1:保留的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 设备会将它们转换成外网地址。 为防止路由错误和引起不必要的麻烦,不允许给外网计算机分配这些保留的私有 IP 地址。 3.2.1 连接状态超时 NAT 转换的问题(至少对于 UDP 而言)在于必须维护一份精确的路由表才能保证 数据转发。 NAT 设备依赖连接状态,而 UDP 没有状态。这种根本上的错配是很多 UDP 数据报传输问题的总根源。况且,客户端前面有很多个 NAT 设备的情况也不 鲜见,问题由此进一步恶化了。 每个 TCP 连接都有一个设计周密的协议状态机,从握手开始,然后传输应用数据, 最后通过明确的信号确认关闭连接。在这种设计下,路由设备可以监控连接状态, 根据情况创建或删除路由表中的条目。而 UDP 呢,没有握手,没有连接终止,实际 根本没有可监控的连接状态机。 发送出站 UDP 不费事,但路由响应却需要转换表中有一个条目能告诉我们本地目标 主机的 IP 和端口。因此,转换器必须保存每个 UDP 流的状态,而 UDP 自身却没有 状态。 更糟糕的是, NAT 设备还被赋予了删除转换记录的责任,但由于 UDP 没有连接终 止确认环节,任何一端随时都可以停止传输数据报,而不必发送通告。为解决这个 问题, UDP 路由记录会定时过期。定时多长?没有规定,完全取决于转换器的制造 商、型号、版本和配置。因此,对于较长时间的 UDP 通信,有一个事实上的最佳做 法,即引入一个双向 keep-alive 分组,周期性地重置传输路径上所有 NAT 设备中转 换记录的计时器。 36 | 第 3 章 TCP 超时和 NAT 从技术角度讲, NAT 设备不需要额外的 TCP 超时机制。 TCP 协议就遵循一个设计 严密的握手与终止过程,通过这个过程就可以确定何时需要添加或删除转换记录。 遗憾的是,实际应用中的 NAT 设备给 TCP 和 UDP 会话应用了类似的超时逻辑。 这样就导致 TCP 连接有时候也需要双向 keep-alive 分组。如果你的 TCP 连接突然 断开,那很有可能就是中间 NAT 超时造成的。 3.2.2 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:由于没有映射规则,入站分组直接被删除 UDP的构成 | 37 需要注意的是,上述行为对客户端应用程序不是问题。客户端应用程序基于内部网 络实现交互,会在交互期间建立必要的转换记录。不过,如果隔着 NAT 设备,那 客户端(作为服务器)处理来自 P2P 应用程序( VoIP、游戏、文件共享)的入站连 接时,就必须面对 NAT 穿透问题。 为解决 UDP 与 NAT 的这种不搭配,人们发明了很多穿透技术( TURN、 STUN、 ICE),用于在 UDP 主机之间建立端到端的连接。 3.2.3 STUN、 TURN与ICE STUN( Session Traversal Utilities for NAT)是一个协议( RFC 5389),可以让应用 程序发现网络中的地址转换器,发现之后进一步取得为当前连接分配的外网 IP 地址 和端口(图 3-5)。为此,这个协议需要一个已知的第三方 STUN 服务器支持,该服 务器必须架设在公网上。 ޜခഗ ஘੨܋ࢅ኷ں๟ྔྪJQ IPǖ 50.76.44.114ā܋੨ǖ 23123 图 3-5: STUN 查询外网 IP 地址和端口 假设 STUN 服务器的 IP 地址已知(通过 DNS 查找或手工指定),应用程序首先向 STUN 服务器发送一个绑定请求。然后, STUN 服务器返回一个响应,其中包含在外 网中代表客户端的 IP 地址和端口号。这种简单的方式解决了前面讨论的一些问题: • 应用程序可以获得外网 IP 和端口,并利用这些信息与对端通信; • 发送到 STUN 服务器的出站绑定请求将在通信要经过的 NAT 中建立路由条目, 使得到达该 IP 和端口的入站分组可以找到内网中的应用程序; • STUN 协议定义了一个简单 keep-alive 探测机制,可以保证 NAT 路由条目不超时。 有了这个机制,两台主机端需要通过 UDP 通信时,它们首先都会向各自的 STUN 服务器发送绑定请求,然后分别使用响应中的外网 IP 地址和端口号交换数据。 但在实际应用中, STUN 并不能适应所有类型的 NAT 和网络配置。不仅如此,某些 情况下 UDP 还会被防火墙或其他网络设备完全屏蔽。这种情况在很多企业网很常 38 | 第 3 章 见。为解决这个问题,在 STUN 失败的情况下,我们还可以使用 TURN( Traversal Using Relays around NAT)协议( RFC 5766)作为后备。 TURN 可以在最坏的情况 下跳过 UDP 而切换到 TCP。 TURN 中的关键词当然是中继( relay)。这个协议依赖于外网中继设备(图 3-6)在 两端间传递数据。 ዐीޜခഗ 图 3-6: TURN 中继服务器 • 两端都要向同一台 TURN 服务器发送分配请求来建立连接,然后再进行权限协商。 • 协商完毕,两端都把数据发送到 TURN 服务器,再由 TURN 服务器转发,从而 实现通信。 很明显,这就不再是端对端的数据交换了! TURN 是在任何网络中为两端提供连接 的最可靠方式,但运维 TURN 服务器的投入也很大。至少,为满足传输数据的需 要,中继设备的容量必须足够大。因此,最好在其他直连手段都失败的情况下,再 使用 TURN。 现实中的 STUN 和 TURN 谷歌的 libjingle 是一个用 C++ 开发的用于构建端到端应用程序的开源库,负责在 后台实现 STUN、 TURN 和 ICE 协商。谷歌聊天软件 Google Talk 使用的就是这个 库,其文档也为我们考量现实中的 STUN 与 TURN 性能提供了有价值的参考: • 92% 的时间可以直接连接( STUN); • 8% 的时间要使用中继器( TURN)。 由于 NAT 设备遍地都是,相当一部分用户不能通过 STUN 直接建立 P2P 连接。 若要提供可靠的 UDP 服务,现实中必须同时支持 STUN 和 TURN。 构建高效的 NAT 穿透方案可不容易。好在,我们还有 ICE( Interactive Connectivity Establishment)协议( RFC 5245)。 ICE 规定了一套方法,致力于在通信各端之间建立一 UDP的构成 | 39 条最有效的通道(图 3-7):能直连就直连,必要时 STUN 协商,再不行使用 TURN。 ዐीޜခഗ ޜခഗ ޜခഗ 图 3-7: ICE 先后尝试直连、 STUN 和 TURN 实际开发中,如果你想构建基于 UDP 的 P2P 应用程序,绝对应该选择现有的平台 API,或者实现了 ICE、 STUN 和 TURN 的第三方库。好了,了解了这些协议的用 途之后,接下来自然就要考虑安装和配置了! 3.3 针对UDP的优化建议 UDP 是一个简单常用的协议,经常用于引导其他传输协议。事实上, UDP 的特色在 于它所省略的那些功能:连接状态、握手、重发、重组、重排、拥塞控制、拥塞预 防、流量控制,甚至可选的错误检测,统统没有。这个面向消息的最简单的传输层 在提供灵活性的同时,也给实现者带来了麻烦。你的应用程序很可能需要从头实现 上述几个或者大部分功能,而且每项功能都必须保证与网络中的其他主机和协议和 谐共存。 与内置流量和拥塞控制以及拥塞预防的 TCP 不同, UDP 应用程序必须自己实现这 些机制。拥塞处理做得不到位的 UDP 应用程序很容易堵塞网络,造成网络性能下 降,严重时还会导致网络拥塞崩溃。如果你想在自己的应用程序中使用 UDP,务必 要认真研究和学习当下的最佳实践和建议。 RFC 5405 就是这么一份文档,它对设计 单播 UDP 应用程序给出了很多设计建议,简述如下: • 应用程序必须容忍各种因特网路径条件; • 应用程序应该控制传输速度; • 应用程序应该对所有流量进行拥塞控制; • 应用程序应该使用与 TCP 相近的带宽; 40 | 第 3 章 • 应用程序应该准备基于丢包的重发计数器; • 应用程序应该不发送大于路径 MTU 的数据报; • 应用程序应该处理数据报丢失、重复和重排; • 应用程序应该足够稳定以支持 2 分钟以上的交付延迟; • 应用程序应该支持 IPv4 UDP 校验和,必须支持 IPv6 校验和; • 应用程序可以在需要时使用 keep-alive(最小间隔 15 秒)。 设计新传输协议必须经过周密的考虑、规划和研究,否则就是不负责任。要尽可能 利用已有的库或框架,这个库或框架应该考虑了 NAT 穿透,而且能够与其他并发 的网络流量和谐共存。 我很高兴告诉大家: WebRTC 就是符合这些要求的框架! 41