From 7c9b51ec3d5bd48ced377ab9116580e117e75984 Mon Sep 17 00:00:00 2001 From: Shogun Date: Tue, 28 Dec 2021 16:56:31 +0100 Subject: [PATCH] net: add new options to `net.Socket` and `net.Server` --- doc/api/http.md | 12 +++++ doc/api/net.md | 19 +++++++ lib/_http_server.js | 6 ++- lib/net.js | 56 +++++++++++++++++++-- test/parallel/test-net-connect-keepalive.js | 56 +++++++++++++++++++++ test/parallel/test-net-connect-nodelay.js | 49 ++++++++++++++++++ 6 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 test/parallel/test-net-connect-keepalive.js create mode 100644 test/parallel/test-net-connect-nodelay.js diff --git a/doc/api/http.md b/doc/api/http.md index 83a401d97ee2eb..f23fcb8ab2c589 100644 --- a/doc/api/http.md +++ b/doc/api/http.md @@ -2865,6 +2865,16 @@ changes: [`--max-http-header-size`][] for requests received by this server, i.e. the maximum length of request headers in bytes. **Default:** 16384 (16 KB). + * `noDelay` {boolean} If set to `true`, it disables the use of Nagle's + algorithm immediately after a new incoming connection is received. + **Default:** `false`. + * `keepAlive` {boolean} If set to `true`, it enables keep-alive functionality + on the socket immediately after a new incoming connection is received, + similarly on what is done in \[`socket.setKeepAlive([enable][, initialDelay])`]\[`socket.setKeepAlive(enable, initialDelay)`]. + **Default:** `false`. + * `keepAliveInitialDelay` {number} If set to a positive number, it sets the + initial delay before the first keepalive probe is sent on an idle socket. + **Default:** `0`. * `requestListener` {Function} @@ -3108,6 +3118,8 @@ changes: * `callback` {Function} * Returns: {http.ClientRequest} +`options` in [`socket.connect()`][] are also supported. + Node.js maintains several connections per server to make HTTP requests. This function allows one to transparently issue requests. diff --git a/doc/api/net.md b/doc/api/net.md index f8bcb8dfc9db2e..ab65ad9ef1c9e2 100644 --- a/doc/api/net.md +++ b/doc/api/net.md @@ -850,6 +850,14 @@ For TCP connections, available `options` are: `0` indicates that both IPv4 and IPv6 addresses are allowed. **Default:** `0`. * `hints` {number} Optional [`dns.lookup()` hints][]. * `lookup` {Function} Custom lookup function. **Default:** [`dns.lookup()`][]. +* `noDelay` {boolean} If set to `true`, it disables the use of Nagle's algorithm immediately + after the socket is established. **Default:** `false`. +* `keepAlive` {boolean} If set to `true`, it enables keep-alive functionality on the socket + immediately after the connection is established, similarly on what is done in + [`socket.setKeepAlive([enable][, initialDelay])`][`socket.setKeepAlive(enable, initialDelay)`]. + **Default:** `false`. +* `keepAliveInitialDelay` {number} If set to a positive number, it sets the initial delay before + the first keepalive probe is sent on an idle socket.**Default:** `0`. For [IPC][] connections, available `options` are: @@ -1405,8 +1413,18 @@ added: v0.5.0 **Default:** `false`. * `pauseOnConnect` {boolean} Indicates whether the socket should be paused on incoming connections. **Default:** `false`. + * `noDelay` {boolean} If set to `true`, it disables the use of Nagle's algorithm immediately + after a new incoming connection is received. **Default:** `false`. + * `keepAlive` {boolean} If set to `true`, it enables keep-alive functionality on the socket + immediately after a new incoming connection is received, similarly on what is done in + [`socket.setKeepAlive([enable][, initialDelay])`][`socket.setKeepAlive(enable, initialDelay)`]. + **Default:** `false`. + * `keepAliveInitialDelay` {number} If set to a positive number, it sets the initial delay before + the first keepalive probe is sent on an idle socket.**Default:** `0`. + * `connectionListener` {Function} Automatically set as a listener for the [`'connection'`][] event. + * Returns: {net.Server} Creates a new TCP or [IPC][] server. @@ -1572,6 +1590,7 @@ net.isIPv6('fhqwhgads'); // returns false [`socket.pause()`]: #socketpause [`socket.resume()`]: #socketresume [`socket.setEncoding()`]: #socketsetencodingencoding +[`socket.setKeepAlive(enable, initialDelay)`]: #socketsetkeepaliveenable-initialdelay [`socket.setTimeout()`]: #socketsettimeouttimeout-callback [`socket.setTimeout(timeout)`]: #socketsettimeouttimeout-callback [`writable.destroy()`]: stream.md#writabledestroyerror diff --git a/lib/_http_server.js b/lib/_http_server.js index 0d06919ea017a4..79a119486f7ef4 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -378,7 +378,11 @@ function Server(options, requestListener) { } storeHTTPOptions.call(this, options); - net.Server.call(this, { allowHalfOpen: true }); + net.Server.call( + this, + { allowHalfOpen: true, noDelay: options.noDelay, + keepAlive: options.keepAlive, + keepAliveInitialDelay: options.keepAliveInitialDelay }); if (requestListener) { this.on('request', requestListener); diff --git a/lib/net.js b/lib/net.js index 3bbe96f1e04af0..78aa756a358a71 100644 --- a/lib/net.js +++ b/lib/net.js @@ -279,6 +279,8 @@ function initSocketHandle(self) { const kBytesRead = Symbol('kBytesRead'); const kBytesWritten = Symbol('kBytesWritten'); const kSetNoDelay = Symbol('kSetNoDelay'); +const kSetKeepAlive = Symbol('kSetKeepAlive'); +const kSetKeepAliveInitialDelay = Symbol('kSetKeepAliveInitialDelay'); function Socket(options) { if (!(this instanceof Socket)) return new Socket(options); @@ -297,6 +299,15 @@ function Socket(options) { 'is not supported' ); } + if (typeof options?.keepAliveInitialDelay !== 'undefined') { + validateNumber( + options?.keepAliveInitialDelay, 'options.keepAliveInitialDelay' + ); + + if (options.keepAliveInitialDelay < 0) { + options.keepAliveInitialDelay = 0; + } + } this.connecting = false; // Problem with this is that users can supply their own handle, that may not @@ -307,7 +318,6 @@ function Socket(options) { this[kHandle] = null; this._parent = null; this._host = null; - this[kSetNoDelay] = false; this[kLastWriteQueueSize] = 0; this[kTimeout] = null; this[kBuffer] = null; @@ -381,6 +391,10 @@ function Socket(options) { this[kBufferCb] = onread.callback; } + this[kSetNoDelay] = Boolean(options.noDelay); + this[kSetKeepAlive] = Boolean(options.keepAlive); + this[kSetKeepAliveInitialDelay] = ~~(options.keepAliveInitialDelay / 1000); + // Shut down the socket when we're finished with it. this.on('end', onReadableStreamEnd); @@ -520,14 +534,18 @@ Socket.prototype.setNoDelay = function(enable) { }; -Socket.prototype.setKeepAlive = function(setting, msecs) { +Socket.prototype.setKeepAlive = function(enable, initialDelayMsecs) { if (!this._handle) { - this.once('connect', () => this.setKeepAlive(setting, msecs)); + this.once('connect', () => this.setKeepAlive(enable, initialDelayMsecs)); return this; } - if (this._handle.setKeepAlive) - this._handle.setKeepAlive(setting, ~~(msecs / 1000)); + if (this._handle.setKeepAlive && enable !== this[kSetKeepAlive]) { + const initialDelay = ~~(initialDelayMsecs / 1000); + this[kSetKeepAlive] = enable; + this[kSetKeepAliveInitialDelay] = initialDelay; + this._handle.setKeepAlive(enable, initialDelay); + } return this; }; @@ -1140,6 +1158,14 @@ function afterConnect(status, handle, req, readable, writable) { } self._unrefTimer(); + if (self[kSetNoDelay] && self._handle.setNoDelay) { + self._handle.setNoDelay(true); + } + + if (self[kSetKeepAlive] && self._handle.setKeepAlive) { + self._handle.setKeepAlive(true, self[kSetKeepAliveInitialDelay]); + } + self.emit('connect'); self.emit('ready'); @@ -1203,6 +1229,15 @@ function Server(options, connectionListener) { } else { throw new ERR_INVALID_ARG_TYPE('options', 'Object', options); } + if (typeof options.keepAliveInitialDelay !== 'undefined') { + validateNumber( + options.keepAliveInitialDelay, 'options.keepAliveInitialDelay' + ); + + if (options.keepAliveInitialDelay < 0) { + options.keepAliveInitialDelay = 0; + } + } this._connections = 0; @@ -1214,6 +1249,9 @@ function Server(options, connectionListener) { this.allowHalfOpen = options.allowHalfOpen || false; this.pauseOnConnect = !!options.pauseOnConnect; + this.noDelay = Boolean(options.noDelay); + this.keepAlive = Boolean(options.keepAlive); + this.keepAliveInitialDelay = ~~(options.keepAliveInitialDelay / 1000); } ObjectSetPrototypeOf(Server.prototype, EventEmitter.prototype); ObjectSetPrototypeOf(Server, EventEmitter); @@ -1565,6 +1603,14 @@ function onconnection(err, clientHandle) { writable: true }); + if (self.noDelay && handle.setNoDelay) { + handle.setNoDelay(true); + } + + if (self.keepAlive && self.setKeepAlive) { + handle.setKeepAlive(true, handle.keepAliveInitialDelay); + } + self._connections++; socket.server = self; socket._server = self; diff --git a/test/parallel/test-net-connect-keepalive.js b/test/parallel/test-net-connect-keepalive.js new file mode 100644 index 00000000000000..3e439c647c36be --- /dev/null +++ b/test/parallel/test-net-connect-keepalive.js @@ -0,0 +1,56 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const truthyValues = [true, 1, 'true', {}, []]; +const delays = [[123, 0], [456123, 456], [-123000, 0], [undefined, 0]]; +const falseyValues = [false, 0, '']; + +const genSetKeepAlive = (desiredEnable, desiredDelay) => (enable, delay) => { + assert.strictEqual(enable, desiredEnable); + assert.strictEqual(delay, desiredDelay); +}; + +for (const value of truthyValues) { + for (const delay of delays) { + const server = net.createServer(); + + server.listen(0, common.mustCall(function() { + const port = server.address().port; + + const client = net.connect( + { port, keepAlive: value, keepAliveInitialDelay: delay[0] }, + common.mustCall(() => client.end()) + ); + + client._handle.setKeepAlive = common.mustCall( + genSetKeepAlive(true, delay[1]) + ); + + client.on('end', common.mustCall(function() { + server.close(); + })); + })); + } +} + +for (const value of falseyValues) { + const server = net.createServer(); + + server.listen(0, common.mustCall(function() { + const port = server.address().port; + + const client = net.connect( + { port, keepAlive: value }, + common.mustCall(() => client.end()) + ); + + client._handle.setKeepAlive = common.mustNotCall(); + + client.on('end', common.mustCall(function() { + server.close(); + })); + })); +} diff --git a/test/parallel/test-net-connect-nodelay.js b/test/parallel/test-net-connect-nodelay.js new file mode 100644 index 00000000000000..6810e339e2b6c4 --- /dev/null +++ b/test/parallel/test-net-connect-nodelay.js @@ -0,0 +1,49 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const net = require('net'); + +const truthyValues = [true, 1, 'true', {}, []]; +const falseyValues = [false, 0, '']; +const genSetNoDelay = (desiredArg) => (enable) => { + assert.strictEqual(enable, desiredArg); +}; + +for (const value of truthyValues) { + const server = net.createServer(); + + server.listen(0, common.mustCall(function() { + const port = server.address().port; + + const client = net.connect( + { port, noDelay: value }, + common.mustCall(() => client.end()) + ); + + client._handle.setNoDelay = common.mustCall(genSetNoDelay(true)); + + client.on('end', common.mustCall(function() { + server.close(); + })); + })); +} + +for (const value of falseyValues) { + const server = net.createServer(); + + server.listen(0, common.mustCall(function() { + const port = server.address().port; + + const client = net.connect( + { port, noDelay: value }, + common.mustCall(() => client.end()) + ); + + client._handle.setNoDelay = common.mustNotCall(); + + client.on('end', common.mustCall(function() { + server.close(); + })); + })); +}