lib: add UV_UDP_REUSEPORT for udp

PR-URL: https://github.com/nodejs/node/pull/55403
Refs: https://github.com/libuv/libuv/pull/4419
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
theanarkh 2024-10-21 18:25:10 +08:00 committed by GitHub
parent ee46d2297c
commit 6a02c2701e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 141 additions and 3 deletions

View File

@ -343,7 +343,9 @@ used when using `dgram.Socket` objects with the [`cluster`][] module. When
`exclusive` is set to `false` (the default), cluster workers will use the same `exclusive` is set to `false` (the default), cluster workers will use the same
underlying socket handle allowing connection handling duties to be shared. underlying socket handle allowing connection handling duties to be shared.
When `exclusive` is `true`, however, the handle is not shared and attempted When `exclusive` is `true`, however, the handle is not shared and attempted
port sharing results in an error. port sharing results in an error. Creating a `dgram.Socket` with the `reusePort`
option set to `true` causes `exclusive` to always be `true` when `socket.bind()`
is called.
A bound datagram socket keeps the Node.js process running to receive A bound datagram socket keeps the Node.js process running to receive
datagram messages. datagram messages.
@ -916,6 +918,9 @@ chained.
<!-- YAML <!-- YAML
added: v0.11.13 added: v0.11.13
changes: changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/55403
description: The `reusePort` option is supported.
- version: v15.8.0 - version: v15.8.0
pr-url: https://github.com/nodejs/node/pull/37026 pr-url: https://github.com/nodejs/node/pull/37026
description: AbortSignal support was added. description: AbortSignal support was added.
@ -935,7 +940,15 @@ changes:
* `type` {string} The family of socket. Must be either `'udp4'` or `'udp6'`. * `type` {string} The family of socket. Must be either `'udp4'` or `'udp6'`.
Required. Required.
* `reuseAddr` {boolean} When `true` [`socket.bind()`][] will reuse the * `reuseAddr` {boolean} When `true` [`socket.bind()`][] will reuse the
address, even if another process has already bound a socket on it. address, even if another process has already bound a socket on it, but
only one socket can receive the data.
**Default:** `false`.
* `reusePort` {boolean} When `true` [`socket.bind()`][] will reuse the
port, even if another process has already bound a socket on it. Incoming
datagrams are distributed to listening sockets. The option is available
only on some platforms, such as Linux 3.9+, DragonFlyBSD 3.6+, FreeBSD 12.0+,
Solaris 11.4, and AIX 7.2.5+. On unsupported platforms this option raises an
an error when the socket is bound.
**Default:** `false`. **Default:** `false`.
* `ipv6Only` {boolean} Setting `ipv6Only` to `true` will * `ipv6Only` {boolean} Setting `ipv6Only` to `true` will
disable dual-stack support, i.e., binding to address `::` won't make disable dual-stack support, i.e., binding to address `::` won't make

View File

@ -74,7 +74,7 @@ const {
const { UV_UDP_REUSEADDR } = internalBinding('constants').os; const { UV_UDP_REUSEADDR } = internalBinding('constants').os;
const { const {
constants: { UV_UDP_IPV6ONLY }, constants: { UV_UDP_IPV6ONLY, UV_UDP_REUSEPORT },
UDP, UDP,
SendWrap, SendWrap,
} = internalBinding('udp_wrap'); } = internalBinding('udp_wrap');
@ -130,6 +130,7 @@ function Socket(type, listener) {
connectState: CONNECT_STATE_DISCONNECTED, connectState: CONNECT_STATE_DISCONNECTED,
queue: undefined, queue: undefined,
reuseAddr: options?.reuseAddr, // Use UV_UDP_REUSEADDR if true. reuseAddr: options?.reuseAddr, // Use UV_UDP_REUSEADDR if true.
reusePort: options?.reusePort,
ipv6Only: options?.ipv6Only, ipv6Only: options?.ipv6Only,
recvBufferSize, recvBufferSize,
sendBufferSize, sendBufferSize,
@ -345,6 +346,10 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
flags |= UV_UDP_REUSEADDR; flags |= UV_UDP_REUSEADDR;
if (state.ipv6Only) if (state.ipv6Only)
flags |= UV_UDP_IPV6ONLY; flags |= UV_UDP_IPV6ONLY;
if (state.reusePort) {
exclusive = true;
flags |= UV_UDP_REUSEPORT;
}
if (cluster.isWorker && !exclusive) { if (cluster.isWorker && !exclusive) {
bindServerHandle(this, { bindServerHandle(this, {

View File

@ -231,6 +231,7 @@ void UDPWrap::Initialize(Local<Object> target,
Local<Object> constants = Object::New(isolate); Local<Object> constants = Object::New(isolate);
NODE_DEFINE_CONSTANT(constants, UV_UDP_IPV6ONLY); NODE_DEFINE_CONSTANT(constants, UV_UDP_IPV6ONLY);
NODE_DEFINE_CONSTANT(constants, UV_UDP_REUSEADDR); NODE_DEFINE_CONSTANT(constants, UV_UDP_REUSEADDR);
NODE_DEFINE_CONSTANT(constants, UV_UDP_REUSEPORT);
target->Set(context, target->Set(context,
env->constants_string(), env->constants_string(),
constants).Check(); constants).Check();

24
test/common/udp.js Normal file
View File

@ -0,0 +1,24 @@
'use strict';
const dgram = require('dgram');
const options = { type: 'udp4', reusePort: true };
function checkSupportReusePort() {
return new Promise((resolve, reject) => {
const socket = dgram.createSocket(options);
socket.bind(0);
socket.on('listening', () => {
socket.close(resolve);
});
socket.on('error', (err) => {
console.log('The `reusePort` option is not supported:', err.message);
socket.close();
reject(err);
});
});
}
module.exports = {
checkSupportReusePort,
options,
};

View File

@ -0,0 +1,35 @@
'use strict';
const common = require('../common');
const { checkSupportReusePort, options } = require('../common/udp');
const assert = require('assert');
const child_process = require('child_process');
const dgram = require('dgram');
if (!process.env.isWorker) {
checkSupportReusePort().then(() => {
const socket = dgram.createSocket(options);
socket.bind(0, common.mustCall(() => {
const port = socket.address().port;
const workerOptions = { env: { ...process.env, isWorker: 1, port } };
let count = 2;
for (let i = 0; i < 2; i++) {
const worker = child_process.fork(__filename, workerOptions);
worker.on('exit', common.mustCall((code) => {
assert.strictEqual(code, 0);
if (--count === 0) {
socket.close();
}
}));
}
}));
}, () => {
common.skip('The `reusePort` is not supported');
});
return;
}
const socket = dgram.createSocket(options);
socket.bind(+process.env.port, common.mustCall(() => {
socket.close();
})).on('error', common.mustNotCall());

View File

@ -0,0 +1,39 @@
'use strict';
const common = require('../common');
if (common.isWindows)
common.skip('dgram clustering is currently not supported on windows.');
const { checkSupportReusePort, options } = require('../common/udp');
const assert = require('assert');
const cluster = require('cluster');
const dgram = require('dgram');
if (cluster.isPrimary) {
checkSupportReusePort().then(() => {
cluster.fork().on('exit', common.mustCall((code) => {
assert.strictEqual(code, 0);
}));
}, () => {
common.skip('The `reusePort` option is not supported');
});
return;
}
let waiting = 2;
function close() {
if (--waiting === 0)
cluster.worker.disconnect();
}
// Test if the worker requests the main process to create a socket
cluster._getServer = common.mustNotCall();
const socket1 = dgram.createSocket(options);
const socket2 = dgram.createSocket(options);
socket1.bind(0, () => {
socket2.bind(socket1.address().port, () => {
socket1.close(close);
socket2.close(close);
}).on('error', common.mustNotCall());
}).on('error', common.mustNotCall());

View File

@ -0,0 +1,21 @@
'use strict';
const common = require('../common');
const { checkSupportReusePort, options } = require('../common/udp');
const dgram = require('dgram');
function test() {
const socket1 = dgram.createSocket(options);
const socket2 = dgram.createSocket(options);
socket1.bind(0, common.mustCall(() => {
socket2.bind(socket1.address().port, common.mustCall(() => {
socket1.close();
socket2.close();
}));
}));
socket1.on('error', common.mustNotCall());
socket2.on('error', common.mustNotCall());
}
checkSupportReusePort().then(test, () => {
common.skip('The `reusePort` option is not supported');
});