この記事は
【HTTP/3】C#でHTTP/3通信してみる その弐 ~Windowsでquicheのサンプルを動かしてみる~
を 2020/5/17 現在の状況に合わせて修正したものです。
Re: C#(Unity)でHTTP/3通信してみる その壱 では quiche の .lib/.dll を作成する方法を紹介しました。
今回はこれらを使って Windows 環境で quiche のサンプルを使って通信をしてみようと思います。
更新履歴
- 2020/5/20
- QuicheWindowsSample : 解放したメモリにアクセスするバグを修正(quiche_h3_event_free の呼び出しタイミング)
- 上記に合わせて記事も微修正
- 2020/5/22
- QuicheWindowsSample のオプションに関するコメントを修正
- max_idle_timeout の説明を追加
- max_udp_payload_size の説明を PMTU ベースに修正
- QuicheWindowsSample のオプションに関するコメントを修正
古い記事 C#でHTTP/3通信してみる その弐 からの変更点まとめ
- サンプルのコード整理
- 1350 byte より大きなデータをダウンロードできない問題を修正
- quiche 0.4.0 に対応
- QUICHE_ERR_DONE に関する記載を修正(公式サンプル側からも消えたのでこちらからも削除)
Windows 版 quiche サンプル
「さっさとサンプルを動かしてみたい!」という方向けに、筆者が作成した Windows 版 quiche サンプルへのリンクを紹介しておきます。
http3sharp - QuicheWindowsSample
ビルド方法等は README.md を参照ください。
HTTP/3 サーバの準備
幸い、世の中には既に HTTP/3 に対応しているページが存在しているので、めんどくさい人はそこを使ってテストさせて貰うのが良いでしょう。
- aioquic のテスト用ページ
- Cloudflare の blog ページ
上記以外にも quicwg/base-drafts - Implementations に Public Server を設置してくれている所の紹介があります。
ただし、サーバ無しにクライアントコードを直し始めるとデバッグが辛いので、個人的には HTTP/3 サーバを自前で用意するのをお勧めします。
自前で HTTP/3 対応サーバを建てたい場合
前述した通り、デバッグの観点からすると、自前で HTTP/3 サーバを建てた方が色々と捗ります。
HTTP/3 サーバを建てる場合は、現状では以下のいずれかを選択すると楽だと思います。
- quiche のサーバサンプルコードを Linux でビルドして使う
- Nginx の HTTP/3 実験実装を有効にして使う (参考 : Experiment with HTTP/3 using NGINX and quiche)
- curl の HTTP/3 実験実装をビルドして使う
- Python 製の HTTP/3 モジュールである aioquic を使う
quiche のサーバサンプルコードを Windows でビルドするのはかなり手間が掛かるのであまりお勧めしません1。
curl や quiche の Linux のサンプルコードや Nginx を使いたい場合、以下の記事で flano_yuki さんがやり方を公開してくれていますのでそれらを参考にしてみてください。
個人的には aioquic も手早く使えて良いと思うので紹介してみます。
aioquic のインストール
以下は Linux 環境に慣れていない読者の方向け導入方法です。
(Linux に慣れている方は公式のヘルプに沿えばサクッとインストールできると思います)
本記事のお題が「C# で HTTP/3 を使う」なので、 Windows 環境での導入を想定して進めてみます。
とは言え、公式ドキュメントの記述に沿ってインストールを進めた方が面倒がないので、今回は Windows 10 の仮想 Linux 環境である WSL を使います。
まず以下の記事を読んで WSL を有効化し、「Ubuntu」をインストールしてください。
※バージョンの指定がない「Ubuntu」を指定しましょう
【WSL入門】第1回 Windows 10標準Linux環境WSLを始めよう
※入れるのは WSL1 にしてください
aioquic の動作には Python 3.6 以降と OpenSSL のヘッダが必要です。
Ubuntu を起動し、まず以下のコマンドでこれらをインストールします。
$ sudo apt-get update
$ sudo apt install libssl-dev python3-dev python3-pip
ちょっと時間が掛かるのでスマホゲーでも遊びながらまったり待ちましょう。
途中で何か色々表示された時は特に考えずに yes を選択して OK です。
上記のインストールが完了したら aioquic を clone してインストールします。
$ git clone https://github.com/aiortc/aioquic.git
$ cd aioquic/
$ pip3 install -e .
$ pip3 install aiofiles asgiref httpbin starlette wsproto
完了!
aioquic サーバの起動
あとは起動するだけです。
証明書もリポジトリに同梱されているので、以下のコマンドをそのまま実行しましょう。
(-v オプションを付けると詳細なログがでるようになります)
$ python3 examples/http3_server.py --certificate tests/ssl_cert.pem --private-key tests/ssl_key.pem
※実行時にファイアウォールの設定が出る場合は許可しておきましょう
無事起動できたか、 aioquic のクライアントで確認してみます。
$ python3 examples/http3_client.py --ca-certs tests/pycacert.pem https://localhost:4433/
以下のような感じで結果が表示されれば OK です。
2020-05-17 11:51:56,945 INFO quic [82a1d100752846ab] ALPN negotiated protocol h3-27
2020-05-17 11:51:56,945 INFO client New session ticket received
2020-05-17 11:51:56,951 INFO client Received 1068 bytes in 0.0 s (1.567 Mbps)
通信結果を取得したい場合は --output-dir
オプションを使います。
$ python3 examples/http3_client.py --ca-certs tests/pycacert.pem --output-dir ./ https://localhost:4433/
上記コマンドを実行すると、通信結果 index.html
がカレントディレクトリに保存されます。
保存した通信結果を表示したい時は cat
コマンドを使用してみてください。
以下のような内容の表示がされれば通信が成功しています。
$ cat index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>aioquic</title>
<link rel="stylesheet" href="/style.css"/>
</head>
<body>
<h1>Welcome to aioquic</h1>
<p>
This is a test page for <a href="https://github.com/aiortc/aioquic/">aioquic</a>,
a QUIC and HTTP/3 implementation written in Python.
</p>
<p>
Congratulations, you loaded this page using HTTP/3!
</p>
<h2>Available endpoints</h2>
<ul>
<li><strong>GET /</strong> returns the homepage</li>
<li><strong>GET /NNNNN</strong> returns NNNNN bytes of plain text</li>
<li><strong>POST /echo</strong> returns the request data</li>
<li>
<strong>CONNECT /ws</strong> runs a WebSocket echo service.
You must set the <em>:protocol</em> pseudo-header to <em>"websocket"</em>.
</li>
<li>There is also an <a href="/httpbin/">httpbin instance</a>.</li>
</ul>
</body>
</html>
通信が上手くいかないときは -v
オプションで実行ログが表示されるようになる(クライアント、サーバ共に)ので、そこから原因を追いかけてみてください。
ちなみに、上記のレスポンス内にもありますが、この aioquic サーバは
GET, GET/NNNNN, POST
に対応しているようです。
(GET/NNNNN
は Z が指定 byte 分返ってきます)
注意事項と対応バージョン
サーバの準備ができたので、次は quiche の基本的な動作を理解しましょう。
今回は C 言語で実装されたサンプルを Windows に対応させますが、 C 言語実装のリファレンスは用意されていません。
幸いなことに Rust 実装のリファレンスは存在しています ので、随時読み替えながら理解を進めていくのが良さそうです。
例えば Rust 側の quiche::h3::Config
名前空間のモジュールは C 言語側では `quiche_h3_config_XXXX`` で定義されています。
注意事項
この後の記事内容は HTTP/3, QUIC の仕様をある程度理解している前提で進めます。
以下の仕様概要を把握していないと実装の流れが理解できない所があるのでご注意ください。
- SCID,DCID 等のコネクション管理の流れ
- ハンドシェイクと初期化パラメータ設定の流れ
- バージョンネゴシエーション
- コネクションマイグレーション
- QPACK
これらの仕様を理解したい場合、以下のいずれかを読むのをお勧めします。
- flano_yuki さんが書いている ASnoKaze blog の各解説記事
- 筆者の著作物である 『くいっく』HTTP/3編 (宣伝)
- HTTP/3, QUIC の draft (現在 Version 27)
バージョン
まず、今回の記事は HTTP/3, QUIC ともに draft version 27 での動作確認となります。
利用した quiche, boringssl, aioquic のバージョンは以下の通りです。
- quiche : 0.4.0
- boringssl : quiche の submodule バージョンに準ずる
- aioquic : a38756bab2cd9039e89a06a1f442f330d349eea0 (master)
サンプルの内容を確認する
quiche のサンプルは C 言語のものと Rust のものが存在します。
筆者は Rust ド素人なので、本記事では C 言語向けのサンプルを改変していきます。
quiche リポジトリでは、 C 言語向けの関連ファイルは以下のパスに配置してあります。
- C Header
- include/quiche.h
- HTTP/3 向けサンプル
- examples/http3-client.c
それでは、中を確認していきましょう。
プラットフォーム依存処理の確認
Windows 向けのビルド環境が用意されていないことから分かるように、 quiche のサンプルは当然 Linux 向けです。
そこで、まずはどの程度プラットフォーム依存があるのか確認してみます。
最初は利用しているヘッダの確認から。
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <ev.h>
予想通り Linux 系のヘッダが色々ありますね……。
型まわりはどうとでもなるとして、一番の障壁として、どうやら libev で非同期の通信処理を実装しているようです。
これをどうするかは後で考えることにします。
とりあえず libev 関連処理に目をつぶりながら実処理を見ていくと socket も自前で叩く必要があるようで、この辺りも Windows 向けに改変する必要がありそうです。
const struct addrinfo hints = {
.ai_family = PF_UNSPEC,
.ai_socktype = SOCK_DGRAM,
.ai_protocol = IPPROTO_UDP
};
quiche_enable_debug_logging(debug_log, NULL);
struct addrinfo *peer;
if (getaddrinfo(host, port, &hints, &peer) != 0) {
perror("failed to resolve host");
return -1;
}
int sock = socket(peer->ai_family, SOCK_DGRAM, 0);
if (sock < 0) {
perror("failed to create socket");
return -1;
}
if (fcntl(sock, F_SETFL, O_NONBLOCK) != 0) {
perror("failed to make socket non-blocking");
return -1;
}
if (connect(sock, peer->ai_addr, peer->ai_addrlen) < 0) {
perror("failed to connect socket");
return -1;
}
その他、SCID の作成に /dev/urandom
を使っています。
これは std::random_device
あたりでサクッと置き換え可能なのであまり気にしないで良いでしょう。
uint8_t scid[LOCAL_CONN_ID_LEN];
int rng = open("/dev/urandom", O_RDONLY);
if (rng < 0) {
perror("failed to open /dev/urandom");
return -1;
}
大きなプラットフォーム固有の処理は上記の三点程度でした。
処理の流れを見る
プラットフォーム依存の処理は把握できました。
今回はサンプルを動かすことを通して quiche の実装を把握するのが目的である為、本筋でない libev をどうにかするようながっつりとした変更はあまりしたくありません……。
とりあえず結論は保留して、 quiche の処理の流れも把握してみてから考えることにしてみます。
初期化の流れ
-
quiche_config_new
で quiche のコンフィグを作成し、quiche_config_set_xxx
等で設定を行う - SCID を生成する
- コンフィグと SCID を引数に
quiche_connect
を呼び出し QUIC のコネクションを作成する
quiche はデータの送受信には socket を使用します。
quiche そのものの初期化ではありませんが、このタイミングで UDP socket を作成し、connect
で host に紐づけしておく必要があります。
ハンドシェイク(1-RTT)の流れ
-
quiche_conn_send
で Initial Packet を生成する -
send
で上記で作成した Initial Packet を送信する -
recv
でサーバからの応答を受け取る(Initial/Handshake/1-RTT Packet) -
quiche_conn_recv
で受信したデータを quiche 側に渡す - 必要なデータを全て受信し終えるまで 3,4 を繰り返す
quiche_conn_send
は呼び出すだけで引数の quiche connection のステータスに応じたデータを勝手に作成してくれます。
このタイミングは quiche_connect
後の初呼び出しとなるので Initial Packet のデータを作成してくれます。
送受信の実処理はユーザ側で実装する必要があるので、初期化時に作成した UDP socket を使って send/recv
を呼び出します。
5 の判定については quiche_conn_is_established
等を使用します(詳細は後述)。
各ストリーム通信の流れ
-
quiche_h3_config_new
でストリーム用のコンフィグの設定を生成する -
quiche_h3_conn_new_with_transport
でストリーム用のデータを作成する -
quiche_h3_header
で HTTP ヘッダを作成する -
quiche_h3_send_request
でストリームの生成を行う(ここで初めてストリーム ID が付与) -
quiche_conn_send
で 4 のストリームデータを取得する -
send
で上記のデータを送信する -
recv
でサーバからの応答を受け取る -
quiche_conn_recv
で受信したデータを quiche 側に渡す - 必要なデータを全て受信し終えるまで繰り返す
-
quiche_h3_conn_poll
で quiche 側のイベントを検出する -
QUICHE_H3_EVENT_DATA
が検出されたらquiche_h3_recv_body
で複合化された HTTP ボディを quiche から受け取る
長いですが、やっていることは
ストリームを作成して HTTP リクエストを送信 → レスポンスを受信
しているだけです。
また、 5~9 の流れはハンドシェイク時と同じで、これは quiche の基本的な処理の流れとなるようです。
サンプルの修正方針を決める
quiche を用いた通信の大まかな流れは把握できました。
(0-RTT 時やコネクションマイグレーション時等の不明な点はとりあえず現時点では忘れます)
これをもとにサンプルをどう直すか改めて考えてみます。
前述したように、最終的なゴールはサンプルを動かすのではなく、サンプル実装を通して quiche の処理の流れや API を把握することです。
処理の流れを見た感じ、イベント駆動でなく雑にポーリングするような実装でも問題はなさそうです。
ですので、思い切ってイベントベースでの動作は諦めて、以下のお手軽な方針で実装をしてみたいと思います(きちんと書くのであれば libuv とか使うのが良さそうです)。
- イベントベースは止めて、ループで回しながらポーリングする実装とする
- ハンドシェイクから HTTP のリクエスト/レスポンスまでを処理の流れのまま実装する
- Windows 依存コードも気にせずバリバリ使う
ビルド環境については、筆者の好みで Visual Studio 2019 を使うことにします。
サンプルの修正
修正したコードを全て解説すると長くなってしまうので、当記事ではポイントのみを取り上げます。
修正したサンプルは以下 にアップしてありますので、当記事で取り上げた個所以外が気になる場合はこちらを参照してみてください。
http3sharp - QuicheWindowsSample
※終了処理絡みが面倒だったのでクラス化してありますが、基本的には examples/http3-client.c
を整理しただけなので C 言語チックな実装となっています
quiche のコンフィグ
初期化の流れは前述したので割愛し、コンフィグについて少し触れようと思います。
quiche では QUIC のコンフィグと HTTP/3 のコンフィグを別々に設定します。
HTTP/3 のコンフィグ
実装 : QuicheWrapper::CreateHttpStream
QPACK 関連の設定が二つとヘッダリストの最大登録数の設定が可能です。
0 を指定するとデフォルト値が使用されます。
// HTTP/3 用のコンフィグを作成する
quiche_h3_config* config = quiche_h3_config_new();
if (config == nullptr)
{
fprintf(stderr, "failed to create HTTP/3 config\n");
return nullptr;
}
// HTTP/3 固有の設定
quiche_h3_config_set_max_header_list_size(config, 1024); // SETTINGS_MAX_HEADER_LIST_SIZE の設定。ヘッダリストに登録できるヘッダの最大数
quiche_h3_config_set_qpack_max_table_capacity(config, 0); // SETTINGS_QPACK_MAX_TABLE_CAPACITY の設定。QPACK の動的テーブルの最大値
quiche_h3_config_set_qpack_blocked_streams(config, 0); // SETTINGS_QPACK_BLOCKED_STREAMS の設定。ブロックされる可能性のあるストリーム数
QUIC のコンフィグ
実装 : QuicheWrapper::CreateQuicheConfig
QUIC 関連の設定は以下の通りです。
数が多いので各項目の詳細はコメントを参照してみてください。
quiche_config* QuicheWrapper::CreateQuicheConfig()
{
// 引数には QUIC のバージョンを渡す
// バージョンネゴシエーションを試したい時は 0xbabababa を渡すこと
quiche_config* config = quiche_config_new(QUICHE_PROTOCOL_VERSION);
if (config == nullptr)
{
fprintf(stderr, "failed to create config\n");
return nullptr;
}
// quiche に HTTP/3 の ALPN token を設定する
// quiche.h に定義されている QUICHE_H3_APPLICATION_PROTOCOL を渡せばいい
quiche_config_set_application_protos(config, (uint8_t*)QUICHE_H3_APPLICATION_PROTOCOL, sizeof(QUICHE_H3_APPLICATION_PROTOCOL) - 1);
// QUIC 固有の設定
// 生成した config に対して設定を適用していく(下記の設定値は quiche の example に準拠)
// 以下に無い quiche_config_set_max_ack_delay, quiche_config_set_ack_delay_exponent はクライアントからは呼ばないこと(サーバから降ってきた値を使用する)
quiche_config_set_max_idle_timeout(config, 100); // max_idle_timeout の設定(ミリ秒)。 0 を指定する事でタイムアウトが無制限になる
quiche_config_set_max_packet_size(config, MAX_DATAGRAM_SIZE); // max_udp_payload_size の設定。 PMTU の実装を鑑みて設定を行うこと(「14. Packet Size」参照)
quiche_config_set_initial_max_data(config, 10000000); // initial_max_data の設定(コネクションに対する上限サイズ)
quiche_config_set_initial_max_stream_data_bidi_local(config, 1000000); // initial_max_stream_data_bidi_local の設定(ローカル始動の双方向ストリームの初期フロー制御値)
quiche_config_set_initial_max_stream_data_bidi_remote(config, 1000000); // initial_max_stream_data_bidi_remote の設定(ピア始動の双方向ストリームの初期フロー制御値)
quiche_config_set_initial_max_stream_data_uni(config, 1000000); // initial_max_stream_data_uni の設定(単方向ストリームの初期フロー制御値)
quiche_config_set_initial_max_streams_bidi(config, 100); // initial_max_streams_bidi の設定(作成可能な双方向ストリームの最大値)
quiche_config_set_initial_max_streams_uni(config, 100); // initial_max_streams_uni の設定(作成可能な単方向ストリームの最大値)
quiche_config_set_disable_active_migration(config, true); // disable_active_migration の設定(コネクションマイグレーションに対応していない場合は true にする)
//quiche_config_verify_peer(config, false); // 証明書の検証の on/off 。オレオレ証明書を使う際には false にする
// TLS の鍵情報のダンプ。WireShark 等でパケットキャプチャする際に用いる
// 一般的に環境変数 SSLKEYLOGFILE で制御する
{
size_t buf;
char buffer[1024];
if (getenv_s(&buf, buffer, 1024, "SSLKEYLOGFILE"))
{
quiche_config_log_keys(config);
}
}
return config;
}
-
バージョンネゴシエーションを試してみたい
quiche_config_new
の初期化時に与えるバージョンに0x?a?a?a?a
の書式パターンに従うバージョンを入れると、バージョンネゴシエーションを強制することが可能です。
バージョンネゴシエーションを試してみる場合は、ハンドシェイクの処理の前にQuicheWrapper::Send
とQuicheWrapper::Receive
を 1 セット追加してください。
(やり取りが 1RTT 分増加するので、その処理が必要です) -
quiche_config_set_initial_max_stream_xxx
の設定の意味が分からない場合
日本語でしっかり解説しているものはまだないので、 QUIC の draft を頑張って読んでみてください。 -
KEYLOG を用いた WireShark でのパケットキャプチャの手順
これまた flano_yuki さんがまとめてくださっているので、そちらを参照してください。
WiresharkでのQUICの復号(decrypt)
quiche は QLOG に対応しているので、デバッグをするのであれば qlog を利用する方が良いとは思います(qlog に関しては記事 その参 で触れます)。
SCID の生成
実装 : QuicheWrapper::CreateQuicheConnection
SCID はクライアント側で一意になるように値を生成する必要がありますが、今回は手抜きで std::mt19937_64
を使って 16 byte のランダム値を生成しています。
// Initial Packet で使用する SCID を乱数から生成する
// SCID は QUIC バージョン 1 までは 20 バイト以内に抑える必要がある(今回は quiche の example の設定値に準拠)
uint8_t scid[16] = {};
std::random_device rd;
std::mt19937_64 mt(rd());
for (auto& id : scid)
{
id = mt() % 256;
}
パケットの受信処理
実装 : QuicheWrapper::Receive
recv
で受け取ったデータを quiche へ受け流せば OK です。
ssize_t QuicheWrapper::Receive(SOCKET sock, quiche_conn* conn)
{
char buf[MAX_DATAGRAM_SIZE] = { 0 };
ssize_t totalRead = 0;
// UDP パケットを受信
while (1)
{
ssize_t read = recv(sock, buf, sizeof(buf), 0);
if (read < 0)
{
auto err = WSAGetLastError();
if (err == WSAEWOULDBLOCK)
{
break;
}
perror("failed to read");
return -1;
}
totalRead += read;
// 受信したパケットを quiche に渡す
ssize_t done = quiche_conn_recv(conn, reinterpret_cast<uint8_t*>(buf), read);
if (done < 0)
{
fprintf(stderr, "failed to process packet: %zd\n", done);
return -1;
}
}
if (0 < totalRead)
{
fprintf(stderr, "recv %zd bytes\n", totalRead);
}
if (quiche_conn_is_closed(conn))
{
fprintf(stderr, "Unintended connection closed\n");
return -1;
}
return 0;
}
パケットの送信処理
実装 : QuicheWrapper::Send
前述したように、 quiche_conn_send
は quiche の内部ステータス(コネクションやストリーム状況)に応じて適切なデータを生成してくれるようです。
なので、実装は quiche_conn_send
で返ってきたデータを send
で送信するだけでよく、特に注意事項はありません。
void QuicheWrapper::Send(quiche_conn* _conn, SOCKET sock)
{
static uint8_t out[MAX_DATAGRAM_SIZE] = { 0 };
ssize_t totalSend = 0;
while (1)
{
// quiche によって送信データ生成する
// quiche_conn_send を呼ぶだけで、内部のステータス(コネクションやストリームの状況)に応じて適切なデータを生成してくれる
ssize_t written = quiche_conn_send(_conn, out, sizeof(out));
if (written == QUICHE_ERR_DONE)
{
break;
}
if (written < 0)
{
fprintf(stderr, "failed to create packet: %zd\n", written);
return;
}
// quiche が生成したデータを UDP ソケットで送信する
ssize_t sent = send(sock, (const char*)out, written, 0);
if (sent != written)
{
perror("failed to send");
return;
}
totalSend += sent;
}
if (0 < totalSend)
{
fprintf(stderr, "sent %zd bytes\n", totalSend);
}
}
パケットの送受信をまとめる
ハンドシェイク時だろうが、 HTTP 通信時であろうが前述のパケットの受信・送信処理は常に行う必要があります。
Update のような関数を作り常に呼ばれるようにしておくと楽そうですので、まとめておきましょう。
int QuicheWrapper::Update()
{
Send(_conn, _sock);
return Receive(_sock, _conn);
}
ハンドシェイク
ハンドシェイクの終了に関しては quiche_conn_is_established
で確認可能です。
// handshake
while (1)
{
if (0 > Update())
{
fprintf(stderr, "failed to recive.");
}
if (quiche_conn_is_established(_conn))
{
// 接続が確立してもすべてのパケットの受信が終わっていないことがある
// 本来であれば非同期で Receive をまわしておくべきだがサンプルなので次でまとめて受け取る
break;
}
// 送受信を少し待つ
Sleep(16);
}
HTTP リクエストの作成
実装 : QuicheWrapper::CreateHttpStream
quiche_h3_conn* QuicheWrapper::CreateHttpStream(quiche_conn* conn, const char* host)
{
// HTTP/3 用のコンフィグを作成する
quiche_h3_config* config = quiche_h3_config_new();
if (config == nullptr)
{
fprintf(stderr, "failed to create HTTP/3 config\n");
return nullptr;
}
// HTTP/3 固有の設定
quiche_h3_config_set_max_header_list_size(config, 1024); // SETTINGS_MAX_HEADER_LIST_SIZE の設定。ヘッダリストに登録できるヘッダの最大数
quiche_h3_config_set_qpack_max_table_capacity(config, 0); // SETTINGS_QPACK_MAX_TABLE_CAPACITY の設定。QPACK の動的テーブルの最大値
quiche_h3_config_set_qpack_blocked_streams(config, 0); // SETTINGS_QPACK_BLOCKED_STREAMS の設定。ブロックされる可能性のあるストリーム数
// HTTP/3 通信用のストリームハンドルを作成(このタイミングではまだ通信しない)
auto http3stream = quiche_h3_conn_new_with_transport(conn, config);
quiche_h3_config_free(config);
if (http3stream == nullptr)
{
fprintf(stderr, "failed to create HTTP/3 connection\n");
return nullptr;
}
// HTTP リクエストの作成
quiche_h3_header headers[] =
{
{
.name = (const uint8_t*) ":method",
.name_len = sizeof(":method") - 1,
.value = (const uint8_t*) "GET",
.value_len = sizeof("GET") - 1,
},
{
.name = (const uint8_t*) ":scheme",
.name_len = sizeof(":scheme") - 1,
.value = (const uint8_t*) "https",
.value_len = sizeof("https") - 1,
},
{
.name = (const uint8_t*) ":authority",
.name_len = sizeof(":authority") - 1,
.value = (const uint8_t*)host,
.value_len = strlen(host),
},
{
.name = (const uint8_t*) ":path",
.name_len = sizeof(":path") - 1,
.value = (const uint8_t*) "/",
.value_len = sizeof("/") - 1,
},
{
.name = (const uint8_t*) "user-agent",
.name_len = sizeof("user-agent") - 1,
.value = (const uint8_t*) "quiche",
.value_len = sizeof("quiche") - 1,
},
};
// quiche にヘッダリストを登録する(このタイミングではまだ通信は実施されない)
int64_t stream_id = quiche_h3_send_request(http3stream, conn, headers, 5, true);
fprintf(stderr, "sent HTTP request %" PRId64 "\n", stream_id);
return http3stream;
}
- HTTP/3 用のコンフィグ
SETTINGS_QPACK_MAX_TABLE_CAPACITY
とSETTINGS_QPACK_BLOCKED_STREAMS
の詳細については、 QPACK の Draft を確認してください。 - HTTP リクエストヘッダの dynamic table への登録
quiche はまだ QPACK の dynamic table への対応が完了していません(0.4.0 で decoder が未実装)。
aioquic では対応しているようなので、 dynamic table を試して見たい場合は aioquic を使ってみるのをお勧めします(筆者も動作は未確認です)。 - 通信のタイミング
quiche_h3_send_request
とかいかにも通信しそうな感じの関数名ですが、実際の通信はsend
側で行われます。
基本的には「quiche の関数では通信は行われない」という認識でいると、処理の流れが分かり易く頭に入ってくると思います。
HTTP レスポンスの受信
実装 : QuicheWrapper::PollHttpResponse
// HTTP のレスポンス待ちをする関数
// -1 : エラー終了、 0 : 正常終了、 1 : 継続
int QuicheWrapper::PollHttpResponse(quiche_conn* conn, quiche_h3_conn* http3stream)
{
quiche_h3_event* ev;
char buf[MAX_DATAGRAM_SIZE] = { 0 };
// quiche 側に HTTP のイベントが来ているかチェック
// イベントを取り切るので while で回す
while (1)
{
// ヘッダ受信、ボディ受信、ストリームのクローズの 3 種のイベントがある
int64_t s = quiche_h3_conn_poll(http3stream, conn, &ev);
if (s < 0)
{
break;
}
auto ev_type = quiche_h3_event_type(ev);
switch (ev_type)
{
case QUICHE_H3_EVENT_HEADERS:
{
// HTTP ヘッダの受信完了
// quiche_h3_event_for_each_header にコールバック関数を渡してヘッダを受け取る
if (quiche_h3_event_for_each_header(ev, for_each_header, nullptr) != 0)
{
perror("failed to process headers");
// ヘッダ不正でもクローズしたいので継続
}
// body の有無をチェックしたい場合は quiche_h3_event_headers_has_body で確認できる
if (!quiche_h3_event_headers_has_body(ev))
{
printf("no body");
// クローズ処理あるので継続
}
break;
}
case QUICHE_H3_EVENT_DATA:
{
// HTTP ボディの受信完了
// ヘッダとは違いこちらはバッファを受け渡す形式
// buf のサイズより受信データが小さい場合は再度呼び出すと途中からコピーしてくれる → 取りきれるまでループで回す必要がある
while (1)
{
ssize_t len = quiche_h3_recv_body(http3stream, conn, s, reinterpret_cast<uint8_t*>(buf), sizeof(buf));
if (len > 0)
{
_body.append(buf);
}
else
{
break;
}
}
break;
}
case QUICHE_H3_EVENT_FINISHED:
{
// ダウンロード終了
printf("download finishe. HTTP body:\n %.*s", (int)_body.size(), _body.c_str());
// ストリームがクローズされたので後始末
quiche_h3_conn_free(http3stream);
quiche_h3_event_free(ev);
if (quiche_conn_close(conn, true, 0, nullptr, 0) < 0)
{
perror("failed to close connection\n");
return -1;
}
else
{
return 0;
}
}
}
quiche_h3_event_free(ev);
}
return 1;
}
quiche_h3_event_for_each_header
と quiche_h3_recv_body
でデータの受け取り方が違うので注意が必要です。
ヘッダを保存したい場合は第三引数 argp
に任意のポインタを渡せるので、これを経由すると良いようです(コールバック関数の最後の引数として入ってくる)。
また、quiche_h3_event_for_each_header
に渡すコールバック関数で 0 以外を返すと処理を中断することができます。
サンプルの動作確認
主要な実装のポイントを押さえた所で、いよいよ修正したサンプルを動かしてみます。
前述した通り、今回修正したサンプルは http3sharp の Example\QuicheWindowsSample
としてアップしてあります。
サンプルのビルド
ビルドには Visual Studio 2019 が必要です。
Re: C#(Unity)でHTTP/3通信してみる その壱 で quiche をビルドした状態であれば、そのままサンプルもビルド・起動できます(quiche のビルド成果物のパスを変えている場合は適宜配置を修正してください)。
サンプルの実行
コマンドライン引数か main.cpp
内の定数を書き換える事により接続先のサーバを指定可能です。
今回は当記事前半で建てた aioquic のサーバと通信を行ってみます。
以下、実行の半生ログです(【】は追記コメント)。
「パケットの受信処理」で書いたように、ハンドシェイク時のパケットの取得が分断されているので少し見づらくなっているので少しログを加工してあります。
1-RTT ハンドシェイク実行からの HTTP リクエスト/レスポンス送受信の流れを追うことができます。
【Initial Packet の送付】
[quiche DEBUG]quiche::tls: 2daf0f8e09de91834035b95c7a0bb2fe write message lvl=Initial len=224
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx pkt Initial version=ff00001b dcid=a5efb3713644c4bc4d5682ac863e3cdd scid=2daf0f8e09de91834035b95c7a0bb2fe len=1157 pn=0
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm CRYPTO off=0 len=224
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm PADDING len=913
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=993.1008ms latest_rtt=0ns srtt=None min_rtt=0ns rttvar=0ns loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=1200 app_limited=true congestion_recovery_start_time=None delivered=0 delivered_time=6.9079ms recent_delivered_packet_sent_time=6.9081ms app_limited_at_pkt=0 hystart=window_end=None last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
sent 1200 bytes
【Initial Packet(ACK)/ Handshake パケット/ 1-RTT パケット 受信】
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Initial version=ff00001b dcid=2daf0f8e09de91834035b95c7a0bb2fe scid=134b4cd3d66862d0 token= len=118 pn=0
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm ACK delay=510 blocks=[0..0]
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe packet newly acked 0
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=none latest_rtt=35.1872ms srtt=Some(35.1872ms) min_rtt=35.1872ms rttvar=17.5936ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=0 app_limited=true congestion_recovery_start_time=None delivered=1200 delivered_time=6.233ms recent_delivered_packet_sent_time=41.421ms app_limited_at_pkt=0 hystart=window_end=None last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm CRYPTO off=0 len=90
[quiche DEBUG]quiche::tls: 2daf0f8e09de91834035b95c7a0bb2fe set write secret lvl=Handshake
[quiche DEBUG]quiche::tls: 2daf0f8e09de91834035b95c7a0bb2fe set read secret lvl=Handshake
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Handshake version=ff00001b dcid=2daf0f8e09de91834035b95c7a0bb2fe scid=134b4cd3d66862d0 len=1095 pn=1
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm CRYPTO off=0 len=1073
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Handshake version=ff00001b dcid=2daf0f8e09de91834035b95c7a0bb2fe scid=134b4cd3d66862d0 len=1007 pn=2
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm CRYPTO off=1073 len=984
[quiche DEBUG]quiche::tls: 2daf0f8e09de91834035b95c7a0bb2fe write message lvl=Handshake len=52
[quiche DEBUG]quiche::tls: 2daf0f8e09de91834035b95c7a0bb2fe set write secret lvl=OneRTT
[quiche DEBUG]quiche::tls: 2daf0f8e09de91834035b95c7a0bb2fe set read secret lvl=OneRTT
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe connection established: proto=Ok("h3-27") cipher=Some(AES256_GCM) curve=Some("X25519") sigalg=Some("rsa_pss_rsae_sha256") resumed=false TransportParams { original_connection_id: None, max_idle_timeout: 60000, stateless_reset_token: None, max_packet_size: 65527, initial_max_data: 1048576, initial_max_stream_data_bidi_local: 1048576, initial_max_stream_data_bidi_remote: 1048576, initial_max_stream_data_uni: 1048576, initial_max_streams_bidi: 128, initial_max_streams_uni: 128, ack_delay_exponent: 3, max_ack_delay: 25, disable_active_migration: false, active_conn_id_limit: 8 }
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Short dcid=2daf0f8e09de91834035b95c7a0bb2fe key_phase=false len=133 pn=3
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm CRYPTO off=0 len=89
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm STREAM id=3 off=0 len=8 fin=false
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm STREAM id=7 off=0 len=1 fin=false
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm STREAM id=11 off=0 len=1 fin=false
recv 2470 bytes
connection established: h3-27
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe open GREASE stream 14
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe tx frm GREASE stream=0
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe tx frm HEADERS stream=0 len=21 fin=true
【Initial Packet(ACK) / Handshale Packet(ACK) / 1-RTT Packet 送信】
sent HTTP request 0
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx pkt Initial version=ff00001b dcid=134b4cd3d66862d0 scid=2daf0f8e09de91834035b95c7a0bb2fe len=1199 pn=1
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm ACK delay=8889 blocks=[0..0]
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm PADDING len=1177
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=none latest_rtt=35.1872ms srtt=Some(35.1872ms) min_rtt=35.1872ms rttvar=17.5936ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=0 app_limited=true congestion_recovery_start_time=None delivered=1200 delivered_time=76.0566ms recent_delivered_packet_sent_time=111.2442ms app_limited_at_pkt=1200 hystart=window_end=None last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx pkt Handshake version=ff00001b dcid=134b4cd3d66862d0 scid=2daf0f8e09de91834035b95c7a0bb2fe len=77 pn=0
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm ACK delay=6325 blocks=[1..2]
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm CRYPTO off=0 len=52
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=4.7315ms latest_rtt=35.1872ms srtt=Some(35.1872ms) min_rtt=35.1872ms rttvar=17.5936ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=111 app_limited=true congestion_recovery_start_time=None delivered=1200 delivered_time=90.6514ms recent_delivered_packet_sent_time=125.8388ms app_limited_at_pkt=1200 hystart=window_end=None last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe dropped epoch 0 state
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx pkt Short dcid=134b4cd3d66862d0 key_phase=false len=52 pn=0
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm ACK delay=5660 blocks=[3..3]
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm STREAM id=2 off=0 len=26 fin=false
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=106.5174ms latest_rtt=35.1872ms srtt=Some(35.1872ms) min_rtt=35.1872ms rttvar=17.5936ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=173 app_limited=true congestion_recovery_start_time=None delivered=1200 delivered_time=107.9099ms recent_delivered_packet_sent_time=143.0974ms app_limited_at_pkt=1200 hystart=window_end=Some(0) last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx pkt Short dcid=134b4cd3d66862d0 key_phase=false len=21 pn=1
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm STREAM id=6 off=0 len=1 fin=false
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=48.3692ms latest_rtt=35.1872ms srtt=Some(35.1872ms) min_rtt=35.1872ms rttvar=17.5936ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=204 app_limited=true congestion_recovery_start_time=None delivered=1200 delivered_time=166.0613ms recent_delivered_packet_sent_time=201.2488ms app_limited_at_pkt=1200 hystart=window_end=Some(0) last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx pkt Short dcid=134b4cd3d66862d0 key_phase=false len=21 pn=2
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm STREAM id=10 off=0 len=1 fin=false
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=4.6312ms latest_rtt=35.1872ms srtt=Some(35.1872ms) min_rtt=35.1872ms rttvar=17.5936ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=235 app_limited=true congestion_recovery_start_time=None delivered=1200 delivered_time=209.7955ms recent_delivered_packet_sent_time=244.9829ms app_limited_at_pkt=1200 hystart=window_end=Some(0) last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx pkt Short dcid=134b4cd3d66862d0 key_phase=false len=46 pn=3
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm STREAM id=14 off=0 len=26 fin=false
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=exp latest_rtt=35.1872ms srtt=Some(35.1872ms) min_rtt=35.1872ms rttvar=17.5936ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=291 app_limited=true congestion_recovery_start_time=None delivered=1200 delivered_time=262.5252ms recent_delivered_packet_sent_time=297.7129ms app_limited_at_pkt=1200 hystart=window_end=Some(0) last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx pkt Short dcid=134b4cd3d66862d0 key_phase=false len=79 pn=4
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe tx frm STREAM id=0 off=0 len=59 fin=true
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=exp latest_rtt=35.1872ms srtt=Some(35.1872ms) min_rtt=35.1872ms rttvar=17.5936ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=380 app_limited=true congestion_recovery_start_time=None delivered=1200 delivered_time=296.6682ms recent_delivered_packet_sent_time=331.8558ms app_limited_at_pkt=1200 hystart=window_end=Some(0) last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
sent 1614 bytes
【HTTP レスポンスの受信】
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Short dcid=2daf0f8e09de91834035b95c7a0bb2fe key_phase=false len=20 pn=4
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm HANDSHAKE_DONE
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe dropped epoch 1 state
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm PADDING len=1
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Short dcid=2daf0f8e09de91834035b95c7a0bb2fe key_phase=false len=24 pn=5
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm ACK delay=127 blocks=[0..0]
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe packet newly acked 0
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=258.028075ms latest_rtt=239.4598ms srtt=Some(60.594275ms) min_rtt=35.1872ms rttvar=64.00935ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=207 app_limited=true congestion_recovery_start_time=None delivered=1262 delivered_time=33.7304ms recent_delivered_packet_sent_time=273.1906ms app_limited_at_pkt=0 hystart=window_end=Some(0) last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Short dcid=2daf0f8e09de91834035b95c7a0bb2fe key_phase=false len=24 pn=6
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm ACK delay=128 blocks=[0..1]
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe packet newly acked 1
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=376.886374ms latest_rtt=240.1273ms srtt=Some(82.907902ms) min_rtt=35.1872ms rttvar=92.634268ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=176 app_limited=true congestion_recovery_start_time=None delivered=1293 delivered_time=7.0225ms recent_delivered_packet_sent_time=291.1431ms app_limited_at_pkt=0 hystart=window_end=Some(0) last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Short dcid=2daf0f8e09de91834035b95c7a0bb2fe key_phase=false len=24 pn=7
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm ACK delay=850 blocks=[0..2]
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe packet newly acked 2
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=396.537451ms latest_rtt=202.1771ms srtt=Some(96.966551ms) min_rtt=35.1872ms rttvar=97.593ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=145 app_limited=true congestion_recovery_start_time=None delivered=1324 delivered_time=4.6652ms recent_delivered_packet_sent_time=305.3856ms app_limited_at_pkt=0 hystart=window_end=Some(0) last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Short dcid=2daf0f8e09de91834035b95c7a0bb2fe key_phase=false len=24 pn=8
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm ACK delay=227 blocks=[0..3]
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe packet newly acked 3
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=367.750317ms latest_rtt=171.5443ms srtt=Some(106.061769ms) min_rtt=35.1872ms rttvar=91.385187ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=89 app_limited=true congestion_recovery_start_time=None delivered=1380 delivered_time=4.4311ms recent_delivered_packet_sent_time=318.4361ms app_limited_at_pkt=0 hystart=window_end=Some(0) last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Short dcid=2daf0f8e09de91834035b95c7a0bb2fe key_phase=false len=70 pn=9
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm STREAM id=0 off=0 len=48 fin=false
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Short dcid=2daf0f8e09de91834035b95c7a0bb2fe key_phase=false len=1094 pn=10
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm STREAM id=0 off=48 len=1071 fin=true
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx pkt Short dcid=2daf0f8e09de91834035b95c7a0bb2fe key_phase=false len=24 pn=11
[quiche DEBUG]quiche: 2daf0f8e09de91834035b95c7a0bb2fe rx frm ACK delay=130 blocks=[0..4]
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe packet newly acked 4
[quiche DEBUG]quiche::recovery: 2daf0f8e09de91834035b95c7a0bb2fe timer=none latest_rtt=144.06ms srtt=Some(110.681547ms) min_rtt=35.1872ms rttvar=77.778447ms loss_time=[None, None, None] loss_probes=[0, 0, 0] cwnd=14520 ssthresh=18446744073709551615 bytes_in_flight=0 app_limited=true congestion_recovery_start_time=None delivered=1469 delivered_time=6.6013ms recent_delivered_packet_sent_time=340.2373ms app_limited_at_pkt=0 hystart=window_end=Some(0) last_round_min_rtt=None current_round_min_rtt=None rtt_sample_count=0 lss_start_time=None
recv 1440 bytes
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe stream id 11 is readable
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 1 bytes on stream 11
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe stream id 7 is readable
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 1 bytes on stream 7
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe stream id 3 is readable
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 1 bytes on stream 3
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe open peer's control stream 3
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 1 bytes on stream 3
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 1 bytes on stream 3
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 5 bytes on stream 3
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe rx frm SETTINGS max_headers=None, qpack_max_table=Some(4096), qpack_blocked=Some(16) stream=3
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe stream id 0 is readable
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 1 bytes on stream 0
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 1 bytes on stream 0
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 46 bytes on stream 0
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe rx frm HEADERS len=46 stream=0
[quiche DEBUG]quiche::h3::qpack::decoder: Header count=0 base=0
[quiche DEBUG]quiche::h3::qpack::decoder: Indexed index=25 static=true
[quiche DEBUG]quiche::h3::qpack::decoder: Literal name_idx=92 static=true value="aioquic/0.8.7"
[quiche DEBUG]quiche::h3::qpack::decoder: Literal name_idx=6 static=true value="Sun, 17 May 2020 02:36:33 GMT"
[quiche DEBUG]quiche::h3::qpack::decoder: Literal name_idx=4 static=true value="1068"
[quiche DEBUG]quiche::h3::qpack::decoder: Indexed index=52 static=true
got HTTP header: :status=200
got HTTP header: server=aioquic/0.8.7
got HTTP header: date=Tue, 19 May 2020 18:10:02 GMT
got HTTP header: content-length=1068
got HTTP header: content-type=text/html; charset=utf-8
[quiche DEBUG]quiche::h3: 2daf0f8e09de91834035b95c7a0bb2fe stream id 0 is readable
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 1 bytes on stream 0
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 1 bytes on stream 0
[quiche DEBUG]quiche::h3::stream: 2daf0f8e09de91834035b95c7a0bb2fe read 1 bytes on stream 0
download finishe. HTTP body:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>aioquic</title>
<link rel="stylesheet" href="/style.css"/>
</head>
<body>
<h1>Welcome to aioquic</h1>
<p>
This is a test page for <a href="https://github.com/aiortc/aioquic/">aioquic</a>,
a QUIC and HTTP/3 implementation written in Python.
</p>
<p>
Congratulations, you loaded this page using HTTP/3!
</p>
<h2>Available endpoints</h2>
<ul>
<li><strong>GET /</strong> returns the homepage</li>
<li><strong>GET /NNNNN</strong> returns NNNNN bytes of plain text</li>
<li><strong>POST /echo</strong> returns the request data</li>
<li>
<strong>CONNECT /ws</strong> runs a WebSocket echo service.
You must set the <em>:protocol</em> pseudo-header to <em>"websocket"</em>.
</li>
<li>There is also an <a href="/httpbin/">httpbin instance</a>.</li>
</ul>
</body>
</html>
まとめ
以上で「Re: C#(Unity)でHTTP/3通信してみる その弐 ~Windowsでquicheのサンプルを動かしてみる~」の手順は完了です。
丁寧に説明したので前回以上に長くなってしまいましたが、ポイントを押さえれば quiche はとても簡単に HTTP/3 通信を実装できます。
興味のある方は是非実装してみてください。
また、 quiche には HTTP/3 だけではなく QUIC で通信を行う API も用意されています。
もう少し深掘りしてみたい方は是非そちらも参照してみてください(quiche.h に HTTP/3 API 同様に定義があります)。
Next!! → Coming soon... (Re: C#(Unity)でHTTP/3通信してみる その参 ~Unityから使ってみる~)
おまけ : Rsut のデバッグ
Visual Studio 2019 では Rust の中にも入ってデバッグ可能です。
ウォッチ式で変数の値も見れます。
便利!
-
クライアントと一緒に解説しようとも思ったのですが、割と修正が多くてしんどい感じだったので今回は割愛します ↩