Node.jsのQUICを先取りして使ってみよう
#Node.js #QUIC
やりたいこと
Node.jsでHTTP/3でも使われるプロトコルQUICを使いたい。現状ビルドする必要があるため、Dockerイメージを作って誰でも再現的にQUICを試せるようにしたい。
現在Node.jsのQUIC対応はオリジナルから分岐して別のリポジトリとして実験的に開発されている。
そのためQUIC付きのNode.jsをビルドすることでQUICが使えるnodeコマンドが得られる。
使い方
以下のdocker-runでNode.jsのREPLが使える。
code:bash
docker run -it nwtgck/node-quic
そのREPLでconst { createQuicSocket } = require('net');とすればQUICのソケットを作る関数が手に入る。
createQuicSocket()でサーバーもクライアントのソケットも作れる。
使用例は後述。
GitHubリポジトリ
リポジトリは以下にある。
https://gh-card.dev/repos/nwtgck/docker-node-quic.svg https://github.com/nwtgck/docker-node-quic
GitHub ActionsでDockerイメージをビルドしてDocker Hubにpushしている。
なるべく再現的にビルドできるように、上記のDockerfileではコミットのハッシュでビルドを固定している。
Node.jsを丸ごとビルドするのでGitHub Actions上ではDockerイメージのビルドに2時間ぐらいかかる。
https://gyazo.com/fbf419560225ccfada5f789533534fc0
echoサーバーを作って動かす
A QUIC Update for Node.jsを参考にしている。この記事の内容と現在では実装が違うのかrequire()すべきなものはconst { createQuicSocket } = require('net');。実際のコードを読むとこれが使われていて公式のドキュメントもこれが使われている。
まず、以下で自己署名証明書を作成する。
下記のコマンドで./ssl_certs/server.key、./ssl_certs/server.csr、./ssl_certs/server.crtが生成される。
code:bash
mkdir ssl_certs
cd ssl_certs
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
cd -
そして今いる場所にmy_echo_server.jsを以下の内容で作成する。
下記のプログラムはエコーサーバーを立て、クライアントのリクエストもする。
標準入力をクライアントはサーバーに送るようになっている。
そのため入力をおうむ返しする。
code:./my_echo_server.js
const { createQuicSocket } = require('net');
const fs = require('fs');
const key = fs.readFileSync('./ssl_certs/server.key');
const cert = fs.readFileSync('./ssl_certs/server.crt');
const ca = fs.readFileSync('./ssl_certs/server.csr');
const port = 1234;
// Create the QUIC UDP IPv4 socket bound to local IP port 1234
const server = createQuicSocket({ endpoint: { port } });
// Tell the socket to operate as a server using the given
// key and certificate to secure new connections, using
// the fictional 'hello' application protocol.
server.listen({ key, cert, alpn: 'hello' });
server.on('session', (session) => {
// The peer opened a new stream!
session.on('stream', (stream) => {
// Echo server
stream.pipe(stream);
});
});
server.on('listening', () => {
// The socket is listening for sessions!
console.log(listening on ${port}...);
console.log('input something!');
});
const socket = createQuicSocket({
client: {
key,
cert,
ca,
requestCert: true,
alpn: 'hello',
servername: 'localhost'
}
});
const req = socket.connect({
address: 'localhost',
port,
});
req.on('secure', () => {
const stream = req.openStream();
// stdin -> stream
process.stdin.pipe(stream);
stream.on('data', (chunk) => console.log('client(on-secure): ', chunk.toString()));
stream.on('end', () => console.log('client(on-secure): end'));
stream.on('close', () => {
// Graceful shutdown
socket.close();
});
stream.on('error', (err) => console.error(err));
});
この時点で今いる場所にmy_echo_server.jsとssl_certs/ディレクトリがある状態になる。
そこで以下でQUICが使えるnodeコマンドが使える環境に入る。
code:bash
docker run -it -v $PWD:/playground nwtgck/node-quic bash
以下のコマンドをコンテナ内で実行する。
code:bash
# プレイグラウンドに移動
cd /playground/
# エコーサーバーのプログラムを実行
node my_echo_server.js
あとは適当にキーボードに入力するとサーバーから同じものが返ってきてそれがクライアントに届いてconsole.logされる。
docker runの-vでコンテナとファイルが共有されている。そのためmy_echo_server.jsをコンテナの外でファイルを編集してもちゃんと反映される。コンテナの外の好きなエディタで色々試行錯誤することができる。
実際の動作
入力したものにclient(on-secure): が先頭について表示されることが分かる。
https://gyazo.com/4d7b4a8f84175c849dab40d76d0474ec
おまけ
本当はserver.on('listening')のコールバックが呼ばれたあとに、クライアントのリクエストした方がいいはずだが、上記でなんどやっても手元の環境で動いている。入れ子が多くなると読みづらくなるのとサンプルコードなのでいいかなという気持ち。
Node.js公式のQUICに関するドキュメント(quic/quic.md at cee2e5d079ca2b55e421d81df1ad131c1bfeecc6 · nodejs/quic)がある。HTTP/3に関することも書かれているので試してみたい。とりあえず今回はDockerで環境を作ることに重きを置いたのでここまで。Node.jsをフルでビルドするの2時間かかってDockerfileをいじると時間結構とられる。
docker run -it nwtgck/node-quic bashした環境にはnpmコマンドもあるので既存のパッケージを取り込んで色々試せる。またQUICを使ったアプリケーションのDockerイメージのベースイメージにも使える。
DockerでUDPを開放したい時にはdocker run -p 1234:1234/udp ...でできる。
QUICではUDPが使われている。