本文是《Nodejs开发进阶-扩展网络模块》的第二个部分,主要内容是UDP。和前一个部分TCP一样,这些内容都属于底层协议和技术,在实际工作中应用的场景和机会并不是很多,所以本文中主要做概念和了解性的探讨,不会过于深入。想要了解更深入详细的内容,可以参考nodejs官方技术文档的相关章节。
UDP概述
UDP(User Datagram Protocol,用户数据报协议)是互联网协议集(UDP/IP)的一部分,和TCP协议一样是传输层协议。但与TCP不同的是,UDP是无连接和不保证可靠性的协议。这意味着在数据传输之前不需要建立连接,并且不保证和确认数据报的正确完整传输,另外,UDP的包结构也比较精简和高效,这些都可以提供更好的连接和传输性能。所以,UDP是为轻量级和可容忍数据丢失的高效网络传输场景而设计的,例如音视频流的传输,这些场景中,偶尔的中断和丢失部分数据,是可以被接受的(降低服务质量,或者在应用层处理)。
和TCP一样,UDP也是一个重要的底层协议,基于UDP的上层应用协议包括:
- NTP (Network Time Protocol,网络时间协议)
- DNS (Domain Name Service,域名服务)
- BOOTP (Bootstrap Protocol,系统启动协议)
- DHCP (Dynamic Host Config Protocal,动态主机配置协议)
- NNP (Network News Protocol,网络新闻协议)
- TFTP (Trivial File Transfer Protocol,简易文件传输协议)
- RTSP (Real Time Stream Protocal,实时流协议)
- RIP (Route Information Protocal,路由信息协议)
UDP和TCP
TCP和UDP都是TCP/IP协议家族的重要成员,是其在传输层的重要实现方式。其中,TCP(Transport Controll Protocol,传输控制协议)是一种面向连接的、可靠的数据传输协议。它确保数据按顺序到达目的地,并提供可靠性措施,例如错误检测和重传。下面的表格例举了它们在工作原理和特性方面的显著区别;
| 特性 | TCP | UDP |
|---|---|---|
| 连接 | 有连接 | 无连接 |
| 状态 | 维持连接状态 | 无状态 |
| 数据 | 比特流 | 包/数据报 |
| 连接 | 有连接 | 无连接 |
| 数据顺序 | 有序传输 | 不保证顺序 |
| 可靠性 | 保证 | 容忍损失 |
| 错误 | 处理错误 | 丢弃错误包 |
| 握手 | 有握手操作 | 无握手操作 |
| 流控 | 有 | 无 |
| 传输速度 | 相对慢 | 相对快速 |
| 广播 | 不支持,点对点 | 支持广播 |
| 安全 | SSL/TLS | DTLS |
在原本的规划和设计中,TCP和UDP是为不同的场景和用途设计的,它们两个可以配合使用,来完善网络数据传输的需求和场景。例如,TCP在需要可靠数据传输和流控制的应用中使用,例如网页浏览、电子商务、电子邮件、文件传输、工业互联网等;而UDP更适合于在需要快速数据传输和低延迟的应用中使用,例如视频流、游戏和实时数据传输等。
作为传统的Web应用开发中,由于UDP的不可靠的特性,这个技术本来处于一个边缘和被遗忘的状态。但是,如果关心互联网应用和网络技术的发展,我们会发现,这种状况正在发生一些有趣的变化。
这里,笔者想要对Google表示尊重和感谢。在“重新发明了浏览器”之后,作为互联网技术发展最重要的推手,Google又试图再造一个(更好的)轮子,将HTTP协议也重新发明一遍。而且,它采取了一个非常巧妙的方式,来保证这个过程尽量的平滑顺利。这就是QUIC(Quick UDP Internet Connection,快速UDP互联网连接)协议。
我们理解,HTTP协议的基础是TCP协议。所以,HTTP应用的性能,很大程度上,其实取决于TCP协议的性能,其中最重要的,就是数据传输的性能。但是,互联网应用发展到现在,TCP已经成为整个体系的一个基础,而且基本上是唯一的基础。了解这一过程的人其实知道,在网络技术发展的早期,TCP/IP其实并不是最好的技术和选择(当时有IPX和ATM等),但主要由于其易用、开放和成本的原因,使它成为了一个事实标准。从技术体系和发展来看,TCP其实是一个非常古老的技术,随着应用越来广泛和需求的不断提升,它原有的一些设计,却慢慢的变成了限制和弱点,比如连接的过程、拥塞控制、顺序传输、队头阻塞等等,传输性能的提升,日益受到其这些设计缺陷的限制。但对其进行修改或者推倒重来,可能造成的影响太大,以至于可能根本就无法实施,唯一的选择就是另辟思路。Google的做法非常聪明,它并没有提出重新发明一个传输层协议,而是充分利用了一个现存的技术-UDP来进行改造,并且分阶段进行实施和市场渗透。事实证明,它的冒险基本上已经成功,并且已经被接收为行业标准,这就是我们看到和逐步应用的QUIC-HTTP2-HTTP3。
关于技术细节,由于篇幅的限制我们这里不进行深入讨论。我们只需要了解,QUIC充分利用了UDP协议的简单和原始,同时在传输层和应用层进行了优化,并借助新的互联网基础设施的更高性能和可靠性,通过多路UDP和简化的传输和控制,克服了TCP协议的一些缺点和限制,在应用层面实现了等同于TCP的可靠性,同时大大降低了传输延迟,并提升了传输性能。QUIC还顺便将TLS的协商集成到连接初始阶段,做到了默认协议安全的同时代价轻微,实现了默认和内置安全。
所以,我们应该理解,UDP不再是一个只适用于特殊场景的专用协议,而是将演化成为新一代互联网技术的基础性技术和协议,甚至有机会替代TCP而成为主流的传输技术协议。
nodejs的UDP模块
在nodejs中,提供了UDP/dgram(数据报)模块,来负责UDP协议的操作和实现。
一些重要概念和选项
在使用和对UDP协议进行开发之前,笔者觉得有必要先来了解一些相关的重要概念,以及协议应用中的选项。
- UDP数据结构
前面已经提到,UDP的传输方式和TCP不同,它并不使用数据流的方式来传输数据,而是使用“数据报”的模式,其特点是结构比较简单,也没有复杂的内容和流控制机制,容易处理和操作。一个典型的UDP数据报的结构如下:
- type:
指upd网络类型,可以包括upd4和upd6,对应ipv4和ipv6网络
- MTU(Maximum Transmission Unit,最大传输单位)
MTU是指在网络通信中,一个数据包的最大长度。为了适应各种传输的数据和场景,在网络中,发送端在发送数据时,需要将数据分割成多个数据包进行传输,这时每个数据包的大小不能超过MTU的限制。然后,接收端在接收数据包时,需要对每个数据包进行重组,再还原成完整的数据。MTU的大小因具体的网络环境和实现不同而不同,常见MTU值有1500字节(以太网)、1492字节(PPPoE)、576字节(DialUp)等。
由于缺乏分片和重组机制, UDP报文的长度不能超过链路MTU,否则会导致IP层分片并丢失。因此,应用层在发送UDP报文时需要考虑MTU的限制。合理配置MTU不仅可提高网络传输效率,还可避免不必要的数据包分片,确保网络通信的顺畅和可靠。
- TTL(Time To Live, 生存期)
TTL在网络通信中用来控制数据包在网络中可以传输的最大跳数(hop)。TTL是一个8位的数值, 每经过一次路由跳转,TTL就会减一,当TTL为零时,路由器就会丢弃这个数据报,并向源端发送相关的ICMP报文。这样,通过设置合理的初始TTL值,就可以控制数据包在网络中的存活时间和传输范围,可以防止其在网络中过多的传输,减缓网络拥塞,并增强安全性,提高网络通信的可靠性。
TTL这个概念在很多信息技术场合特别是通信机制中都会遇到,比如UDP、DNS等都有。
实现和示例代码
其实,UDP的基本工作原理和实现代码,和TCP是很像的,这里还是使用一个改进过的示例代码来进行展示和分析:
const
dgram = require('node:dgram'),
PORT = 20000;
// create server
const server = dgram.createSocket('udp4')
.on('error', (err) => {
console.error(`server error:\n${err.stack}`);
server.close();
})
.on('message', (msg, rinfo) => {
msg = Buffer.from(msg).toString();
console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
// reply
msg = msg.split("").reverse().join("");
server.send(msg,rinfo.port,rinfo.address);
})
.on('listening', () => {
console.log(`udp server listening at ${PORT}`);
})
.bind(PORT);
// client
const send = ()=>{
const message = Buffer.from('China中国');
const client = dgram.createSocket('udp4');
client.on("message", (msg, rinfo)=>{
console.log(`client got: ${msg} `);
client.close();
});
client
.connect(PORT, 'localhost', (err) => {
client.send(message);
});
}; send();
简单分析说明一下:
- 在nodejs中UDP的模块名称为dgram
- 可以使用createSocket方法并指定udp类型,来创建一个插座socket,可以理解成为一个逻辑通道
- 这个socket,在客户端和服务端的用法是一致的,差异就是是否启动侦听(bind)
- 调用socket.bind方法,并指定端口,来侦听UDP网络端口,就创建了UDP服务端
- 监听onMessage方法,可以用于接收数据
- 在客户端和服务端都调用send来发送数据
- 在客户端,使用connect来尝试连接服务端,客户端使用的端口是随机的
- 服务端可以在消息事件回调中,查看并使用客户端网络参数
从这个流程可以看到,使用UDP协议实现数据的收发,其实更加简单,当然,UDP不保证服务质量,可能需要在应用层自己实现数据的完整性校验和重发机制,来保证数据的正确传输(就跟Google做的那样)。
此外,如果要对UDP模块进行更细致的控制,可用的事件方法包括:
- close: socket关闭
- connect: socket连接成功
- error: 错误
- listening: socket侦听
- message: 消息到达和接收
其他UDP模块提供的特性和选项,很多和TCP模块类似。关于UDP的一些独特的项目还包括多播设置、TTL、BufferSize等。这已经不是本文重点关注的内容了,有兴趣的读者可以自行深入研究和了解。
小结
本文讨论了TCP/IP协议家族中的另一个重要的协议UDP,包括简单的工作原理并和TCP协议进行了比较。然后通过示例代码,简单探讨了Nodejs中关于UDP实现和操作的模块dgram。