net: add UV_TCP_REUSEPORT for tcp

PR-URL: https://github.com/nodejs/node/pull/55408
Refs: https://github.com/libuv/libuv/pull/4407
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Paolo Insogna <paolo@cowtech.it>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>
This commit is contained in:
theanarkh 2024-10-21 21:10:53 +08:00 committed by GitHub
parent 6a02c2701e
commit 7bc3e16da1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 156 additions and 9 deletions

View File

@ -471,6 +471,9 @@ Listening on a file descriptor is not supported on Windows.
<!-- YAML <!-- YAML
added: v0.11.14 added: v0.11.14
changes: changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/55408
description: The `reusePort` option is supported.
- version: v15.6.0 - version: v15.6.0
pr-url: https://github.com/nodejs/node/pull/36623 pr-url: https://github.com/nodejs/node/pull/36623
description: AbortSignal support was added. description: AbortSignal support was added.
@ -487,6 +490,11 @@ changes:
* `ipv6Only` {boolean} For TCP servers, setting `ipv6Only` to `true` will * `ipv6Only` {boolean} For TCP servers, setting `ipv6Only` to `true` will
disable dual-stack support, i.e., binding to host `::` won't make disable dual-stack support, i.e., binding to host `::` won't make
`0.0.0.0` be bound. **Default:** `false`. `0.0.0.0` be bound. **Default:** `false`.
* `reusePort` {boolean} For TCP servers, setting `reusePort` to `true` allows
multiple sockets on the same host to bind to the same port. Incoming connections
are distributed by the operating system to listening sockets. This 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+. **Default:** `false`.
* `path` {string} Will be ignored if `port` is specified. See * `path` {string} Will be ignored if `port` is specified. See
[Identifying paths for IPC connections][]. [Identifying paths for IPC connections][].
* `port` {number} * `port` {number}

View File

@ -164,8 +164,15 @@ const {
} = require('internal/perf/observe'); } = require('internal/perf/observe');
const { getDefaultHighWaterMark } = require('internal/streams/state'); const { getDefaultHighWaterMark } = require('internal/streams/state');
function getFlags(ipv6Only) { function getFlags(options) {
return ipv6Only === true ? TCPConstants.UV_TCP_IPV6ONLY : 0; let flags = 0;
if (options.ipv6Only === true) {
flags |= TCPConstants.UV_TCP_IPV6ONLY;
}
if (options.reusePort === true) {
flags |= TCPConstants.UV_TCP_REUSEPORT;
}
return flags;
} }
function createHandle(fd, is_server) { function createHandle(fd, is_server) {
@ -1833,12 +1840,12 @@ function createServerHandle(address, port, addressType, fd, flags) {
if (err) { if (err) {
handle.close(); handle.close();
// Fallback to ipv4 // Fallback to ipv4
return createServerHandle(DEFAULT_IPV4_ADDR, port); return createServerHandle(DEFAULT_IPV4_ADDR, port, undefined, undefined, flags);
} }
} else if (addressType === 6) { } else if (addressType === 6) {
err = handle.bind6(address, port, flags); err = handle.bind6(address, port, flags);
} else { } else {
err = handle.bind(address, port); err = handle.bind(address, port, flags);
} }
} }
@ -2022,7 +2029,7 @@ Server.prototype.listen = function(...args) {
toNumber(args.length > 2 && args[2]); // (port, host, backlog) toNumber(args.length > 2 && args[2]); // (port, host, backlog)
options = options._handle || options.handle || options; options = options._handle || options.handle || options;
const flags = getFlags(options.ipv6Only); const flags = getFlags(options);
// Refresh the id to make the previous call invalid // Refresh the id to make the previous call invalid
this._listeningId++; this._listeningId++;
// (handle[, backlog][, cb]) where handle is an object with a handle // (handle[, backlog][, cb]) where handle is an object with a handle
@ -2055,6 +2062,9 @@ Server.prototype.listen = function(...args) {
if (typeof options.port === 'number' || typeof options.port === 'string') { if (typeof options.port === 'number' || typeof options.port === 'string') {
validatePort(options.port, 'options.port'); validatePort(options.port, 'options.port');
backlog = options.backlog || backlogFromArgs; backlog = options.backlog || backlogFromArgs;
if (options.reusePort === true) {
options.exclusive = true;
}
// start TCP server listening on host:port // start TCP server listening on host:port
if (options.host) { if (options.host) {
lookupAndListen(this, options.port | 0, options.host, backlog, lookupAndListen(this, options.port | 0, options.host, backlog,
@ -2062,7 +2072,7 @@ Server.prototype.listen = function(...args) {
} else { // Undefined host, listens on unspecified address } else { // Undefined host, listens on unspecified address
// Default addressType 4 will be used to search for primary server // Default addressType 4 will be used to search for primary server
listenInCluster(this, null, options.port | 0, 4, listenInCluster(this, null, options.port | 0, 4,
backlog, undefined, options.exclusive); backlog, undefined, options.exclusive, flags);
} }
return this; return this;
} }

View File

@ -123,6 +123,7 @@ void TCPWrap::Initialize(Local<Object> target,
NODE_DEFINE_CONSTANT(constants, SOCKET); NODE_DEFINE_CONSTANT(constants, SOCKET);
NODE_DEFINE_CONSTANT(constants, SERVER); NODE_DEFINE_CONSTANT(constants, SERVER);
NODE_DEFINE_CONSTANT(constants, UV_TCP_IPV6ONLY); NODE_DEFINE_CONSTANT(constants, UV_TCP_IPV6ONLY);
NODE_DEFINE_CONSTANT(constants, UV_TCP_REUSEPORT);
target->Set(context, target->Set(context,
env->constants_string(), env->constants_string(),
constants).Check(); constants).Check();
@ -246,9 +247,12 @@ void TCPWrap::Bind(
int port; int port;
unsigned int flags = 0; unsigned int flags = 0;
if (!args[1]->Int32Value(env->context()).To(&port)) return; if (!args[1]->Int32Value(env->context()).To(&port)) return;
if (family == AF_INET6 && if (args.Length() >= 3 && args[2]->IsUint32()) {
!args[2]->Uint32Value(env->context()).To(&flags)) { if (!args[2]->Uint32Value(env->context()).To(&flags)) return;
return; // Can not set IPV6 flags on IPV4 socket
if (family == AF_INET) {
flags &= ~UV_TCP_IPV6ONLY;
}
} }
T addr; T addr;

23
test/common/net.js Normal file
View File

@ -0,0 +1,23 @@
'use strict';
const net = require('net');
const options = { port: 0, reusePort: true };
function checkSupportReusePort() {
return new Promise((resolve, reject) => {
const server = net.createServer().listen(options);
server.on('listening', () => {
server.close(resolve);
});
server.on('error', (err) => {
console.log('The `reusePort` option is not supported:', err.message);
server.close();
reject(err);
});
});
}
module.exports = {
checkSupportReusePort,
options,
};

View File

@ -0,0 +1,35 @@
'use strict';
const common = require('../common');
const { checkSupportReusePort, options } = require('../common/net');
const assert = require('assert');
const child_process = require('child_process');
const net = require('net');
if (!process.env.isWorker) {
checkSupportReusePort().then(() => {
const server = net.createServer();
server.listen(options, common.mustCall(() => {
const port = server.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) {
server.close();
}
}));
}
}));
}, () => {
common.skip('The `reusePort` option is not supported');
});
return;
}
const server = net.createServer();
server.listen({ ...options, port: +process.env.port }, common.mustCall(() => {
server.close();
})).on('error', common.mustNotCall());

View File

@ -0,0 +1,41 @@
'use strict';
const common = require('../common');
const { checkSupportReusePort, options } = require('../common/net');
const assert = require('assert');
const cluster = require('cluster');
const net = require('net');
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();
}
const server1 = net.createServer();
const server2 = net.createServer();
// Test if the worker requests the main process to create a socket
cluster._getServer = common.mustNotCall();
server1.listen(options, common.mustCall(() => {
const port = server1.address().port;
server2.listen({ ...options, port }, common.mustCall(() => {
server1.close(close);
server2.close(close);
}));
}));
server1.on('error', common.mustNotCall());
server2.on('error', common.mustNotCall());

View File

@ -0,0 +1,26 @@
'use strict';
const common = require('../common');
const { checkSupportReusePort, options } = require('../common/net');
const net = require('net');
function test(host) {
const server1 = net.createServer();
const server2 = net.createServer();
server1.listen({ ...options, host }, common.mustCall(() => {
const port = server1.address().port;
server2.listen({ ...options, host, port }, common.mustCall(() => {
server1.close();
server2.close();
}));
}));
server1.on('error', common.mustNotCall());
server2.on('error', common.mustNotCall());
}
checkSupportReusePort()
.then(() => {
test('127.0.0.1');
common.hasIPv6 && test('::');
}, () => {
common.skip('The `reusePort` option is not supported');
});