HTTP/2: ちょっと詳細: フレームフォーマット編

# この記事について

今さら、個人的に HTTP/2 の少し詳細をまとめたもの。内容はフレームフォーマットについて。

# フレームのおさらい

- フレームは HTTP/2 でメッセージ(リクエスト/レスポンス)を送受信するためのバイナリ構造
- HTTP/2 での通信の最小単位になり、送信するデータ種別や目的に応じて以下の種類が定義されている。


| 名称          | タイプ |    役割    |
|:--------------|:------:|:-------------------|
| DATA          |      0 | メッセージのボディを送信  |
| HEADERS       |      1 | メッセージのヘッダーを送信  |
| PRIORITY      |      2 | ストリームの優先度を指定  |
| RST_STREAM    |      3 | ストリームの終了要求       |
| SETTINGS      |      4 | コネクションの設定とそのACK |
| PUSH_PROMISE  |      5 | サーバプッシュのための事前通知       |
| PING          |      6 | アイドル状態のコネクションの確認    |
| GOAWAY        |      7 | コネクションの終了       |
| WINDOW_UPDATE |      8 | フロー制御        |
| CONTINUATION  |      9 | フレームの継続送信        |

以降では、フレームタイプごとにその構造を説明する。

# フレームの構造

## 固定長ヘッダー
全てのフレームは9バイトの固定長ヘッダーから始まり、その後に各フレームタイプごとのペイロードが続く。以下は、固定長ヘッダーのフォーマット。

```
+-----------------------------------------------+
|                 Length (24)                   |
+---------------+---------------+---------------+
|   Type (8)    |   Flags (8)   |
+-+-+-----------+---------------+-------------------------------+
|R|                 Stream Identifier (31)                      |
+=+=============================================================+
|                   Frame Payload (0...)                      ...
+---------------------------------------------------------------+
```
- Length : フレームペイロードの長さ。符号なし24ビット整数。
- Type : フレームタイプ。
- Flags : フレームタイプごとに定義されたフラグ
- R : 予約済みのフィールド
- Stream Identifier : ストリームID
- Frame Payload : フレームタイプ固有のペイロード

以下に実例を添付する。この例では、1つのパケットに2つのフレームが含まれており、それぞれ固定長ヘッダー＋各フレームタイプ固有のペイロードという構造になっている。

![frame_header2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/358ffe0e-85b2-7bd6-3bf6-dd1c83a35bc9.png)

## フレームペイロードの最大サイズ

全ての HTTP/2 の実装は、16KBまでのフレームペイロード(＋9バイトの固定長ヘッダー)を最低限処理出来る必要がある。言い換えると、16KBより大きなフレームペイロードをいきなり送りつけることはできない。

そうしたい場合、事前に SETTINGS フレームの SETTINGS_MAX_FRAME_SIZE でフレームペイロードの最大サイズを相手に通知する。指定できる値は 16KB (2^14) から 16MB-1 (2^24-1)。

# DATA フレーム (Type = 0x0)
HTTP リクエスト/レスポンスのボディを送信するのに使う。あくまでボディ部分だけであり、メソッド、パス、ヘッダー、ステータスコードなどは HEADERS フレームで別途送信する。

次の特徴がある。

- HTTP/2 のフロー制御の対象になる(DATAフレーム以外は対象にならない)
- ストリームの状態が `open` または `half-closed (remote)` の場合だけ送信できる

**DATA フレームのフォーマット:**

```
+---------------+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
|                            Data (*)                         ...
+---------------------------------------------------------------+
|                           Padding (*)                       ...
+---------------------------------------------------------------+
```

**DATA フレームの実例：**
サーバーからのレスポンスボディが DATA フレームとして返されているところ。
![data_frame.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/c8289923-cb6b-d6ed-df54-98bf297259c5.png)

# HEADERS フレーム (Type = 0x1)
HTTP ヘッダーを送信するのに使う。リクエスト時のメソッドやパス、レスポンス時のステータスコードなども HEADERS フレームで送信する。なお、HTTP/2 では HTTP/1.x と異なり、ヘッダーは HPACK という方式で圧縮されて送信される。

HTTP/2 のヘッダーにはいくつかの関連用語があるので以下に整理する。

| 用語             |       説明 | 
|:-----------------|:------------------|
|ヘッダーフィールド (header field) | 1つ以上の値を持つ名前(Key-Values ペア)。つまり1つのヘッダーを表す。|
|ヘッダーリスト (header list)| 0個以上のヘッダーフィールドの集まり。つまり送信する全てのヘッダー。 |
|ヘッダーブロック (header block)| ヘッダーリストをHPACK で圧縮・シリアライズしたもの。 |
|ヘッダーブロックフラグメント (header block fragments)| ヘッダーブロックを1つ以上のバイト列に分割したもの。 |

ヘッダーブロックをヘッダーブロックフラグメントに分割するのは、巨大なヘッダーフィールド(例えば Set-Cookie) があると1つのフレームに収めることができないため。
※ 送信が保証されるフレームペイロードのサイズは16KBまで。

1つのフレームに収まらない場合、HEADERS フレームで最初のヘッダーブロックフラグメントを送り、続きは CONTINUATION フレームで送る。受信側は、1つ以上のヘッダーブロックフラグメントからヘッダーブロックを再構成する。

参考として、CONTINUATION を用いる理由について [Why the rules around Continuation on HEADERS frames?](https://http2.github.io/faq/#why-the-rules-around-continuation-on-headers-frames)より引用。

> Continuation exists since a single value (e.g. Set-Cookie) could exceed 16KiB - 1, which means it couldn’t fit into a single frame. It was decided that the least error-prone way to deal with this was to require that all of the headers data come in back-to-back frames, which made decoding and buffer management easier.
  
**HEADERS フレームのフォーマット:**

```
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E|                 Stream Dependency? (31)                     |
+-+-------------+-----------------------------------------------+
|  Weight? (8)  |
+-+-------------+-----------------------------------------------+
|                   Header Block Fragment (*)                 ...
+---------------------------------------------------------------+
|                           Padding (*)                       ...
+---------------------------------------------------------------+
```

## PRIORITY フレーム (Type = 0x2)

ストリームの依存関係と重みづけにより、送信側から該当ストリームの優先度を通知する。依存関係と重みづけの意味については[ストリームの優先順位](https://developers.google.com/web/fundamentals/performance/http2/#%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%A0%E3%81%AE%E5%84%AA%E5%85%88%E9%A0%86%E4%BD%8D)を参照。

**PRIORITY フレームのフォーマット:**

```
+-+-------------------------------------------------------------+
|E|                  Stream Dependency (31)                     |
+-+-------------+-----------------------------------------------+
|   Weight (8)  |
+-+-------------+
```

## RST_STREAM フレーム (Type = 0x3)

ストリームの終了要求や、エラー発生の通知を行う。ペイロードはエラーコードだけを含み、RST_STREAM が送られたストリームは "closed" の状態に遷移して完全に終了する。

**RST_STREAM フレームのフォーマット:**

```
+---------------------------------------------------------------+
|                        Error Code (32)                        |
+---------------------------------------------------------------+
```

## SETTINGS フレーム (Type = 0x4)

通信に関わる設定パラメータの送信、および、それに対する確認応答(ACK)に使われる。

SETTINGS フレームには、以下の特徴がある。

- 設定は、個々のストリームではなくコネクションに対して適用される
- ストリームID は必ず 0 が使われる
- コネクション開始時にクライアント/サーバーの両方から SETTINGS フレームを送信する必要がある
   - その後、接続中はいつでも SETTINGS フレームを送ることが出来る
   - その場合、既存の設定値を上書きする
- １つの SETTINGS フレームは、0個以上の設定パラメータから構成される
- ACK として SETTINGS フレームを送る場合、固定長ヘッダー部分で ACK フラグがセットされる

**SETTINGS フレームのフォーマット:**

```
+-------------------------------+
|       Identifier (16)         |
+-------------------------------+-------------------------------+
|                        Value (32)                             |
+---------------------------------------------------------------+
```

**SETTINGS フレームで渡せる設定パラメータ:**

- SETTINGS_HEADER_TABLE_SIZE (0x1)
   - ヘッダー圧縮テーブルの最大サイズ
   - デフォルトでは 4,096 バイト
- SETTINGS_ENABLE_PUSH (0x2)
   - サーバープッシュの有効化・無効化
   - デフォルトでは有効
- SETTINGS_MAX_CONCURRENT_STREAMS (0x3)
   - 同時最大ストリーム数
   - デフォルトでは制限無し
- SETTINGS_INITIAL_WINDOW_SIZE (0x4)
   - 初期ウインドウサイズ
   - デフォルトでは 65,535 バイト
- SETTINGS_MAX_FRAME_SIZE (0x5)
   - フレームペイロードの最大サイズ
   - デフォルトでは16KB(2^14)
- SETTINGS_MAX_HEADER_LIST_SIZE (0x6)
   - 受信可能なヘッダーリストの未圧縮状態での最大サイズ
   - デフォルトでは無制限

**SETTINGS フレームの実例：**

![settings_frame.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/a778a11b-5af3-da4a-b733-c957aa75e451.png)


## PUSH_PROMISE フレーム (Type = 0x5)
サーバープッシュを行うために、事前にサーバーからクライアントへ次の情報を通知する。

- サーバープッシュに用いるストリームID
- クライアントが送ると想定した HTTP リクエストの情報

後者は、例えば最初にクライアントが index.html をリクエストして、次にその index.html に記述された xxx.js に対して `GET /xxx.js` というリクエストが来るとサーバーが想定した場合、サーバーはその想定リクエストを PUSH_PROMISE フレームに含めるためのもの。これにより、クライアントは自分が xxx.js へリクエストすべきか判断できる。

なお、PUSH_PROMISE フレームの送信には順序が重要になる。元のリクエスト(この例では `GET /index.html`)のレスポンスの前に PUSH_PROMISE を送らないと、クライアントが `xxx.js` へのリクエストを発行してしまう可能性がある。


![push_promise.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/0ab57a68-4c2f-27f1-9d0f-6ef9399d32ba.png)

**PUSH_PROMISE フレームのフォーマット:**

```
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R|                  Promised Stream ID (31)                    |
+-+-----------------------------+-------------------------------+
|                   Header Block Fragment (*)                 ...
+---------------------------------------------------------------+
|                           Padding (*)                       ...
+---------------------------------------------------------------+
```

## PING フレーム (Type = 0x6)
アイドル中のコネクションがまだ機能するかの確認と、最小のラウンドトリップタイムの計測に用いる。

次の特徴がある。
 
- ペイロードは 8バイト固定(ペイロードの中身は任意)
- PING フレームの受信側は、ACK フラグを付けて PING フレームを返信する
- 返信時のペイロードは、受信したものと同一のペイロードにする
- PING フレームは、特定のストリームに関連付けられない。つまり、ストリームID 0 で送信する。

**PING フレームのフォーマット:**

```
+---------------------------------------------------------------+
|                                                               |
|                      Opaque Data (64)                         |
|                                                               |
+---------------------------------------------------------------+
```

## GOAWAY フレーム (Type = 0x7)

TCPコネクションの終了開始、または、致命的なエラーの通知に使う。次の特徴がある。

- 現在のコネクションで通信相手が開始したストリームのうち、最後に処理したストリームIDを含める。
  - 通信相手が、どのストリームまで処理されたか分かるようにするため
- GOAWAY は、特定のストリームではなくコネクション全体に適用される。つまり、ストリームID 0 で送信する。

**GOAWAY フレームのフォーマット:**

```
+-+-------------------------------------------------------------+
|R|                  Last-Stream-ID (31)                        |
+-+-------------------------------------------------------------+
|                      Error Code (32)                          |
+---------------------------------------------------------------+
|                  Additional Debug Data (*)                    |
+---------------------------------------------------------------+
```

## WINDOW_UPDATE フレーム (Type = 0x8) 

個々のストリーム、および、コネクション全体でフロー制御を行うために現在のウィンドウサイズに対する増加量(バイト数)を通知する。なお、ウィンドウサイズを減少させたい場合は SETTINGS フレームの SETTINGS_INITIAL_WINDOW_SIZE を用いる。

WINDOW_UPDATE フレームには、次の特徴がある。

- フロー制御は DATA フレームのみが対象
- フロー制御はホップ間で行われる(End to End ではない)
- ウインドウの増加量は 1 から 2,147,483,647(2^31-1)の範囲
- フレームのストリーム ID が 0 の場合はコネクション全体が対象。そうでない場合は、指定されたストリームIDが対象。
- コネクションとストリームのデフォルトの初期ウィンドウサイズは 65,535 バイト
- SETTINGS フレームの SETTINGS_INITIAL_WINDOW_SIZE で初期ウィンドウサイズを調整できる

```
    +-+-------------------------------------------------------------+
    |R|              Window Size Increment (31)                     |
    +-+-------------------------------------------------------------+
```

## CONTINUATION フレーム (Type = 0x9) 

ヘッダーブロックフラグメントが複数ある場合に続きのものを送信するために使う(詳細は HEADERS フレームの説明を参照)。

最後のヘッダーブロックフラグメントを含むフレームには END_HEADERS フラグをセットする。

**CONTINUATION フレームのフォーマット:**

```
+---------------------------------------------------------------+
|                   Header Block Fragment (*)                 ...
+---------------------------------------------------------------+
```

# 参考

- [Hypertext Transfer Protocol Version 2 (HTTP/2)](https://tools.ietf.org/html/rfc7540)
- [Hypertext Transfer Protocol Version 2 (HTTP/2) 日本語訳](https://summerwind.jp/docs/rfc7540)
- [HTTP/2 Frequently Asked Questions](https://http2.github.io/faq/)
- [HTTP/2 の概要](https://developers.google.com/web/fundamentals/performance/http2/)
- [初めてのHTTP/2サーバプッシュ](https://labs.gree.jp/blog/2014/12/11987/)