3
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

【HTTP/3】C#でHTTP/3通信してみる その弐 ~Windowsでquicheのサンプルを動かしてみる~

前回の記事では quiche の .lib/.dll を作成する方法を紹介しました。
今回はこれらを使って C# から呼び出し易いような quiche のラッパを作成するところまで……
のつもりだったのですが、思った以上にサンプルを動作させるところまでに手間が掛かったので、今回はサンプルコードの修正と解説までを実施したいと思います。

Windows 版 quiche サンプル

「さっさとサンプルを動かしてみたい!」という方向けに、先に GitHub のページへのリンクを紹介しておきます。
http3sharp - WindowsNativeClient
ビルド方法等は README.md を参照ください。

HTTP/3 サーバの準備

サーバ無しにクライアントコードを直し始めるとデバッグが辛いので、まずは HTTP/3 サーバを用意しましょう。
幸い、世の中には既に HTTP/3 に対応しているページが存在しているので、めんどくさい人はそこを使ってテストさせて貰うのが良いでしょう。

自前で HTTP/3 対応サーバを建てたい場合

デバッグの観点からすると、自前で HTTP/3 サーバを建てた方が色々と捗ります。
HTTP/3 サーバを建てる場合は、現状では以下の 3 通りのいずれかを選択すると楽だと思います。

  • quiche のサーバサンプルコードを Linux でビルドして使う
  • curl の HTTP/3 実験実装をビルドして使う
  • Python 製の HTTP/3 モジュールである aioquic を使う

quiche のサーバサンプルコードを Windows でビルドするのはかなり手間が掛かるのであまりお勧めしません。
今回、当 blog でもクライアントと一緒に解説しようとも思ったのですが、割と修正が多くてしんどい感じだったので今回は割愛します(需要があれば書きます)。

curl や quiche の Linux のサンプルコードを使いたい場合、 flano_yuki さんがやり方を公開してくれていますのでそれらを参考にしてみてください。

個人的には aioquic も手早く使えて良いと思います。
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 サーバの起動

あとは起動するだけです。
証明書もリポジトリに同梱されているので、以下のコマンドをそのまま実行しましょう。

$ 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 です。

2019-09-28 12:02:28,392 INFO quic [aa64aef99471e880] ALPN negotiated protocol h3-23
2019-09-28 12:02:28,393 INFO client New session ticket received
2019-09-28 12:02:28,394 INFO quic [aa64aef99471e880] Stream 3 created by peer
2019-09-28 12:02:28,394 INFO quic [aa64aef99471e880] Stream 7 created by peer
2019-09-28 12:02:28,394 INFO quic [aa64aef99471e880] Stream 11 created by peer
2019-09-28 12:02:28,398 INFO quic [aa64aef99471e880] Stream 15 created by peer
2019-09-28 12:02:28,412 INFO client Received 1068 bytes in 0.0 s (0.520 Mbps)
:status: 200
server: aioquic
date: Sat, 28 Sep 2019 03:02:28 GMT
content-length: 1068
content-type: text/html; charset=utf-8

<!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>HeadersReceived(headers=[(b':status', b'200'), (b'server', b'aioquic'), (b'date', b'Sat, 28 Sep 2019 03:02:28 GMT'), (b'content-type', b'text/css; charset=utf-8'), (b'content-length', b'82'), (b'last-modified', b'Sat, 28 Sep 2019 01:53:19 GMT'), (b'etag', b'f893451f51efc97d90db9952178d88db')], stream_id=15, stream_ended=False, push_id=0)

ちなみに上記のレスポンス内にもありますが、この aioquic サーバは
 GET, GET/NNNNN, POST
に対応しているようです。
(GET/NNNNN は Z が指定 byte 分返ってきます)

quiche のサンプルを Windows 対応させる

サーバの準備ができたので、次は quiche の基本的な動作を理解しましょう。
ドキュメントはまだ用意されていないようなので、今回はクライアントのサンプルコードを動かしながら理解してみます。

注意事項

この後の記事内容は HTTP/3, QUIC の仕様をある程度理解している前提で進めます。
以下の仕様概要を把握していないと実装の流れが理解できない所があるのでご注意ください。

  • SCID,DCID 等のコネクション管理の流れ
  • ハンドシェイクと初期化パラメータ設定の流れ
  • バージョンネゴシエーション
  • コネクションマイグレーション
  • QPACK

これらの仕様を理解したい場合、以下のいずれかを読むのをお勧めします。

バージョン関係

まず、今回の記事は HTTP/3,QUIC ともに draft version 23 での動作確認となります。
利用した quiche, aioquic, boringssl のバージョンは以下の通りです。

  • quiche : d646a60b497be4a92046fbd705161164388b73d2 (master)
  • aioquic : 0.8.1
  • boringssl : b82f945ebcc9252ea79d2df9e38ee45afae9535c (master)

サンプルの内容を確認する

quiche のサンプルは C/C++ 向けのものと Rust 向けのものが存在します。
筆者は Rust ド素人なので、本記事では C/C++ 向けのサンプルを改変していきます。
quiche リポジトリでは、 C/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;
    }

大きなプラットフォーム固有の処理は上記の三点程度でした。

処理の流れを見る

プラットフォーム依存の処理は把握できました。
サンプルを動かすのがゴールではないため、 libev をどうにかするようながっつりとした変更はあまりしたくありません……。
とりあえず結論は保留して、 quiche の処理の流れも把握してみてから考えることにしてみます。

初期化の流れ

  1. quiche_config_new で quiche のコンフィグを作成し、quiche_config_set_xxx 等で設定を行う
  2. SCID を生成する
  3. コンフィグと SCID を引数に quiche_connect を呼び出し QUIC のコネクションを作成する

quiche はデータの送受信には socket を使用します。
quiche そのものの初期化ではありませんが、このタイミングで UDP socket を作成し、connect で host に紐づけしておく必要があります。

ハンドシェイク(1-RTT)の流れ

  1. quiche_conn_send で Initial Packet を生成する
  2. send で上記で作成した Initial Packet を送信する
  3. recv でサーバからの応答を受け取る(Initial/Handshake/1-RTT Packet)
  4. quiche_conn_recv で受信したデータを quiche 側に渡す
  5. 必要なデータを全て受信し終えるまで 3,4 を繰り返す

quiche_conn_send は呼び出すだけで引数の quiche connection のステータスに応じたデータを勝手に作成してくれます。
このタイミングは quiche_connect 後の初呼び出しとなるので Initial Packet のデータを作成してくれます。
送受信の実処理はユーザ側で実装する必要があるので、初期化時に作成した UDP socket を使って send/recv を呼び出します。
5 の判定については quiche_conn_is_established 等を使用します(詳細は後述)。

各ストリーム通信の流れ

  1. quiche_h3_config_new でストリーム用のコンフィグの設定を生成する
  2. quiche_h3_conn_new_with_transport でストリーム用のデータを作成する
  3. quiche_h3_header で HTTP ヘッダを作成する
  4. quiche_h3_send_request でストリームの生成を行う(ここで初めてストリーム ID が付与)
  5. quiche_conn_send で 4 のストリームデータを取得する
  6. send で上記のデータを送信する
  7. recv でサーバからの応答を受け取る
  8. quiche_conn_recv で受信したデータを quiche 側に渡す
  9. 必要なデータを全て受信し終えるまで繰り返す
  10. quiche_h3_conn_poll で quiche 側のイベントを検出する
  11. 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 - WindowsNativeClient
※終了処理絡みが面倒だったのでクラス化してありますが、基本的には examples/http3-client.c を整理しただけなので C 言語チックな実装となっています

quiche.h

Visual Studio 環境では ssize_t の定義がない為に quiche.h をそのまま使うことができません。
今回は雑に以下のような感じで _MSC_VER 定義時のみ ssize_tSSIZE_T に typedef する方向で逃げています。

#if defined(_MSC_VER)
#include <BaseTsd.h>
typedef SSIZE_T ssize_t;
#endif

quiche のコンフィグ

実装 : QuicheWrapper::CreateQuicheConfig
初期化の流れは前述したので割愛し、コンフィグについて少し触れようと思います。

quiche_config* QuicheWrapper::CreateQuicheConfig()
{
    // 引数には QUIC のバージョンを渡す
    // バージョンネゴシエーションを試したい時は 0xbabababa を渡すこと
    quiche_config* config = quiche_config_new(0xff000017);
    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);

    // 生成した config に対して設定を適用していく(下記の設定値は quiche の example に準拠)
    // 以下に無い quiche_config_set_max_ack_delay, quiche_config_set_ack_delay_exponent はクライアントからは呼ばないこと(サーバから降ってきた値を使用する)
    quiche_config_set_idle_timeout(config, 100);
    quiche_config_set_max_packet_size(config, MAX_DATAGRAM_SIZE);               // UDP パケット最大サイズ。 Google の調査により QUIC では 1350 が推奨
    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_migration(config, true);                          // disable_active_migration の設定(コネクションマイグレーションに対応していない場合は false にする)
    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::SendQuicheWrapper::Receive を 1 セット追加してください。
    (やり取りが 1RTT 分増加するので、その処理が必要です)

  • quiche_config_set_initial_max_stream_xxx の設定の意味が分からない場合
    日本語でしっかり解説しているものはまだないので、 QUIC の draftを頑張って読んでみてください。

  • KEYLOG を用いた WireShark でのパケットキャプチャの手順
    これまた flano_yuki さんがまとめてくださっているので、そちらを参照してください。
    WiresharkでのQUICの復号(decrypt)

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
コメントにもありますが、下記の QUICHE_ERR_DONE に関して注意事項があります。

    // 受信したパケットを quiche に渡す
    ssize_t done = quiche_conn_recv(conn, reinterpret_cast<uint8_t*>(buf), read);
    if (done == QUICHE_ERR_DONE)
    {
        // Windows 版ではここに入らないことがあるので別のトリガーで完了を見る必要がある
        fprintf(stderr, "done reading\n");
        return read;
    }

Linux 版の quiche のサンプルでは quiche_conn_recv の戻り値で QUICHE_ERR_DONE が返ったら次の処理に進むような実装となっています。
しかし、 Windows 版サンプルではハンドシェイク終了時に QUICHE_ERR_DONE が返ってきません。
そこで、 quiche_conn_is_established で true が返り、コネクションが確立されたら次に進むような実装としています。
しかし、 quiche_conn_is_established で true が返るようになったタイミングではハンドシェイクの全てのレスポンスパケットを受け取れていないことがあります。
本来であれば非同期で受信処理をまわして、残りのレスポンスも取得しておくべきですが、今回はサンプルの動作確認なのでここは手を抜いて、次の HTTP リクエストに対するレスポンス受信処理側で取得が行われるような実装となっています。

パケットの送信処理

実装 : QuicheWrapper::Send
前述したように、 quiche_conn_send は quiche の内部のステータス(コネクションやストリームの状況)に応じて適切なデータを生成してくれるようです。
なので、実装は quiche_conn_send で返ってきたデータを send で送信するだけでよく、特に注意事項はありません。

HTTP リクエストの作成

実装 : QuicheWrapper::CreateHttpStream

quiche_h3_conn* QuicheWrapper::CreateHttpStream(quiche_conn* conn, const char* host)
{
    // HTTP/3 用のコンフィグを作成する
    // quiche_h3_config_new 引数は以下(設定値は quiche サンプルのものをそのまま適用)
    // uint64_t num_placeholders : プライオリティに関するプレースホルダーの設定(プライオリティ削除済みの為に不使用)
    // uint64_t max_header_list_size : ヘッダリストに登録できるヘッダの最大数
    // uint64_t qpack_max_table_capacity : QPACK の動的テーブルの最大値
    // uint64_t qpack_blaocked_streams : ブロックされる可能性のあるストリーム数
    quiche_h3_config* config = quiche_h3_config_new(0, 1024, 0, 0);
    if (config == nullptr)
    {
        fprintf(stderr, "failed to create HTTP/3 config\n");
        return nullptr;
    }

    // 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 用のコンフィグ
    qpack_max_table_capacityqpack_blaocked_streams の詳細については、 QPACK の仕様を確認してください。
  • HTTP リクエストヘッダの dynamic table への登録
    quiche はまだ QPACK の dynamic table への対応が完了していません。
    aioquic では対応しているようなので、 dynamic table を試して見たい場合は aioquic を使ってみるのが良いかもしれません(筆者も動作は未確認です)。
    参考 : IETF QUIC Interop Matrix
  • 通信のタイミング
    quiche_h3_send_request とかいかにも通信しそうな感じの関数名ですが、実際の通信は send 側で行われます。
    基本的には「quiche の関数では通信は行われない」という認識でいると、処理の流れが分かり易く頭に入ってくると思います。

HTTP レスポンスの受信処理

実装 : QuicheWrapper::PollHttpResponse

int QuicheWrapper::PollHttpResponse(quiche_conn* conn, quiche_h3_conn* http3stream)
{
    quiche_h3_event* ev;
    char buf[MAX_DATAGRAM_SIZE] = { 0 };

    while (1)
    {
        // quiche 側に HTTP のイベントが来ているかチェック
        // ヘッダ受信、ボディ受信、ストリームのクローズの 3 種のイベントがある
        int64_t s = quiche_h3_conn_poll(http3stream, conn, &ev);
        if (s < 0)
        {
            break;
        }
        auto ev_type = quiche_h3_event_type(ev);
        quiche_h3_event_free(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");
                    // ヘッダ不正でもクローズしたいので継続
                }
                break;
            }

            case QUICHE_H3_EVENT_DATA:
            {
                // HTTP ボディの受信完了
                // ヘッダとは違いこちらはバッファを受け渡す形式
                ssize_t len = quiche_h3_recv_body(http3stream, conn, s, reinterpret_cast<uint8_t*>(buf), sizeof(buf));
                if (len > 0)
                {
                    printf("got HTTP body:\n %.*s", (int)len, buf);
                }
                break;
            }

            case QUICHE_H3_EVENT_FINISHED:
            {
                // ストリームがクローズされた
                if (quiche_conn_close(conn, true, 0, nullptr, 0) < 0)
                {
                    perror("failed to close connection\n");
                    return -1;
                }
                else
                {
                    return 0;
                }
            }
        }
    }

    return 1;
}

quiche_h3_event_for_each_headerquiche_h3_recv_body でデータの受け取り方が違うので注意が必要です。
ヘッダを保存したい場合は第三引数の argp を経由すると良いようです(コールバック関数の最後の引数として入ってくる)。
また、quiche_h3_event_for_each_header に渡すコールバック関数で 0 以外を返すと処理を中断することができます。

サンプルの動作確認

主要な実装のポイントを押さえた所で、いよいよ修正したサンプルを動かしてみます。
今回修正したサンプルは http3sharpExample\WindowsNativeClient としてアップしてあります。

サンプルのビルド

ビルドには Visual Studio 2019 が必要です。
Example\WindowsNativeClient\external\quiche にビルドした以下の quiche を配置して QuicheWrapperExample.sln でビルドしてください。

  • quiche.dll
  • quiche.dll.lib

quiche の .lib/.dll ビルド方法は前回の記事を参照してください。
【HTTP/3】C#でHTTP/3通信してみる その壱 ~OSSの選定からquicheの.lib/.dllビルドまで~

サンプルの実行

コマンドライン引数か main.cpp 内の定数を書き換える事により接続先のサーバを指定可能です。
今回は当記事前半で建てた aioquic のサーバと通信を行ってみます。

以下、実行の半生ログです(【】は追記コメント)。
「パケットの受信処理」で書いたように、ハンドシェイク時のパケットの取得が分断されているので少し見づらくなっているので少しログを加工してあります。
1-RTT ハンドシェイク実行からの HTTP リクエスト/レスポンス送受信の流れを追うことができます。

【Initial Packet の送付】
[quiche DEBUG]quiche::tls: 3b1b61478e29654196bfda658af1edae tls write message lvl=Initial len=512
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx pkt Initial version=ff000017 dcid=0bddcd2c7bc901a7fe76da72d63841a5 scid=3b1b61478e29654196bfda658af1edae len=1157 pn=0
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm CRYPTO off=0 len=512
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm PADDING len=625
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=984.5309ms crypto=1200 inflight=1200 cwnd=14520 latest_rtt=0ns srtt=None min_rtt=0ns rttvar=0ns probes=0
sent 1200 bytes
done writing

【Initial Packet(ACK)/ Handshake パケット/ 1-RTT パケット 受信】
recv 1280 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Initial version=ff000017 dcid=3b1b61478e29654196bfda658af1edae scid=e19ec70c5860841e token= len=149 pn=0
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm ACK delay=0 blocks=[0..0]
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae packet newly acked 0
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=none crypto=0 inflight=0 cwnd=15720 latest_rtt=28.3828ms srtt=Some(28.3828ms) min_rtt=28.3828ms rttvar=14.1914ms probes=0
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm CRYPTO off=0 len=122
[quiche DEBUG]quiche::tls: 3b1b61478e29654196bfda658af1edae tls set encryption secret lvl=Handshake
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Handshake version=ff000017 dcid=3b1b61478e29654196bfda658af1edae scid=e19ec70c5860841e len=1064 pn=1
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm CRYPTO off=0 len=1042
recv 1237 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Handshake version=ff000017 dcid=3b1b61478e29654196bfda658af1edae scid=e19ec70c5860841e len=1054 pn=2
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm CRYPTO off=1042 len=1031
[quiche DEBUG]quiche::tls: 3b1b61478e29654196bfda658af1edae tls set encryption secret lvl=OneRTT
[quiche DEBUG]quiche::tls: 3b1b61478e29654196bfda658af1edae tls write message lvl=Handshake len=52
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae connection established: cipher=Ok(AES256_GCM) proto=Ok("h3-23") resumed=false idle_timeout=60000 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_migration=false
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Short dcid=3b1b61478e29654196bfda658af1edae key_phase=false len=133 pn=3
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm CRYPTO off=0 len=89
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm STREAM id=3 off=0 len=8 fin=false
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm STREAM id=7 off=0 len=1 fin=false
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm STREAM id=11 off=0 len=1 fin=false
connection established: h3-23
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae open GREASE stream 14
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae tx frm GREASE stream=0
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae tx frm HEADERS stream=0 len=21 fin=true
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx pkt Initial version=ff000017 dcid=e19ec70c5860841e scid=3b1b61478e29654196bfda658af1edae len=1199 pn=1
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm ACK delay=14715 blocks=[0..0]
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm PADDING len=1177
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=none crypto=0 inflight=0 cwnd=15720 latest_rtt=28.3828ms srtt=Some(28.3828ms) min_rtt=28.3828ms rttvar=14.1914ms probes=0

【Initial Packet(ACK) / Handshale Packet(ACK) / 1-RTT Packet 送信】
sent HTTP request 0
sent 1234 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx pkt Handshake version=ff000017 dcid=e19ec70c5860841e scid=3b1b61478e29654196bfda658af1edae len=77 pn=0
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm ACK delay=13099 blocks=[1..2]
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm CRYPTO off=0 len=52
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=41.7074ms crypto=111 inflight=111 cwnd=15720 latest_rtt=28.3828ms srtt=Some(28.3828ms) min_rtt=28.3828ms rttvar=14.1914ms probes=0
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae dropped epoch 0 state
sent 111 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx pkt Short dcid=e19ec70c5860841e key_phase=false len=52 pn=0
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm ACK delay=11286 blocks=[3..3]
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm STREAM id=2 off=0 len=26 fin=false
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=19.7998ms crypto=111 inflight=173 cwnd=15720 latest_rtt=28.3828ms srtt=Some(28.3828ms) min_rtt=28.3828ms rttvar=14.1914ms probes=0
sent 62 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx pkt Short dcid=e19ec70c5860841e key_phase=false len=21 pn=1
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm STREAM id=6 off=0 len=1 fin=false
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=2.7795ms crypto=111 inflight=204 cwnd=15720 latest_rtt=28.3828ms srtt=Some(28.3828ms) min_rtt=28.3828ms rttvar=14.1914ms probes=0
sent 31 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx pkt Short dcid=e19ec70c5860841e key_phase=false len=21 pn=2
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm STREAM id=10 off=0 len=1 fin=false
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=exp crypto=111 inflight=235 cwnd=15720 latest_rtt=28.3828ms srtt=Some(28.3828ms) min_rtt=28.3828ms rttvar=14.1914ms probes=0
sent 31 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx pkt Short dcid=e19ec70c5860841e key_phase=false len=46 pn=3
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm STREAM id=14 off=0 len=26 fin=false
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=exp crypto=111 inflight=291 cwnd=15720 latest_rtt=28.3828ms srtt=Some(28.3828ms) min_rtt=28.3828ms rttvar=14.1914ms probes=0
sent 56 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx pkt Short dcid=e19ec70c5860841e key_phase=false len=79 pn=4
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae tx frm STREAM id=0 off=0 len=59 fin=true
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=exp crypto=111 inflight=380 cwnd=15720 latest_rtt=28.3828ms srtt=Some(28.3828ms) min_rtt=28.3828ms rttvar=14.1914ms probes=0
sent 89 bytes
done writing

【HTTP レスポンスの受信】
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae stream id 3 is readable
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 1 bytes on stream 3
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae open peer's control stream 3
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 1 bytes on stream 3
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 1 bytes on stream 3
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 5 bytes on stream 3
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae rx frm SETTINGS placeholders=None, max_headers=None, qpack_max_table=Some(256), qpack_blocked=Some(16)  stream=3
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae stream id 11 is readable
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 1 bytes on stream 11
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae stream id 7 is readable
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 1 bytes on stream 7
recv 56 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Handshake version=ff000017 dcid=3b1b61478e29654196bfda658af1edae scid=e19ec70c5860841e len=23 pn=4
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm ACK delay=0 blocks=[0..0]
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae packet newly acked 0
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=182.028412ms crypto=0 inflight=269 cwnd=15831 latest_rtt=185.8309ms srtt=Some(48.063812ms) min_rtt=28.3828ms rttvar=50.005575ms probes=0
recv 40 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Short dcid=3b1b61478e29654196bfda658af1edae key_phase=false len=23 pn=5
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm ACK delay=0 blocks=[0..0]
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae packet newly acked 0
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=262.84701ms crypto=0 inflight=207 cwnd=15893 latest_rtt=184.2637ms srtt=Some(65.088798ms) min_rtt=28.3828ms rttvar=71.554153ms probes=0
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae dropped epoch 1 state
recv 40 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Short dcid=3b1b61478e29654196bfda658af1edae key_phase=false len=23 pn=6
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm ACK delay=0 blocks=[0..1]
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae packet newly acked 1
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=305.179658ms crypto=0 inflight=176 cwnd=15924 latest_rtt=190.9592ms srtt=Some(80.822598ms) min_rtt=28.3828ms rttvar=85.133215ms probes=0
recv 40 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Short dcid=3b1b61478e29654196bfda658af1edae key_phase=false len=23 pn=7
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm ACK delay=0 blocks=[0..2]
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae packet newly acked 2
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=323.404667ms crypto=0 inflight=145 cwnd=15955 latest_rtt=196.4596ms srtt=Some(95.277223ms) min_rtt=28.3828ms rttvar=92.759161ms probes=0
recv 40 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Short dcid=3b1b61478e29654196bfda658af1edae key_phase=false len=23 pn=8
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm ACK delay=0 blocks=[0..3]
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae packet newly acked 3
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=327.425817ms crypto=0 inflight=89 cwnd=16011 latest_rtt=202.1727ms srtt=Some(108.639157ms) min_rtt=28.3828ms rttvar=96.29324ms probes=0
recv 82 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Short dcid=3b1b61478e29654196bfda658af1edae key_phase=false len=65 pn=9
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm STREAM id=0 off=0 len=43 fin=false
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae stream id 0 is readable
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 1 bytes on stream 0
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 1 bytes on stream 0
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 41 bytes on stream 0
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae rx frm HEADERS len=41 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"
[quiche DEBUG]quiche::h3::qpack::decoder: Literal name_idx=6 static=true value="Sat, 05 Oct 2019 12:19:45 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: ・1=200
got HTTP header: server=aioquic
got HTTP header: date=Sat, 05 Oct 2019 12:19:45 GMT
got HTTP header: content-length=1068
got HTTP header: content-type=text/html; charset=utf-8
recv 1116 bytes
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx pkt Short dcid=3b1b61478e29654196bfda658af1edae key_phase=false len=1099 pn=10
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm ACK delay=0 blocks=[0..4]
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae packet newly acked 4
[quiche DEBUG]quiche::recovery: 3b1b61478e29654196bfda658af1edae timer=none crypto=0 inflight=0 cwnd=16100 latest_rtt=273.2287ms srtt=Some(129.212849ms) min_rtt=28.3828ms rttvar=113.367315ms probes=0
[quiche DEBUG]quiche: 3b1b61478e29654196bfda658af1edae rx frm STREAM id=0 off=43 len=1071 fin=true
[quiche DEBUG]quiche::h3: 3b1b61478e29654196bfda658af1edae stream id 0 is readable
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 1 bytes on stream 0
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 1 bytes on stream 0
[quiche DEBUG]quiche::h3::stream: 3b1b61478e29654196bfda658af1edae read 1 bytes on stream 0
got 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>

Rsut のデバッグ

Visual Studio 2019 では Rust の中にも入ってデバッグ可能です。
debug.PNG
ウォッチ式で変数の値も見れます。
便利!

まとめ

以上で「HTTP/3】C#でHTTP/3通信してみる その弐 ~Windowsでquicheのサンプルを動かしてみる~」の手順は完了です。
丁寧に説明したので前回以上に長くなってしまいましたが、ポイントを押さえれば quiche はとても簡単に HTTP/3 通信を実装できます。
興味のある方は是非実装してみてください。

また、 quiche には HTTP/3 だけではなく QUIC で通信を行う API も用意されています。
もう少し深掘りしてみたい方は是非そちらも参照してみてください(quiche.h に HTTP/3 API 同様に定義があります)。

次回は今度こそ「C#から呼び易いような 1 をラッピングする .dll を更に作成する」を解説したいと思います。
ここまで来れば、あとは自分の使い易いようにライブラリを整えていくだけなので、今回ほど長くはならない見込みです。
(並列に複数のストリームで通信するのをどう実装するか、くらいが悩み所かな、と)

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
3
Help us understand the problem. What are the problem?