第3章UDP篇
1980年8月,用户数据报协议(UDP)由John Postel添加到到核心网络协议族中,UDP协议起始于TCP/IP协议之后,但和TCP和IP规范被分裂成为两个独立的RFC的时间差不多。这个时机是非常重要的,因为正如我们将看到的,UDP重要的特点不是他带了什么新特性,而是他忽略了的那些特性。UDP(RFC 768)是通俗称为空协议,它描述的操作,基本上可以容纳在一张餐巾纸上。
数据报 一个自包含的,独立的数据实体,其承载了足够的信息,使其可以从源路由到达目标路由,而不依赖于在网络节点间和底层传输网络中的前面数据包。
数据报文(Datagram)和数据包(Packet)两个术语往交替使用,但其实二者有一些细微差别。数据包(packet)一般用来描述任何格式的数据块,而数据报(Datagram)往往被保留用来描述通过一个不可靠的服务传输的数据包(Packet) - 没有传输保障,没有失败通知。正因为如此,你经常发现有人用不可靠(Unreliable)来替代UDP官方定义中的User,我们可以理解成“不可靠的数据报协议”。这也是为什么UDP包一般或者说更准确的被称为数据报(Datagram)。
UDP的一个最著名的也是所有浏览器和网络应用都要依赖的应用就是DNS服务,任何一个Host名称,我们需要在数据交换之前获取它的IP地址。不过,即使是浏览器本身依赖于UDP的,但UDP从来没有作为网页获取和浏览器应用传输协议的第一选择。当然,WebRTC的出现,情况有所变化了。
新的Web实时通信(WebRTC)标准,由IETF和W3C工作组共同制定,实现实时通信,如语音和视频呼叫,以及其他形式的对等(P2P)通信,其就是在浏览器中采用了UDP。WebRTC中,UDP是首选的传输协议。我们将在第18章中深入讨论WebRTC ,但在此之前,我们首先探讨一下UDP协议的内部运作,了解一下WebRTC为什么选择UDP协议。
空协议服务
要了解UDP和为什么它通常被称为“空协议”,我们首先需要了解一下互联网协议(IP),它位于TCP和UDP协议层下面。
IP层主要任务就是基于地址将数据报从源主机发送到目的主机。要做到这一点,消息都封装在一个IP包( 图3-1 ),标识源和目的地址,以及一些其他路由参数。
我们再次强调一下上面提到的数据报这个术语的含义:IP层提供了不可靠的数据传输,既没有消息确认,也没有丢失通知, IP层直接把这一层的不可靠性暴露给上层。如果一个数据报在传输过程中因为某个路由节点拥塞,高负荷,或因其他原因丢失,那么由IP上层的协议来检测,恢复,并重传数据 - 当然这是在上层有这个需求的时候!
图3-1 IPv4报头(20字节)
UDP协议在IP包的基础上增加了新的报头( 图3-2 ),它只增加了四个额外的字段:源端口,目的端口,数据包长度,数据校验消息。因此,当IP层传送数据包到目的主机时,主机解开UDP数据包,通过目的端口识别目标应用程序,并发送消息。除此之外,再无其他。
图3-2 UDP报头(8字节)
事实上,UDP报头中的源端口和校验字段都是可选字段。IP数据包中已经包含它自己的报头校验和,应用层完全可以选择忽略UDP的校验字段,这意味着UDP层所有的错误检测和纠错,可以委托给上述应用层校验。其核心,UDP只是在IP层之上提供了“应用层复用”特性,也就是嵌入了源和目标端口。考虑到这一点,我们现在可以总结UDP所有不能提供的服务:
无消息传输保证
没有确认,重发,或超时
无法保证按序传输
没有数据包的序列号,没有重新排序,无线头阻塞
无连接状态跟踪
没有连接建立或关闭的状态机
没有拥塞控制
无内置的客户端或网络反馈机制
TCP是一个面向字节流的协议,能够通过多个数据包发送应用程序的消息数据,包内本身没有任何明确的消息边界。为了实现这一目标,连接两端都分配了连接状态,并且数据包被排序,重发丢包,按顺序发送。相反UDP数据报有明确的界限:每一个数据报都被打包到一个IP包中,应用层读到的每一个UDP包都是完整的信息 - 数据包不能被分割。
UDP是一个简单的,无状态的协议,适合于引导上层的其他应用层协议 - 几乎所有的协议决策都留给它上面的应用层。然而,在你想实现自己的协议来取代TCP,你应该仔细考虑有关的复杂性,如UDP与其它层的交互(比如NAT穿越),以及网络协议一些最佳实践。没有仔细的规划和设计,设计一个新的协议不是一个好主意,最终也许实现成一个的简陋的TCP版本。各种算法和TCP状态机已经过几十年的锤炼和提升,并已采取几十种机制来保证他的性能。
UDP和网络地址转换
非常不幸,IPv4地址只有32位长,它提供了最多42.9亿的IP地址。1994年中旬(RFC 1631),IP网络地址转换(NAT)规范,作为一个临时的解决方案,被提出来解决IPv4地址枯竭的问题 - 在上世纪90年代初期,互联网上的主机的数量开始成倍增长,我们根本无法为每台主机分配一个唯一的IP。
建议的IP重用的解决方案是在边缘网络中引入NAT设备,NAT将负责为维护本地IP和端口的元组到一个或多个全球唯一的(公共)IP地址和端口的元组的映射关系(图3 -3 )。NAT内部的本地IP地址空间可以被许多不同的子网络重用,从而解决地址耗尽的问题。
图3-3 IP网络地址转换
不幸的也是经常发生的是,临时方案最后总是变成最终方案。NAT设备不仅仅用来解决IP地址枯竭的问题,他们也很快成为一个无处不在的网络部件,包括许多企业和家庭代理和路由器,安全设备,防火墙,和几十个其他的硬件和软件设备都包含了NAT功能。NAT不再是临时方案了,它已经成为互联网基础设施的一个组成部分。
保留的私人网络地址范围
互联网编号分配机构(IANA),这是一个负责全球IP地址分配的机构,预留了三个著名的私人网络段,经常用在NAT设备内部网络:
表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地址。
连接状态超时
NAT转换的关键问题,至少对UDP而言,是它必须保存数据传输的路由表。NAT依赖网络连接状态,而UDP恰好没有 - 这是一个严重的不匹配,这也为UDP传输问题的根源。此外,现在很普通的一个情况就是NAT内网的设备有很多层,这只会使问题进一步复杂化。
每个TCP连接有一个明确的协议状态机,开始三次握手,跟着开始数据传输,最后关闭连接,有一个完整的流程。基于这种流程,NAT可以观察到每个连接状态,并可以根据需要创建和删除的路由条目。而UDP,既没有握手,也没有连接终止,同时没有任何状态机来监控连接状态。
通过UDP往外发送数据并不需要任何额外的工作,但请求的答复却需要NAT维护路由表,用来识别本地目标主机的IP和端口。因此,NAT必须保持每个UDP流的路由表信息,因为UDP是无状态的。
更糟的是,NAT需要知道什么时候清除路由记录,但UDP没有连接终止序列,任何时候,两端都可以停止发送数据包,不带任何通知。为了解决这个问题,UDP路由记录有一个老化定时器。这个定时器到底多长?”基本上没有明确的答案,而是取决于设备提供商,版本,配置等。因此,事实上长时间运行的UDP会话的最佳实践之一就是引入双向 keepalive报文,定期的重置路由上所有的NAT设备的老化计时器。
TCP超时和NAT
从技术原理上来说,在NAT设备没有必要为TCP连接提供超时老化机制。TCP协议有一个良好握手机制和终止序列包,NAT可以清晰的根据这些信息来添加或者删除路由记录。
不幸的是,在实际应用中,许多NAT设备为TCP提供了类似UDP的老化计时器。其结果是,在某些情况下,TCP连接也需要双向Keepalive报文。如果你的TCP连接突然下降,也许就是NAT设备的老化机制惹的祸。
NAT穿越
不可预知的连接状态管理是NAT的一个严重问题,但对于许多应用程序的一个更大的问题是根本无法建立UDP连接。这对很多应用譬如P2P,如VoIP,游戏,文件共享等来说更是如此。这些应用往往通信双方需要同时充当客户端和服务端角色,使其能双向通信。
第一个问题是,在有NAT的场景下,内部客户端不知道它的公网IP??:它只知道它的内部IP地址,NAT设备对每一个UDP数据包进行重写,修改UDP包的源端口和地址,以及IP层的源IP地址。但是,如果客户端将私有IP地址作为应用层数据的一部分与外部网络地址进行通信,那么连接将不可避免地失败。因此,NAT这种“透明”的转换就有问题了,应用程序必须先发现它的公网IP??地址,如果它需要与外部网络中的一个地址进行通信。
然而,仅仅知道的自己的公网IP是无法保证UDP传输成功的。任何数据包到达拥有公网IP的NAT设备后??,也??有一个目的端口,NAT路由表中必须有一个外网IP端口与内网地址和端口的映射记录,数据才能真正达到目的地址。如果这个记录不存在,那么数据包被简单地丢弃(图3-4 )。NAT作为一个简单的包过滤器,它没有办法自动确定内部路由映射关系,除非用户通过端口转发或者类似机制显式在NAT上进行了登记。
图3-4 由于缺少映射记录,收到的包被丢弃
需要注意的是,上面描述的问题对于客户端应用程序来说不是一个问题,客户端从内部网络中先发起连接,NAT自然会添加相应的路由。但是,对于那种需要主动接收连接(内网主机作为服务器)的应用如P2P应用(如VoIP),游戏终端,文件共享等等,就会碰到这个问题。
为了解决这种UDP的穿越问题,各种穿越技术(STUN,TURN,ICE)被提出了,用于建立在 两个内网主机之间建立端至端的连接。
STUN,TURN,ICE
STUN(RFC 5389)协议是一种允许主机应用程序发现网络中的NAT设备,并借助其来为当前连接分配公网IP??和端口元组(图3-5 )的方案。要做到这一点,该协议需要借助一个第三方部署在公网上的STUN服务器。
图3-5 STUN查询公网IP和端口
假设STUN服务器的IP地址是可知的(通过DNS发现,或通过手动指定的地址),应用程序首先发送绑定请求到STUN服务器。相应的,STUN服务器回复一个响应,其中包含为其分配的客户端对外暴露的公网IP??地址和端口。这个简单的流程解决我们了我们前面讨论中遇到的几个问题:
- 该程序通过该方式获取了其公网IP和端口的元组,并使用这个信息,作为其应用数据的一部分,就能够与对端进行通信。
- 向STUN服务器发送的请求,也同时在NAT上建立了路由映射记录,这确保了对端的请求可以准备达到内部网络中的应用。
- STUN协议定义了一个简单的机制来保持NAT上的路由老化。
有了这个机制,两端需要通过UDP进行通信时,他们会先发送绑定请求到各自的STUN服务器,收到各自STUN服务器的响应,然后他们可以使用各自分配的公共IP地址和端口进行数据交换了。
然而,在实际应用中,STUN是不足以处理所有的NAT的拓扑结构和网络配置。此外,不幸的是,在某些情况下,UDP可能会被防火墙或其他一些网络设备完全阻止 - 这种许多企业网络中不是一种罕见的情景。为了解决这个问题,只要STUN失败,我们还可以使用TURN协议(RFC 5766)作为备用方案,它可以运行在UDP上,还可以将UDP转换成TCP。
TRUN方案的关键就是中继(relay)。该协议依赖于公网上的中继来保证私网主机的可见性和可用性( 图3-6 )。
图3-6 TURN中继服务器
- 两端都向相同的TURN服务器发送地址分配请求,其次是权限协商。
- 一旦协商完成后,两端通过将数据发送到TURN服务器,并由TURN进行转发到对端的方式进行互相通信。
当然,这种通信方式的最明显的缺点就是他不再是P2P的通信。他需要依赖于TURN服务器来保证可靠的传输,TURN服务器成为一个瓶颈,维护TURN的成本将很高,至少TURN服务器需要足够的带宽来保证所有的数据流。因此,TURN方案最好作为最后的备用方案,只有在其他方案都失效的情况下才能使用。
STUN和TURN实践
google提供的Libjingle,是一个开放源码的C + +库,可以用它来创建P2P的应用,它在底层实现了STUN,TURN,ICE等协商。这个库用在Google Talk中,库文档为STUN与 TURN在现实世界中的性能提供了有价值的参考点:
- 92%的时间可以直接连接方案(STUN)
- 8%的时间连接需要一个中转器(TURN)
不幸的是,即使采用STUN方案,有部分用户还是无法建立直接的P2P隧道。为了提供可靠的服务,我们还需要TURN中继,它可以作为STUN方案不可用情况下的一个备选方案。
建立一个有效的NAT穿越解决方案,不是一件简单容易的事情。值得庆幸的是,我们可以借助ICE协议(RFC 5245)来帮助我们完成这一任务。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中,特别强调了应用程序通过单播UDP传送数据设计指南。下面是一个简短的例子:
- 应用必须忍受变化的互联网路径
- 应用应控制传输速率
- 应用应当实现所有流量拥塞控制
- 应用应该使用和TCP同等的带宽
- 应用当丢包时应该回退重传计数器
- 应用不应该发送超过MTU的数据报
- 应用应该处理数据报的丢失,重复,重新排序
- 应用应该是确保可以支持两分钟的延迟
- 应用应该启用IPv4 UDP校验,必须启用IPv6校验
- 应用可能在需要的时候使用保活(最小间隔15秒)
设计一个新的传输协议,需要很多的认真思考,规划和研究 - 做你的尽职调查。在可能的情况下,充分利用现有的库或已经采用了一个现有框架来实现NAT穿越,使其能够与其他来源的网络建立某种程度的公平通信。
关于这一点,好消息, WebRTC就是这样一个框架!