HTTP/2: プロトコルの概要編

# この記事について
今さらだが、個人的に HTTP/2 の概要をまとめたもの。

# HTTP/2 に至る背景

## HTTP/1.x のコネクション管理

HTTP/1.x では、トランスポートのコネクション(通常はTCPコネクション)をどう使用/制御するか、HTTP メッセージ(リクエストとレスポンス)をどう送受信するかについて、3つのモデルがある。

- Short-lived
  - HTTP リクエストを送信するたびに新しいコネクションを作成し、レスポンスを受信するとコネクションをクローズする
  - HTTPの初期から存在するモデル (HTTP/1.0 のデフォルト)
  - パフォーマンスやリソース消費に問題あり
- Persistent (Keep-Alive)
  - コネクションはしばらく開いたままにして、いくつかのリクエストで再利用する。いわゆる Keep-Alive。
  - HTTP/1.1 で導入  (HTTP/1.1 のデフォルト)
  - １つのリクエストへレスポンスが返らないと、次のリクエストを送れない (HOLが生じる)
- Pipeline
  - １つのコネクション上で連続してリクエストを送信できる(でも、レスポンスはリクエストされた順に従い返される)
  - HTTP/1.1 で導入
  - 結局、HOL の問題は残る
  - 実装が難しく不具合のあるサーバーやプロキシも多いことから、ほとんどのブラウザでデフォルトで無効化されてる

これらの課題に対して、ブラウザは複数のTCPコネクションを開き、並列でリクエストを送ることで対処する(同時コネクション数は、1つのドメインに対して最大で6つ)。

さらに、コネクション数を増やしたい場合は、サーバー上のリソースを複数のドメインに分散して配置するという方法がある(ドメインシャーディング)。
  
しかし、サーバーのリソース消費・負荷増大とさらに、コンテンツ提供者が頑張らなければいけないという問題がある。

(参考)

- [HTTP/1.x のコネクション管理](https://developer.mozilla.org/ja/docs/Web/HTTP/Connection_management_in_HTTP_1.x)

## HTTP/1.x の問題

Webサイトのコンテンツ数増加やサイズ増大に伴い、1つのページを表示するのに大量のリクエスト送信が必要になり、上記のコネクション管理とリクエスト送信の問題によるパフォーマンスの問題が顕著になってきた。

また他にも HTTP ヘッダーのサイズが増大してきたが(特に Cookie やアクセストークンが含まれる場合)、圧縮せずそのままテキストとして送受信するため不要なネットワークトラフィックが発生するという問題も生じてきた。

# HTTP/2

## 目標
HTTP/1.1のパフォーマンス問題に対処して、ウェブページの読み込みレイテンシを短縮すること。Google の [HTTP/2 の概要](https://developers.google.com/web/fundamentals/performance/http2/)によると、HTTP/2 の元となった試験的プロトコル SPDY の目標は以下。

- ページ読み込み時間（PLT）の 50% 短縮を目指す。
- ウェブサイト作成者によるコンテンツ変更の必要性を回避する。
- デプロイメントの複雑さを最小限に抑え、ネットワーク インフラの変更を回避する。
- この新しいプロトコルをオープンソース コミュニティと協力して開発する。
- 実際のパフォーマンス データを収集し、試験的なプロトコルを検証（または無効化）する。

なお、HTTP/2 は IETF にて SPDY を元に改良を加え標準化したもの。

## HTTP/2 のコア技術

コアとなる技術は **フレーム** と **ストリーム**。

- HTTP メッセージ(リクエスト/レスポンス)をテキストではなくバイナリで送受信するための「フレーム」と呼ばれる構造と、それを処理するフレームレイヤー
- 1つのTCPコネクション上に「ストリーム」と呼ばれる仮想的な経路を複数作成できるようになり、リクエスト/レスポンスのフレームを並列で処理する。

他にも、ヘッダー圧縮、サーバープッシュ、優先度付けなどの機能があるが順次説明。

## フレーム

HTTP/2 では、1つの HTTP メッセージを複数のフレームというバイナリ構造に分解して送受信する。そして、受信側は受け取ったフレーム群をメッセージとして再構築する。後で述べるように、異なるメッセージのフレームを混在させて並行に送受信可能であり、このフレームの分解/混在/再構築が HTTP/2 で一番大事な機能。

それら処理を行うのがフレームレイヤーで Socket インターフェースとアプリケーションレイヤーの間に位置づけられる。

([HTTP/2 の概要](https://developers.google.com/web/fundamentals/performance/http2/)から引用)
![frame_layer.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/3ac153fb-beaa-c143-2a19-937256db78c3.png)

フレームは、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 | フレームの継続送信        |

例えば、以下は GET リクエストを送る HEADERS フレームの例(Wireshark のキャプチャ)。

![frame_get.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/210fa122-5ef6-a9a7-819c-82c4dd4d31b9.png)

## ストリーム

HTTP/1.x ではサーバーに対して複数の TCP コネクションを張って並行にメッセージを送受信していたが、**HTTP/2 では1のドメイン(正確に言うとオリジン)に対しては1つの TCP コネクションしか張らない**。

この1つの TCP コネクション上で、複数のフレームを同時に送受信するための仮想的な経路をストリームと呼ぶ。ストリームはクライアントとサーバーの双方向でデータ転送が可能。

次の特徴がある。

- クライアントとサーバーのどちからからもストリームを開始できる
- ストリームは整数のIDを割り当てて識別する
  - クライアントは奇数、サーバーは偶数のIDでストリームを開始する(IDの重複を防ぐため)
  - ただし、ID 0 はコネクションを制御するための特殊なストリームとして使われる
  - フレームのヘッダーにはストリームIDが格納されている
- ストリームは同時に複数開始可能(上限は設定できる)
- ストリームはそれぞれ独立している(他のストリームに影響を与えない)

なお、先に添付した Wireshark の画面では、ストリームID 1 で通信している(`Stream Identifier: 1` となっている個所)。

ストリームと異なるリクエストのフレームが多重化され送受信されるイメージは以下のような感じ。

![frame_mux.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539081/9c2cbef9-7d7f-601a-7a1d-a660d563629a.png)
([HTTP/2 の概要](https://developers.google.com/web/fundamentals/performance/http2/)から引用)

## ストリームの優先度

クライアント側はストリームを開始する際に、依存関係(他のどのストリームに依存するか)と重みを指定することにより、サーバーにストリームを処理する際の優先度を伝えることができる。これにより、サーバーはストリームの処理に必要なリソースの割り当てを変更できる。

ロジックについては[ストリームの優先順位](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)に分かりやすく説明されてる。

ただし、この優先度は強制力はなく、サーバー側で指定した通りに処理される保証は無い。

## フロー制御

ストリームごとに、受信側が受信可能なデータ量を制御する仕組み。

TCP でもフロー制御は実装されているが HTTP/2 では TCP コネクションが1つのため、それだとストリーム単位でのフロー制御ができない。ストリーム単位のフロー制御は、例えば以下の目的で使用する。

- 特定のストリームがリソースを専有することを防ぐ
  - 大容量ファイルのダウンロードや動画を見てる場合など
- Proxy サーバーにて、ダウンストリームとアップストリームの回線速度が大きく異なる場合に、低速な方に合わせて通信速度を制御したい

HTTP/2 のフロー制御には次の特徴がある。

- ウィンドウサイズの更新は、WINDOW_UPDATE フレームを用いて行う
- フロー制御は DATA フレームにのみ適用される
- ホップ間で行われる (End-to-Endではない)

## サーバープッシュ

クライアントからの1つのリクエストに対して、サーバーから複数のレスポンスを返せる機能。例えば、HTML に対して GET リクエストが来た際、サーバーは、続いてその HTML に記述されているリソース(JavaScript, CSSなど)がリクエストされることを想定できる。

そういった場合に、サーバーが想定したリソースを予めクライアントに送る仕組み。余分な通信を削減してレイテンシーを短縮する。

次の特徴がある。

- サーバープッシュは PUSH_PROMISE フレームで事前通知してから開始される
- もしクライアント側でリソースがキャッシュ済みなどの理由でサーバープッシュが不要な場合は、それを拒否できる

## ヘッダー圧縮

HTTP/2 では、HPACK という方式でリクエストとレスポンスのヘッダーを圧縮する。HPACK は HTTP/2 のために作られた圧縮方式。

なお、ヘッダーは HEADERS フレームで送信されるが、HTTP のメソッドやパス、レスポンスのステータスコードなども HEADERS フレームで送信される。

ヘッダー圧縮には次の特徴がある。

- 1度送信したヘッダーは基本的に再送信する必要は無く、差分のヘッダーだけを送信する
- ハフマン符号化を用いて、転送サイズを削減する

# Appendix

## HTTP の歴史 概要
- HTTP/0.9 (1991年)
  - 初版
  - メソッドはGETだけ。レスポンスは単にドキュメントの内容を返すだけで、レスポンスコードの規定もない。
  - ワンライン プロトコル

- HTTP/1.0 (1996年)
  - RFC 1945 で規定。
  - メソッドに POST が追加
  - レスポンスにヘッダーが付くようになり、ステータスコードが返るようになった
  - コネクション管理は short-lived 

- HTTP/1.1 (1997年)
  - RFC 2068 で規定。(その後、RFC 7230～7235で規定)
  - 1.0から大幅に機能が追加される
    - HOST ヘッダが追加され、バーチャルホストがサポートされた
    - 新しく2つのコネクション管理モデル(persistent, pipeline)が導入された

(参考) [HTTP の進化](https://developer.mozilla.org/ja/docs/Web/HTTP/Basics_of_HTTP/Evolution_of_HTTP)