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 的第二个参数指定了要包含在 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 生成两个响应。在写入响应之前调用 Push 可以完全避免这种情况。

何时使用服务器推送

只要您的网络连接处于空闲状态,就可以考虑使用服务器推送。刚刚发送完您的 Web 应用的 HTML?不要浪费时间等待,开始推送客户端所需的资源。您是否将资源内联到 HTML 文件中以减少延迟?与其内联,不如尝试推送。重定向是另一个使用推送的好时机,因为客户端跟随重定向时几乎总是会浪费一次往返。使用推送的可能性有很多——我们才刚刚开始。

如果我们不提一些注意事项,那将是失职的。首先,您只能推送您的服务器具有权威性的资源——这意味着您不能推送托管在第三方服务器或 CDN 上的资源。其次,除非您确信客户端确实需要这些资源,否则不要推送,否则您的推送会浪费带宽。一个推论是,当客户端很可能已经缓存了这些资源时,避免推送。第三,天真地推送页面上的所有资源往往会使性能变差。有疑问时,进行衡量。

以下链接是很好的补充阅读材料

结论

通过 Go 1.8,标准库提供了对 HTTP/2 服务器推送的开箱即用支持,为您优化 Web 应用提供了更大的灵活性。

前往我们的 HTTP/2 服务器推送演示页面,亲身体验一下。

下一篇文章:介绍开发者体验工作组
上一篇文章:Go 2016 年度调查结果
博客索引