HTTP/2が問題克服のために取り入れた仕様
それでは、HTTP/1.1で起こっていた数々の問題を克服するために、HTTP/2で策定された主な仕様を紹介しましょう。先に述べたとおり、HTTP/2のほとんどの仕様はSPDYですでに取り入れられていたものに、さらに改良を加えたものとなっています。
1コネクション内での多重化
HTTP/2では、リクエストからレスポンスまでの一連のやりとりをストリームと呼び、1つのコネクション上でいくらでも並列に扱うことができます。これによってHTTPパイプラインのHoLブロッキングの問題を解決します。
ストリームはクライアント、サーバのどちらからでも開始することができます。クライアント側から開始したストリームには奇数のID、サーバ側から開始したストリームには偶数のIDをそれぞれ割り当てて、競合を防ぎます。
各ストリームにおけるメッセージのやり取りはフレームという単位で行います。フレームはバイナリで定義され、各フレームは9オクテットのヘッダと、可変長のペイロードで構成されます。
ヘッダに定義されるストリームIDによって、各ストリームに処理が割り当てられます。フレームには次のような種類があります。
| Type | フレームの種類 | 役割 |
|---|---|---|
| 0 | DATA | リクエスト/レスポンスのボディ部分に相当 |
| 1 | HEADERS | リクエスト/レスポンスのヘッダ部分に相当 |
| 2 | PRIORITY | ストリームの優先順位を指定(クライアントのみ送信可能) |
| 3 | RST_STREAM | エラーなどの理由でストリームを終了するために用いる |
| 4 | SETTINGS | 接続設定を変更する |
| 5 | PUSH_PROMISE | サーバプッシュを予告します(サーバのみ送信可能) |
| 6 | PING | 接続の生存状態を調べる |
| 7 | GOAWAY | エラーなどの理由で接続を終了するために用いる |
| 8 | WINDOW_UPDATE | ウインドウサイズを変更する |
| 9 | CONTINUATION | サイズの大きなHEADERS/PUSH_PROMISEフレームの断片 |
次の図は、最も簡単なリクエストのやり取りをフレームで表現しています。
ストリームの優先度
ストリームの多重化によりHoLブロッキングの問題は解決しましたが、今度は別の問題が発生します。あまり重要でないストリームが先にリソースを占有してしまい、重要なストリームが返却を待たされてしまう可能性があるのです。これを解決するために、HTTP/2では、クライアントがPRIORITYフレームを用いてストリーム間に優先順位を付けることができます。
優先順位付けの方法には、「重み付け」と「依存関係」の2つがあります[1]。例えば、上図の指定ではストリーム1は他のストリームよりも優先されて転送されます。その後、ストリーム2とストリーム3がリソースを1:3で分け合うようにして転送されます。これはあくまでもリソースに余裕がないときの動作で、後続のストリームをブロックするものではありません。
このようにクライアントが積極的にリソースを取得する順序を制御することで、「JavaScriptはbodyの末尾に」といった現在の常識は消えていくかもしれません。なお、PRIORITYフレームの送信が許されているのはクライアント側のみで、サーバー側でこの優先度を指定したり変更する方法はありません。
下に示した図は、Firefoxが実際に指定した優先度のツリーを表しています(執筆時点でのバージョンは38)。Firefoxは最初のリクエストより前にあらかじめid=3, 5, 7, 9, 11という5つのストリームを使って、重み付きのツリーを構築します。そして、実際にリクエストを開始すると、リソースの種類に応じて最適なノードに依存するようにストリームを割り当てていきます。例えば、図ではjquery.jsよりもstyle.cssのほうが優先して転送されます。
現在、優先度制御を完全を実装しているブラウザはFirefoxだけですが、他のブラウザの今後の実装によっては、戦略や重み値に大きく違いが出ることも考えられるので注目すべきでしょう。
[1] 優先順位付けの方法のうち、依存関係はSPDYの仕様にはありません。HTTP/2で新たに依存関係による方法が加えられることにより、クライアント(ブラウザ)はより緻密な制御ができるようになりました。
フロー制御
HTTP/2ではデータ転送量を制御するために、フロー制御の仕組みを用意しています。この仕組みは、受信者の許容量を超えるデータが送信され、受信者のバッファが溢れること防ぐために必要です。
次の図は単純化した例です。例えば、クライアントの初期ウインドウサイズが80KBであるとすると、サーバは94KBのファイルを一気に送信することはできません。この場合、サーバまず80KBで送信を一時的に止め、WINDOW_UPDATEフレームによってクライアントのウインドウサイズが回復したことを確認した後に残りの14KBを再送信します。
フロー制御の仕組みはTCPでも実装されていますが、なぜHTTP/2で再定義する必要があるのでしょうか。1つの理由は、多重化されたストリームに独立した制御を行いたいという要求に応えるためです。例えば、「動画のストリーミング中に[停止]ボタンが押されたら、そのストリームのみ転送量を絞る」といったことも可能です。HTTP/2では、このような一見低レイヤーの役割に思える機能も、アプリケーション層から柔軟に制御することが可能になっています。
ヘッダ圧縮
従来、HTTPのコンテンツボディをgzip圧縮することはありましたが、ヘッダ部分を圧縮することはありませんでした。しかし、ヘッダのサイズは想像以上に大きく、これによるネットワーク帯域の圧迫は無視できません。SPDYが発表された当時の調査結果によると、典型的なリクエストヘッダは700~800バイトに及ぶそうです。
こうしたことから、HTTP/2では「HPACK」と呼ばれる形式でヘッダを圧縮します。SPDYでは当初、ヘッダの圧縮にgzipを用いていましたが、後に「CRIME」と呼ばれる攻撃手法が発見されたため、これを置き換えるものとしてHPACKが考案されました。HPACKもHTTP/2と同時にRFCが公開されています。
HPACKは次のような特徴を持っています。
- バイナリ形式
- キーを小文字に統一
- 高頻度で用いられるヘッダのキーと値を組にした辞書を持つ(下図)
- 動的に辞書を更新し、2回目以降はインデックスを用いる
- キーや値の文字列を任意でハフマン符号化[2]によって圧縮する
[2] 頻出する文字を短いビット列で表すことでサイズを小さくする圧縮方式。HPACK仕様では、膨大な調査結果から得た最適なテーブルを定義しています(Appendix B. Huffman Code)。
サーバプッシュ
HTTP/2では、サーバ側からクライアントにデータをプッシュすることができます。典型的な例としては、HTMLに関連付けられているCSSや画像などのリソースを、リクエストを待たずに送りつけることができます。
サーバプッシュによるフレームのやり取りは、次のような流れになります。
- クライアントがストリーム1でindex.htmlを要求します。
- サーバはindex.htmlにstyle.cssが関連付けられていることを知っているので、style.cssをストリーム2でプッシュすることをクライアントに知らせます(PUSH_PROMISE)。
- ストリーム1でindex.htmlを受け取ったクライアントは、すでにstyle.cssがプッシュされることを知っているため、待ち状態に入ります。
- サーバ側から開始したストリーム2によって、クライアントはstyle.cssを受け取ります。
SPDYと比べると、リソース本体を送信するタイミングがレスポンスの後でも可能という点で、より柔軟になっています。
注意点としては、HTTP/2サーバを導入しても自動的にこのような挙動をするわけではありません。何をどのようにプッシュするかは、アプリケーション側で設定あるいはロジックを実装する必要があります。
サーバがプッシュしようとするコンテンツが、すでにクライアントによってキャッシュされていたらどうなるでしょうか。
例えば、index.htmlに関連するstyle.cssがクライアントによってキャッシュされているかどうかを、サーバは知ることができません。この場合、クライアントはプッシュ予告を受けたら、即座にこれをキャンセルするようにサーバに要求することができます(REST_STREAMフレームによる)。
しかし、タイミング次第では、サーバはキャンセル要求を受けた時点でプッシュを開始しているかもしれません。これでは無駄に帯域を消費してしまいます。また、キャンセル要求だけでなく、プッシュされるコンテンツの優先度の要求も遅れてしまいます。
以上を勘案すると、条件によってはプッシュによって思う効果が得られないことがあるでしょう。ただし、今までプッシュの効果を得るためにリソースのインライン化(画像をHTMLに埋め込むなど)をしていたのであれば、その代替にはなるだろうと言われています。そもそもインライン化をすると毎回帯域を消費するので、それよりはマシというわけです。
HTTP/2における接続の開始方法
HTTP/1.1と互換性を持たせるために、HTTP/2は、HTTP/1.1と同じURIスキーマ上(httpまたはhttps)で動作します。そのため、接続開始時にどのプロトコル(HTTP/1.1、HTTP/2、SPDYなど)を使うのか、サーバとクライアントの間で合意を取る必要があります。
この合意(ネゴシエーション)プロセスには次のような方法があります。
- TLS拡張(ALPN)によるネゴシエーション
- HTTP/1.1からのアップグレード
- 事前知識によるHTTP/2の開始
ここでは、最もよく用いられるTLS拡張(ALPN)を用いた方法について説明します。
TLS拡張(ALPN)によるネゴシエーション
“https”スキーマでHTTP/2接続を行う場合、「ALPN(Application-Layer Protocol Negotiation)」というTLS拡張を使います。これは、クライアントが接続可能なプロトコルの一覧をサーバに提示し、サーバがプロトコルを選択する方式です。HTTP/2サーバは、クライアントが提示した一覧の中にHTTP/2接続を示すトークン“h2”を見つけると、これを選択してクライアントにHTTP/2接続を開始する旨を伝えます。逆に、クライアントが提示する一覧に“h2”が含まれない場合、サーバはHTTP/1.1での接続を開始します。
コネクションプリフェイス
クライアントとサーバーからの最初のメッセージをコネクションプリフェイスと呼びます。クライアントからの最初のメッセージは、次に示す24オクテットの配列で始まっている必要があります。
0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
これは、文字列表現では、
PRI * HTTP/2.0 SM
となり、HTTP/1.1サーバはこれをPRIというメソッドでのリクエストと解釈します。これにより、誤ってHTTP/1.1サーバに接続しようとした際に、サーバは「PRIメソッドはありません!」というメッセージと共に以降のデータの受信を拒否することができます。
このメッセージの後、クライアント/サーバそれぞれからSETTINGSフレームを送ることによって初期設定を完了し、以降リクエスト/レスポンスをやりとりできるようになります。
ここまで、HTTP/2の主な仕様を見てきました。次ページでは、HTTP/2を導入する方法を紹介します。
