(再)node.js v15 で QuicTransport 試してみよう
こちらがうまくいかなかったので、今度は QUIC が消える直前のバージョンである node.js v15.7.0 を使用して、 QUIC 通信出来ないか調べてみたいと思います。
参考文献
Docker image を作成する
公式の Dockerfile の v15.7.0 をベースに、ソースコードからビルドするように Dockerfile を書き換えます。
ビルドは CPU 論理数と同等の 16 並列(make -j16
のとこ)で回します。さすがに10分弱かかりました。
#!/bin/sh
set -e
if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ]; then
set -- node "$@"
fi
exec "$@"
docker-entrypoint.sh も用意しておきます。
version: '3'
services:
node:
build: .
ports:
- '1234:1234'
volumes:
- '.:/home/node/app'
networks:
- nodejs
working_dir: /home/node/app
networks:
nodejs: {}
めんどいので docker-compose を用意してマウントとかを自動化します。
console.log('Hello world ' + process.version);
const { createQuicSocket } = require('net');
console.log(createQuicSocket);
$ docker-compose run --rm node index.js
Creating click-online_node_run ... done
Hello world v15.7.0
[Function: createQuicSocket]
とりあえずビルドが通った確認は出来ました。
typings はどうやらないようなので、 TypeScript ではなく JavaScript で書くことにします。
QUIC は dTLS(datagram TLS) を使っているので、証明書が必要です。今回はテストなので自己署名証明書で代用しましょう。
$ openssl genrsa 2048 > server.key
$ openssl req -new -key server.key -subj "/C=JP" > server.csr
$ openssl x509 -req -days 3650 -signkey server.key < server.csr > server.crt
とりあえずリッスンサーバを立ち上げてみます。
'use strict';
const { createQuicSocket } = require('net');
const { readFile } = require('fs/promises');
console.log('Hello node ' + process.version);
async function main() {
const server = await generateServer();
server.listen();
console.log(`The socket is listening on ${process.env.HOST || '0.0.0.0'}:${process.env.PORT || 1234}`);
}
async function generateServer() {
const host = process.env.HOST || '0.0.0.0';
const port = process.env.PORT || 1234;
const key = await readFile(__dirname + '/server.key');
const cert = await readFile(__dirname + '/server.crt');
const ca = await readFile(__dirname + '/server.csr');
const alpn = 'click-online';
return createQuicSocket({
endpoint: {
host,
port,
},
server: {
key,
cert,
ca,
alpn,
},
});
}
process.on('SIGINT', () => {
console.log(`Ctrl+C detected. Stopping server...`);
process.exit(2);
});
main().catch(err => {
console.error(err);
});
これで HOST:PORT
に対してリッスンするサーバを立ち上げることが出来るようになりました。 SIGINT をトラッキングしているので、 Ctrl+C で終了させることが出来ます。
よくわからんけどクライアントが全然接続してくれないのでまた今度。
$ docker-compose run --rm node bash
Creating click-online_node_run ... done
root@0944cb12b3fa:/home/node/app# node index.js
(node:8) ExperimentalWarning: The QUIC protocol is experimental and not yet supported for production use
(Use `node --trace-warnings ...` to show where the warning was created)
SERVER: Server is listening on { address: '0.0.0.0', family: 'IPv4', port: 12345 }
CLIENT: session closed
どうも socket.connect
を叩いてもすぐにそのセッションが閉じてしまう様子。
CLIENT: endpoints QuicEndpoint {
address: {},
fd: undefined,
type: 'udp4',
destroyed: false,
bound: false,
pending: false
}
起動から500ms置いて socket.endpoints
を見るとこんな感じでバウンドされていないのが原因な気がする。
↑別にそんなことなかった。多分 socket.listen
か socket.connect
を叩くまでバウンドされないだけっぽかった。
SERVER: Server is listening on { address: '127.0.0.1', family: 'IPv4', port: 12345 }
CLIENT: endpoints { address: '0.0.0.0', family: 'IPv4', port: 37827 }
CLIENT: session closed
QuicClientSession {
alpn: 'hello',
cipher: {},
closing: false,
closeCode: { code: 10, family: 0, silent: true },
destroyed: true,
earlyData: false,
maxStreams: { bidi: 0, uni: 0 },
servername: 'localhost',
streams: 0
}
closeCode: 10, family: 0(=QUIC プロトコルエラー) らしい。
quiche では code: 10 は Server is not authoritative for this URL.
のようだ(同じコードかどうかはわからない)。
RFC9000 のエラーコードだとすると、
PROTOCOL_VIOLATION (0x0a): An endpoint detected an error with
protocol compliance that was not covered by more specific error
codes.
プロトコルバイオレーションか...よりわからなくなってきた。
console.log(`CLIENT: session closed : `, session.closeCode);
console.log(`CLIENT: duration : ${session.duration / 1000 / 1000}ms`);
console.log(`CLIENT: Bytes Sent/Received : ${session.bytesSent}/${session.bytesReceived}`);
console.log(`CLIENT: authError : `, session.authenticationError);
こうすると
CLIENT: session closed : { code: 10, family: 0, silent: true }
CLIENT: duration : 2.2287ms
CLIENT: Bytes Sent/Received : 0/0
CLIENT: authError : undefined
こうかえってくる。 duration がゼロじゃないので動き自体はしているが、特段エラーが出ているわけでもないので、experimentalだからではあるが非常にデバッグしづらい。
手動で socket.destroy()
を呼ぶと、
CLIENT: session closed : { code: 0, family: 2, silent: false }
CLIENT: duration : 1.1493ms
CLIENT: Bytes Sent/Received : 0/0
CLIENT: authError : undefined
CLIENT: endpoint closed
CLIENT: clientSocket closed
このようにちゃんと endpoint と socket が close されるので、異常な状態で destroy されたと見える。
node.js "createQuicSocket"
誰も使わなかったらしく全然検索に引っかからなくて大変。
クライアントコードの一番最後に debugger
を入れて、 node inspect index.js
とすることでステップ実行が出来る。そこで追ってみても、クライアントコード終了直後に reject が走っているっぽい。
...
break in node:internal/process/task_queues:98
96 setHasTickScheduled(false);
97 setHasRejectionToWarn(false);
> 98 }
99
100 // `nextTick()` will not enqueue any callback when the process is about to
debug> n
break in node:internal/quic/core:305
303 // Synchronously cleanup and destroy the JavaScript QuicSession.
304 function onSessionClose(code, family, silent, statelessReset) {
>305 this[owner_symbol][kDestroy](code, family, silent, statelessReset);
306 }
307
debug> n
break in node:internal/quic/core:306
304 function onSessionClose(code, family, silent, statelessReset) {
305 this[owner_symbol][kDestroy](code, family, silent, statelessReset);
>306 }
307
こんな感じで Promise 処理直後に onSessionClose が呼ばれている様子。どこから呼ばれたんだろうか。
NODE_DEBUG=* node index.js
でデバッグログを出力出来るのに気付いたので出力してみた。が、 STREAM のログがあるだけで有力な情報はつかめず。
とりあえずこのバージョンでは QUIC は正常に動かないっぽい、という所で幕を下ろすことにしよう。
一応書いたコードとかはここに残しておくとする。