mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
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:
1 parent
4d09c04253
commit
eea8d72052
3 files changed
+264
No files matched your search
@@ -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
|
||||
@@ -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];
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
Reference in new issue
Block a user