HTTP-2 Server Push в Go 1.8

в 15:28, , рубрики: Go, http2, server push

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

Введение

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

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


HTTP-2 Server Push в Go 1.8 - 1

На уровне протокола, 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 были "запушены" сервером.

HTTP-2 Server Push в Go 1.8 - 2

Делайте push вначале ответа

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

<html>
<head>
    <link rel="stylesheet" href="a.css">...

и затем вызываете 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 в действии на этой странице.

Автор: divan0

Источник

Поделиться

* - обязательные к заполнению поля