Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions doc/api/dgram.md
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,42 @@ 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 and any error such as
`ECONNREFUSED` is thrown synchronously rather than reported via the `'error'`
event:

```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.

`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
Expand Down Expand Up @@ -1070,4 +1106,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
55 changes: 55 additions & 0 deletions lib/dgram.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -501,6 +507,55 @@ Socket.prototype.connect = function(port, address, callback) {
FunctionPrototypeCall(_connect, this, port, address, callback);
};

// Synchronous counterpart of connect(). For a UDP socket connect(2) only sets
// the default peer address and is a local, non-blocking operation, so this
// connects inline and throws synchronously on error. The address must be a
// numeric IP literal: asynchronous name resolution is the only genuinely
// blocking part of connect(), so callers resolve names separately. If the
// socket is still unbound it is bound synchronously first (see bindSync()).
// The 'connect' event is emitted on the next tick.
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();

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];
Expand Down
142 changes: 142 additions & 0 deletions test/parallel/test-dgram-connect-sync.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const dgram = require('dgram');

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

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