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
フレームだけを送信します。
ヘッダやリソースが一つのフレームに入りきらない場合, HEADERS
も DATA
も、複数のフレームに跨ることがあります。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++
-
- class Http2Reader
- {
- typedef void (*Func)(const char* buf, int payload_length);
-
- std::streambuf* m_conn;
-
- char* m_framebuf;
- int m_bufsize;
-
- Func m_func;
-
- public:
- Http2Reader() : m_conn(nullptr), m_framebuf(nullptr), m_bufsize(0) { }
-
- virtual ~Http2Reader()
- {
- if (m_framebuf) {
- free(m_framebuf);
- m_framebuf = nullptr;
- m_bufsize = 0;
- }
- }
-
- void add_listener( Func func );
-
- void attach(std::streambuf* conn);
-
-
- char* read_frame();
-
-
- std::string getline();
- };
実装
ヘッダの解析, ヘッダの送信
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++
-
-
-
- int dump_HEADERS_payload( const char* payload, int payload_length )
- {
- int isfinal = 1;
- int rv;
-
- const char* in;
- for ( in = payload; in < payload + payload_length; ) {
- nghttp2_nv nv;
- int inflate_flags = 0;
- rv = nghttp2_hd_inflate_hd2(m_inflater, &nv, &inflate_flags,
- (unsigned char*) in, payload_length - (in - payload),
- isfinal);
- if (rv < 0) {
- fprintf(stderr, "inflate failed: %d\n", rv);
- return -1;
- }
- if ( (inflate_flags & NGHTTP2_HD_INFLATE_EMIT) != 0 ) {
- string name, value;
- name.assign((char*) nv.name, nv.namelen);
- value.assign((char*) nv.value, nv.valuelen);
- printf("%s: %s\n", name.c_str(), value.c_str());
- }
-
- in += rv;
- }
-
- return 0;
- }
一方, HEADERS
フレームの送信は, 無圧縮と決め打ちすれば, それほど難しくありません。
C++
- int send_HEADERS_frame( streambuf* conn, int stream_id,
- const map<string, string>& m )
- {
- assert(conn);
-
- char buf[1000];
- char* p = buf;
-
-
- *p++ = 0; *p++ = 0; *p++ = 0;
- *p++ = 0x01;
- *p++ = 0x04;
- uint32_t nid = htonl(stream_id);
- memcpy(p, &nid, 4); p+= 4;
-
-
- for ( auto& req : m ) {
- *p++ = 0x00;
- int len = req.first.size();
- assert(len <= 255);
- *p++ = len;
- memcpy(p, req.first.c_str(), len); p += len;
- len = req.second.size();
- assert(len <= 255);
- *p++ = len;
- memcpy(p, req.second.c_str(), len); p += len;
- }
-
- uint32_t n3 = htonl(p - buf - FRAME_HDR_LENGTH);
- memcpy(buf, ((char*) &n3) + 1, 3);
-
- dump_frame_header('S', buf);
- dump_HEADERS_payload( buf + FRAME_HDR_LENGTH, p - buf - FRAME_HDR_LENGTH );
- ssize_t r = conn->sputn( buf, p - buf );
- if ( r < 0 ) {
-
-
- return r;
- }
-
- return r;
- }
ソケットを読む
HTTP/2 は, ソケットを読むとき, 行指向の部分と, フレーム単位のバイナリ列の部分があります。どちらの形でも読めるようにしなければなりません。
まずはフレーム単位。ストリームの長さは非常に巨大になることがありますが、フレームは, メモリに載る大きさと仮定して大丈夫です。
C++
-
- char* Http2Reader::read_frame()
- {
- assert(m_conn);
- assert(m_bufsize >= FRAME_HDR_LENGTH);
-
-
- m_conn->sgetn(m_framebuf, FRAME_HDR_LENGTH);
-
- int payload_length = 0;
- ntoh3(m_framebuf, &payload_length);
-
- if (FRAME_HDR_LENGTH + payload_length > m_bufsize) {
- m_bufsize = FRAME_HDR_LENGTH + payload_length;
- m_framebuf = (char*) realloc(m_framebuf, m_bufsize);
- }
-
-
- m_conn->sgetn(m_framebuf + FRAME_HDR_LENGTH, payload_length);
-
- return m_framebuf;
- }
行として読む場合。streambuf
がバッファリングの難しい部分をやってくれるので、非常に簡単です。
C++
-
- string Http2Reader::getline()
- {
- assert(m_conn);
-
- string ret;
-
-
- int ch;
- while ( ch = m_conn->sbumpc(), (ch != EOF && ch != '\r' && ch != '\n') )
- ret.append(1, ch);
-
- if (ch == EOF)
- return ret;
-
- if (ch == '\r') {
- ch = m_conn->sgetc();
- if (ch == '\n')
- m_conn->sbumpc();
- }
-
- return ret;
- }
フレームの受信
いよいよフレームを受信し, DATA
フレームを待ち構えます. frame type の解析がうまく実装できておらず, だいぶ手抜いています。
また, 別のストリームのフレームが交互に届く可能性を考慮できていません。
C++
-
-
- void recv_streams(Http2Reader& reader)
- {
- int frame_type = 0;
- char* frame = nullptr;
-
-
- while (true) {
- frame = reader.read_frame();
- dump_frame_header('R', frame);
- if (!frame)
- return;
-
-
- int payload_length = 0;
- ntoh3(frame, &payload_length);
- memcpy(&frame_type, frame + 3, 1);
-
-
-
- if (memcmp(frame, settingframeAck, FRAME_HDR_LENGTH) == 0) {
-
- continue;
- }
-
- if (frame_type == 0x3) {
- printf("error code = %x\n", *((int*) (frame + 9)));
- continue;
- }
-
- if (frame_type == 0x1) {
-
- int payload_length = 0;
- ntoh3(frame, &payload_length);
- dump_HEADERS_payload(frame + 9, payload_length);
- continue;
- }
-
- if (frame_type == 0x5) {
- int payload_length = 0;
- ntoh3(frame, &payload_length);
- int stream_id = ntohl( *(int*) (frame + 9) );
- printf("promised stream-id: %d\n", stream_id);
- dump_HEADERS_payload(frame + 13, payload_length - 4);
- continue;
- }
- if (frame_type == 0x7) {
- printf("last stream-id = %d; ", ntohl(*(int*) (frame + 9)));
- printf("error code = 0x%x\n", ntohl(*(int*) (frame + 13)));
- return;
- }
- if (frame_type == 0 )
- break;
- }
-
-
-
-
- int payload_length = 0;
- ntoh3(frame, &payload_length);
-
- frame[FRAME_HDR_LENGTH + 100 ] = '\0';
- printf("====DATA====\n");
- printf("%s", frame + FRAME_HDR_LENGTH);
- printf("====/DATA====\n");
- }