注意
この記事は、HTTP/2を勉強する過程での学びのメモを残したものです。 できるだけ、論文や仕様書、信頼できるソースを元に学習しているが、HTTP/2を(意識して)実務で使用したことはないレベルの人が書いた記事ですので間違った知識などもあるかもしれないので注意。
HTTP/2とは
HTTP/2は、ウェブにおけるアプリケーション層のプロトコルとして最も使用されているHTTPのうち、2015年に新しく公開されたものである。
現在広く普及しているHTTP/1.1における後述の課題を解決するために作成された。
HTTP/2が作成された理由などについて説明するために、HTTPの歴史を軽く振り返る。
HTTPの歴史
HTTP/0.9
初代。GETメソッドのみ存在し、ヘッダもステータスコードもない。
HTTP/1.0がでてから初めて、0.9と呼ばれるようになったらしい。
HTTP/1.0
HTTP/1.0は、1996年に公開されている。 https://tools.ietf.org/html/rfc1945
大きな変更として、以下の機能が追加されたバージョンである。
- POSTメソッドなどが追加
- ステータスコードが追加
- レスポンスヘッダが追加
HTTP/1.0では、与えられたTCP接続に対して、同時に1つのリクエストしか送信することができない。
HTTPでは、1つのリクエストで1つのレスポンスを返すというのが基本であり、そのため複数のコンテンツが含まれているWebサイトを閲覧する場合、全てのコンテンツをダウンロードするまでに時間がかかってしまうという問題点があった。
HTTP/1.1
HTTP/1.1は、1997年に公開されたバージョンである。 https://tools.ietf.org/html/rfc2068
以下の機能が追加されたバージョンでもある。
- バーチャルホスト (1つのサーバコンピュータで複数のドメインを運用)
- Persistent Connections(一度作ったTCP Connectionを使いまわせる。従来は各リクエストごとにTCPコネクションを確立していた。)
- PipeLining
Pipeliningとは?
Persistent Connections
によって実現できた技術で、簡単に言えば、リクエストのレスポンスをを待たずに同時に複数のリクエストを送信できる技術。
しかし、Pipeliningには、リクエストが来た順番と同じ順番でレスポンスを返さなければいけないという制約がある。
Persistent HTTP connections have a number of advantages: ... o HTTP requests and responses can be pipelined on a connection. Pipelining allows a client to make multiple requests without waiting for each response, allowing a single TCP connection to be used much more efficiently, with much lower elapsed time.
8.1.2.2 Pipelining
A client that supports persistent connections MAY "pipeline" its requests (i.e., send multiple requests without waiting for each response). A server MUST send its responses to those requests in the same order that the requests were received.
https://tools.ietf.org/html/rfc2068
そのため、前のリクエストによるレスポンスのサイズが大きいなど、処理に時間がかかってしまった場合に、後続のレスポンスまでも待たされてしまう、Head of Line Blocking (HoLブロッキング)が発生してしまう。
また、Pipeliningは多くのブラウザで実装されていたが、デフォルトでoffになっていることから、実際にはPipeliningが普及しているとはいえない。
どうやって高速化しようか
Webサイトは時間の経過に比例してどんどんリッチになり、ダウンロードしなければいけないコンテンツが増加していく。
しかし、Pipeliningが機能していないので、アプリ開発者たちはそれぞれ様々な工夫をしてHTTP/1.1を高速化していた。
CSSスプライト
複数の画像を一つの画像ファイルとして連結し、CSSの画像の表示範囲を指定してそれぞれ取り出す手法。
一度のリクエストで複数のコンテンツが取得できるので、一つ一つリクエストするよりも結果的に高速になる。
コンテンツをインライン化
htmlに画像やcssなどを埋め込むことでリクエスト数が減るので高速化。
ドメインシャーディング
ブラウザは、アクティブな接続数をドメインごとに制限する。Chromeなどのブラウザでは、同一ドメインに対して6リクエストまでしか同時に行わない。 そのため、リクエストするコンテンツが大量にある場合は、従来同様HoLブロッキングが発生する。
そこで、コンテンツを複数のドメインに分散させることで同時にリクエストすることができる数が上がり、高速化する。
ただし、DNS名前解決の数が増加するため、初回ダウンロードの時間が増加する問題点もある。
HTTP/2の登場
HTTP/2は、2015年に公開された。 https://tools.ietf.org/html/rfc2068
元々、Google社が開発したSPDYというプロトコルがあり、それがすでにChromeなど実サービスで運用されていたことから、HTTP/2の初期バージョンとしてSPDYが採用された。
HTTP/2に変わってもリクエストやレスポンス、ヘッダの仕組みなどは変わらない。(セマンティクスを維持したまま)
HTTP/2では、様々な機能が導入されたが、中でも最も注目すべき変更点は、ストリーム
と呼ばれる概念が導入されたことである。
それ以外にも、ヘッダのバイナリ化や、優先度など興味深い新機能も追加されている。
今回紹介するのは以下の通り
- ストリーム
- ヘッダの圧縮 HPACK
- サーバプッシュ
- フロー制御
- プライオリティ制御
ストリーム
ストリームは、クライアント、サーバ間における仮想的なコネクションで、1コネクションで同時に複数のストリームを確立することが可能になった。
なぜストリームが必要になるかというと、HTTP/2では、順番を関係なくリクエスト、レスポンスを送りたいのだが、そのためにリクエストとレスポンスを関連づけるための仕組みが必要になるからである。
ストリームはストリームID(後述)によって識別される。
ストリームはそれぞれ独立している、つまり同時に複数のリクエストを送信できるようになったので、従来のように重いリクエスト/レスポンスによって後続のリクエストがブロックされることがなくなり、高速化することが可能になる。
フレーム
ストリーム上では、複数のフレームがやりとりされる。 全てのフレームは9オクテットの固定長のヘッダから始まり、その後可変長のペイロード続く。
4.1. Frame Format
All frames begin with a fixed 9-octet header followed by a variable-
length payload.
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
Figure 1: Frame Layout
https://tools.ietf.org/html/rfc7540#section-4.1
ストリームそれぞれの識別は、符号なし31ビットの整数のストリームIDによって行われる。 クライアントが開始したストリームは奇数のストリームID、サーバが開始したストリームは偶数のストリームIDを利用しなければならない。
フレームには10種類ある。
- DATA: データ本体
- HEADERS: リクエストやレスポンスのヘッダ
- PRIORITY: 優先度制御(後述)
- RST_STREAM: ストリームの強制終了
- SETTINGS: いろいろコネクションの設定
- PUSH_PROMISE: サーバプッシュすることの告知(サーバのみ送信)
- PING: アイドル状態のコネクションがまだ機能しているかどうかの確認をしたり、送信者からのRTTも計測するためのフレーム。
- GOAWAY: コネクション終了の開始や、重大なエラー状態の通知
- WINDOW_UPDATE: フロー制御に利用
- CONTINUATION: 1フレームで送りきれなかったHEADERS、PUSH_PROMISE、またはCONTINUATIONフレームの続き。
ヘッダの圧縮
HTTP/1.1ではヘッダ領域が圧縮されなかった。
現在のWebのように数十や数百ものリクエストを送信する場合など、 何度も同じヘッダをやりとりして冗長である。
そこでHPACKと呼ばれるヘッダ圧縮方式によって、無駄を削減する。
1度送信したヘッダーを基本的には再度送信することはなく、新たに送信が必要なヘッダーのみを差分として抽出して送信することで無駄を削減する。
HPACKでは2つの圧縮方法を利用する。
- index table
- static
- dinamic
- ハフマン符号化
index table
HPACKでは、ヘッダフィールドとindexを関連づけるためのindex table
を利用する。
index tableには static table
とdynamic table
の2種類がある。
static table
static tableは、静的なindexとヘッダ名の対応表である。
HTTPのヘッダは特定の単語が頻出する傾向にある。
そのため、予め頻繁に頻繁に利用されるヘッダ名と、それに対応するindex定義しておき、indexでヘッダ名を指定することで効率的に圧縮をすることが可能である。
static tableは、以下の通りである。
+-------+-----------------------------+---------------+
| Index | Header Name | Header Value |
+-------+-----------------------------+---------------+
| 1 | :authority | |
| 2 | :method | GET |
| 3 | :method | POST |
| 4 | :path | / |
| 5 | :path | /index.html |
| 6 | :scheme | http |
| 7 | :scheme | https |
| 8 | :status | 200 |
| 9 | :status | 204 |
| 10 | :status | 206 |
| 11 | :status | 304 |
| 12 | :status | 400 |
| 13 | :status | 404 |
| 14 | :status | 500 |
| 15 | accept-charset | |
| 16 | accept-encoding | gzip, deflate |
| 17 | accept-language | |
| 18 | accept-ranges | |
| 19 | accept | |
| 20 | access-control-allow-origin | |
| 21 | age | |
| 22 | allow | |
| 23 | authorization | |
| 24 | cache-control | |
| 25 | content-disposition | |
| 26 | content-encoding | |
| 27 | content-language | |
| 28 | content-length | |
| 29 | content-location | |
| 30 | content-range | |
| 31 | content-type | |
| 32 | cookie | |
| 33 | date | |
| 34 | etag | |
| 35 | expect | |
| 36 | expires | |
| 37 | from | |
| 38 | host | |
| 39 | if-match | |
| 40 | if-modified-since | |
| 41 | if-none-match | |
| 42 | if-range | |
| 43 | if-unmodified-since | |
| 44 | last-modified | |
| 45 | link | |
| 46 | location | |
| 47 | max-forwards | |
| 48 | proxy-authenticate | |
| 49 | proxy-authorization | |
| 50 | range | |
| 51 | referer | |
| 52 | refresh | |
| 53 | retry-after | |
| 54 | server | |
| 55 | set-cookie | |
| 56 | strict-transport-security | |
| 57 | transfer-encoding | |
| 58 | user-agent | |
| 59 | vary | |
| 60 | via | |
| 61 | www-authenticate | |
+-------+-----------------------------+---------------+
https://tools.ietf.org/html/rfc7541
dynamic table
dynamic tableは、static tableのindex61に続く、62以降のindexとヘッダ名の対応表である。
static tableはindexとヘッダ名の組み合わせが固定であったが、dynamic tableの場合は、一度使用したヘッダ名を動的に追加できる。 dynamic tableにヘッダを追加するかどうかは選択可能。
また、エンコードとデコードでそれぞれ独立したテーブルを管理する。
dynamic tableにおける最大サイズはSETTINGS_HEADER_TABLE_SIZEによって決定される。
4.2. Maximum Table Size
Protocols that use HPACK determine the maximum size that the encoder
is permitted to use for the dynamic table. In HTTP/2, this value is
determined by the SETTINGS_HEADER_TABLE_SIZE setting (see
Section 6.5.2 of [HTTP2]).
https://tools.ietf.org/html/rfc7541
ハフマン符号化
ハフマン符号は、データ量の削減を目的として出現頻度が高い文字列に短いビット列、出現頻度が低い文字列に長いビット列を割り当てる、エントロピー符号の一つ。 文字列をはじめとするデータの可逆圧縮に利用される。身近な例ではJPEGやZIPなどがある。
HPACKのハフマン符号はあらかじめ、リクエスト、レスポンスの膨大なログからそれぞれの文字列の出現頻度を解析して生成された。
https://tools.ietf.org/html/rfc7541#appendix-B
ちなみにHPACKにおいてハフマン符号化は必須ではない。
サーバプッシュ
https://tools.ietf.org/html/rfc7540#section-8.2
サーバプッシュは、その名の通りサーバからクライアントにデータをプッシュする仕組みである。
最も簡単にイメージできる例として、クライアントからindex.htmlがリクエストされたら、そこに含まれるcss, jsなどもリクエストが来ると予想し、リクエストが来る前に渡してしまう、などが挙げられる。
まだ広く利用されているわけではなく、徐々に普及しているといった感じ。
Changes with nginx 1.13.9 20 Feb 2018
*) Feature: HTTP/2 server push support; the "http2_push" and "http2_push_preload" directives.
https://nginx.org/en/CHANGES-1.14
フロー制御
https://tools.ietf.org/html/rfc7540#section-5.2
フロー制御はストリーム
とコネクション
の2段階で行われる。
これは、HTTP/2が、TCPコネクションの上でストリームが多重化されているからである。
ストリームレベルのフロー制御
ストリームを多重化することで、TCP接続を使用する上で競合が発生する可能性がある。
具体的には、巨大なファイルの通信が帯域を占有してしまったりするなど、特定のリクエストの処理にサーバのリソースが占有されてしまうことで、ストリームがブロックされてしまう可能性がある。
これを防ぐため、フロー制御が行われる。
フロー制御は、WINDOW_UPDATEフレームを利用する。 動作としては、以下の通り。
- ウィンドウサイズを超えないサイズのパケットであれば一気に送信可能。
- ウィンドウサイズを超えたサイズのパケットを相手に送る場合はウィンドウサイズ分送信した時点で処理を一旦停止する。
- WINDOW_UPDATEによって相手のウィンドウサイズが更新されたら、再び送信を再開する。
また、初期値としてSETTINGSフレームにおけるSETTINGS_INITIAL_WINDOW_SIZEで、送信者の初期ウィンドウサイズ(初期値は 2^16-1 (65,535)オクテット )を設定することが可能。
SETTINGS_INITIAL_WINDOW_SIZE (0x4): Indicates the sender's initial window size (in octets) for stream-level flow control. The initial value is 2^16-1 (65,535) octets.
コネクションレベルのフロー制御
コネクションレベルのフロー制御においてのウィンドウサイズは、WINDOW_UPDATEフレームを利用してのみ変更できる。
The connection flow-control window can only be changed using WINDOW_UPDATE frames.
プライオリティ(優先度)制御
https://tools.ietf.org/html/rfc7540#section-5.3
プライオリティとは、クライアントがサーバに対して伝えるもので、ストリームに対して優先度をつけることによって先に欲しいリソースを優先的に送信してもらう機能である。
プライオリティは、実際にはフロー制御においてリソースを消費する割合のイメージに近い。
だからプライオリティの高いストリームが低いストリームを完全にブロックしたりするようなためのものではない。
実は、ストリームには依存関係を与えることが可能。
それによって、依存しているストリームよりも親ストリームを優先的に割り当てたりするなどの表現できる。
重みは8bitで指定でき、デフォルトは16が与えられる。
HTTP/2はどれくらい普及しているのか
こちらのサイトによると、2020/09/14でのHTTP/2普及率は、48.1%とそこまで高くないみたい。
参考資料
- 仕様書
HTTP/1.0: https://tools.ietf.org/html/rfc1945
HTTP/1.1: https://tools.ietf.org/html/rfc2068
HTTP/2: https://tools.ietf.org/html/rfc7540
HPACK: https://tools.ietf.org/html/rfc7541
- Reducing HTTP latency with SPDY
https://lwn.net/Articles/362473/
- Head of Line Blocking
https://en.wikipedia.org/wiki/Head-of-line_blocking
- CSS Sprites
https://css-tricks.com/css-sprites/#
- Domain sharding
https://developer.mozilla.org/ja/docs/Glossary/Domain_sharding
- Qiita
https://qiita.com/hirooka0527/items/13767855358f83db5e02 https://qiita.com/Jxck_/items/622162ad8bcb69fa043d https://qiita.com/Jxck_/items/16a5a9e9983e9ea1129f