第4話: WebSocket の雑多なメモ 〜 REAL WORLD HTTP第7章より 〜

yagisuke.hatenadiary.com の第4話です。

WebSocket

WebSocketとは、サーバー/クライアント間で、オーバーヘッドの小さい双方向通信を実現する仕組みです。

WebSocketはフレーム探知で送受信しますが、相手が決まっているために送信先の情報などはもちません。 HTTPの基本要素のうち、ボディのみを送っているようなものです。 フレームはデータサイズなどを持っているだけで、オーバーヘッドも2から14バイトしかありません。 一度コネクションを確立した後は、サーバーとクライアントのどちらからも通信を行うことが可能です。

WebSocketは何がうれしいのか

Ajax, Commet, WebSocketの通信の比較を電話で表現してみたの図です。

f:id:yagi_suke:20180128175545p:plain

画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.34
(備考: 一回返事をもらうごとに通信が切れる気がするけど、)

f:id:yagi_suke:20180128175547p:plain f:id:yagi_suke:20180128175551p:plain

画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.35
画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.36

f:id:yagi_suke:20180128175555p:plain

画像: https://www.slideshare.net/You_Kinjoh/websocket-10621887 P.37

通信方式 特徴
Ajax 負荷が高い
サーバー側の情報取得は定期的
クライアント側の情報送信は別接続で行うことが多い
Commet 負荷が非常に高い
サーバー側の情報取得はリアルタイム
クライアント側の情報送信は別接続のAjax
WebSocket 負荷が小さい
サーバー側の情報取得はリアルタイム
クライアント側の情報送信も同じ接続内(双方向通信)

WebSocketはステートフル

WebSocketが他のHTTPベースのプロトコル異なるのは「ステートフルな通信である」という点です。
ステートフルとかステートレスについて: http://yohei-y.blogspot.jp/2007/10/blog-post.html

HTTPは高速性のためにKeep-Aliveなどの複雑な機構も備えるようになってきていますが、 基本的にリクエスト単位で接続が切れてもセマンティクス上は問題ありません。 ロードバランサーを使ってサーバーを複数台に分散しておき、リクエストのたびに別のサーバーが 応答していても問題ありません。

ちなみに、Server-Sent Eventsも送信済みIDを一元管理して保証できれば、 リクエストを捌くサーバーが入れ替わっても問題がないように設計されています。

JavaScriptのクライアントAPI

WebSocketは、HTTPの下のレイヤーであるTCPソケットに近い機能を提供するAPIです。 JavaScriptAPITCPソケットのAPIに近い形態になっています。 通信はサーバーが受信を受けている状態で、必ずクライアントから接続します。 (クライアント起点の通信)

  1. サーバーが特定のIPアドレス、ポート番号でサーバーを起動(Listen)
  2. クライアント(ブラウザ)がサーバーに通信開始の宣言をする(Connect)
  3. サーバーにクライアントから接続依頼がくるので、それを受け入れる(Accept)
  4. サーバーにはソケットクラスのインスタンスが渡される
  5. サーバーが受理すると、クライアントのソケットのインスタンスの送受信機能が有効になる

このListen/Connect/Acceptは筆者が参考のために追加したもので、 システムプログラミングの文脈でソケットについて説明するときに使われる関数名です。

JavaScriptのWebSocketのAPI名はこれと異なりますが、基本的な考え方に差はありません。 実際には一旦HTTPで接続してからアップグレードするため、内部の手順はやや複雑ですが、 外部から見たシーケンスは同じです。 基本的な接続と送信のコードを示します。 WebSocketクラスのコンストラクタで接続先のURLを指定して、send()メソッドでデータを送信します。

上記の手順でクライアントが接続に当たってすべきことは2つだけです。 このコンストラクタと、onopenイベントリスナの裏でこれだけのことが行われています。

const socket = new WebSocket('ws://game.example.com:12010/updates')

socket.onopen = () => { // データの送受信可能となる
  setInterval(() => {
    if (socket.bufferedAmount === 0) {
      socket.send(getUpdateData())
    }
  })
}

接続後のソケットに対して、クライアント側で行う操作は次の3つです。 - send([データ]): WebSocket接続を利用して、データをサーバーに送信する - onmessage: サーバーからメッセージを受信した時に呼ばれるイベントリスナ - close([コード [, 理由]]): WebSocket接続を切断

データとしては文字列、Blob, ArrayBufferなどを使うことができます。 受信には、onmessageメソッドを使います。 onmessageイベントの使い方はServer-Sent Eventsと同じです。

参考: https://developer.mozilla.org/ja/docs/Web/API/WebSocket

接続

まず通常のHTTPとして接続をスタートし、その中でプロトコルのアップグレードを行い、 WebSocketにアップグレードすることを要請します。

通常開始のリクエス
GET /chat HTTP/1.1             // 普通のHTTPリクエスト
Host: server.example.com
Origin: http://example.com
Upgrade: websocket             // この2つで、HTTPからWebSocketへの
Connection: Upgrade            // プロトコルのアップグレードを表現している
Sec-WebSocket-Key: A4QSEcsepWr4m2PLS2PJHA==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
  • Sec-WebSocket-Key クライアントとのコネクションの確立を立証する為に使われる。 ランダムに選ばれた16バイトの値をBASE64エンコードした文字列
  • Sec-WebSocket-Versions 接続のプロトコルバージョンを指示するためにクライアントからサーバへ送信され、 現在のWebSocketの最新バージョンは13なので13を固定する
  • Sec-WebSocket-Protocol WebSocketは単にソケット通信の機能だけを提供する。 その中でどのような形式を使うかはアプリケーションで決める必要がある。 コンテントネゴシエーションのように複数のプロトコルが選択できるように使われる。 このヘッダーはオプション
サーバーレスポンス
HTTP/1.1 101 Switching Protocols   // HTTPがUpgrade!!
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: 7eQChgCtQMnVILefJAO6dK5JwPc=
Sec-WebSocket-Protocol: chat
  • Sec-WebSocket-Accept Sec-WebSocket-Keyを決まったルールで変換した文字列。 これにより、クライアントはサーバーとの通信確立を検証できる。
  • Sec-WebSocket-Protocol クライアントからサブプロトコルのリストを受け取ったときに、その中野一つを選択して返す。 クライアントは送信したプロトコル以外を受け取ったら接続を拒否しなければならない

ランダムな文字列からSec-WebSocket-Acceptの値は特定の文字列を接合した後にSHA1ハッシュを計算し、 それをBASE64エンコードした内容になります。

この後は双方向通信を開始します。

Socket.IO

WebSocketは強力なAPIですが、多くの場合、それをさらに使いやすくするSocket.IOという ライブラリを経由して使う方法に人気がありました。

Socket.IOのメリットは次の3点 - WebSocketが使用できないときは、XMLHttpRequestによるロングポーリングでエミュレーションし、 サーバー側からの送信を実現する機能がある - WebSocketの再接続を自動で行う - クライアントだけではなく、サーバー側で使える実装もあり、クライアントが期待する手順で フォールバックのXMLHttpRequest通信をハンドシェイクしたりできる - ロビー機能

WebSocketが使われ始めた当初は後方互換性を広く維持できるということで、 WebSocketといえばSocket.IOでした。

しかし、現在ではWebSocketが使えないブラウザは殆どありません。 企業内のセキュリティ管理のためのウェブプロキシなど、WebSocketが使えなくなる要因はいくつか ありますが、後方互換性目的で他のライブラリを使う理由はかなり減ってきています。 再接続処理などメリットがゼロではありませんが、今後は直接使われるか、 使うにしてもXMLHttpRequestフォールバックをオフにされることが増えるでしょう。

引用元や参考情報

第3話: Server-Sent Eventsの雑多なメモ 〜 REAL WORLD HTTP第7章より 〜

yagisuke.hatenadiary.com の第3話です。

Server-Sent Events(SSE)

SSEはHTML5の機能のひとつです。 巨大なファイルコンテンツを「少しずつ送信」するHTTP/1.1のChunked形式の通信機能を応用し、 サーバーから任意のタイミングでクライアントにイベント通知できる機能です。

2014年にはGREEさんがすでにバックエンドに採用していました。
http://labs.gree.jp/blog/2014/08/11070/

サーバーから情報を送る方法には、Comet(4章参照)がありました。 クライアントから定期的にリクエストを送ることでサーバー側のイベントを検出する(ポーリング)、 あるいはリクエストを受け取った状態で返事を保留する(ロングポーリング)方法がよく使われていました。

ロングポーリングのフロー f:id:yagi_suke:20180126010540p:plain

画像: http://kimulla.hatenablog.com/entry/2016/01/17/リアルタイムなwebアプリを実現する方法%28ポーリン

Cometのロングポーリング + Chunked レスポンス を組み合わせて、 1度のリクエストに対して、サーバーから複数のイベント送信を実現したのがSSEです。

イベントストリームについて

SSEはChunked形式を使っていますが、HTTPの上に別のテキストプロトコルを載せています。 これはイベントストリームと呼ばれ、MIMEタイプはtext/event-streamです。

イベントストリームの例

id: 10
event: ping
data: { "time": 2017-09-25T01:27:00+000 }

id: 11
data: Message from pySpa
data: #eng channel
タグの種類 説明
id イベントを識別するID。再送処理で使用
event イベント名を設定
data イベントと共におくられるデータ(jsonがよく使われる)
retry 再接続の待ち時間のパラメータ(ミリ秒)

SSEのサンプルソース

// SSE APIは、EventSourceインターフェイスに含まれています。
// イベントを受け取るためにサーバへの接続を開始には、
// イベントを生成するスクリプトのURIを指定する、新たなEventSourceオブジェクトを作成します。
// インスタンスを生成したら、メッセージの受け取りを始めることができます。
const evtSource = new EventSource('ssedemo.php')

// このコードはeventフィールドを持たない、サーバからの通知を受信して、
// メッセージのテキストをドキュメントのHTMLにあるリストへ追加します。
evtSource.onmessage = function(event) {
  const newElement = document.createElement('li')

  newElement.innerHTML = `message: ${event.data}`
  eventList.appendChild(newElement)
}

// addEventListener() を使用して特定のイベントを待ち受けることもできます
// 前のコードと似ていますが、eventフィールドに"ping"が設定されたメッセージが
// サーバから送られたときに、自動的に呼び出されることが異なります。
// こちらはdataフィールドのJSONをパースして、情報を出力します。
evtSource.addEventListener('ping', function(event) {
  const newElement = document.createElement('li')
  const obj = JSON.parse(event.data)

  newElement.innerHTML = `ping at ${obj.time}`
  eventList.appendChild(newElement);
}, false);

ソース元: https://developer.mozilla.org/ja/docs/Server-sent_events/Using_server-sent_events

注意 - IEが対応していません。polyfillで対応可能。 https://developer.mozilla.org/ja/docs/Server-sent_events/Using_server-sent_events

第2話: Fetch API の雑多なメモ 〜 REAL WORLD HTTP第7章より 〜

yagisuke.hatenadiary.com の第2話です。

Fetch API

Fetch APIXMLHttpRequestと同様の、サーバーアクセスを行う関数です。特徴としては、 デフォルトで厳格な設定がされており、必要に応じて明示的に解除するという設計となっていることです。

JavaScriptから用いられ以下の特徴を持ちます。 - XMLHttpRequestよりも、オリジンサーバー外へのアクセスなど、CORS(Cross-Origin Resource Sharing)の取り扱いが制御しやすい。 - JavaScriptのモダンな非同期処理の記述法であるPromiseに準拠している - キャッシュを制御できる - リダイレクトを制御できる - リファラーのポリシーを設定できる - Service Worker内から利用できる

注意 - IE対応してない。 http://caniuse.com/#search=fetch - Safariも対応していない? https://developer.mozilla.org/ja/docs/Web/API/Fetch_API

Fetch APIの基本

Fetch APIの使用例

fetch('news.json', { // 1
  method: 'GET', // 2
  mode: 'cors',
  credentials: 'include',
  cache: 'default',
  headers: {
    'Content-Type': 'application/json'
  }
}).then(response => { // 3
  return response.json()  // 4
}).then(json => {
  console.log(json)
})

基本要素は次の4つ

  1. XMLHttpRequestのようにオブジェクトを作成するのではなく、fetch()関数を呼び出す
  2. fetch()関数の第二引数はオプションのオブジェクト(省略可能)
  3. .then()関数に、サーバーレスポンスが帰ってきた後に呼び出されるコールバックを渡す(Promise)
  4. .then()に渡すコールバックがさらに時間がかかる処理を行い、それがPromiseを返すときは.then()を連結する

Fetch APIが対応するデータ

上記サンプルではjsonでした。以下のサンプルではstringで受け取ってます。
サンプル: https://codepen.io/osublake/pen/OMRdKq (string型でうけとってる)

メソッド 説明
arrayBuffer() ArrayBuffer 固定長のバイナリデータ。Typed Arrayを使って読み書き可能
blob() Blob ファイルコンテンツを表す MIMEタイプ + バイナリデータ。FileReaderを経由してArrayBufferに変換可能
formData() FormData HTMLフォームと互換の名前とあたいのペア
json() Object JSONを解釈してJavaScriptのオブジェクト、配列などで構成されるオブジェクト
text() string 文字列

Fetch APIで利用可能なメソッド

対応 メソッド一覧
CORS安全 GET, HEAD, POST
禁止メソッド CONNECT, TRACE, TRACK

参照: https://triple-underscore.github.io/Fetch-ja.html#methods

Fetch APIのCORSモード

Fetch APIではセキュリティ対策としてCORSをどのレベルまで許容するかを表明できます。 XMLHttpRequestの場合モードは変更できないようです。

設定値 Fetchデフォルト XHRデフォルト 説明
cors 別のオリジンサーバーへのアクセスを許容する
same-origin 別のオリジンサーバーへのアクセスをエラーにする
no-cors CORS接続は無視され空のレスポンスが返る

他にも、navigate、websocketがあります(HTML用の特別な値)。
参照: https://triple-underscore.github.io/Fetch-ja.html#concept-request-mode

Fetch APIのcredentials

クッキーの制限についてです。 XMLHttpRequestの場合、withCredentialプロパティにtrueを設定するとincludeできたのと同等に、 Fetch APIではcredentialsに設定します。

設定値 Fetchデフォルト XHRデフォルト 説明
omit クッキーを送信しない
same-origin 同一のオリジンの場合にのみクッキーを送信する
include クッキーを送信する

参照: https://triple-underscore.github.io/Fetch-ja.html#concept-request-credentials-mode

Fetch APIにしかできないこと

キャッシュの制御

キャッシュが細かく制御できることが特徴です。

設定値 デフォルト 説明
default 標準的なブラウザの動作
no-store キャッシュがないものとしてリクエストをする。結果もキャッシュしない
reload キャッシュがないものとしてリクエストをする。ETag等は送らない。キャッシュ可能であれば結果をキャッシュする
no-cache 期限内のキャッシュがあってもHTTPリクエストを送信する。ローカルのキャッシュのETag等も送り、サーバーが304を返したらキャッシュしたコンテンツを使う
force-cache 期限外のキャッシュでもあればそれを利用。なければHTTPリクエストする
only-if-cached 期限外のキャッシュでもあれば利用する。なければエラーにする

参照: https://triple-underscore.github.io/Fetch-ja.html#concept-request-cache-mode

リダイレクトの制御

設定値 デフォルト 説明
follow リダイレクトをたどる(最大20リダイレクト)
manual リダイレクトをたどらず、リダイレクトがあった旨をつたえる
error ネットワークエラーになる

注意 - Chrome47からはmanualになった模様。 https://developer.mozilla.org/ja/docs/Web/API/GlobalFetch/fetch - どうやらService Workersのみのお話しっぽい。

Service Worker対応

Fetch APIにできて、XMLHttpRequestにできないことは細々ありますが、 一番大きなものはService Workerへの対応です。 現在、Service Worker内から外部サービスへの接続にはFetch APIしかできないようになっています。

Service Workerに対応したWebサービスはオフライン動作が可能になったり、 通知を扱えるようになったりします。

引用元や参考情報

第1話: HTTP/2 の雑多なメモ 〜 REAL WORLD HTTP第7章より 〜

yagisuke.hatenadiary.com の第1話です。

HTTP/2って

ワーキンググループにおいて HTTP/2.0 で制定作業をしていたが、 後に HTTP/2 に名称を置き換えることになりました。
参考: https://ja.wikipedia.org/wiki/HTTP/2#.E5.90.8D.E7.A7.B0

マイナーアップデートなんてしてられないぜってことかな。

大型アップデートだがこれまで通り

HTTP/2はHTTP1.1から16年ぶりの大きなアップデートでした。 HTTP/2においても「メソッド」、「ヘッダー」、「ステータスコード」、「ボディ」という HTTPの提供する4つの基本要素は変わらない。
基本要素を忘れちゃった方はこちら: http://www.tohoho-web.com/ex/http.htm

目的は高速化

HTTP/2の目的は高速化に尽きます。
HTTP/1.0からHTTP/1.1にかけて、TCPソケットレベルで見ると次のような改善が行われてきました。

機能 効果
キャッシュ(max-age) 通信そのものをキャンセル
キャッシュ(ETag, Date) 変更がなければボディ送信をキャンセル
Keep-Alive アクセスごとに接続にかかる時間(1.5TTL)を削減
圧縮 レスポンスのボディサイズの削減
チャンク レスポンス送信開始を早める
パイプライニング 通信の多重化

1回の通信では、接続の確立リクエスト送信、受信と何往復もパケットが飛び交います。 また、データサイズを通信速度で割った時間だけ通信時間がかかります。 通信待ちがあれば、その分通信完了までの時間は長くなります。

これまで行われた高速化はこの通信のあらゆる箇所での高速化に寄与してきました。 HTTP/2ではこれまで手がつけられてこなかった「ヘッダー部の圧縮」や、 規格化されたもののいまひとつ活用されていなかった「パイプライニングの代替実装」が追加されました。
端的にまとめてくれている記事: http://blog.redbox.ne.jp/http2-cdn.html#HTTP11

ストリームによる通信の高速化

バイナリフレームの採用

HTTP/2の一番の大きな変化は、テキストベースのプロトコルから バイナリベースへのプロトコルに変化したという点です。

コンピューターは、バイナリしか理解できないので、 テキストで送られてきたらデータを解析する必要があり、 データを解析するオーバーヘッドが解消されたという訳です。

f:id:yagi_suke:20180121232213p:plain

画像: http://webcommu.net/http2/

各データは「フレーム」と呼ばれる単位で送受信が行われます。

TYPE FRAME TYPE ROLE
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フレームの断片

ストリームの多重化

これまでの問題
HTTPの問題

従来のHTTPでは、リクエストとレスポンスの組を1つずつしか同時に送受信できないことが、 パフォーマンス上のボトルネックになっています。

パイプライニングの欠点

このような制約のなか、HTTP/1.1では前回のリクエストの完了を待たなくてもいい「パイプライニング」 という仕組みがありました。パイプライニングを使うとクライアント側は前回のリクエストのレスポンスが 返ってくる前に余裕があれば、次のリクエストを送ることができます。

しかし、サーバー側はクライアントからのリクエストの順番通りに返さなくてはいけなく、 どこかで重いリクエストが来たらその処理以降のレスポンスがブロックされてしまいます。 これを「ヘッドオブライン・ブロッキング」問題と言います。

そして残念なことに、現在ほとんどのブラウザはパイプライニング機能を全く実装していないか、 デフォルトでオフにしています。 これには、実装の困難さから、 パイプライニングを正しく実装したサーバが少ないという事情があるようです。

f:id:yagi_suke:20180121232214p:plain

画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.20

更に、ブラウザから同時接続できるTCP接続数は最大6本であるため、 Webサイトに画像が100枚あっても、同時にリクエストとレスポンスできる画像は、たった6枚。 非常にリソースの無駄と考えたGOOGLE先生はHTTP/1.1を進化させるために頑張ることになります。

ストリームの多重化をすることで

リクエストとレスポンスの組を1つずつしか同時に送受信できないという問題に対して、 HTTP/2では1本のTCP接続の内部に、ストリームという仮想のTCPソケットを作って通信を行うことで対応しています。

ストリームはフレームに付随するフラグで簡単に作ったり閉じたりできるルールになっており、 通常のTCPソケットのようなハンドシェイクは必要ありません。 そのため、IDの数値とTCPの通信容量が許す限り、簡単に数万接続でも並列化できます。

HTTP/1.1
f:id:yagi_suke:20180121232221p:plain

画像: http://webcommu.net/http2/

HTTP/2
f:id:yagi_suke:20180121232252p:plain

画像: http://webcommu.net/http2/

要するに、あるストリーム上ではリクエストにあたるフレームが送信中だとしても、 別のストリームではレスポンスにあたるフレームを受信するといったことが可能になります。 これにより、全体的なパフォーマンスが向上し以下のようなリクエストとレスポンスが可能となりました。

f:id:yagi_suke:20180121232229p:plain

画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.20

ストリームの優先度

フロントエンドとしてはファイルの読み込む順番はきになるもの。

ストリームの多重化によりヘッドオブライン・ブロッキングの問題は解決しましたが、 あまり重要でないストリームが先にリソースを占有してしまい、 重要なストリームが返却を待たされてしまう可能性があります。

これを解決するために、HTTP/2ではクライアントがPRIORITYフレームを用いて、 ストリーム間に優先順位を付けることが可能となりました。 これはあくまでもリソースに余裕がないときの動作で、 後続のストリームをブロックするものではありません。

(ちなみに、PRIORITYフレームの送信が許されているのはクライアント側のみ)

参考: https://qiita.com/Jxck_/items/16a5a9e9983e9ea1129f

HTTP/2のアプリケーション層

ヘッダーとボディだけ

HTTP/2においても「メソッド」、「ヘッダー」、「ステータスコード」、「ボディ」という基本要素が 存在するのは変わりありませんが、メソッドとパス、ステータスコードプロトコルバージョンは すべて擬似ヘッダーフィールド化され、ヘッダーの中に組み込まれました。 上位のアプリケーションから見た機能性には変更ありませんが、 実装上は「ヘッダー」「ボディ」しかありません。

リクエストメッセージ
f:id:yagi_suke:20180121232205p:plain

画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.16

レスポンスメッセージ
f:id:yagi_suke:20180121232204p:plain

画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.17

高度な並列化もどんとこい

HTTP/1.1はテキストプロトコルでした。 ヘッダーの終端を探すには空行を見つけるまで1バイトずつ先読みして発見する必要がありました。 エラー処理などもあるため、サーバーとしてはパースまで込みで逐次処理をせざるを得ず、 高度な並列化は難しかったようです。

HTTP/2はバイナリ化され、最初にフレームサイズがはいっています。 TCPソケットのレイヤーでは、データをフレーム単位へと簡単に切り分けられるため、 受信側のTCPソケットのバッファを素早く空にでき、通信相手に対して、 次のデータを高速にリクエストできるようになっています。

フローコントロール

フローコントロールとは、ひとつのストリームがリソースを占有してしまうことで、 他のストリームがブロックしてしまうことを防ぐ仕組みです。

具体的な状況はいくつか考えられます。 - 大きなファイルの通信が帯域を食いつぶし、他の通信を妨害する。 - あるリクエストの処理にサーバが占有され、他のリクエストをサーバが処理できなくなる。 - 高速なアップロードを行うクライアントと、低速な書き込みをしているサーバとの間に挟まった プロキシが、調整のためにデータを貯めているバッファが溢れる。

こうした状況を防ぐために、HTTP/2ではTCPと、その上に多重化されたストリームの両者について、 フローコントロールする仕組みを備えています。

実際にはウィンドウサイズという閾値を設定し、この値の範囲内であればデータを送ることができ、 その値を使い切った場合、送信者はデータ送信を停止します。 受信者は、リソースが回復したことをWINDOW_UPDATEというフレームで通知し、 そこに設定された分の値だけウィンドウサイズを回復されることで、 送信者はデータの送信を再開するというものです。

詳細はこちらがオススメ: https://qiita.com/Jxck_/items/622162ad8bcb69fa043d

サーバープッシュ

HTTP/1.1では、ブラウザーがページを要求するとサーバーはHTMLを応答として送信しますが、 サーバーがHTMLに埋め込まれているJavaScript、画像、CSSのアセットを送信するには、 ブラウザーがHTMLを解析し、それらのリクエストを送信するまで待たなければなりません。

HTTP/2では、クライアントからのリクエスト内容をもとに、 サーバー側で必要ファイルを判別して事前にクライアントに送信できます。 例えば、リクエストしたHTMLにJavaScriptファイルの読み込み記載があった場合、 そのJavaScriptファイルを最初のHTMLリクエストの段階で送信しておくといった具合になります。

f:id:yagi_suke:20180121232242p:plain

画像: https://www.digicert.ne.jp/ssl/http2.html

HPACKによるヘッダーの圧縮

gzip圧縮の予定だった

HTTP1.xでは、CookieやUserAgentといったHTTPヘッダーがなんども同じ内容で送信されており、 オーバーヘッドが生じています。

そこでHTTP/2の前進であるSPDYでは当初、ヘッダーの圧縮にgzipを用いていましたが、 後に「CRIME」と呼ばれる攻撃手法が発見され、 圧縮率をさげざるをえなくなってしまいました。

CRIMEについて: https://www.scutum.jp/information/waf_tech_blog/2012/09/waf-blog-014.html

HPACKの誕生

そこでHTTP/2では、ヘッダーの差分だけを送るアルゴリズムを使ったHPACKという 圧縮機構を採用しました。

ただ肝心のCRIMEの脆弱性対策ですが、gzipよりも困難になりましたが、 完全に対策できているものにはなっていないそうです。 結局フレームにパディングを入れてサイズをごまかしたり、 機密度の高いヘッダー情報は圧縮用にインデックスしないなどHTTP/2時代でも CRIME攻撃を意識してHPACKを使わないといけない状況なんだそうです。

参照: http://d.hatena.ne.jp/jovi0608/20150527/1432723310

HPACKで使われる用語

Reference Set

前回送信したヘッダー名/値ペアのセット。クライアントやサーバーは、 ヘッダーの差分を計算するために接続毎にリファレンスセットを保持します。

Static Table

よく送信されるヘッダー名/値ペアにIDを設定したものを管理するテーブルです。 クライアントとサーバーの両方で保持されます。 送信するヘッダーがこのテーブルに含まれていた場合は、 設定されたIDを使用してヘッダーを送信することができます。

フロー

f:id:yagi_suke:20180121232233p:plain

画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.24

f:id:yagi_suke:20180121232237p:plain

画像: https://www.slideshare.net/techblogyahoo/http2-35029629 P.25

SPDYとQUIC

SPDY

SPDYはGOOGLE先生が開発していたHTTPの代替プロトコルでこれがそのままHTTP/2となりました。 2010年からCHROMEにSPDYを搭載し、2014年にHTTP/2にバトンタッチしました。

GOOGLE先生がSPDYを開発したのは、これまでのHTTPが改善してきた転送速度を一段と向上させるためです。 ウェブサイトの構成によって効果が大きく変わるため、導入の効果としては30%から、3倍以上まで、 色々な数値があげられましたが、並列アクセスでのブロッキングが減るため、 小さなファイルをたくさん転送するほど高速化します。

HTTP/1.x時代ではJavaScriptCSS、画像などなるべく少ないファイル数にまとめることで 高速化させるというのが常でした。

SPDYとHTTP/2時代では、ファイルをまとめる効果は小さくなります。 圧縮されているとはいえ、サイズは0ではないので、結合した方が通信量は減りますが、 細かい方が変更があったときもキャッシュが生き残る確率は上がります。 なので、結合してもしなくてもどちらも一長一短で差はあまりないかもしれません。

QUIC

SPDYなりQUICなり、早くして〜って感じ。

QUICが登場した背景

HTTP/2はTCP上で転送されるプロトコルであることに起因した問題が残されています。

このTCP接続を開始するためには、3wayハンドシェイクが行われています。 つまりこれは、接続を開始するたびにラウンドトリップ(ネットワークパケットの往復)が 追加されるということであり、新たな接続先に対し大幅な遅延が発生します。

それに加えて、暗号化された安全なHTTP接続を行うために、 TLSとのネゴシエーションも必要ということになると、 さらに多くのパケットをネットワーク間でやり取りしなければなりません。

一方、UDPはFire and Forget(撃ちっ放しの)プロトコルだと言えます。 メッセージがUDPで送信されると、後は宛先に到達するものだと想定されています。 なので、ネットワークでパケットを検証するために費やされる時間が少なくなります。 ただし欠点もあり、信頼性を確保するには、パケットのデリバリ確認ができるよう、 UDPの上に何かを構築しなければいけません。

そこで、登場するのがQUICプロトコルです。 QUICプロトコルは、接続を開始すると、全てのTLSHTTPS)パラメータを1つか2つのパケットで ネゴシエートすることができます。 QUICにおけるGoogleの狙いは、UDPTCPの良いところを取り、 最新のセキュリティー技術と組み合わせることということです。

f:id:yagi_suke:20180121232257p:plain

画像: http://jp.techcrunch.com/2015/04/20/20150418google-wants-to-speed-up-the-web-with-its-quic-protocol/

以下はHTTP/2 over TLS/TCPと、HTTP/2 over QUICの構成です。

f:id:yagi_suke:20180121232301p:plain

画像: http://internet.watch.impress.co.jp/docs/column/ietf2017/1060156.html

左側がHTTP/2 over TLS/TCP、右側がHTTP/2 over QUICです。 QUICは、TCPの機能に相当する輻輳(ふくそう)制御、ロスリカバリー、 およびHTTP/2の機能の一部であるストリーム制御、フローコントロールなどを提供します。 暗号方式のネゴシエーションや鍵交換にはTLS1.3の仕組みを使うそうです。

QUICの進捗はこちら

QUIC WGでは、2018年中にQUICベースプロトコルおよびHTTP/2へのマッピングRFCにするというマイルストーンで動いているので現在絶賛稼働中。 - git: https://github.com/quicwg/base-drafts - IETF: https://datatracker.ietf.org/wg/quic/about/

引用元や参考情報

Index: Real World HTTPの輪読会をした

Real World HTTPの輪読会をした

昨年の秋頃にReal World HTTPの輪読会をおこなった。 ぼくは2章と7章を担当。 この本はけっこうヘビーな内容だったけど、しっかり読んだらだんだん楽しくなっていった。 けど、また読もうとは思わない。笑 www.oreilly.co.jp

ということで、当時のメモ書きが出てきたので7章の 「HTTP/2のシンタックス: プロトコルの再定義」の部分を6回に分けて掲載します。文面はほとんど人のものを引用していて、自分の文章ではなく引用元のままなことが多めです。 (ご指摘あれば削除いたします)

HTTP/2のシンタックス: プロトコルの再定義

内容は以下のとおりです。

  • 1話. HTTP/2
  • 2話. Fetch API
  • 3話. Server-Sent Events
  • 4話. WebSocket
  • 5話. WebRTC
  • 6話. HTTPウェブプッシュ