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.createSocket() 创建新的 dgram.Socket 实例。不要使用 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 上监听数据报消息。如果未指定 port 或为 0,操作系统将尝试绑定到随机端口。如果未指定 address,操作系统将尝试监听所有地址。绑定完成后,将发出 'listening' 事件,并调用可选的 callback 函数。

同时指定 'listening' 事件监听器和将 callback 传递给 socket.bind() 方法不会造成危害,但没有太大用处。

绑定的数据报套接字使 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 上监听数据报消息,这些 portaddress 作为 options 对象的属性传递,该对象作为第一个参数传递。如果未指定 port 或为 0,操作系统将尝试绑定到随机端口。如果未指定 address,操作系统将尝试监听所有地址。绑定完成后,将发出 'listening' 事件,并调用可选的 callback 函数。

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

同时指定 'listening' 事件监听器和将 callback 传递给 socket.bind() 方法不会造成危害,但没有太大用处。

options 对象可能包含一个额外的 exclusive 属性,该属性在使用 dgram.Socket 对象与 cluster 模块一起使用时使用。当 exclusive 设置为 false(默认值)时,集群工作者将使用相同的底层套接字句柄,允许共享连接处理职责。但是,当 exclusivetrue 时,句柄不会共享,并且尝试共享端口会导致错误。

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

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

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

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

socket.close([callback])#

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

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

socket[Symbol.asyncDispose]()#

稳定性:1 - 实验性

调用 socket.close() 并返回一个 promise,该 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,或者如果失败,将发出 '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 绑定,则套接字将分配一个随机端口号并绑定到“所有接口”地址('0.0.0.0' 对于 udp4 套接字,'::0' 对于 udp6 套接字)。

可以指定一个可选的 callback 函数作为报告 DNS 错误或确定何时可以安全地重用 buf 对象的一种方式。DNS 查找会延迟发送时间,至少需要 Node.js 事件循环的一个滴答。

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

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

如果在未绑定套接字上调用此方法,则会抛出ERR_SOCKET_BAD_PORT

localhost上的端口发送 UDP 数据包的示例;

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();
});

127.0.0.1上的端口发送由多个缓冲区组成的 UDP 数据包的示例;

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)#

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

将套接字的默认出站多播接口设置为选定的接口,或返回到系统接口选择。multicastInterface必须是套接字族中 IP 的有效字符串表示形式。

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

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

如果在未绑定套接字上调用此方法,则会抛出 EBADF

示例:IPv6 出站多播接口#

在大多数系统上,作用域格式使用接口名称

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

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

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

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');
}); 
调用结果#

在未准备好发送或不再打开的套接字上进行调用可能会抛出 未运行 错误

如果 multicastInterface 无法解析为 IP,则会抛出 EINVAL 系统错误

在 IPv4 上,如果 multicastInterface 是有效地址但不匹配任何接口,或者地址与族不匹配,则会抛出 系统错误,例如 EADDRNOTAVAILEPROTONOSUP

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

套接字的地址族 ANY 地址(IPv4 '0.0.0.0' 或 IPv6 '::')可用于将套接字的默认出站接口的控制权返回给系统,以便用于将来的组播数据包。

socket.setMulticastLoopback(flag)#

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

如果在未绑定套接字上调用此方法,则会抛出 EBADF

socket.setMulticastTTL(ttl)#

设置 IP_MULTICAST_TTL 套接字选项。虽然 TTL 通常代表“生存时间”,但在这种情况下,它指定数据包允许通过的 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 通常代表“生存时间”,但在这种情况下,它指定了数据包允许通过的 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
    • ipv6Only <boolean>ipv6Only 设置为 true 将禁用双栈支持,即绑定到地址 :: 不会使 0.0.0.0 被绑定。默认值: false
    • recvBufferSize <number> 设置 SO_RCVBUF 套接字值。
    • sendBufferSize <number> 设置 SO_SNDBUF 套接字值。
    • lookup <Function> 自定义查找函数。默认值: dns.lookup().
    • signal <AbortSignal> 一个 AbortSignal,可用于关闭套接字。
  • callback <Function> 作为 'message' 事件的监听器附加。可选。
  • 返回值:<dgram.Socket>

创建一个 dgram.Socket 对象。创建套接字后,调用 socket.bind() 将指示套接字开始监听数据报消息。当 addressport 未传递给 socket.bind() 时,该方法将把套接字绑定到随机端口上的“所有接口”地址(它对 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() 将指示套接字开始监听数据报消息。当没有将 addressport 传递给 socket.bind() 时,该方法将套接字绑定到随机端口上的“所有接口”地址(对 udp4udp6 套接字都执行正确的操作)。可以使用 socket.address().addresssocket.address().port 检索绑定地址和端口。