UDP/数据报套接字#

稳定性:2 - 稳定

源代码: lib/dgram.js

node:dgram 模块提供了 UDP 数据报套接字的实现。

import dgram from 'node:dgram';

const server = dgram.createSocket('udp4');

server.on('error', (err) => {
  console.error(`server error:\n${err.stack}`);
  server.close();
});

server.on('message', (msg, rinfo) => {
  console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
});

server.on('listening', () => {
  const address = server.address();
  console.log(`server listening ${address.address}:${address.port}`);
});

server.bind(41234);
// Prints: server listening 0.0.0.0:41234const dgram = require('node:dgram');
const server = dgram.createSocket('udp4');

server.on('error', (err) => {
  console.error(`server error:\n${err.stack}`);
  server.close();
});

server.on('message', (msg, rinfo) => {
  console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
});

server.on('listening', () => {
  const address = server.address();
  console.log(`server listening ${address.address}:${address.port}`);
});

server.bind(41234);
// Prints: server listening 0.0.0.0:41234

类:dgram.Socket#

封装了数据报的功能。

dgram.Socket 的新实例是使用 dgram.createSocket() 创建的。不应使用 new 关键字来创建 dgram.Socket 实例。

事件:'close'#

当套接字通过 close() 关闭后,会触发 'close' 事件。一旦触发,此套接字上将不会再触发新的 'message' 事件。

事件:'connect'#

成功调用 connect() 后,套接字与远程地址关联时会触发 'connect' 事件。

事件:'error'#

每当发生任何错误时,都会触发 'error' 事件。事件处理函数会接收一个 Error 对象作为参数。

事件:'listening'#

一旦 dgram.Socket 可寻址并可以接收数据,就会触发 'listening' 事件。这可以是通过 socket.bind() 显式触发,也可以是在首次使用 socket.send() 发送数据时隐式触发。在 dgram.Socket 处于监听状态之前,底层的系统资源不存在,调用诸如 socket.address()socket.setTTL() 等方法将会失败。

事件:'message'#

当套接字上有新的数据报可用时,会触发 'message' 事件。事件处理函数会接收两个参数:msgrinfo

如果传入数据包的源地址是 IPv6 链路本地地址,则接口名称会附加到 address 上。例如,在 en0 接口上收到的数据包,其地址字段可能被设置为 'fe80::2618:1234:ab11:3b9c%en0',其中 '%en0' 是作为区域 ID 后缀的接口名称。

socket.addMembership(multicastAddress[, multicastInterface])#

通知内核使用 IP_ADD_MEMBERSHIP 套接字选项,在给定的 multicastAddressmulticastInterface 上加入一个多播组。如果未指定 multicastInterface 参数,操作系统将选择一个接口并向其添加成员关系。要向每个可用接口添加成员关系,需要多次调用 addMembership,每个接口调用一次。

在未绑定的套接字上调用此方法时,它将隐式绑定到一个随机端口,并监听所有接口。

当在多个 cluster 工作进程间共享一个 UDP 套接字时,socket.addMembership() 函数必须只调用一次,否则会发生 EADDRINUSE 错误。

import cluster from 'node:cluster';
import dgram from 'node:dgram';

if (cluster.isPrimary) {
  cluster.fork(); // Works ok.
  cluster.fork(); // Fails with EADDRINUSE.
} else {
  const s = dgram.createSocket('udp4');
  s.bind(1234, () => {
    s.addMembership('224.0.0.114');
  });
}const cluster = require('node:cluster');
const dgram = require('node:dgram');

if (cluster.isPrimary) {
  cluster.fork(); // Works ok.
  cluster.fork(); // Fails with EADDRINUSE.
} else {
  const s = dgram.createSocket('udp4');
  s.bind(1234, () => {
    s.addMembership('224.0.0.114');
  });
}

socket.addSourceSpecificMembership(sourceAddress, groupAddress[, multicastInterface])#

通知内核使用 IP_ADD_SOURCE_MEMBERSHIP 套接字选项,在给定的 sourceAddressgroupAddress 上加入一个特定源的多播频道,使用 multicastInterface。如果未指定 multicastInterface 参数,操作系统将选择一个接口并向其添加成员关系。要向每个可用接口添加成员关系,需要多次调用 socket.addSourceSpecificMembership(),每个接口调用一次。

在未绑定的套接字上调用此方法时,它将隐式绑定到一个随机端口,并监听所有接口。

socket.address()#

返回一个包含套接字地址信息的对象。对于 UDP 套接字,此对象将包含 addressfamilyport 属性。

如果在未绑定的套接字上调用此方法,将抛出 EBADF 错误。

socket.bind([port][, address][, callback])#

对于 UDP 套接字,使 dgram.Socket 在一个指定的 port 和可选的 address 上监听数据报消息。如果未指定 portport0,操作系统将尝试绑定到一个随机端口。如果未指定 address,操作系统将尝试监听所有地址。绑定完成后,会触发一个 'listening' 事件,并调用可选的 callback 函数。

同时指定一个 'listening' 事件监听器并向 socket.bind() 方法传递一个 callback 是无害的,但没有太大用处。

一个已绑定的数据报套接字会使 Node.js 进程保持运行以接收数据报消息。

如果绑定失败,会产生一个 'error' 事件。在极少数情况下(例如,尝试绑定一个已关闭的套接字),可能会抛出一个 Error

一个在 41234 端口监听的 UDP 服务器示例

import dgram from 'node:dgram';

const server = dgram.createSocket('udp4');

server.on('error', (err) => {
  console.error(`server error:\n${err.stack}`);
  server.close();
});

server.on('message', (msg, rinfo) => {
  console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
});

server.on('listening', () => {
  const address = server.address();
  console.log(`server listening ${address.address}:${address.port}`);
});

server.bind(41234);
// Prints: server listening 0.0.0.0:41234const dgram = require('node:dgram');
const server = dgram.createSocket('udp4');

server.on('error', (err) => {
  console.error(`server error:\n${err.stack}`);
  server.close();
});

server.on('message', (msg, rinfo) => {
  console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
});

server.on('listening', () => {
  const address = server.address();
  console.log(`server listening ${address.address}:${address.port}`);
});

server.bind(41234);
// Prints: server listening 0.0.0.0:41234

socket.bind(options[, callback])#

对于 UDP 套接字,使 dgram.Socket 在一个指定的 port 和可选的 address 上监听数据报消息,这些参数作为第一个参数 options 对象的属性传入。如果未指定 portport0,操作系统将尝试绑定到一个随机端口。如果未指定 address,操作系统将尝试监听所有地址。绑定完成后,会触发一个 'listening' 事件,并调用可选的 callback 函数。

options 对象可能包含一个 fd 属性。当设置了一个大于 0fd 时,它将包装一个具有给定文件描述符的现有套接字。在这种情况下,portaddress 的属性将被忽略。

同时指定一个 'listening' 事件监听器并向 socket.bind() 方法传递一个 callback 是无害的,但没有太大用处。

当与 cluster 模块一起使用 dgram.Socket 对象时,options 对象可能包含一个额外的 exclusive 属性。当 exclusive 设置为 false(默认值)时,集群工作进程将使用相同的底层套接字句柄,从而允许共享连接处理职责。然而,当 exclusivetrue 时,句柄不被共享,尝试共享端口会导致错误。创建 dgram.Socket 时,如果将 reusePort 选项设置为 true,那么在调用 socket.bind()exclusive 将始终为 true

一个已绑定的数据报套接字会使 Node.js 进程保持运行以接收数据报消息。

如果绑定失败,会产生一个 'error' 事件。在极少数情况下(例如,尝试绑定一个已关闭的套接字),可能会抛出一个 Error

下面显示了一个在独占端口上监听的套接字示例。

socket.bind({
  address: 'localhost',
  port: 8000,
  exclusive: true,
}); 

socket.close([callback])#

  • callback <Function> 当套接字关闭时调用。

关闭底层套接字并停止在其上监听数据。如果提供了回调函数,它将被添加为 'close' 事件的监听器。

socket[Symbol.asyncDispose]()#

调用 socket.close() 并返回一个在套接字关闭时履行的 Promise。

socket.connect(port[, address][, callback])#

dgram.Socket 与远程地址和端口关联。此句柄发送的每条消息都会自动发送到该目标。此外,套接字将只接收来自该远程对等方的消息。尝试在已连接的套接字上调用 connect() 将导致 ERR_SOCKET_DGRAM_IS_CONNECTED 异常。如果未提供 address,则默认使用 '127.0.0.1'(对于 udp4 套接字)或 '::1'(对于 udp6 套接字)。连接完成后,会触发一个 'connect' 事件,并调用可选的 callback 函数。如果失败,则调用 callback,或者,如果没有 callback,则触发一个 'error' 事件。

socket.disconnect()#

一个同步函数,用于解除已连接的 dgram.Socket 与其远程地址的关联。尝试在未绑定或已断开连接的套接字上调用 disconnect() 将导致 ERR_SOCKET_DGRAM_NOT_CONNECTED 异常。

socket.dropMembership(multicastAddress[, multicastInterface])#

指示内核使用 IP_DROP_MEMBERSHIP 套接字选项离开位于 multicastAddress 的多播组。当套接字关闭或进程终止时,内核会自动调用此方法,因此大多数应用程序永远没有理由调用此方法。

如果未指定 multicastInterface,操作系统将尝试在所有有效接口上退出成员关系。

socket.dropSourceSpecificMembership(sourceAddress, groupAddress[, multicastInterface])#

指示内核使用 IP_DROP_SOURCE_MEMBERSHIP 套接字选项离开位于给定 sourceAddressgroupAddress 的特定源多播频道。当套接字关闭或进程终止时,内核会自动调用此方法,因此大多数应用程序永远没有理由调用此方法。

如果未指定 multicastInterface,操作系统将尝试在所有有效接口上退出成员关系。

socket.getRecvBufferSize()#

  • 返回:<number> SO_RCVBUF 套接字接收缓冲区的大小(以字节为单位)。

如果在未绑定的套接字上调用此方法,将抛出 ERR_SOCKET_BUFFER_SIZE 错误。

socket.getSendBufferSize()#

  • 返回:<number> SO_SNDBUF 套接字发送缓冲区的大小(以字节为单位)。

如果在未绑定的套接字上调用此方法,将抛出 ERR_SOCKET_BUFFER_SIZE 错误。

socket.getSendQueueSize()#

  • 返回:<number> 排队等待发送的字节数。

socket.getSendQueueCount()#

  • 返回:<number> 当前在队列中等待处理的发送请求数量。

socket.ref()#

默认情况下,绑定一个套接字会阻止 Node.js 进程退出,只要该套接字是打开的。socket.unref() 方法可用于将套接字从保持 Node.js 进程活动的引用计数中排除。socket.ref() 方法将套接字重新添加回引用计数,并恢复默认行为。

多次调用 socket.ref() 不会产生额外效果。

socket.ref() 方法返回对套接字的引用,因此可以进行链式调用。

socket.remoteAddress()#

返回一个包含远程端点的 addressfamilyport 的对象。如果套接字未连接,此方法将抛出 ERR_SOCKET_DGRAM_NOT_CONNECTED 异常。

socket.send(msg[, offset, length][, port][, address][, callback])#

在套接字上广播一个数据报。对于无连接的套接字,必须指定目标 portaddress。而对于已连接的套接字,它们将使用其关联的远程端点,因此不得设置 portaddress 参数。

msg 参数包含要发送的消息。根据其类型,可能会有不同的行为。如果 msg 是一个 Buffer、任何 TypedArrayDataView,那么 offsetlength 分别指定了消息在 Buffer 中的起始偏移量和消息的字节数。如果 msg 是一个 String,那么它会自动转换为使用 'utf8' 编码的 Buffer。对于包含多字节字符的消息,offsetlength 将根据字节长度而不是字符位置来计算。如果 msg 是一个数组,则不能指定 offsetlength

address 参数是一个字符串。如果 address 的值是一个主机名,将使用 DNS 来解析该主机的地址。如果未提供 address 或其为 nullish,则默认使用 '127.0.0.1'(对于 udp4 套接字)或 '::1'(对于 udp6 套接字)。

如果套接字之前没有通过调用 bind 进行绑定,那么套接字将被分配一个随机的端口号,并绑定到“所有接口”地址(udp4 套接字为 '0.0.0.0'udp6 套接字为 '::0')。

可以指定一个可选的 callback 函数,用于报告 DNS 错误或确定何时可以安全地重用 buf 对象。DNS 查找会使发送时间延迟至少一个 Node.js 事件循环的 tick。

唯一能确定数据报已发送的方法是使用 callback。如果发生错误并且提供了 callback,错误将作为第一个参数传递给 callback。如果没有提供 callback,错误将作为 'error' 事件在 socket 对象上触发。

偏移量和长度是可选的,但如果使用其中任何一个,则两者都必须设置。它们仅在第一个参数是 BufferTypedArrayDataView 时才受支持。

如果在未绑定的套接字上调用此方法,将抛出 ERR_SOCKET_BAD_PORT 错误。

发送 UDP 包到 localhost 上某个端口的示例:

import dgram from 'node:dgram';
import { Buffer } from 'node:buffer';

const message = Buffer.from('Some bytes');
const client = dgram.createSocket('udp4');
client.send(message, 41234, 'localhost', (err) => {
  client.close();
});const dgram = require('node:dgram');
const { Buffer } = require('node:buffer');

const message = Buffer.from('Some bytes');
const client = dgram.createSocket('udp4');
client.send(message, 41234, 'localhost', (err) => {
  client.close();
});

将由多个缓冲区组成的 UDP 包发送到 127.0.0.1 上的某个端口的示例:

import dgram from 'node:dgram';
import { Buffer } from 'node:buffer';

const buf1 = Buffer.from('Some ');
const buf2 = Buffer.from('bytes');
const client = dgram.createSocket('udp4');
client.send([buf1, buf2], 41234, (err) => {
  client.close();
});const dgram = require('node:dgram');
const { Buffer } = require('node:buffer');

const buf1 = Buffer.from('Some ');
const buf2 = Buffer.from('bytes');
const client = dgram.createSocket('udp4');
client.send([buf1, buf2], 41234, (err) => {
  client.close();
});

发送多个缓冲区的速度可能会更快或更慢,具体取决于应用程序和操作系统。应通过基准测试来逐案确定最佳策略。然而,一般来说,发送多个缓冲区会更快。

使用连接到 localhost 某个端口的套接字发送 UDP 包的示例

import dgram from 'node:dgram';
import { Buffer } from 'node:buffer';

const message = Buffer.from('Some bytes');
const client = dgram.createSocket('udp4');
client.connect(41234, 'localhost', (err) => {
  client.send(message, (err) => {
    client.close();
  });
});const dgram = require('node:dgram');
const { Buffer } = require('node:buffer');

const message = Buffer.from('Some bytes');
const client = dgram.createSocket('udp4');
client.connect(41234, 'localhost', (err) => {
  client.send(message, (err) => {
    client.close();
  });
});
关于 UDP 数据报大小的说明#

IPv4/v6 数据报的最大大小取决于 MTU(最大传输单元)和 Payload Length 字段的大小。

  • Payload Length 字段是 16 位宽,这意味着一个正常的有效载荷不能超过 64K 八位字节,包括互联网头部和数据(65,507 字节 = 65,535 − 8 字节 UDP 头部 − 20 字节 IP 头部);这通常适用于回环接口,但对于大多数主机和网络来说,如此长的数据报消息是不切实际的。

  • MTU 是给定的链路层技术可以支持的数据报消息的最大尺寸。对于任何链路,IPv4 规定了最小 MTU 为 68 个八位字节,而 IPv4 推荐的 MTU 是 576(通常推荐作为拨号类应用的 MTU),无论它们是完整到达还是分片到达。

    对于 IPv6,最小 MTU 是 1280 个八位字节。然而,强制的最小分片重组缓冲区大小是 1500 个八位字节。68 个八位字节的值非常小,因为大多数当前的链路层技术,如以太网,其最小 MTU 为 1500。

无法预先知道一个数据包可能经过的每个链路的 MTU。发送大于接收方 MTU 的数据报将不会成功,因为数据包会被静默丢弃,而不会通知源端数据未到达其预定接收者。

socket.setBroadcast(flag)#

设置或清除 SO_BROADCAST 套接字选项。当设置为 true 时,UDP 包可以发送到本地接口的广播地址。

如果在未绑定的套接字上调用此方法,将抛出 EBADF 错误。

socket.setMulticastInterface(multicastInterface)#

本节中所有对 scope 的引用都指的是 IPv6 区域索引,该索引由 RFC 4007 定义。在字符串形式中,带有 scope 索引的 IP 写作 'IP%scope',其中 scope 是接口名称或接口编号。

将套接字的默认出站多播接口设置为所选接口,或恢复为系统接口选择。multicastInterface 必须是套接字族中一个 IP 的有效字符串表示。

对于 IPv4 套接字,这应该是为所需物理接口配置的 IP。所有在套接字上发送到多播地址的数据包都将通过最近一次成功调用此方法所确定的接口发送。

对于 IPv6 套接字,multicastInterface 应包含一个 scope 来指示接口,如下例所示。在 IPv6 中,单个 send 调用也可以在地址中使用显式 scope,因此只有发送到未指定显式 scope 的多播地址的数据包才会受到最近一次成功调用此方法的影响。

如果在未绑定的套接字上调用此方法,将抛出 EBADF 错误。

示例:IPv6 出站多播接口#

在大多数系统中,scope 格式使用接口名称

const socket = dgram.createSocket('udp6');

socket.bind(1234, () => {
  socket.setMulticastInterface('::%eth1');
}); 

在 Windows 上,scope 格式使用接口编号

const socket = dgram.createSocket('udp6');

socket.bind(1234, () => {
  socket.setMulticastInterface('::%2');
}); 
示例:IPv4 出站多播接口#

所有系统都使用所需物理接口上主机的 IP

const socket = dgram.createSocket('udp4');

socket.bind(1234, () => {
  socket.setMulticastInterface('10.0.0.2');
}); 
调用结果#

在未准备好发送或已不再打开的套接字上调用,可能会抛出“Not running”Error

如果 multicastInterface 无法被解析为 IP,则会抛出“EINVAL”System Error

在 IPv4 上,如果 multicastInterface 是一个有效的地址但与任何接口都不匹配,或者地址与族不匹配,则会抛出像 EADDRNOTAVAILEPROTONOSUP 这样的System Error

在 IPv6 上,大多数指定或省略 scope 的错误将导致套接字继续使用(或返回到)系统的默认接口选择。

套接字地址族的任意地址(IPv4 的 '0.0.0.0' 或 IPv6 的 '::')可用于将套接字默认出站接口的控制权交还给系统,以用于未来的多播数据包。

socket.setMulticastLoopback(flag)#

设置或清除 IP_MULTICAST_LOOP 套接字选项。当设置为 true 时,多播数据包也将在本地接口上接收。

如果在未绑定的套接字上调用此方法,将抛出 EBADF 错误。

socket.setMulticastTTL(ttl)#

设置 IP_MULTICAST_TTL 套接字选项。虽然 TTL 通常代表“生存时间”(Time to Live),但在这种情况下,它指定了数据包允许经过的 IP 跳数,特别适用于多播流量。每个转发数据包的路由器或网关都会将 TTL 减一。如果 TTL 被路由器减为 0,数据包将不会被转发。

ttl 参数的值可以在 0 到 255 之间。大多数系统上的默认值是 1

如果在未绑定的套接字上调用此方法,将抛出 EBADF 错误。

socket.setRecvBufferSize(size)#

设置 SO_RCVBUF 套接字选项。设置套接字最大接收缓冲区的大小(以字节为单位)。

如果在未绑定的套接字上调用此方法,将抛出 ERR_SOCKET_BUFFER_SIZE 错误。

socket.setSendBufferSize(size)#

设置 SO_SNDBUF 套接字选项。设置套接字最大发送缓冲区的大小(以字节为单位)。

如果在未绑定的套接字上调用此方法,将抛出 ERR_SOCKET_BUFFER_SIZE 错误。

socket.setTTL(ttl)#

设置 IP_TTL 套接字选项。虽然 TTL 通常代表“生存时间”(Time to Live),但在这种情况下,它指定了数据包允许经过的 IP 跳数。每个转发数据包的路由器或网关都会将 TTL 减一。如果 TTL 被路由器减为 0,数据包将不会被转发。更改 TTL 值通常用于网络探测或进行多播。

ttl 参数的值可以在 1 到 255 之间。大多数系统上的默认值是 64。

如果在未绑定的套接字上调用此方法,将抛出 EBADF 错误。

socket.unref()#

默认情况下,绑定一个套接字会阻止 Node.js 进程退出,只要该套接字是打开的。socket.unref() 方法可用于将套接字从保持 Node.js 进程活动的引用计数中排除,从而允许进程在套接字仍在监听时退出。

多次调用 socket.unref() 不会产生额外效果。

socket.unref() 方法返回对套接字的引用,因此可以进行链式调用。

node:dgram 模块函数#

dgram.createSocket(options[, callback])#

  • options <Object> 可用选项包括:
    • type <string> 套接字族。必须是 'udp4''udp6'。必需。
    • reuseAddr <boolean> 当为 true 时,socket.bind() 将重用地址,即使另一个进程已经绑定了该地址上的套接字,但只有一个套接字可以接收数据。默认值: false
    • reusePort <boolean> 当为 true 时,socket.bind() 将重用端口,即使另一个进程已经绑定了该端口上的套接字。传入的数据报将被分发到监听的套接字。此选项仅在某些平台上可用,例如 Linux 3.9+、DragonFlyBSD 3.6+、FreeBSD 12.0+、Solaris 11.4 和 AIX 7.2.5+。在不支持的平台上,当套接字被绑定时,此选项会引发错误。默认值: false
    • ipv6Only <boolean>ipv6Only 设置为 true 将禁用双栈支持,即绑定到地址 :: 不会使 0.0.0.0 被绑定。默认值: false
    • recvBufferSize <number> 设置 SO_RCVBUF 套接字值。
    • sendBufferSize <number> 设置 SO_SNDBUF 套接字值。
    • lookup <Function> 自定义查找函数。默认值: dns.lookup()
    • signal <AbortSignal> 一个可用于关闭套接字的 AbortSignal。
    • receiveBlockList <net.BlockList> receiveBlockList 可用于丢弃发往特定 IP 地址、IP 范围或 IP 子网的入站数据报。如果服务器位于反向代理、NAT 等之后,此功能将不起作用,因为与阻止列表进行检查的地址是代理的地址,或 NAT 指定的地址。
    • sendBlockList <net.BlockList> sendBlockList 可用于禁用对特定 IP 地址、IP 范围或 IP 子网的出站访问。
  • callback <Function> 作为 'message' 事件的监听器附加。可选。
  • 返回:<dgram.Socket>

创建一个 dgram.Socket 对象。套接字创建后,调用 socket.bind() 将指示套接字开始监听数据报消息。当未向 socket.bind() 传递 addressport 时,该方法会将套接字绑定到“所有接口”地址的一个随机端口(对于 udp4udp6 套接字都能正确处理)。可以使用 socket.address().addresssocket.address().port 来检索绑定的地址和端口。

如果启用了 signal 选项,在相应的 AbortController 上调用 .abort() 类似于在套接字上调用 .close()

const controller = new AbortController();
const { signal } = controller;
const server = dgram.createSocket({ type: 'udp4', signal });
server.on('message', (msg, rinfo) => {
  console.log(`server got: ${msg} from ${rinfo.address}:${rinfo.port}`);
});
// Later, when you want to close the server.
controller.abort(); 

dgram.createSocket(type[, callback])#

创建一个指定 typedgram.Socket 对象。

套接字创建后,调用 socket.bind() 将指示套接字开始监听数据报消息。当未向 socket.bind() 传递 addressport 时,该方法会将套接字绑定到“所有接口”地址的一个随机端口(对于 udp4udp6 套接字都能正确处理)。可以使用 socket.address().addresssocket.address().port 来检索绑定的地址和端口。