Перевод небольшого туториала об использовании HTTP/2 Server Push в стандартной библиотеке Go.
Введение
HTTP/2 был придуман, чтобы решить многие из проблем HTTP/1.x. Современные веб-страницы используют массу дополнительных ресурсов — HTML, скрипты и таблицы стилей, картинки и так далее. В HTTP/1.x каждый из этих ресурсов должен быть запрошен явно отдельным запросом и это может очень замедлять загрузку страницы. Браузер начинает с загрузки HTML, узнаёт про новые необходимые ресурсы по мере разбора страницы. В итоге сервер ожидает пока браузер запросит очередной ресурс и сеть просто простаивает и не используется эффективно.
Чтобы улучшить latency, в HTTP/2 появилась поддержка server push, которая позволяет серверу самому послать ресурсы браузеру ещё до того, как они будут запрошены явно. Часто сервер знает наперёд какие дополнительные ресурсы будут запрошены данной веб-страницей и может начать передавать их вместе с ответом на начальный запрос страницы. Это позволяет серверу максимально эффективно использовать сетевой канал, который бы простаивал в противном случае, и улучшить время загрузки страницы.
На уровне протокола, 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 были "запушены" сервером.
Делайте 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 в действии на этой странице.