gRPC over HTTP/3のプロトコルと実装を眺める

gRPC over HTTP/3のプロポーザルと、実装が出てきています。

今回は、仕様を眺めつつ、Ubuntuで実装を動かすところまで試してみようと思います

プロトコル仕様

プロポーザルは、現在 "In review" の状態となっています。
github.com

HTTP/3の基本

HTTP/3はHTTP/2と機能上は大きな違いはありません。HTTPリクエストで通信が始まり、各HTTPリクエスト・レスポンスはQUICのストリームによって多重化されます。そのため、gRPC over HTTP/2にほぼマッピングされます。

HTTP/3では、トランスポートとしてQUICを使用します。これにより、ストリームが異なる場合は、パケットロスやパケットの順番が入れ替わったとしても、受信した後続のパケットを処理しすることができます
(TCPでは一般的にOSがそのパケットを回復するまで、後続のパケットを受信してても処理できません)

また、各コネクションはコネクションIDによって識別されるため、IPアドレスやポート番号が変わってもコネクションを維持することができます。
(サーバ間の通信が主なgRPCではそのメリットは少ないかもしれない)

一方で、QUICの通信は暗号化されます。暗号化しない手段は提供されていません。QUICは通信の中で、TLSハンドシェイク相当の処理を行うので、一般にはサーバ証明書が必要になります。

なお、1度通信した相手とは、0-RTTハンドシェイクを行うことで、コネクションの確立を早く行えます。

おまけ: QUIC及びHTTP/3の詳細

QUIC及びHTTP/3の詳細については、過去にガッツリ解説を書いたのでこちらを参照ください
asnokaze.hatenablog.com

gRPC over HTTP/3のプロトコル仕様

ここでは、gRPC over HTTP/2 との違うところ/同じところを簡単にかいつまんで紹介します。

  • ストリームID: HTTP/3ではQUICにより提供されるストリームを使用しますが、HTTP/2のストリームIDと同じように機能する
  • データフレーム: DATAフレームの使い方は、HTTP/3では変更されない
  • エラーコード: HTTP/3では、HTTP/2と異なるエラーコードをもちます。そのため、gRPCのエラーコードと改めてマッピングが行われます(仕様参照)
  • 接続の管理: GOAWAYフレームおよび、PINGフレームはHTTP/2のときと同様に使用できます。

そのため細かい点はありますが、HTTP/3とHTTP/2を変換するProxyを間に挟むだけで、プロトコル上は問題なく動作すると思われます。

また、HTTP/2はTCPであり、HTTP/3はQUIC(UDP)ですのでどちらで通信を行うのかうまく選択しなければなりません。その戦略には、一般的に2種類の方法があります。

  • クライアントがHTTP/3を使うように設定されている場合は、HTTP/3で最初に接続しに行く (Happy Eyeballsや、フォルバックは実装依存)
  • ブラウザが行うのと同じように、クライアントはまずHTTP/2で接続してから、alt-svcヘッダの情報をもとにサーバがHTTP/3に対応している事を知ります。その後、HTTP/3で通信を開始します。

実装

Ubuntu 20.04で、.Net core6 を[公式ドキュメント]にそってインストールします。

その後、下記のexampleを実行します。このGreeterは、nameをつけてSayHelloすると、「hello name」と返してくれます。
github.com

サーバ

.Net 6.0でビルドして、HTTPSと証明書の設定をし、ドキュメントに沿ってHTTP/3を有効化します。

要所としてはこんな感じで動きました。

webBuilder.ConfigureKestrel(options => {
	X509Certificate2 cert = new X509Certificate2("/path/to/server.pfx");
	options.ListenLocalhost(5001, o => {
		o.Protocols = HttpProtocols.Http1AndHttp2AndHttp3;
		o.UseHttps(cert);
	});
});

その後、ビルドして起動するだけです。

動作確認

今回は単純に、HTTP/3に対応したcurl で grpcなリクエストを投げ込んで動作確認しました

クライアントログ (出力はgrpc形式のバイナリがかえってきています)

$ hexdump -C ./req.bin 
00000000  00 00 00 00 0f 0a 0d 47  72 65 65 74 65 72 43 6c  |.......GreeterCl|
00000010  69 65 6e 74                                       |ient|
00000014

$ cat ./req.bin | curl -sS -X POST --data-binary @- https://localhost:5001/greet.Greeter/SayHello  -k -H"content-type:application/grpc"   --http3 -o - | hexdump -C
00000000  00 00 00 00 15 0a 13 48  65 6c 6c 6f 20 47 72 65  |.......Hello Gre|
00000010  65 74 65 72 43 6c 69 65  6e 74                    |eterClient|
0000001a

サーバ側ログ

:~/work/grpc-dotnet/examples/Greeter$ ./Server/bin/Debug/net6.0/Server
...

info: Microsoft.AspNetCore.Hosting.Diagnostics[1]
      Request starting HTTP/3 POST https://localhost:5001/greet.Greeter/SayHello application/grpc 20
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
      Executing endpoint 'gRPC - /greet.Greeter/SayHello'
info: Server.GreeterService[0]
      Sending hello to GreeterClient
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[1]
      Executed endpoint 'gRPC - /greet.Greeter/SayHello'
info: Microsoft.AspNetCore.Hosting.Diagnostics[2]
      Request finished HTTP/3 POST https://localhost:5001/greet.Greeter/SayHello application/grpc 20 - 200 - application/grpc 6.2228ms

一旦、ちゃんと動いてそう。

最後に

他の言語でも繋いでみたいですね(冬休みの宿題)