HTTP/2サンプル実装 (共通ルーチン)

HTTP/2 を理解するために, 簡単に実装してみました。

ここでは, 次の2種類のサンプルの, 共通ルーチンを実装します。HTTP/2プロトコルの説明も行います。

HTTP/2

ストリーム, フレーム

HTTP/2 は,「ストリーム」と「フレーム」という単位で, メッセージをやりとりします。

Connection はソケット接続です。基本的なコンセプトとして, 一つのソケット接続のなかで、並列で, リソースのリクエストとレスポンスをやり取りします. 一つ一つの論理的なデータのやり取りが「ストリーム」です。


図の出典: https://developers.google.com/web/fundamentals/performance/http2/

実際には, ある程度細切れにした「フレーム」を並べることで, 多重化します。各ストリームを一つのソケットに畳み込むために, 各フレームにストリームIDを付け, どのストリームのデータなのかを区別します。

一つ一つのフレームは, 9バイトのヘッダと, 内容のペイロードからなります。

フレームは, 次のいずれかの frame type を持ちます.

0x0 DATA リクエストボディや、レスポンスボディ.
0x1 HEADERS 非圧縮または圧縮されたHTTPヘッダ
0x2 PRIORITY ストリームの優先度を変更. RFC 9113 (June 2022) で非推奨になった. 互換性のために送信することはできるが、単に無視される.
0x3 RST_STREAM ストリームの終了を通知
0x4 SETTINGS 接続に関する設定
0x5 PUSH_PROMISE サーバからのリソースのプッシュ通知
0x6 PING 接続状況を確認する
0x7 GOAWAY 接続の終了を通知
0x8 WINDOW_UPDATE フロー制御ウィンドウを更新する
0x9 CONTINUATION HEADERSフレームやPUSH_PROMISEフレームの続きのデータ

HTTP/1.1 のリクエストとレスポンスはそれぞれ, HTTP/2 では HEADERSフレームと DATA フレームに分かれます。GETメソッドなら, HEADERSフレームだけを送信します。

ヘッダやリソースが一つのフレームに入りきらない場合, HEADERSDATA も、複数のフレームに跨ることがあります。HEADERS が長いときは CONTINUATIONフレーム, DATA のほうは、このフレーム自体に継続フラグがあります。

双方向

HTTP/1.1 と同様に, クライアントからリクエストを出し、サーバからレスポンスとしてデータを返す方法が一つ。HTTP/2 では, サーバからデータを push することもできます。そのため, ストリームは, クライアントからも, サーバからも開始できます。

クライアントがリソースを要求した場合, サーバのほうから関連するリソースもついでに送信できます。サーバから, PUSH_PROMISE フレームを開始します。

[2020.12] サーバpush には, すでにクライアント側でキャッシュされていれば無駄になる、などの問題があり、Google Chrome では廃止予定。デフォルトではこの設定は有効のため, SETTINGS_ENABLE_PUSH = 0 (無効化) をすべての接続の冒頭に送信する。See Intent to Remove: HTTP/2 and gQUIC server push

ヘッダ

TLS版と非TLS (平文) 版で共通化できるように, Http2Reader クラスを用意します。

実際の通信は, std::streambuf を拡張した, SocketStreamBuf クラスで行います; streambuf を拡張し, ソケット対応 [C++]

http2_common.h ファイル:

C++
[RAW]
  1. // HTTP/2 フレーム単位での読み込み.
  2. class Http2Reader
  3. {
  4. typedef void (*Func)(const char* buf, int payload_length);
  5. std::streambuf* m_conn;
  6. char* m_framebuf;
  7. int m_bufsize;
  8. Func m_func;
  9. public:
  10. Http2Reader() : m_conn(nullptr), m_framebuf(nullptr), m_bufsize(0) { }
  11. virtual ~Http2Reader()
  12. {
  13. if (m_framebuf) {
  14. free(m_framebuf);
  15. m_framebuf = nullptr;
  16. m_bufsize = 0;
  17. }
  18. }
  19. void add_listener( Func func );
  20. void attach(std::streambuf* conn);
  21. // フレーム (バイナリデータ)
  22. char* read_frame();
  23. // 行
  24. std::string getline();
  25. };

実装

ヘッダの解析, ヘッダの送信

HTTP/2 のヘッダ (HEADERSフレーム) は, HPACK という方法で圧縮されます。これが地味に難しい。

次のようなバイト列の繰り返しです。最初のバイトで, フィールド名を省略するのかどうか, インデックスをどうするのか, などを選択し, それによって後続のバイト列が決まります。

     0   1   2   3   4   5   6   7
   +---+---+---+---+---+---+---+---+
   | 0 | 0 | 0 | 0 |       0       | このオクテットの上位ビットで、後続が変わる。
   +---+---+-----------------------+
   | H |     Name Length (7+)      |
   +---+---------------------------+
   |  Name String (Length octets)  |
   +---+---------------------------+
   | H |     Value Length (7+)     |
   +---+---------------------------+
   | Value String (Length octets)  |
   +-------------------------------+

バイト列を短くするために, 仕様で事前定義された主なヘッダフィールド名があります。インデックスによる既出のヘッダフィールドの省略も可能です。

実装では, インデックスのデコード, フィールド値のデコードが必要です。文字列は, ハフマン符号化によって圧縮されています。

(2018.7 update) 全部を実装するのは大変で、ギブアップしました。ここでは、既存のライブラリを使います。

HPACK の実装としては, ほかにも例えば, こちらがあります; jnferguson/hpack-rfc7541: header-only c++ implementation of a HTTPv2 HPACK/RFC7541 encoder /decoder. ヘッダ1ファイルで実装しています。これを見ると, 複雑さが分かります。

C++
[RAW]
  1. /*
  2. * RFC 7541 HPACK: Header Compression for HTTP/2
  3. */
  4. int dump_HEADERS_payload( const char* payload, int payload_length )
  5. {
  6. int isfinal = 1; // TODO:
  7. int rv;
  8. const char* in;
  9. for ( in = payload; in < payload + payload_length; ) {
  10. nghttp2_nv nv;
  11. int inflate_flags = 0; // ret
  12. rv = nghttp2_hd_inflate_hd2(m_inflater, &nv, &inflate_flags,
  13. (unsigned char*) in, payload_length - (in - payload),
  14. isfinal);
  15. if (rv < 0) {
  16. fprintf(stderr, "inflate failed: %d\n", rv);
  17. return -1;
  18. }
  19. if ( (inflate_flags & NGHTTP2_HD_INFLATE_EMIT) != 0 ) {
  20. string name, value;
  21. name.assign((char*) nv.name, nv.namelen);
  22. value.assign((char*) nv.value, nv.valuelen);
  23. printf("%s: %s\n", name.c_str(), value.c_str());
  24. }
  25. in += rv;
  26. }
  27. return 0;
  28. }

一方, HEADERS フレームの送信は, 無圧縮と決め打ちすれば, それほど難しくありません。

C++
[RAW]
  1. int send_HEADERS_frame( streambuf* conn, int stream_id,
  2. const map<string, string>& m )
  3. {
  4. assert(conn);
  5. char buf[1000];
  6. char* p = buf;
  7. // ヘッダ (9バイト)
  8. *p++ = 0; *p++ = 0; *p++ = 0;
  9. *p++ = 0x01; // HEADERS
  10. *p++ = 0x04;
  11. uint32_t nid = htonl(stream_id);
  12. memcpy(p, &nid, 4); p+= 4;
  13. // payload
  14. for ( auto& req : m ) {
  15. *p++ = 0x00; // 圧縮情報
  16. int len = req.first.size();
  17. assert(len <= 255);
  18. *p++ = len;
  19. memcpy(p, req.first.c_str(), len); p += len;
  20. len = req.second.size();
  21. assert(len <= 255);
  22. *p++ = len;
  23. memcpy(p, req.second.c_str(), len); p += len;
  24. }
  25. uint32_t n3 = htonl(p - buf - FRAME_HDR_LENGTH); // ヘッダの長さは含まない
  26. memcpy(buf, ((char*) &n3) + 1, 3);
  27. dump_frame_header('S', buf);
  28. dump_HEADERS_payload( buf + FRAME_HDR_LENGTH, p - buf - FRAME_HDR_LENGTH );
  29. ssize_t r = conn->sputn( buf, p - buf );
  30. if ( r < 0 ) {
  31. //int error = get_error();
  32. //conn->close();
  33. return r;
  34. }
  35. return r;
  36. }

ソケットを読む

HTTP/2 は, ソケットを読むとき, 行指向の部分と, フレーム単位のバイナリ列の部分があります。どちらの形でも読めるようにしなければなりません。

まずはフレーム単位。ストリームの長さは非常に巨大になることがありますが、フレームは, メモリに載る大きさと仮定して大丈夫です。

C++
[RAW]
  1. // 1フレーム分を読む.
  2. char* Http2Reader::read_frame()
  3. {
  4. assert(m_conn);
  5. assert(m_bufsize >= FRAME_HDR_LENGTH);
  6. // ヘッダ (9バイト)
  7. m_conn->sgetn(m_framebuf, FRAME_HDR_LENGTH);
  8. int payload_length = 0;
  9. ntoh3(m_framebuf, &payload_length);
  10. if (FRAME_HDR_LENGTH + payload_length > m_bufsize) {
  11. m_bufsize = FRAME_HDR_LENGTH + payload_length;
  12. m_framebuf = (char*) realloc(m_framebuf, m_bufsize);
  13. }
  14. // payload
  15. m_conn->sgetn(m_framebuf + FRAME_HDR_LENGTH, payload_length);
  16. return m_framebuf;
  17. }

行として読む場合。streambuf がバッファリングの難しい部分をやってくれるので、非常に簡単です。

C++
[RAW]
  1. // @return 改行文字 (CRLF or LF) を含めない, 文字列.
  2. string Http2Reader::getline()
  3. {
  4. assert(m_conn);
  5. string ret;
  6. // sgetc() は位置を進めない. 罠か.
  7. int ch;
  8. while ( ch = m_conn->sbumpc(), (ch != EOF && ch != '\r' && ch != '\n') )
  9. ret.append(1, ch);
  10. if (ch == EOF)
  11. return ret;
  12. if (ch == '\r') {
  13. ch = m_conn->sgetc();
  14. if (ch == '\n')
  15. m_conn->sbumpc();
  16. }
  17. return ret;
  18. }

フレームの受信

いよいよフレームを受信し, DATAフレームを待ち構えます. frame type の解析がうまく実装できておらず, だいぶ手抜いています。

また, 別のストリームのフレームが交互に届く可能性を考慮できていません。

C++
[RAW]
  1. // 複数のストリームが、多重化されて送られてくる.
  2. // TODO: フレームをストリームに割り振る.
  3. void recv_streams(Http2Reader& reader)
  4. {
  5. int frame_type = 0;
  6. char* frame = nullptr;
  7. // HEADERS フレームを受信して, リソースの長さを取得する。
  8. while (true) {
  9. frame = reader.read_frame();
  10. dump_frame_header('R', frame);
  11. if (!frame)
  12. return;
  13. // payloadの長さ, frame typeを取得する。
  14. int payload_length = 0;
  15. ntoh3(frame, &payload_length);
  16. memcpy(&frame_type, frame + 3, 1);
  17. // ACKが返ってくる場合があるのでACKなら無視して次を読む。
  18. // TODO: listener を呼び出すようにする.
  19. if (memcmp(frame, settingframeAck, FRAME_HDR_LENGTH) == 0) {
  20. // 読みとばす
  21. continue;
  22. }
  23. if (frame_type == 0x3) { // RST_STREAM
  24. printf("error code = %x\n", *((int*) (frame + 9)));
  25. continue;
  26. }
  27. if (frame_type == 0x1) { // HEADERS
  28. // TODO: リソースの長さ
  29. int payload_length = 0;
  30. ntoh3(frame, &payload_length);
  31. dump_HEADERS_payload(frame + 9, payload_length);
  32. continue;
  33. }
  34. if (frame_type == 0x5) { // PUSH_PROMISE
  35. int payload_length = 0;
  36. ntoh3(frame, &payload_length);
  37. int stream_id = ntohl( *(int*) (frame + 9) );
  38. printf("promised stream-id: %d\n", stream_id);
  39. dump_HEADERS_payload(frame + 13, payload_length - 4);
  40. continue;
  41. }
  42. if (frame_type == 0x7) { // GOAWAY
  43. printf("last stream-id = %d; ", ntohl(*(int*) (frame + 9)));
  44. printf("error code = 0x%x\n", ntohl(*(int*) (frame + 13)));
  45. return;
  46. }
  47. if (frame_type == 0 ) // DATA
  48. break;
  49. }
  50. //------------------------------------------------------------
  51. // DATAフレームの受信.
  52. //------------------------------------------------------------
  53. int payload_length = 0;
  54. ntoh3(frame, &payload_length);
  55. frame[FRAME_HDR_LENGTH + 100 /*payload_length*/] = '\0'; // 手抜き
  56. printf("====DATA====\n");
  57. printf("%s", frame + FRAME_HDR_LENGTH);
  58. printf("====/DATA====\n");
  59. }