Skip to content
This repository has been archived by the owner on Aug 11, 2020. It is now read-only.

quic: allow multiple independent .connect() calls #176

Closed
wants to merge 1 commit into from
Closed
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
4 changes: 3 additions & 1 deletion doc/api/quic.md
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,9 @@ added: REPLACEME
be `'udp4'`, indicating UDP over IPv4, or `'udp6'`, indicating UDP over
IPv6. Defaults to `'udp4'`.

Create a new `QuicClientSession`.
Create a new `QuicClientSession`. This function can be called multiple times
to create sessions associated with different endpoints on the same
client endpoint.

### quicsocket.destroy([error])
<!-- YAML
Expand Down
33 changes: 15 additions & 18 deletions lib/internal/quic/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -727,15 +727,18 @@ class QuicSocket extends EventEmitter {
}

// Bind the UDP socket on demand, only if it hasn't already been bound.
// Function is a non-op if the socket is already bound
// Function is a non-op if the socket is already bound or in the process of
// being bound, and will call the callback once the socket is ready.
[kMaybeBind](callback = () => {}) {
if (this.#state !== kSocketUnbound) {
if (this.bound)
return process.nextTick(callback);
else
return this.on('ready', callback);
}
// This socket will be in a pending state until it is bound. Once bound,
// the this[kReady]() method will be called, switching the state to
// kSocketBound and notifying the associated sessions
// TODO(@jasnell): If the socket is already bound, the callback should
// be invoked with an error.
addaleax marked this conversation as resolved.
Show resolved Hide resolved
if (this.#state !== kSocketUnbound)
return;
// kSocketBound and notifying the associated sessions.
this.#state = kSocketPending;
this.#lookup(this.#address, afterLookup.bind(this, callback));
}
Expand Down Expand Up @@ -916,18 +919,12 @@ class QuicSocket extends EventEmitter {
if (typeof callback === 'function')
session.on('ready', callback);

const afterBind =
connectAfterBind.bind(
this,
session,
this.#lookup,
address,
getSocketType(type));
if (this.#state === kSocketBound) {
afterBind();
} else {
this[kMaybeBind](afterBind);
}
this[kMaybeBind](connectAfterBind.bind(
this,
session,
this.#lookup,
address,
getSocketType(type)));

return session;
}
Expand Down
75 changes: 75 additions & 0 deletions test/parallel/test-quic-client-connect-multiple-parallel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use strict';
const common = require('../common');
if (!common.hasQuic)
common.skip('missing quic');

// Test that .connect() can be called multiple times with different servers.

const assert = require('assert');
const quic = require('quic');

const fixtures = require('../common/fixtures');
const key = fixtures.readKey('agent1-key.pem', 'binary');
const cert = fixtures.readKey('agent1-cert.pem', 'binary');
const ca = fixtures.readKey('ca1-cert.pem', 'binary');

const { once } = require('events');

(async function() {
const servers = [];
for (let i = 0; i < 3; i++) {
const server = quic.createSocket({ port: 0, validateAddress: true });

server.listen({
key,
cert,
ca,
rejectUnauthorized: false,
maxCryptoBuffer: 4096,
alpn: 'meow'
});

server.on('session', common.mustCall((session) => {
session.on('secure', common.mustCall((servername, alpn, cipher) => {
const stream = session.openStream({ halfOpen: true });
stream.end('Hi!');
}));
}));

server.on('close', common.mustCall());

servers.push(server);
}

await Promise.all(servers.map((server) => once(server, 'ready')));

const client = quic.createSocket({
port: 0,
client: {
key,
cert,
ca,
alpn: 'meow'
}
});

let done = 0;
for (const server of servers) {
const req = client.connect({
address: 'localhost',
port: server.address.port
});

req.on('stream', common.mustCall((stream) => {
stream.on('data', common.mustCall(
(chk) => assert.strictEqual(chk.toString(), 'Hi!')));
stream.on('end', common.mustCall(() => {
server.close();
req.close();
if (++done === servers.length) client.close();
}));
}));

req.on('close', common.mustCall());
}
})().then(common.mustCall());
74 changes: 74 additions & 0 deletions test/parallel/test-quic-client-connect-multiple-sequential.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
'use strict';
const common = require('../common');
if (!common.hasQuic)
common.skip('missing quic');

// Test that .connect() can be called multiple times with different servers.

const quic = require('quic');

const fixtures = require('../common/fixtures');
const key = fixtures.readKey('agent1-key.pem', 'binary');
const cert = fixtures.readKey('agent1-cert.pem', 'binary');
const ca = fixtures.readKey('ca1-cert.pem', 'binary');

const { once } = require('events');

(async function() {
const servers = [];
for (let i = 0; i < 3; i++) {
const server = quic.createSocket({ port: 0, validateAddress: true });

server.listen({
key,
cert,
ca,
rejectUnauthorized: false,
maxCryptoBuffer: 4096,
alpn: 'meow'
});

server.on('session', common.mustCall((session) => {
session.on('secure', common.mustCall((servername, alpn, cipher) => {
const stream = session.openStream({ halfOpen: true });
stream.end('Hi!');
}));
}));

server.on('close', common.mustCall());

servers.push(server);
}

await Promise.all(servers.map((server) => once(server, 'ready')));

const client = quic.createSocket({
port: 0,
client: {
key,
cert,
ca,
alpn: 'meow'
}
});

let done = 0;
for (const server of servers) {
const req = client.connect({
address: 'localhost',
port: server.address.port
});

const [ stream ] = await once(req, 'stream');
stream.resume();
await(stream, 'end');

server.close();
req.close();
await once(req, 'close');
}

client.close();

await once(client, 'close');
})().then(common.mustCall());