[Перевод] HTTP/2 Server Push в Go 1.8

Перевод небольшого туториала об использовании HTTP/2 Server Push в стандартной библиотеке Go.


Введение

HTTP/2 был придуман, чтобы решить многие из проблем HTTP/1.x. Современные веб-страницы используют массу дополнительных ресурсов — HTML, скрипты и таблицы стилей, картинки и так далее. В HTTP/1.x каждый из этих ресурсов должен быть запрошен явно отдельным запросом и это может очень замедлять загрузку страницы. Браузер начинает с загрузки HTML, узнаёт про новые необходимые ресурсы по мере разбора страницы. В итоге сервер ожидает пока браузер запросит очередной ресурс и сеть просто простаивает и не используется эффективно.


Чтобы улучшить latency, в HTTP/2 появилась поддержка server push, которая позволяет серверу самому послать ресурсы браузеру ещё до того, как они будут запрошены явно. Часто сервер знает наперёд какие дополнительные ресурсы будут запрошены данной веб-страницей и может начать передавать их вместе с ответом на начальный запрос страницы. Это позволяет серверу максимально эффективно использовать сетевой канал, который бы простаивал в противном случае, и улучшить время загрузки страницы.


8067af9d87b0433b8f9e8556cdc67562.png

На уровне протокола, HTTP/2 server push работает с помощью специального типа фреймов — PUSH_PROMISE. Фрейм PUSH_PROMISE описывает запрос, который, по мнению сервера, будет вскоре запрошен браузером. При получении PUSH_PROMISE, браузер знает, что сервер скоро пришлёт этот ресурс. Есть чуть позже браузер затребует этот ресурс, то будет ожидать сервер закончить push, а не инициировать новый HTTP-запрос. Это уменьшает суммарное время, которое браузер тратит на сеть.


Server Push в пакете net/http

В Go 1.8 появилась поддержка server push для http.Server. Эта функция автоматически доступна, если сервер работке в режиме HTTP/2 и входящее соединение также открыто с помощью HTTP/2 протокола. Дальше дело техники — в любом HTTP обработчике вы проверяете, поддерживает ли переменная типа http.ResponseWriter server push простого приведения типа к интерфейсу http.Pusher…


Например, если сервер знает, что скрипт app.js будет нужен для отрисовки страницы, обработчик может принудительно отправить его с помощью server push, если http.Pusher доступен в данном соединении:


  http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if pusher, ok := w.(http.Pusher); ok {
            // Push поддерживается.
            if err := pusher.Push("/app.js", nil); err != nil {
                log.Printf("Failed to push: %v", err)
            }
        }
        // ...
    })

Метод Push создает новый запрос к /app.js, собирает его во фрейм PUSH_PROMISE и отправляет обработчик запроса сервера, который сгенерирует необходимый ответ клиенту. Второй аргумент метода содержит дополнительные заголовки, если они необходимы для этого фрейма. Например, если ответ для /app.js должен быть с иным Accept-Endoding, то PUSH_PROMISE должен содержать Accept-Endoding заголовок:


    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        if pusher, ok := w.(http.Pusher); ok {
            // Pushп оддерживается
            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)
            }
        }
        // ...
    })

Полный рабочий пример можно попробовать тут:


$ go get golang.org/x/blog/content/h2push/server

Если вы запустите этот сервер и зайдёте на http://localhost:8080, инспектор сетевых запросов в вашем браузере должен показать, что app.js и style.css были «запушены» сервером.


0a71510816fb45d281eefe1302660e9e.png
Делайте push вначале ответа

Есть смысл вызывать метод Push до того, как отправлять что-либо в основном ответе. В противном случае есть вариант нечаянно сгенерировать повторяющиеся ответы. Например, представьте, что в вашем обработчике вы пишете в ответ часть HTML кода:




    ...

и затем вызываете Push («a.css», nil). Но браузер, возможно, уже успел распарсить этот фрагмент HTML до того, как получил фрейм PUSH_PROMISE, и в этом случае отправит новый запрос для a.css в дополнение к фрейму. Сервер теперь должен обслужить запрос к a.css дважды. Вызов Push перед тем, как отдавать тело ответа клиенту избавляет от этой проблемы.


Когда использовать server push?

Общий ответ тут — тогда, когда сетевое соединение простаивает. Закончили отправлять HTML веб-приложению? Не тратьте время, начинайте отдавать ресурсы, которые заведомо понадобятся. Возможно вы инлайните ресурсы прямо в HTML, чтобы уменьшить latency? Вместо инлайнинга, попробуйте pushing. Также хороший пример — редиректы страниц, которые почти всегда являются лишним запросом от клиента. Есть масса различных сценариев, где server push может быть полезен — мы только начинаем их осваивать.


Было бы упущением не упомянуть подводные камни. Во-первых, с помощью server push вы можете только отдавать ресурсы, которыми владеет ваш сервер — то есть, ресурсы с других сайтов или CDN отдавать не получится. Второе — не отдавайте ресурсы, если вы не уверены, что клиенту они понадобятся, это будет лишняя трата трафика. Как следствие — избегать отдавать ресурсы, которые, скорее всего, уже получены клиентом и закодированы. И третье — наивный подход «запушить все ресурсы» обычно приводит к ухудшению производительности. Как обычно, в случае сомнений — делайте измерения.


Несколько полезных ссылок для более углублённого понимания:


• HTTP/2 Push: The Details
• Innovating with HTTP/2 Server Push
• Cache-Aware Server Push in H2O
• The PRPL Pattern
• Rules of Thumb for HTTP/2 Push
• Server Push in the HTTP/2 spec


Заключение

В Go 1.8 стандартная библиотека предоставляет функционал HTTP/2 Server Push из коробки, позволяя создавать более эффективные и оптимизированные веб-приложения.


Вы можете посмотреть HTTP/2 Server Push в действии на этой странице.

Комментарии (3)

  • 31 марта 2017 в 19:18

    0

    Еще один повод для изучения Go!)
  • 31 марта 2017 в 20:02

    +1

    во всех этих server push меня смущает только пару вопросов:
    1) клиенту может это все и не нужно (мобильный интернет)
    2) у клиента уже есть эти данные закешированные, все что ему хочется знать это 304 NOT MODIFIED

    не могу понять почему многие текущие разработки идут по принципу:
    1) у пользователя простаивает cpu, надо его загрузить хоть чем-то
    2) у пользователя простаивает канал в интернет, давайте мы по push закешируем весь сайт и пару поддоменов в нагрузку, может понадобится

    • 31 марта 2017 в 20:59

      0

      Мне кажется эти моменты в конце статьи озвучены.

© Habrahabr.ru