6. http2プロトコル

我々がここに至った背景や歴史、政治的な事柄はもう十分でしょう。プロコトルの詳しい仕様について話しましょう。

6.1. バイナリー

http2はバイナリープロトコルです。

この事実を受け入れるまで少し時間を取りましょう。インターネットのプロトコルに関与してきた人なら、衝動的にこの選択について反対し、telnetなどで人間がリクエストを入力できるテキスト/アスキーベースのプロトコルがいかに優れているかを説明しだすことでしょう。

http2はフレーミングを遥かに簡単にするためにバイナリーになりました。フレームの始まりと終わりを判断することは、HTTP 1.1だけでなくテキストベースのプロトコル全般において大変複雑なのです。オプショナルな空白や、同じことを違う方法で書けるという仕様をなくすことで、実装がより簡単になるのです。

また実際のプロトコル部分とフレーミングを分離することも簡単にします。HTTP 1.1ではこれらは一体となっていたのでした。

プロトコルは圧縮をサポートし、ほとんどの場合TLS上で使われるであろうと思われるため、テキストの価値は下がりました。どうせ通信路上では読めないのです。Wiresharkやそれに似たツールを使ってhttp2プロトコルレベルで何が起こっているかを調べるようになればいいのです。

このプロトコルをデバッグには、おそらくcurlのようなツールを使うか、Wiresharkのhttp2ディセクターでネットワークを解析することになるでしょう。

6.2. バイナリーフォーマット

http2はバイナリーフレームを送信します。数種類のフレームタイプがありますがそれらは共通して以下を含んでいます:

長さ、タイプ、フラグ、ストリーム識別子(ID)、フレームペイロード

10個のフレームがhttp2仕様書に定義されていて、その中でもHTTP 1.1の機能に対応づけるための基本的なフレームはDATAとHEADERSです。いくつかのフレームについては後で詳しく述べます。

6.3. 多重化されたストリーム

先のバイナリーフレームフォーマットのセクションで述べたストリームIDはhttp2で送受信されるフレームを”ストリーム”に関連付けます。ストリームは論理的な関連付けです。http2接続では、クライアントとサーバー間で独立した、双方向のフレームの列が送受信されます。

一つのhttp2接続は複数の並行して開かれた状態のストリームを含むことができ、両エンドポイントは複数のストリームからのフレームを、フレーム単位で互い違いに送信することができます。ストリームは一方的に使うこともできるし、クライアントかサーバーで共有され、どちらかによって閉じることができます。ストリーム内でのフレームの順番は意味をもっています。受信者はフレームを受信した順に処理します。

ストリームの多重化は多くのストリームのフレームが同一接続上でフレーム単位で混合されるということを意味します。2つあるいはそれ以上の独立したデータの列車が、一つの列車に結合され、受信側でまた分離されます。ここに2編成の列車があります:

列車 他の列車

2編成の列車が同じ接続上で多重化されました:

多重化された列車

6.4. 優先度と依存関係

各ストリームには優先度(”重み”としても知られています)があり、リソースの制約でサーバーがどのストリームを先に送るか決める時に、どのストリームが重要かをサーバーに知らせます。

クライアントはPRIORITYフレームを使ってサーバーにこのストリームが他のどのストリームに依存するのか指定することができます。これにより、クライアントは”子ストリーム”が”親ストリーム”の完了に依存するような優先度”木”を作ることができます。

優先度や依存関係は動的に変更することができるので、画像がたくさんあるページをユーザーがスクロールしたときにどの画像が最も重要であるかを伝えることや、タブを切り替えたときにフォーカスされる新しいストリームの集合の優先度を上げる、ということができます。

6.5. ヘッダー圧縮

HTTPはステートレスなプロトコルです。つまりサーバーが、以前のリクエストに含まれる情報やメタデータを保存することなく、次のリクエストを処理するためには、必要な情報を毎回送信する必要があります。 http2はこのパラダイムを変えてはいないので、同じことをする必要があります。

これによりHTTPは冗長になります。クライアントが同じサーバーに多くのリソース(webページの画像など)を要求した場合、ほとんど同じようなリクエストが大量に送信されることになります。ほとんど同じものが連続するような時は、圧縮の出番です。

私が先に言及したようにwebページ毎のオブジェクトの数が増加していますが、cookieやリクエストのサイズも同様に年々増加を続けています。Cookieは全てのリクエストに含める必要があり、リクエスト毎にほとんど違いはありません。

HTTP 1.1のリクエストのサイズはとても大きくなってきていて、初期TCPウインドウよりも大きくなる場合があり、サーバーからACKを受信するため完全なラウンドトリップを必要とすることから、リクエスト送信完了までの時間がとても長くなります。これは圧縮の必要性を示唆する理由の一つです。

6.5.1. 圧縮は注意を要する困難な問題

HTTPSとSPDYの圧縮はBREACHCRIME攻撃に対して脆弱でした。文字列をストリームに挿入し出力がどのように変化するかを観測することで、攻撃者は何が送信されているのか知ることができます。

プロトコルの動的なコンテンツに対する圧縮を、これらの攻撃に対して脆弱ではない方法で行うには、注意深く考える必要があります。これこそHTTPbisチームが行おうとするところのものです。

そこでHPACK、HTTP/2のためのヘッダー圧縮、が誕生しました。名前が示す通り、http2ヘッダーのために生み出されたヘッダー圧縮フォーマットであり、独立したインターネットドラフトで定義されています。この新しいフォーマットは、中間装置に対しヘッダーフィールド単位に圧縮しないように指定するビットや、フレームにパッディングを付け加えるオプションもあいまって、悪用しにくくなっているはずです。

Roberto Peon(HPACKを生み出した人々の中の一人)の言葉です:

”HPACKは、仕様に沿う実装が情報を漏洩するのが困難であり、エンコードとデコードが高速で必要なリソースも少なく、 受信側が圧縮コンテキストのサイズを制御でき、プロキシーが再インデックス(プロキシー内部のフロントエンドとバックエンド間の共有状態)でき、ハフマンエンコードされた文字列の比較が高速である、ように設計されています。”

6.6. リセット - 考えを改めましょう

HTTP 1.1の一つの欠点は、HTTPメッセージがContent-Length付きで送信された場合、簡単に停止させることができないということです。殆どの場合(常にではありません)TCP接続を切断して実現しますが、新しいTCP接続を再度確立するという代償を払う必要があります。

よりよい解決方法はメッセージを停止させ、新しいメッセージを開始することです。http2のRST_STREAMフレームを使うとこれが実現できます。これは帯域が無駄に使われてしまうことを防ぎ、接続が切断されてしまうことを回避することに役立ちます。

6.7. サーバープッシュ

これは”キャッシュプッシュ”とも呼ばれている機能です。背後にあるアイデアはこうです。クライアントがリソースXを要求したとき、サーバーはクライアントはほとんどの場合リソースZも必要であると知っている可能性があるから、それをクライアントが要求する前に送信してしまおう。こうすることでクライアントはZをキャッシュに入れておくことができ、必要なときに使うことができます。

サーバープッシュはクライアントが明示的にサーバーに許可を与える必要がある代物であり、許可した場合でも、プッシュされたストリームが必要ないと判断した場合RST_STREAMで即座に閉じることができます。

6.8. フロー制御

http2上のストリームはそれぞれ独立にフローウインドウを持っていて、それはピアがストリームへ送信できるデータ量を制限します。SSHがどのように動いているかご存知なら、それとよく似た様式や背景を持っています。

各ストリームにおいて両エンドポイントはピアに対してどれくらいデータを受信できるか伝えなければなりません。ピアはウインドウが拡張されるまで伝えられたデータ量までしか送信することができません。DATAフレームのみがフロー制御されています。