Closed15

(再)node.js v15 で QuicTransport 試してみよう

やまゆやまゆ

https://zenn.dev/yamayuski/scraps/1f2bc1588f422d

こちらがうまくいかなかったので、今度は QUIC が消える直前のバージョンである node.js v15.7.0 を使用して、 QUIC 通信出来ないか調べてみたいと思います。

参考文献

https://blog.leko.jp/post/http-over-quic-on-nodejs15/

https://github.com/nodejs/node/blob/7657f62b1810b94acbe7db68089b608213b34749/doc/api/quic.md

https://scrapbox.io/nwtgck/Node.jsのQUICを先取りして使ってみよう

https://github.com/nodejs/node/blob/7657f62b1810b94acbe7db68089b608213b34749/test/parallel/test-quic-client-server.js

Docker image を作成する

https://gist.github.com/il-m-yamagishi/0b15988ad8aea86841bd0ff64df23748

公式の Dockerfile の v15.7.0 をベースに、ソースコードからビルドするように Dockerfile を書き換えます。

ビルドは CPU 論理数と同等の 16 並列(make -j16 のとこ)で回します。さすがに10分弱かかりました。

docker-entryppoint.sh
#!/bin/sh
set -e

if [ "${1#-}" != "${1}" ] || [ -z "$(command -v "${1}")" ]; then
  set -- node "$@"
fi

exec "$@"

docker-entrypoint.sh も用意しておきます。

docker-compose.yml
version: '3'
services:
    node:
        build: .
        ports:
            - '1234:1234'
        volumes:
            - '.:/home/node/app'
        networks:
            - nodejs
        working_dir: /home/node/app
networks:
    nodejs: {}

めんどいので docker-compose を用意してマウントとかを自動化します。

index.js
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
やまゆやまゆ

とりあえずリッスンサーバを立ち上げてみます。

server.js
'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.listensocket.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 プロトコルエラー) らしい。

https://quiche.googlesource.com/quiche/+/refs/heads/master/quic/core/quic_error_codes.h#44

quiche では code: 10 は Server is not authoritative for this URL. のようだ(同じコードかどうかはわからない)。

やまゆやまゆ
        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 は正常に動かないっぽい、という所で幕を下ろすことにしよう。

https://github.com/il-m-yamagishi/nodejs-quic-sample

一応書いたコードとかはここに残しておくとする。

このスクラップは2021/08/10にクローズされました