在 TCP 之外,Node.js 通过 dgram 模块提供了对 UDP(用户数据报协议) 的支持。与 TCP 不同,UDP 是一种无连接的、基于数据报的传输层协议,它不保证数据的可靠送达,也不保证接收顺序,但它省去了握手、确认和重传的开销,因此在实时音视频、在线游戏、服务发现、日志收集等对延迟敏感而对少量丢包容忍度较高的场景中非常有用。
UDP 通信的特点
- 无连接:通信前无需建立连接,发送端直接将数据发往目标地址和端口,接收端直接监听端口等待数据报到达。
- 不可靠传输:数据报可能丢失、重复或乱序,应用层需要自行处理可靠性(如果需要)。
- 面向数据报:每次发送的数据是一整条消息,接收端要么收到完整消息,要么收不到,不存在 TCP 中的“粘包”问题。
- 支持广播和多播:UDP 可以向子网内所有主机发送(广播),或向一组订阅者发送(多播),实现一对多的快速通信。
- 首部开销小:相较于 TCP 的最小 20 字节,UDP 首部只有 8 字节,有效负载更高。
dgram 模块提供的正是这种轻量级的数据报通信能力。
创建 UDP 套接字
const dgram = require('dgram');
// 创建 IPv4 UDP 套接字,可带 options 配置
const server = dgram.createSocket('udp4');
// 或创建 IPv6 套接字:dgram.createSocket('udp6');
dgram.createSocket 返回一个 Socket 对象,该对象继承自 EventEmitter,我们可以通过事件监听来处理数据收发和错误。
实现一个简单的 UDP 服务端
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
server.on('message', (msg, rinfo) => {
console.log(`收到消息: ${msg} 来自 ${rinfo.address}:${rinfo.port}`);
// 可以选择回复客户端
const response = Buffer.from('收到');
server.send(response, rinfo.port, rinfo.address, (err) => {
if (err) console.error('回复失败', err);
});
});
server.on('listening', () => {
const address = server.address();
console.log(`UDP 服务器正在监听 ${address.address}:${address.port}`);
});
// 绑定端口
server.bind(41234);
服务端通过 bind 绑定到一个端口,之后可以接收任意客户端发来的数据报。当有消息到达时,触发 message 事件,回调中的 msg 是 Buffer,rinfo 包含来源地址和端口。服务端可以使用 send 方法向来源回复数据。
实现一个 UDP 客户端
const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const message = Buffer.from('Hello UDP');
client.send(message, 0, message.length, 41234, 'localhost', (err) => {
if (err) console.error('发送失败', err);
else console.log('消息已发送');
});
// 接收服务端的回复
client.on('message', (msg, rinfo) => {
console.log(`收到回复: ${msg} 来自 ${rinfo.address}:${rinfo.port}`);
client.close(); // 完成任务后关闭套接字
});
客户端通过 send 向指定端口发送数据,也可以绑定一个端口(不指定时系统会自动分配)。注意客户端和服务器本质上是一样的,UDP 没有“客户端”和“服务器”的固定区分,任何套接字都可以接收数据。
发送和接收的细节
send 方法的完整签名如下:
socket.send(msg, offset, length, port, address, callback);
// 或使用字符串和回调
socket.send(msg, port, address, callback);
msg 可以是 Buffer 或字符串(内部转为 Buffer)。offset 和 length 用于从 Buffer 中截取要发送的片段,方便从复用缓冲区。
message 事件提供的 msg 是接收到的完整数据报,rinfo 对象包含 address、port 和 family (IPv4/IPv6)。如果消息来自 IPv6,family 为 'IPv6'。
常用事件与错误处理
除了 message 和 listening,Socket 还有几个关键事件:
error:当发生错误时触发,务必监听它,否则错误会导致进程退出。
server.on('error', (err) => {
console.error('服务器异常:', err);
server.close();
});
close:套接字关闭后触发。调用socket.close()后,套接字会停止接收新数据报,并触发此事件。
UDP 广播和多播
广播和多播是 UDP 擅长的领域。dgram 模块直接支持这两种通信模式。
广播:向局域网内所有设备发送消息,需要显式启用广播。
const dgram = require('dgram');
const socket = dgram.createSocket('udp4');
// 启用广播
socket.bind(() => {
socket.setBroadcast(true);
});
const msg = Buffer.from('服务发现广播');
// 广播到 255.255.255.255 的 41235 端口
socket.send(msg, 0, msg.length, 41235, '255.255.255.255', (err) => {
if (err) console.error(err);
else console.log('广播已发送');
});
注意事项:
- 只能使用
udp4套接字。 - 目标地址应为
'255.255.255.255'(受限广播)或特定子网的广播地址(如'192.168.1.255')。 - 发送广播时不需要与接收端建立任何连接。
多播:向一组订阅的多播地址发送数据报,组内所有主机可以接收。
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
server.on('listening', () => {
// 加入一个多播组地址(如 239.1.2.3)
server.addMembership('239.1.2.3');
console.log('已加入多播组');
});
server.on('message', (msg, rinfo) => {
console.log(`收到多播消息: ${msg}`);
});
server.bind(12345);
发送多播消息与普通 UDP 发送相同,只需将目标地址设置为多播组地址(224.0.0.0 到 239.255.255.255 范围内的地址)。多播同样需要先绑定套接字,并通过 addMembership 加入目标组。
实用场景:简单的服务发现
利用 UDP 广播可以轻松实现局域网内的服务发现,例如微服务架构中,生产者广播自己的服务信息,消费者监听该端口获取列表。
生产者(广播自身信息):
const dgram = require('dgram');
const sock = dgram.createSocket('udp4');
sock.bind(() => sock.setBroadcast(true));
setInterval(() => {
const info = JSON.stringify({ name: 'user-service', host: '192.168.1.10', port: 3001 });
const msg = Buffer.from(info);
sock.send(msg, 0, msg.length, 41234, '255.255.255.255');
}, 5000); // 每5秒广播一次
消费者(监听服务信息):
const dgram = require('dgram');
const listener = dgram.createSocket('udp4');
listener.on('message', (msg) => {
const info = JSON.parse(msg.toString());
console.log('发现服务:', info);
});
listener.bind(41234, () => {
console.log('开始监听服务发现消息...');
});
这种方式简单直接,但要注意在真实生产环境中,UDP 数据报可能丢失,可能需要结合重试、超时和心跳机制来确保可靠性。
性能与注意事项
- 缓冲区大小:UDP 数据报有最大长度限制,IPv4 理论上最大为 65507 字节(64KB 减去 IP 和 UDP 头),但实际中发送大于网络 MTU(通常 1500 字节)的数据报会被分片,增加延迟和丢包风险,建议控制单个数据报在 1400 字节以内。
- 无流控和背压:UDP 不会因为接收方处理不过来而减速,过量发送会导致接收缓冲区溢出而丢包。应用需要根据业务特征自行实现速率控制或重传策略。
- 并发与线程安全:Node.js 的
dgram套接字是线程安全的,无需额外同步。 - 错误处理是强制项:千万不要忘记监听
error事件,否则未捕获的错误会立即终止进程。
与 TCP 的对比总结
| 特性 | TCP | UDP |
|-----|-----|-----|
| 连接 | 面向连接 | 无连接 |
| 可靠传输 | 保证无差错、不丢失、有序 | 不保证,可能丢包、乱序 |
| 传输单元 | 字节流(需处理粘包) | 数据报(天然保留消息边界) |
| 适用场景 | 文件传输、Web 服务、邮件 | 音视频流、在线游戏、服务发现 |
| 额外开销 | 较高(握手、确认、重传) | 较低(仅需少量头部) |
dgram 模块补充了 Node.js 在网络编程方面的能力,它与 net 模块一起,构成了 Node.js 服务端通信的完整低层基础。在需要快速、轻量、一对多通信的场景中,UDP 往往是一个值得考虑的选择。