Go ブログ

HTTP/2 サーバープッシュ

Jaana Burcu Dogan と Tom Bergan
2017年3月24日

はじめに

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://localhost:8080 をロードすると、ブラウザの開発者ツールに app.jsstyle.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 を呼び出すと、この可能性が完全に回避されます。

サーバープッシュを使用する場合

ネットワークリンクがアイドル状態のときはいつでもサーバープッシュの使用を検討してください。 Web アプリの HTML の送信が完了しましたか? 待つ時間を無駄にせず、クライアントが必要とするリソースのプッシュを開始します。レイテンシを 줄이기 위해 HTML ファイルにリソースをインライン化していますか? インライン化する代わりに、プッシュしてみてください。リダイレクトはプッシュを使用するのに適したもう1つの機会です。これは、クライアントがリダイレクトに従う間、ほとんどの場合、無駄なラウンドトリップがあるためです。プッシュを使用するための多くの可能なシナリオがあります。私たちはまだ 시작에 불과합니다.

いくつかの注意点について触れないわけにはいきません。 まず、サーバーが権限を持っているリソースのみをプッシュできます。つまり、サードパーティサーバーまたは CDN でホストされているリソースをプッシュすることはできません。 第二に、クライアントが実際に必要とすることに自信がない限り、リソースをプッシュしないでください。そうしないと、プッシュは帯域幅を無駄にします。 系としては、クライアントがすでにそれらのリソースをキャッシュしている可能性が高い場合は、リソースのプッシュを避けることです。 第三に、ページ上のすべてのリソースをプッシュするという素朴なアプローチは、パフォーマンスを悪化させることがよくあります。 疑わしい場合は、測定してください.

以下のリンクは、補足資料として最適です

結論

Go 1.8 では、標準ライブラリは HTTP/2 サーバープッシュのすぐに使えるサポートを提供し、Web アプリケーションを最適化する際の柔軟性を高めます。

HTTP/2 サーバープッシュのデモ ページにアクセスして、実際に動作していることを確認してください。

次の記事:開発者エクスペリエンスワーキンググループの紹介
前の記事:Go 2016 調査結果
ブログインデックス