The Go Blog
HTTP/2 サーバープッシュ
はじめに
HTTP/2 は、HTTP/1.x の多くの欠点を解決するために設計されました。現代のウェブページは、HTML、スタイルシート、スクリプト、画像など、多くのリソースを使用します。HTTP/1.x では、これらのリソースはそれぞれ明示的にリクエストされなければなりませんでした。これは遅いプロセスになる可能性があります。ブラウザはまず HTML を取得し、ページを解析して評価するにつれて、段階的に他のリソースを学習します。サーバーはブラウザが各リクエストを行うのを待たなければならないため、ネットワークはしばしばアイドル状態になり、十分に活用されません。
レイテンシを改善するために、HTTP/2 はサーバーが明示的にリクエストされる前にリソースをブラウザにプッシュできるようにする、サーバープッシュを導入しました。サーバーは、ページが必要とする追加のリソースの多くを事前に知っていることが多く、初期リクエストに応答する際にそれらのリソースをプッシュし始めることができます。これにより、サーバーはアイドル状態のネットワークを完全に活用し、ページ読み込み時間を短縮できます。
プロトコルレベルでは、HTTP/2 サーバープッシュは PUSH_PROMISE フレームによって駆動されます。PUSH_PROMISE は、サーバーがブラウザが近い将来に行うと予測するリクエストを記述します。ブラウザが PUSH_PROMISE を受信するとすぐに、サーバーがそのリソースを配信することがわかります。ブラウザが後でこのリソースが必要であると判断した場合、新しいリクエストを送信するのではなく、プッシュが完了するのを待ちます。これにより、ブラウザがネットワークを待つ時間が短縮されます。
net/http におけるサーバープッシュ
Go 1.8 では、http.Server からの応答をプッシュするサポートが導入されました。この機能は、実行中のサーバーが HTTP/2 サーバーであり、受信接続が HTTP/2 を使用している場合に利用できます。任意の HTTP ハンドラで、http.ResponseWriter が新しい http.Pusher インターフェースを実装しているかどうかを確認することで、サーバープッシュをサポートしているかどうかをアサートできます。
たとえば、サーバーが app.js がページのレンダリングに必要であることを知っている場合、http.Pusher が利用可能であれば、ハンドラはプッシュを開始できます。
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if pusher, ok := w.(http.Pusher); ok {
// Push is supported.
if err := pusher.Push("/app.js", nil); err != nil {
log.Printf("Failed to push: %v", err)
}
}
// ...
})
Push 呼び出しは、/app.js の合成リクエストを作成し、そのリクエストを PUSH_PROMISE フレームに合成し、その合成リクエストをサーバーのリクエストハンドラに転送します。これにより、プッシュされた応答が生成されます。Push の2番目の引数は、PUSH_PROMISE に含める追加のヘッダを指定します。たとえば、/app.js への応答が Accept-Encoding によって異なる場合、PUSH_PROMISE に Accept-Encoding の値を含める必要があります。
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if pusher, ok := w.(http.Pusher); ok {
// Push is supported.
options := &http.PushOptions{
Header: http.Header{
"Accept-Encoding": r.Header["Accept-Encoding"],
},
}
if err := pusher.Push("/app.js", options); err != nil {
log.Printf("Failed to push: %v", err)
}
}
// ...
})
完全に機能する例はこちらで入手できます。
サーバーを実行し、https://:8080 を読み込むと、ブラウザの開発者ツールで app.js と style.css がサーバーによってプッシュされたことが表示されるはずです。
応答する前にプッシュを開始する
応答のバイトを送信する前に Push メソッドを呼び出すことをお勧めします。そうしないと、誤って重複した応答を生成してしまう可能性があります。たとえば、HTML 応答の一部を書き込んだとします。
<html>
<head>
<link rel="stylesheet" href="a.css">...
次に、Push(“a.css”, nil) を呼び出します。ブラウザは PUSH_PROMISE を受信する前にこの HTML の断片を解析する可能性があり、その場合、ブラウザは PUSH_PROMISE を受信することに加えて、a.css のリクエストを送信します。これでサーバーは a.css に対して2つの応答を生成することになります。応答を書き込む前に Push を呼び出すことで、この可能性を完全に回避できます。
サーバープッシュを使用するタイミング
ネットワークリンクがアイドル状態のときはいつでもサーバープッシュの使用を検討してください。ウェブアプリの HTML の送信を終えたばかりですか?待機時間を無駄にせず、クライアントが必要とするリソースのプッシュを開始してください。レイテンシを減らすためにリソースを HTML ファイルにインライン化していますか?インライン化の代わりにプッシュを試してみてください。リダイレクトもプッシュを使用する良い機会です。なぜなら、クライアントがリダイレクトをたどる間、ほとんど常に無駄なラウンドトリップが発生するからです。プッシュを使用する可能性のあるシナリオはたくさんあります。まだ始まったばかりです。
いくつかの注意点に触れておかないと、説明不足になってしまいます。第一に、サーバーが権威を持つリソースのみをプッシュできます。つまり、サードパーティのサーバーや CDN でホストされているリソースをプッシュすることはできません。第二に、クライアントが本当に必要としていると確信できる場合を除き、リソースをプッシュしないでください。そうしないと、プッシュによって帯域幅を無駄にします。当然のことながら、クライアントがすでにキャッシュしている可能性が高いリソースのプッシュは避けるべきです。第三に、ページのすべてのリソースをプッシュするナイーブなアプローチは、パフォーマンスを悪化させることがよくあります。迷ったら、測定してください。
以下のリンクは、補足的な読み物として適しています。
- HTTP/2 プッシュ: 詳細
- HTTP/2 サーバープッシュによるイノベーション
- H2O におけるキャッシュ対応サーバープッシュ
- PRPL パターン
- HTTP/2 プッシュのための経験則
- HTTP/2 仕様におけるサーバープッシュ
まとめ
Go 1.8 では、標準ライブラリが HTTP/2 サーバープッシュをすぐにサポートし、ウェブアプリケーションを最適化するための柔軟性が向上します。
HTTP/2 サーバープッシュデモページで実際の動作をご覧ください。
次の記事: 開発者体験ワーキンググループの紹介
前の記事: Go 2016 調査結果
ブログインデックス