dgram: add synchronous Socket connectSync()

Signed-off-by: Guy Bedford <guybedford@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/63932
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Guy Bedford authored and GitHub committed 2026-06-17 19:09:05 +00:00
1 parent 4d09c04253
commit eea8d72052
3 files changed
+264

No files matched your search

+44
View File
@@ -447,6 +447,47 @@ will be used by default. Once the connection is complete, a `'connect'` event
is emitted and the optional `callback` function is called. In case of failure,
the `callback` is called or, failing this, an `'error'` event is emitted.
### `socket.connectSync(port[, address])`
<!-- YAML
added: REPLACEME
-->
* `port` {integer}
* `address` {string} A numeric IP address to connect to. Unlike
[`socket.connect()`][], no DNS resolution is performed, so a host name is not
accepted. If omitted, `'127.0.0.1'` (for `udp4` sockets) or `'::1'` (for
`udp6` sockets) is used.
The synchronous counterpart of [`socket.connect()`][]. For a UDP socket
`connect(2)` only records the default peer address and is a local, non-blocking
system call, so the association is performed inline. Any error raised by the
call itself (for example `EAFNOSUPPORT` for a mismatched address family) is
thrown synchronously rather than reported via the `'error'` event. Because
`connect(2)` does not probe reachability, errors such as `ECONNREFUSED` are
still surfaced asynchronously on a later send or receive, exactly as for
[`socket.connect()`][]:
```js
const dgram = require('node:dgram');
const socket = dgram.createSocket('udp4');
socket.connectSync(41234, '127.0.0.1');
console.log(socket.remoteAddress()); // { address: '127.0.0.1', family: 'IPv4', port: 41234 }
```
If the socket is still unbound it is bound synchronously first. After
`connectSync()` returns, [`socket.remoteAddress()`][] is valid synchronously
and the `'connect'` event is emitted on the next tick. Trying to call
`connectSync()` on an already connected socket throws an
[`ERR_SOCKET_DGRAM_IS_CONNECTED`][] exception, and calling it while an
asynchronous [`socket.bind()`][] is still in progress throws an
[`ERR_SOCKET_ALREADY_BOUND`][] exception.
`address` must be a numeric IP literal; `connectSync()` never performs DNS
resolution (asynchronous name resolution being the only genuinely blocking part
of connecting).
### `socket.disconnect()`
<!-- YAML
@@ -1054,6 +1095,7 @@ and `udp6` sockets). The bound address and port can be retrieved using
[RFC 4007]: https://tools.ietf.org/html/rfc4007
[`'close'`]: #event-close
[`'message'`]: #event-message
[`ERR_SOCKET_ALREADY_BOUND`]: errors.md#err_socket_already_bound
[`ERR_SOCKET_BAD_PORT`]: errors.md#err_socket_bad_port
[`ERR_SOCKET_BUFFER_SIZE`]: errors.md#err_socket_buffer_size
[`ERR_SOCKET_DGRAM_IS_CONNECTED`]: errors.md#err_socket_dgram_is_connected
@@ -1070,4 +1112,6 @@ and `udp6` sockets). The bound address and port can be retrieved using
[`socket.address()`]: #socketaddress
[`socket.bind()`]: #socketbindport-address-callback
[`socket.close()`]: #socketclosecallback
[`socket.connect()`]: #socketconnectport-address-callback
[`socket.remoteAddress()`]: #socketremoteaddress
[byte length]: buffer.md#static-method-bufferbytelengthstring-encoding
+54
View File
@@ -221,6 +221,12 @@ function emitListeningNT(socket) {
socket.emit('listening');
}
function emitConnectNT(socket) {
// Ensure the socket was not closed before the next tick.
if (socket[kStateSymbol].handle)
socket.emit('connect');
}
function replaceHandle(self, newHandle) {
const state = self[kStateSymbol];
const oldHandle = state.handle;
@@ -501,6 +507,54 @@ Socket.prototype.connect = function(port, address, callback) {
FunctionPrototypeCall(_connect, this, port, address, callback);
};
// Synchronous counterpart of connect(). connect(2) on a UDP socket only records
// the default peer locally, so it runs inline and throws on errors raised by
// the call itself; unreachable-peer errors stay asynchronous as for connect().
// The address must be a numeric IP literal since DNS resolution is async.
Socket.prototype.connectSync = function(port, address) {
healthCheck(this);
port = validatePort(port, 'Port', false);
const state = this[kStateSymbol];
if (state.connectState !== CONNECT_STATE_DISCONNECTED)
throw new ERR_SOCKET_DGRAM_IS_CONNECTED();
// Validate arguments before mutating state so a bad argument leaves the
// socket reusable.
if (address == null || address === '') {
address = this.type === 'udp4' ? '127.0.0.1' : '::1';
} else {
validateString(address, 'address');
if (isIP(address) === 0) {
throw new ERR_INVALID_ARG_VALUE(
'address', address,
'must be a numeric IP address; connectSync does not perform DNS resolution');
}
}
if (state.bindState === BIND_STATE_UNBOUND)
this.bindSync();
else if (state.bindState !== BIND_STATE_BOUND)
throw new ERR_SOCKET_ALREADY_BOUND();
state.connectState = CONNECT_STATE_CONNECTING;
if (state.sendBlockList?.check(address, `ipv${isIP(address)}`)) {
state.connectState = CONNECT_STATE_DISCONNECTED;
throw new ERR_IP_BLOCKED(address);
}
const err = state.handle.connect(address, port);
if (err) {
state.connectState = CONNECT_STATE_DISCONNECTED;
throw new ExceptionWithHostPort(err, 'connect', address, port);
}
state.connectState = CONNECT_STATE_CONNECTED;
process.nextTick(emitConnectNT, this);
};
function _connect(port, address, callback) {
const state = this[kStateSymbol];
+166
View File
@@ -0,0 +1,166 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const dgram = require('dgram');
const net = require('net');
// connectSync() connects synchronously, binding the socket first when needed,
// and remoteAddress() is valid immediately.
{
const sock = dgram.createSocket('udp4');
sock.connectSync(12345, '127.0.0.1');
const peer = sock.remoteAddress();
assert.strictEqual(peer.address, '127.0.0.1');
assert.strictEqual(peer.family, 'IPv4');
assert.strictEqual(peer.port, 12345);
// The socket was bound synchronously as part of connecting.
assert.ok(sock.address().port > 0);
// The 'connect' event still fires on the next tick.
sock.on('connect', common.mustCall(() => sock.close()));
}
// Closing synchronously after connectSync() suppresses the deferred 'connect'.
{
const sock = dgram.createSocket('udp4');
sock.connectSync(12345, '127.0.0.1');
sock.on('connect', common.mustNotCall());
sock.close();
}
// Defaults the address to the udp4 loopback when omitted.
{
const sock = dgram.createSocket('udp4');
sock.connectSync(12345);
assert.strictEqual(sock.remoteAddress().address, '127.0.0.1');
sock.close();
}
// Works on a socket already bound with bindSync().
{
const sock = dgram.createSocket('udp4');
sock.bindSync({ address: '127.0.0.1', port: 0 });
const boundPort = sock.address().port;
sock.connectSync(12345, '127.0.0.1');
assert.strictEqual(sock.address().port, boundPort);
assert.strictEqual(sock.remoteAddress().port, 12345);
sock.close();
}
// Datagrams flow to the connected peer after a synchronous connect.
{
const receiver = dgram.createSocket('udp4');
const addr = receiver.bindSync({ address: '127.0.0.1', port: 0 });
receiver.on('message', common.mustCall((msg) => {
assert.strictEqual(msg.toString(), 'hello');
receiver.close();
}));
const sender = dgram.createSocket('udp4');
sender.connectSync(addr.port, '127.0.0.1');
sender.send('hello', common.mustCall(() => sender.close()));
}
// disconnect() works after a synchronous connect, allowing a reconnect.
{
const sock = dgram.createSocket('udp4');
sock.connectSync(12345, '127.0.0.1');
sock.disconnect();
sock.connectSync(12346, '127.0.0.1');
assert.strictEqual(sock.remoteAddress().port, 12346);
sock.close();
}
// Throws synchronously on a non-numeric address (no DNS resolution).
{
const sock = dgram.createSocket('udp4');
assert.throws(() => {
sock.connectSync(12345, 'localhost');
}, {
code: 'ERR_INVALID_ARG_VALUE',
name: 'TypeError',
});
sock.close();
}
// Rejects a non-string address.
{
const sock = dgram.createSocket('udp4');
assert.throws(() => sock.connectSync(12345, 12345), {
code: 'ERR_INVALID_ARG_TYPE',
});
sock.close();
}
// A rejected argument leaves the socket unbound and reusable.
{
const sock = dgram.createSocket('udp4');
assert.throws(() => sock.connectSync(-1, '127.0.0.1'), {
code: 'ERR_SOCKET_BAD_PORT',
});
sock.connectSync(12345, '127.0.0.1');
assert.strictEqual(sock.remoteAddress().port, 12345);
sock.close();
}
// Throws when already connected.
{
const sock = dgram.createSocket('udp4');
sock.connectSync(12345, '127.0.0.1');
assert.throws(() => sock.connectSync(12346, '127.0.0.1'), {
code: 'ERR_SOCKET_DGRAM_IS_CONNECTED',
});
sock.close();
}
// Throws synchronously for a blocked address, leaving the socket reusable.
{
const blockList = new net.BlockList();
blockList.addAddress('127.0.0.1');
const sock = dgram.createSocket({ type: 'udp4', sendBlockList: blockList });
assert.throws(() => sock.connectSync(12345, '127.0.0.1'), {
code: 'ERR_IP_BLOCKED',
});
sock.connectSync(12345, '127.0.0.2');
assert.strictEqual(sock.remoteAddress().address, '127.0.0.2');
sock.close();
}
// Throws when an asynchronous bind() is still in progress.
{
const sock = dgram.createSocket('udp4');
sock.bind(0, '127.0.0.1');
assert.throws(() => sock.connectSync(12345, '127.0.0.1'), {
code: 'ERR_SOCKET_ALREADY_BOUND',
});
sock.on('listening', common.mustCall(() => sock.close()));
}
// udp6 loopback default.
if (common.hasIPv6) {
const sock = dgram.createSocket('udp6');
sock.connectSync(12345);
const peer = sock.remoteAddress();
assert.strictEqual(peer.address, '::1');
assert.strictEqual(peer.family, 'IPv6');
assert.strictEqual(peer.port, 12345);
sock.close();
}
// udp6 datagrams flow to the connected peer after a synchronous connect.
if (common.hasIPv6) {
const receiver = dgram.createSocket('udp6');
const addr = receiver.bindSync({ address: '::1', port: 0 });
receiver.on('message', common.mustCall((msg) => {
assert.strictEqual(msg.toString(), 'hello');
receiver.close();
}));
const sender = dgram.createSocket('udp6');
sender.connectSync(addr.port, '::1');
sender.send('hello', common.mustCall(() => sender.close()));
}