Nodejs开发进阶O-扩展网络模块UDP

570 阅读10分钟

本文是《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,传输控制协议)是一种面向连接的、可靠的数据传输协议。它确保数据按顺序到达目的地,并提供可靠性措施,例如错误检测和重传。下面的表格例举了它们在工作原理和特性方面的显著区别;

特性TCPUDP
连接有连接无连接
状态维持连接状态无状态
数据比特流包/数据报
连接有连接无连接
数据顺序有序传输不保证顺序
可靠性保证容忍损失
错误处理错误丢弃错误包
握手有握手操作无握手操作
流控
传输速度相对慢相对快速
广播不支持,点对点支持广播
安全SSL/TLSDTLS

在原本的规划和设计中,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数据报的结构如下:

UDP-header (1).jpg

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