Skip to content

Commit f8c967c

Browse files
guybedfordaduh95
authored andcommitted
dgram: add synchronous Socket.prototype.bindSync()
Provides sync non-blocking bind(2), without DNS lookup Signed-off-by: Guy Bedford <[email protected]> PR-URL: #63838 Reviewed-By: James M Snell <[email protected]> Reviewed-By: Matteo Collina <[email protected]>
1 parent 4f4245d commit f8c967c

3 files changed

Lines changed: 277 additions & 2 deletions

File tree

doc/api/dgram.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,44 @@ socket.bind({
363363
});
364364
```
365365

366+
### `socket.bindSync([options])`
367+
368+
<!-- YAML
369+
added: REPLACEME
370+
-->
371+
372+
* `options` {Object}
373+
* `port` {integer} If omitted or `0`, the operating system will assign an
374+
arbitrary unused port. **Default:** `0`.
375+
* `address` {string} A numeric IP address to bind to. Unlike
376+
[`socket.bind()`][], no DNS resolution is performed, so a host name is not
377+
accepted. If omitted, the operating system binds to all addresses
378+
(`'0.0.0.0'` for `udp4` sockets, `'::'` for `udp6`).
379+
* Returns: {Object} The bound address as returned by [`socket.address()`][].
380+
381+
The synchronous counterpart of [`socket.bind()`][]. `bind(2)` is a local,
382+
non-blocking system call, so the bind is performed inline and the resolved
383+
address is returned immediately, including the operating-system-assigned
384+
ephemeral port when `port` is `0`:
385+
386+
```js
387+
const dgram = require('node:dgram');
388+
389+
const socket = dgram.createSocket('udp4');
390+
const address = socket.bindSync({ address: '0.0.0.0', port: 0 });
391+
console.log(address); // e.g. { address: '0.0.0.0', family: 'IPv4', port: 53124 }
392+
```
393+
394+
A bind failure such as `EADDRINUSE` is thrown synchronously rather than emitted
395+
as an `'error'` event. After `bindSync()` returns, [`socket.address()`][] is
396+
valid synchronously and the `'listening'` event is emitted on the next tick.
397+
398+
`address` must be a numeric IP literal; `bindSync()` never performs DNS
399+
resolution (asynchronous name resolution being the only genuinely blocking part
400+
of binding). Incoming datagrams continue to be delivered asynchronously via the
401+
[`'message'`][] event. `bindSync()` always binds the socket's own handle and
402+
does not participate in [`cluster`][] handle sharing.
403+
366404
### `socket.close([callback])`
367405

368406
<!-- YAML
@@ -1017,6 +1055,7 @@ and `udp6` sockets). The bound address and port can be retrieved using
10171055
[IPv6 Zone Indexes]: https://en.wikipedia.org/wiki/IPv6_address#Scoped_literal_IPv6_addresses
10181056
[RFC 4007]: https://tools.ietf.org/html/rfc4007
10191057
[`'close'`]: #event-close
1058+
[`'message'`]: #event-message
10201059
[`ERR_SOCKET_BAD_PORT`]: errors.md#err_socket_bad_port
10211060
[`ERR_SOCKET_BUFFER_SIZE`]: errors.md#err_socket_buffer_size
10221061
[`ERR_SOCKET_DGRAM_IS_CONNECTED`]: errors.md#err_socket_dgram_is_connected
@@ -1030,6 +1069,7 @@ and `udp6` sockets). The bound address and port can be retrieved using
10301069
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
10311070
[`socket.address().address`]: #socketaddress
10321071
[`socket.address().port`]: #socketaddress
1072+
[`socket.address()`]: #socketaddress
10331073
[`socket.bind()`]: #socketbindport-address-callback
10341074
[`socket.close()`]: #socketclosecallback
10351075
[byte length]: buffer.md#static-method-bufferbytelengthstring-encoding

lib/dgram.js

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ const {
3939
codes: {
4040
ERR_BUFFER_OUT_OF_BOUNDS,
4141
ERR_INVALID_ARG_TYPE,
42+
ERR_INVALID_ARG_VALUE,
4243
ERR_INVALID_FD_TYPE,
4344
ERR_IP_BLOCKED,
4445
ERR_MISSING_ARGS,
@@ -59,13 +60,14 @@ const { isIP } = require('internal/net');
5960
const {
6061
isInt32,
6162
validateAbortSignal,
63+
validateObject,
6264
validateString,
6365
validateNumber,
6466
validatePort,
6567
validateUint32,
6668
} = require('internal/validators');
6769
const { Buffer } = require('buffer');
68-
const { deprecate, guessHandleType, promisify } = require('internal/util');
70+
const { deprecate, guessHandleType, kEmptyObject, promisify } = require('internal/util');
6971
const { isArrayBufferView } = require('internal/util/types');
7072
const EventEmitter = require('events');
7173
const { addAbortListener } = require('internal/events/abort_listener');
@@ -193,7 +195,7 @@ function createSocket(type, listener) {
193195
}
194196

195197

196-
function startListening(socket) {
198+
function startReceiving(socket) {
197199
const state = socket[kStateSymbol];
198200

199201
state.handle.onmessage = onMessage;
@@ -207,10 +209,19 @@ function startListening(socket) {
207209

208210
if (state.sendBufferSize)
209211
bufferSize(socket, state.sendBufferSize, SEND_BUFFER);
212+
}
210213

214+
function startListening(socket) {
215+
startReceiving(socket);
211216
socket.emit('listening');
212217
}
213218

219+
function emitListeningNT(socket) {
220+
// Ensure the socket was not closed before the next tick.
221+
if (socket[kStateSymbol].handle)
222+
socket.emit('listening');
223+
}
224+
214225
function replaceHandle(self, newHandle) {
215226
const state = self[kStateSymbol];
216227
const oldHandle = state.handle;
@@ -410,6 +421,58 @@ Socket.prototype.bind = function(port_, address_ /* , callback */) {
410421
return this;
411422
};
412423

424+
// Synchronous counterpart of bind(). bind(2) is a local, non-blocking system
425+
// call, so this binds inline and returns the resolved address (including the
426+
// OS-assigned ephemeral port when port is 0), throwing synchronously on bind
427+
// errors such as EADDRINUSE. The address must be a numeric IP literal:
428+
// asynchronous name resolution is the only genuinely blocking part of bind(),
429+
// so callers resolve names separately. Message delivery stays asynchronous
430+
// ('message' events flow as usual); the 'listening' event is emitted on the
431+
// next tick.
432+
Socket.prototype.bindSync = function(options = kEmptyObject) {
433+
healthCheck(this);
434+
validateObject(options, 'options');
435+
const state = this[kStateSymbol];
436+
437+
if (state.bindState !== BIND_STATE_UNBOUND)
438+
throw new ERR_SOCKET_ALREADY_BOUND();
439+
440+
// Validate arguments before mutating state so a bad argument leaves the
441+
// socket unbound and reusable.
442+
const port = validatePort(options.port ?? 0, 'options.port');
443+
let { address } = options;
444+
if (!address) {
445+
address = this.type === 'udp4' ? '0.0.0.0' : '::';
446+
} else {
447+
validateString(address, 'options.address');
448+
if (isIP(address) === 0) {
449+
throw new ERR_INVALID_ARG_VALUE(
450+
'options.address', address,
451+
'must be a numeric IP address; bindSync does not perform DNS resolution');
452+
}
453+
}
454+
455+
state.bindState = BIND_STATE_BINDING;
456+
457+
let flags = 0;
458+
if (state.reuseAddr)
459+
flags |= UV_UDP_REUSEADDR;
460+
if (state.ipv6Only)
461+
flags |= UV_UDP_IPV6ONLY;
462+
if (state.reusePort)
463+
flags |= UV_UDP_REUSEPORT;
464+
465+
const err = state.handle.bind(address, port, flags);
466+
if (err) {
467+
state.bindState = BIND_STATE_UNBOUND;
468+
throw new ExceptionWithHostPort(err, 'bind', address, port);
469+
}
470+
471+
startReceiving(this);
472+
process.nextTick(emitListeningNT, this);
473+
return this.address();
474+
};
475+
413476
Socket.prototype.connect = function(port, address, callback) {
414477
port = validatePort(port, 'Port', false);
415478
if (typeof address === 'function') {
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
'use strict';
2+
const common = require('../common');
3+
const assert = require('assert');
4+
const dgram = require('dgram');
5+
6+
// bindSync() binds synchronously and returns the resolved address, including
7+
// the OS-assigned ephemeral port when port is 0.
8+
{
9+
const sock = dgram.createSocket('udp4');
10+
const addr = sock.bindSync({ address: '127.0.0.1', port: 0 });
11+
12+
assert.strictEqual(addr.address, '127.0.0.1');
13+
assert.strictEqual(addr.family, 'IPv4');
14+
assert.strictEqual(typeof addr.port, 'number');
15+
assert.ok(addr.port > 0);
16+
17+
// address() is valid synchronously and matches the returned address.
18+
assert.deepStrictEqual(sock.address(), addr);
19+
20+
// The 'listening' event still fires on the next tick.
21+
sock.on('listening', common.mustCall(() => sock.close()));
22+
}
23+
24+
// Closing synchronously after bindSync() suppresses the deferred 'listening'.
25+
{
26+
const sock = dgram.createSocket('udp4');
27+
sock.bindSync({ port: 0 });
28+
sock.on('listening', common.mustNotCall());
29+
sock.close();
30+
}
31+
32+
// Defaults the address to the udp4 wildcard when omitted.
33+
{
34+
const sock = dgram.createSocket('udp4');
35+
const addr = sock.bindSync();
36+
assert.strictEqual(addr.address, '0.0.0.0');
37+
assert.ok(addr.port > 0);
38+
sock.close();
39+
}
40+
41+
// 'message' events still flow asynchronously after a synchronous bind.
42+
{
43+
const receiver = dgram.createSocket('udp4');
44+
const addr = receiver.bindSync({ address: '127.0.0.1', port: 0 });
45+
46+
receiver.on('message', common.mustCall((msg) => {
47+
assert.strictEqual(msg.toString(), 'hello');
48+
receiver.close();
49+
}));
50+
51+
const sender = dgram.createSocket('udp4');
52+
sender.send('hello', addr.port, '127.0.0.1', common.mustCall(() => {
53+
sender.close();
54+
}));
55+
}
56+
57+
// Throws synchronously on EADDRINUSE.
58+
{
59+
const first = dgram.createSocket('udp4');
60+
const addr = first.bindSync({ address: '127.0.0.1', port: 0 });
61+
62+
const second = dgram.createSocket('udp4');
63+
assert.throws(() => {
64+
second.bindSync({ address: '127.0.0.1', port: addr.port });
65+
}, {
66+
code: 'EADDRINUSE',
67+
syscall: 'bind',
68+
});
69+
70+
first.close();
71+
second.close();
72+
}
73+
74+
// Throws synchronously on a non-numeric address (no DNS resolution).
75+
{
76+
const sock = dgram.createSocket('udp4');
77+
assert.throws(() => {
78+
sock.bindSync({ address: 'localhost', port: 0 });
79+
}, {
80+
code: 'ERR_INVALID_ARG_VALUE',
81+
name: 'TypeError',
82+
});
83+
sock.close();
84+
}
85+
86+
// Rejects a non-string address.
87+
{
88+
const sock = dgram.createSocket('udp4');
89+
assert.throws(() => sock.bindSync({ address: 12345 }), {
90+
code: 'ERR_INVALID_ARG_TYPE',
91+
});
92+
sock.close();
93+
}
94+
95+
// A rejected argument leaves the socket unbound and reusable.
96+
{
97+
const sock = dgram.createSocket('udp4');
98+
assert.throws(() => sock.bindSync({ port: -1 }), {
99+
code: 'ERR_SOCKET_BAD_PORT',
100+
});
101+
const addr = sock.bindSync({ port: 0 });
102+
assert.ok(addr.port > 0);
103+
sock.close();
104+
}
105+
106+
// Throws when already bound.
107+
{
108+
const sock = dgram.createSocket('udp4');
109+
sock.bindSync({ port: 0 });
110+
assert.throws(() => sock.bindSync({ port: 0 }), {
111+
code: 'ERR_SOCKET_ALREADY_BOUND',
112+
});
113+
sock.close();
114+
}
115+
116+
// Rejects a non-object options argument.
117+
{
118+
const sock = dgram.createSocket('udp4');
119+
assert.throws(() => sock.bindSync(0), { code: 'ERR_INVALID_ARG_TYPE' });
120+
sock.close();
121+
}
122+
123+
// udp6 wildcard default.
124+
if (common.hasIPv6) {
125+
const sock = dgram.createSocket('udp6');
126+
const addr = sock.bindSync();
127+
assert.strictEqual(addr.address, '::');
128+
assert.strictEqual(addr.family, 'IPv6');
129+
assert.ok(addr.port > 0);
130+
sock.close();
131+
}
132+
133+
// udp6 loopback with an OS-assigned ephemeral port, and async 'message' flow.
134+
if (common.hasIPv6) {
135+
const receiver = dgram.createSocket('udp6');
136+
const addr = receiver.bindSync({ address: '::1', port: 0 });
137+
138+
assert.strictEqual(addr.address, '::1');
139+
assert.strictEqual(addr.family, 'IPv6');
140+
assert.ok(addr.port > 0);
141+
assert.deepStrictEqual(receiver.address(), addr);
142+
143+
receiver.on('message', common.mustCall((msg) => {
144+
assert.strictEqual(msg.toString(), 'hello');
145+
receiver.close();
146+
}));
147+
148+
const sender = dgram.createSocket('udp6');
149+
sender.send('hello', addr.port, '::1', common.mustCall(() => {
150+
sender.close();
151+
}));
152+
}
153+
154+
// A zone-indexed (scoped) IPv6 literal is accepted as a numeric IP; no DNS
155+
// resolution occurs. Interface names are platform-specific, so this binds the
156+
// scoped loopback only where the interface name is known (Linux: 'lo').
157+
if (common.hasIPv6 && process.platform === 'linux') {
158+
const sock = dgram.createSocket('udp6');
159+
const addr = sock.bindSync({ address: '::1%lo', port: 0 });
160+
assert.strictEqual(addr.address, '::1');
161+
assert.strictEqual(addr.family, 'IPv6');
162+
assert.ok(addr.port > 0);
163+
sock.close();
164+
}
165+
166+
// The ipv6Only flag is honored by the synchronous bind.
167+
if (common.hasIPv6) {
168+
const sock = dgram.createSocket({ type: 'udp6', ipv6Only: true });
169+
const addr = sock.bindSync({ address: '::', port: 0 });
170+
assert.strictEqual(addr.family, 'IPv6');
171+
sock.close();
172+
}

0 commit comments

Comments
 (0)