# 网络UDP dgram
30 秒速记
UDP= 寄明信片:不建连接、不保证送到、不保证顺序,可能重复- 每次
send就是一个独立数据报,边界保留,不会像TCP那样粘包 - 包别太大:
IPv4理论上限65507字节,实际尽量控制在1472字节以内,避免IP分片 - 要可靠就自己加序号、确认、重传、去重,或者直接用
QUIC这类成熟协议 - 服务端要校验来源和包长,回包不要比请求大太多,防止被拿去做放大攻击
UDP 可以理解成寄明信片,写好地址扔进邮筒就完事,不用先打电话确认对方在不在,代价是可能寄丢、可能乱序。 好处是快、开销小、每次发的就是一个完整的包,不用像 TCP 那样自己切消息边界,所以实时音视频、游戏状态同步、DNS 查询、日志上报都爱用它。要注意包大小,以太网 MTU 是 1500 字节,扣掉 IP 和 UDP 头大约剩 1472 字节,超过就会分片,任何一片丢了整个包都没了。业务真要可靠送达,要么自己补确认重传,要么直接换 TCP 或 QUIC。
理解「网络UDP dgram」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 network 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
UDP 和 TCP 的差别放在一起看更好记:
TCP(net) |
UDP(dgram) |
|
|---|---|---|
| 连接 | 先三次握手 | 无连接,直接发 |
| 可靠性 | 丢了自动重传 | 丢了就丢了 |
| 顺序 | 保证有序 | 可能乱序 |
| 边界 | 字节流,会粘包拆包 | 一次 send 就是一个包 |
| 典型场景 | HTTP、数据库连接 |
DNS、音视频、游戏、日志上报 |
包大小是 UDP 里最实际的坑。IPv4 下一个 UDP 包的负载理论上最多 65507 字节(65535 减去 20 字节 IP 头和 8 字节 UDP 头),但以太网 MTU 一般是 1500,超过 1472 字节就要分片。分片在公网上丢失率高,很多协议因此把单包控制在 1200 字节左右,QUIC 默认也是这个量级。
在 Node 里,超过上限的包 send 会直接报 EMSGSIZE;没超上限但需要分片的包,发送端不会有任何提示,只会在接收端「神秘消失」,排查时要想到这一层。
面试官追问
追问 1send 回调没报错,接收端却少了几条消息,是 dgram 模块的问题吗?
不是。send 回调成功只说明数据交给了操作系统,UDP 本身不保证送达。要确认就在应用层给消息加序号,接收方回 ack,发送方超时重发。
追问 2单个包塞了 8KB 的 JSON,内网好好的,跨机房就经常丢,为什么?
8KB 远超 MTU,会被拆成好几个 IP 分片,跨网络时任一分片丢了,整包就作废,有的防火墙还直接丢分片。把单包控制在 1200 到 1400 字节左右,大数据要么切片编号,要么换 TCP。
追问 3游戏位置同步用 UDP,聊天消息要不要也走 UDP?
位置这种「新的覆盖旧的」数据丢一两个没关系,适合 UDP;聊天必须送达且有序,走 TCP 或 WebSocket 省心得多。硬要统一 UDP,就得自己写一套确认、重传和排序。
追问 4收到的 UDP 包为什么不能直接按 rinfo.address 信任来源?
UDP 没有握手,源地址可以伪造。拿它做鉴权不靠谱,敏感操作要在包里带签名或令牌;回包也别比请求大太多,否则会被伪造源地址拿去做反射放大攻击。
# 模块概览
30 秒速记
dgram=Node对UDP socket的封装,比net简单:没有连接、没有流- 创建:
dgram.createSocket('udp4' | 'udp6'),收消息要先bind(port) - 核心事件:
listening、message(msg, rinfo)、error、close msg是Buffer,rinfo里有address、family、port、size- 用完要
close(),不然socket一直挂在事件循环上,进程不退出
dgram 就是 Node 里用来收发 UDP 包的模块,API 很少,记住「创建、绑定、收、发、关」五步就够。 dgram.createSocket('udp4') 建一个 socket,要收数据就 bind 到一个端口,然后监听 message,回调里第一个参数是 Buffer,第二个 rinfo 告诉你是谁发来的;发数据用 socket.send(buf, port, host, cb)。它比 net 简单是因为不用管连接状态和流的背压,但可靠性、顺序这些活就落到业务自己头上了。另外 error 一定要监听,端口被占用这种错误不监听会直接把进程搞崩。
dgram模块是对UDP socket的一层封装,相对net模块简单很多,下面看例子。
理解「模块概览」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1dgram 比 net 简单,所以 UDP 服务一定更好维护?
不一定。API 简单是因为它什么都不保证,一旦业务要确认、重传、去重、排序,这些逻辑全得自己写,最后往往比直接用 TCP 复杂。
追问 2服务端 bind(33333) 启动就崩了,报 EADDRINUSE,怎么处理?
端口被占了,而且没监听 error 所以直接抛异常。加上 socket.on('error', ...) 记日志并退出;确实需要多进程共用端口时,创建时传 { type: 'udp4', reuseAddr: true }。
追问 3UDP 小工具发完一个包,进程一直不退出,为什么?
socket 还开着,事件循环认为还有活要干。发完在 send 回调里 socket.close();或者调用 socket.unref(),让它不阻止进程退出。
追问 4message 回调里直接 JSON.parse(msg),偶尔整个服务挂掉,怎么改?
UDP 谁都能往端口扔包,收到半截或乱码很正常。先看 rinfo.size 是否在合理范围,再用 try/catch 包住解析,非法包直接丢弃并计数。
追问 5只跟一个固定的对端通信,每次 send 都要写地址吗?
Node 12 起可以先 socket.connect(port, host),之后 send(buf) 不用再带地址,而且只会收到这个对端发来的包。这里的 connect 只是在本地记住对端,不会真的握手。
# UDP客户端 vs UDP服务端
30 秒速记
UDP里的「客户端/服务端」只是角色叫法:一个bind等着收,一个直接send- 两边都用
dgram.createSocket('udp4')创建,没有握手和连接 - 服务端:
listening确认绑定成功 →message(msg, remote)拿到内容和来源 - 客户端:
send(buf, port, host, cb),回调成功 ≠ 对方收到 - 一次性客户端在回调里
close();连续发送的要复用同一个socket,别每条关一次
UDP 没有连接,所谓服务端就是先 bind 到一个固定端口等包的那一方,客户端就是知道这个地址、直接往里扔包的那一方。 服务端监听 listening 确认绑好了,再在 message 里拿到 msg 和 remote,remote.address 和 remote.port 就是发送方,想回包直接 server.send(reply, remote.port, remote.address)。客户端不用 bind,第一次 send 时系统会自动分一个随机端口,日志里那个 58940 就是这么来的。要注意 send 的回调只说明包已经交给系统了,对方收没收到它不知道。
首先,启动UDP server,监听来自端口33333的请求。
server.js
// 例子:UDP服务端
var PORT = 33333;
var HOST = '127.0.0.1';
var dgram = require('dgram');
var server = dgram.createSocket('udp4');
server.on('listening', function () {
var address = server.address();
console.log('UDP Server listening on ' + address.address + ":" + address.port);
});
server.on('message', function (message, remote) {
console.log(remote.address + ':' + remote.port +' - ' + message);
});
server.bind(PORT, HOST);
然后,创建UDP socket,向端口33333发送请求。
client.js
// 例子:UDP客户端
var PORT = 33333;
var HOST = 