mozblog - もずぶろぐ -

自分のための備忘録や日記など。

HTTP/2 は何が変わったのか

Real World HTTP を読みました。

前回の文字コードの件と同じく、HTTP というプロトコルの歴史が概観されていて、すごく良かったです。

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

Real World HTTP ―歴史とコードに学ぶインターネットとウェブ技術

 

 Azure App Service も HTTP/2 に対応したことですし、HTTP/1.0 から HTTP/2 までの機能追加の遍歴をまとめます。

HTTP/1.0

1992 年に初期案が提案され、現在の HTTP というプロトコルのセマンティクスを形作った。

1. メソッド

1.0 以前は GET 以外のメソッドはなかったが、数多くのメソッドが提案された。
主に生き残ったメソッドは以下の 3 種類。

GET : サーバーに対してヘッダーとボディを要求
HEAD : サーバーに対してヘッダーを要求
POST : 新しいドキュメントを投稿

2. ステータスコード

3桁の数字をサーバーが応答することで、サーバーがどのような応答を返したか分かるようになった。

100 番台から 500 番台までのステータスコードが定義された。

3. リダイレクト

300 番台のステータスの一部では、Location ヘッダーを使って、リダイレクト先をサーバーからクライアントに伝えるものとされた。

受け取ったクライアントは、Location ヘッダーの指示通りに、接続しなおすことが期待される。

4. URL (Uniform Resource Locators)

HTTP/1.0 の規格化を見越して、先行して規格化されたもの。

スキーマ : // ユーザ : パスワード @ ホスト名 : ポート / パス # フラグメント ? クエリー

という形でリソースの場所を示す。

5. ボディ

1.0 以前は、リクエストにデータを含めることができなかったが、1.0 からリクエストにも含めることができるようになった。

ヘッダーとの間に空行を挟み、それ以降がボディとなる。

GET や HEAD でも、リクエストにボディを含めることは可能ではあるが、メソッドのセマンティクスと一致しないため、サーバーでは無視することが推奨される。

HTTP/1.1 

HTTP/1.1 は 1997 年に最初のバージョンが RFC 2068 にて策定された。
HTTP/1.0 から多くの改良が行われ、今日の HTTP 通信の基礎が作られた。
2015 年に HTTP/2 が登場するまで、20 年近く最新バージョンであり続けた。

1. Keep-Alive

これまでは HTTP の通信 1 回ごとに、TCP ソケットをオープン / クローズしていた。
しかし、HTTP/1.1 では TCP ソケットをオープンしたあと、クライアントもしくはサーバーが、ヘッダーに Connection: Close を含めるまで、オープンのままにする。

これにより、TCP ソケットの開閉に要するオーバーヘッドが削減できる。

2. パイプライニング

前項の Keep-Alive の利用を前提としており、ある HTTP リクエストに対するレスポンスが返る前に、次の HTTP リクエストを送信する。
サーバーは、HTTP リクエストが到達した順にレスポンスを返すことが求められる。

しかし、ある HTTP リクエストが重いと、後続のレスポンスに影響が出る (Head-of-Line Blocking) など、あまり利用されなかった。

3. TLS (Transport Layer Security) の規格化

TLS 自体は HTTP に限らず、ほかのプロトコルでも利用できるため、厳密には HTTP/1.1 とは異なる。

しかし、TLS は HTTP/1.1 と共同利用されることを強く想定して、SSL 3.0 をもとに規格化された。

4. PUT と DELETE メソッドの標準化

HTTP/1.0 ではオプション扱いであった PUT と DELETE メソッドが必須扱いとされ、現在の REST の元になる GET / POST / PUT / DELETE の 4 メソッドが出そろった。

PUT と DELETE は HTML の form からは送信できないので、JavaScript の Fetch APIXMLHttpRequest を利用する。

5. OPTIONS, TRACE, CONNECT メソッドの追加

OPTIONS は、サーバーが受け取り可能なメソッドの一覧を返す。

TRACE は、入力した HTTP リクエストの内容(ヘッダー・ボディ)をそのまま、サーバーがオウム返しに、ステータスコード 200 で返却する。
HTTP における Ping の役割を果たすメソッドとして追加されたが、TRACE メソッドが他人のパスワードを詐取する攻撃の温床になりやすいなど、セキュリティ上の懸念が有名になり、多くのサーバーで無効化された。

CONNECT は、HTTP で確立したトンネルに、他のプロトコルのパケットを流すことができるようになる。

6. プロトコルのアップグレード機能

HTTP から TLS を利用した通信や、WebSocket、HTTP/2 など、プロトコルをアップグレードできるようになった。
これはクライアントからもサーバーからも要請できる。

クライアントから行う場合、Upgrade ヘッダーにプロトコル名を入力し、Connection ヘッダーに Upgrade という文字列を入力する。
サーバーは、アップグレードが可能であれば、ステータスコード 101 (Switching Protocols) を返す。

サーバーから行う場合、同じく Upgrade と Connection ヘッダーをレスポンスで返し、ステータスコード 426 (Upgrade Required) を返す。
クライアントは 426 を受け取ると、上述の「クライアントから行う場合」と同様のフローで、プロトコルのアップグレードを行う。

7. バーチャルホスト機能

HTTP/1.0 までは、サーバー 1 台あたり 1 ドメインだった。
そのため、http://example.com/hello という URI の場合、サーバーは /hello という情報しか受け取ることができなかった。

HTTP/1.1 からは、Host というヘッダーを含め、そこに Web サイトのドメインを記述することが義務付けられた。
このヘッダーに応じて、サーバーはバーチャルホストにリクエストをルーティングすることができるようになり、サーバーで複数のドメインがホストできるようになった。

8. チャンクのサポート

サーバーが、データ全体を一括で送信するのではなく、分割して送信できるようになった。
JPEG のような画像ファイルであれば、クライアントが受け取った分だけ、インターレース表示ができるようになる。

サーバーは、チャンクで送る際は、Transfer-Encoding ヘッダーに chunked と入力する。

レスポンスは下記のようになり、データサイズを示す 16 進数のあとに、データを入れる。

HTTP/1.1 200 OK
Transfer-Encoding: chunked

186a0
(100KB のデータ)
186a0
(100KB のデータ)
・・・

186a0 は 10 進数で 100000 を表す。

9. クライアントによるボディ送信の確認

クライアントからサーバーにデータを送信する際、データサイズが受け入れ可能か、確認ができるようになった。

確認の際は、クライアントは Expect ヘッダーに 100-continue と入力し、Content-Length ヘッダーにサイズを入力して送信する。

サーバーは受け入れ可能であれば、ステータスコード 100 (Continue) を返す。
受け入れることができなければ、417 (Expectation Failed) を返す。

 

HTTP/2

HTTP/1.1 が、HTTP というプロトコルの拡張、および高速化が目的であったのに対し、HTTP/2 が策定された目的は、通信の高速化に特化している。

HTTP/2 は、メソッド、ヘッダー、ステータスコード、ボディで構成される、HTTP のスキームは維持しているものの、内部では HTTP/1.1 と比較して全く別のプロトコルになっている。

そのため、プロトコルのアップグレード機能を利用して、サーバーに HTTP/2 を利用するように伝える仕組みとなっている。

 

1. ストリーム

HTTP/1.1 までは、ひとつのリクエストが TCP ソケットを占有していたので、複数本の TCP 接続を作成し、並列にリクエストを送信していた。
そのため、2 本から 6 本程度の並列度が限界だった。

HTTP/2 では、TCP 接続の内部に、ストリームと呼ばれる仮想の TCP ソケットを作る。
ストリームは、通常の TCP 接続のようにハンドシェイクが不要で、より簡単にソケットの開閉ができるようになっている。
これにより、TCP 接続の通信容量が許す限り、ストリームをいくらでも並列化できるようになった。

2. バイナリベースのプロトコル

HTTP/1.1 までは、通信がテキストベースで行われていた。
そのため、例えばヘッダーの終端を探すには、空の行を見つけるまで、1 バイトずつ走査する必要があった。

HTTP/2 では、通信がバイナリベースとなり、ヘッダーやボディなどのデータが「フレーム」という単位で分割され、管理される。

各フレームは独立しているため、HTTP/1.1 では重いレスポンスが後続のレスポンスの返却を阻害していたが、HTTP/2 では他のレスポンスのフレームを先行して返却できるようになった。

3. フローコントロール

ストリームの通信効率を向上させるために、速い方が遅い方に大量にデータを送信するような事象を防止する。

データの送信元は、送信先のバッファサイズの上限として、初期値で 64 KB を仮定する。
送信先では、バッファサイズに余裕が出てくると、送信元に対し WINDOW_UPDATE というフレームを使い、余裕ができたサイズを通知する。

このようにして、お互いのバッファサイズを調整しながら、データを送受信する。

4. サーバープッシュ

サーバーから、優先度の高いコンテンツを、クライアントに対し先に送信してキャッシュさせることができるようになった。

また、クライアントが、対象のコンテンツに対する要求をサーバーに送信するまで、サーバーからクライアントに対し、事前にコンテンツが送られていることは検知できない。

WebSocket のような双方向の通信方式とは異なり、CSSJavaScript や画像など、サイトを構成するコンテンツに用いられる。
そのため、チャットのような用途に使うことはできない。

5. HPACK によるヘッダー圧縮

ヘッダーを HPACK と呼ばれる方式で圧縮することで、サイズを小さくしている。

HTTP のヘッダーは、出てくる語句がある程度決まっているため、効率良く圧縮することができる特性を利用している。