做者:陆佳浩,就任于饿了么大前端部,目前负责开发和维护Sopush。css
导读:多路复用,是HTTP/2众多协议优化中最使人振奋的特性,它大大下降了网络延迟对性能的影响,而对于资源之间的依赖关系致使的“延迟”,Server Push则提供了手动优化方案。本文将对Server Push进行深度解读,并分享它在饿了么业务中的应用。html
做为HTTP协议的第二个主要版本,HTTP/2备受瞩目。HTTP/2使用了一系列协议层面的优化手段来减小延迟,提高页面在浏览器中的加载速度。其中,Server Push是一项十分重要而吸引人的特性。本文将依次介绍Server Push的背景、使用方法、基本原理和在饿了么的应用。前端
要了解Server Push是什么,以及它可以解决什么问题,须要对Server Push诞生的背景有一个基本的认知。HTTP协议一般是在TCP上实现的,昂贵的TCP链接推进咱们采起各类优化手段来复用链接。HTTP/2的多路复用从协议层解决了这个问题。node
HTTP/1不支持多路复用,浏览器一般会与服务器创建多个底层的TCP链接。TCP链接很昂贵,所以在优化性能的时候每每也是从减小请求数的角度考虑的。好比开启HTTP持久链接尽量地复用TCP链接、使用CSS Sprites技术、内联静态资源等。web
这样的优化手段能够极大提高页面的加载速度,可是也有一些反作用:CSS Sprites增长了必定的复杂度,也让图片变得不那么容易维护;内联静态资源更是把静态资源的缓存策略与页面的缓存策略绑在了一块儿,用以后的页面加载速度换取首次的加载速度。算法
能够说,这些优化方式多少都含有一些妥协。然而,即使使用了这些优化方式,也不能彻底抵消因缺少多路复用带来的低下的链接利用率。要治根,只能从协议自己入手。chrome
随着HTTPS的普及,链接变得更昂贵了。除了创建和断开TCP链接的消耗,还须要与服务器协商加密算法和交换密钥。HTTP/2带来了一系列协议上的优化,包括多路复用、头部压缩等等。最使人振奋的莫过于多路复用了。编程
HTTP/2定义了流(Stream)和帧(Frame)。基本协议单元变小了,从消息(Message)变成了帧;流做为一种虚拟的通道,用来传输帧。与建立TCP链接相比,建立流的成本几乎为零。基本协议单元的变小也大大提升了链接的利用效率。后端
能够说,HTTP/2的多路复用大大下降了因为网络延迟或者某个响应阻塞所带来的传输效率的损耗。若是说网络延迟对性能的影响能够经过多路复用减少,那么另外一种因为资源之间的依赖关系致使的“延迟”是难以自动优化的。为此,Server Push提供了一种手动优化的方案。浏览器
一般,只有在浏览器请求某个资源的时候,服务器才会向浏览器发送该资源。Server Push则容许服务器在收到浏览器的请求以前,主动向浏览器推送资源。好比说,网站首页引用了一个CSS文件。浏览器在请求首页时,服务器除了返回首页的HTML以外,能够将其引用的 CSS文件也一并推给客户端。
有些人对Server Push存在必定程度上的误解,认为这种技术可以让服务器向浏览器发送“通知”,甚至将其与WebSocket进行比较。事实并不是如此,Server Push只是省去了浏览器发送请求的过程。只有当“若是不推送这个资源,浏览器就会请求这个资源”的时候,浏览器才会使用推送过来的内容。若是浏览器自己就不会请求某个资源,那么推送这个资源只会白白消耗带宽。
资源内联是指将CSS和JavaScript内联到HTML中。这是一种面对昂贵的链接所达成的妥协,减小了请求数量,下降了延迟带来的影响,提高了页面的首次加载速度,却让这些本来能够缓存好久的资源文件遵循与HTML页面同样的缓存策略。
Server Push和资源内联是相似的。Server Push一样以减小请求数量和提高页面加载速度为目标。与资源内联的不一样之处在于,Server Push推送的资源是独立的、完整的响应,能够与HTML页面有着不一样的缓存策略,从而更有效地使用缓存。
要使用Server Push,有3种方案可供选择:
本身实现一个HTTP/2服务器;
使用支持Server Push的CDN;
使用支持Server Push的HTTP/2服务器。
第一种方案并不是是指从零开始实现一个HTTP/2服务器,仅仅是指从程序入手,直接对外暴露一个支持HTTP/2的服务器。大多数状况下,咱们会使用现成的HTTP/2库。好比node-http2,或者是Go 1.8的net/http。
第二和第三种方案经过设置响应头或者修改HTTP服务器的配置文件,告知HTTP服务器要推送的资源,让HTTP服务器完成资源的推送。
第一种方案更灵活,能够编程决定推送的资源和推送的时机;第二和第三种方案更简单,可是缺少必定的灵活性。
为了方便起见,我将使用Go标准库中的net/http来写一个Server Push的Demo。Go 1.8开始支持Server Push,所以请确保使用了Go 1.8或1.8 以上的版本。
鉴于Server Push是HTTP/2的“专利”,目前的浏览器又广泛只支持HTTP/2 over TLS(h2),所以咱们须要一张证书。建立自签名证书的方法有不少,这里就再也不赘述。若是你不知道怎么建立自签名证书,能够查阅相关资料,或者登陆http://www.selfsignedcertific...在线生成、下载。
假设证书的文件名为server.crt和server.key。
如下代码实现了一个简单的HTTPS服务器。将其保存为server.go,在终端运行go run server.go。
package main import ( "fmt" "log" "net/http" ) const indexHTML = ` <!doctype html> <link rel="stylesheet" type="text/css" href="style.css" /> <p>Hello Server Push</p> ` const styleCSS = ` p { color: red; } ` func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, indexHTML) }) http.HandleFunc("/style.css", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/css") fmt.Fprint(w, styleCSS) }) log.Fatal(http.ListenAndServeTLS(":4000", "server.crt", "server.key", nil)) }
运行后终端不会有任何提示。用浏览器打开 https://localhost:4000,会提示不是私密链接,见图1。这是正常的,由于自签名证书是不受操做系统和浏览器信任的。
图1 自签名证书不受操做系统和浏览器信任
展开“高级”,点击“继续前往localhost(不安全)”,或者在页面上输入“badidea”,便可看到红色的“Hello Server Push”字样,见图2。
图2 运行结果最终页
在Go语言里,使用Server Push 推送资源很简单。若是客户端支持Server Push,传入的 ResponseWriter会实现Pusher接口。在处理到达首页的请求时,若是发现客户端支持 Server Push,就把style.css也推回去。
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if pusher, hasPusher := w.(http.Pusher); hasPusher { pusher.Push("/style.css", nil) } fmt.Fprint(w, indexHTML) })
重启服务器以后刷新页面,观察开发者工具中的Network面板。若是style.css的Initiator列中含有“Push”字样,就说明推送成功了,见图3。
图3 在开发者工具的Network面板中查看推送成功状况
2016年4月底,CloudFlare宣布支持HTTP/2 Server Push。要启用Server Push,只须要在响应里加入一个特定格式的Link头:
Link: </style.css>; rel=preload; as=stylesheet
这源于W3C的Preload草案。草案还算比较很宽松,服务器能够为这些preload link资源发起Server Push,也能够提供一个可选的nopush参数给开发者使用,以显式声明不推送某个资源。
CloudFlare实现了Preload草案中的Server Push,也提供了可选的nopush参数。当CloudFlare读到源站服务器发来的Link头时,它会向浏览器推送那些资源,而后从Link头中移除那些资源。除此以外,CloudFlare会在响应里增长一个Cf-H2-Pushed头,其内容是推送的资源列表,以方便开发者调试。
一样是上面的例子,配置Nginx添加Link头。固然,你也能够用别的HTTP服务器,甚至直接用PHP之类的后端语言作这件事。
server { server_name server-push-test.codehut.me; root /path/to/your/website; add_header Link "</style.css>; rel=preload; as=stylesheet"; }
CloudFlare会自动为咱们签发一张证书。若是源站不支持HTTPS,能够在CloudFlare的 Crypto设置中将SSL选项修改成“Flexible”,来容许CloudFlare使用HTTP回源。
图4 使用Server Push先后对比
一样是h2协议,使用Server Push后加载时间有所减小,style.css的时间线变化尤其明显,请见图4。查看HTML的响应,其中确实包含有Cf-H2-Pushed头,而且告诉咱们CloudFlare 向浏览器推送了style.css。
图5 CloudFlare完成了向浏览器推送style.css
惋惜的是,目前国内尚未支持Server Push的CDN。若是不使用国外的CDN,就只能放弃CDN,用本身的服务器流量推送资源。
目前,支持Server Push的服务器软件并很少。很遗憾,Nginx并不支持。Apache的mod_http2模块支持Server Push,用法与CloudFlare差很少,一样是经过设置Link头来告诉服务器须要推送哪些资源。
Caddy是一个打着“Every Site on HTTPS”口号的HTTP/2服务器。Caddy使用Go语言编写,今年4月份也正式发行了支持Server Push的版本。与CloudFlare和Apache不一样,Caddy提供了push指令来配置要推送的资源。要实现上面的例子,配置文件只须要三行:
localhost:4000 tls self_signed push / /style.css
第一行是主机头和监听的端口号。第二行代表咱们但愿使用自签名证书,Caddy会在启动时自动在内存中为咱们生成。第三行使用push指令,告诉Caddy在浏览器请求首页的时候,用Server Push把/style.css一并推送给浏览器。
HTTP/2与HTTP/1最大的不一样之处在于,前者在后者的基础上定义了流和帧,实现了多路复用。这是Server Push的基础。
HTTP/2的流用于传输数据。客户端建立新的流来发送请求,服务端则在客户端请求的流上发送响应。一样地,Server Push也须要把请求和响应“绑定”到某个流上。
HTTP/2定义了10种帧。当服务器想用Server Push推送资源时,会先向客户端发送PUSH_PROMISE帧。规范规定推送的响应必须与客户端的某个请求相关联,所以服务器会在客户端请求的流上发送PUSH_PROMISE帧。PUSH_PROMISE帧的格式如图6。其中须要关注的是Promise流ID和Header块区域。
图6 PUSH_PROMISE帧的格式
PUSH_PROMISE帧中包含完整的请求头。然而,若是一个请求带有请求体,服务器就无法用 Server Push推送对这个请求的响应了。构造PUSH_PROMISE帧时,服务器会保留一个可用的流ID,用来在以后发送响应。服务器会经过PUSH_PROMISE帧告知客户端这个流ID,以便让客户端将这个流与推送的响应相关联。服务器发送完PUSH_PROMISE帧以后,就能够开始在以前保留的流上发送响应了。
图7 流的状态转移图
图7为流的状态转移图。其中的缩写分别为:
H——HEADERS帧
PP——PUSH_PROMISE帧
ES——END_STREAM标记
R——RST_STREAM帧
服务器必须先发送PUSH_PROMISE帧,再发送引用了推送资源的内容。好比说,使用Server Push推送页面上引用的CSS,必须先发送PUSH_PROMISE帧,再发送HTML。一旦浏览器收到并解析HTML(的一部分),发现了引用的资源,就会发起请求。若是没法确保浏览器先接收到PUSH_PROMISE帧,那么浏览器接收到PUSH_PROMISE帧和浏览器开始请求即将被推送的资源之间就出现了竞争。这种竞争会致使服务器有几率推送失败,甚至可能浪费带宽。
使用Chrome的Net-Internals能够更清晰地看到这一过程,帮助咱们理解Server Push的原理。在Server Push的行为与预期的不一致时,也能够用它来调试。
打开Net-Internals(chrome://net-internals/#http2),页面中会显示全部的HTTP/2会话。打开测试页面,选中相应的会话,就能在右侧面板能够看到收发的每一帧,以及相关联的流ID,见图8。
图8 Net-Internals中查看HTTP/2会话过程
浏览器在主动请求某个资源以前,会优先从缓存中取。若是命中了本地缓存,就能够再也不请求该资源了。Server Push则不一样,服务器很难根据客户端的缓存状况决定是否要推送某个资源。因此,大多数Server Push的实现不考虑客户端的缓存,每次收到客户端的请求,老是会发起推送。
规范中考虑到了这种状况。客户端在收到PUSH_PROMISE帧的时候,若是发现服务器要推送的资源命中了本地的缓存,能够在接收推送资源响应的流上发送一个RST_STREAM帧来重置该流,来告知服务器中止发送数据。然而,服务器开始推送响应和收到客户端发来的RST_STREAM帧之间也存在竞争关系。一般,服务器收到RST_STREAM帧的时候,已经发送了一部分响应了。
为了缓解这种“多推”的状况,一方面,客户端能够限制推送的数量、调整窗口大小,服务器也能够为流设置优先级和依赖,另外一方面,可使用“缓存感知Server Push”机制。
“缓存感知Server Push”机制的原理相似If-None-Match,只不过为了让客户端在发送页面请求的同时把资源文件的缓存状态也发给服务器,服务器会在推送资源文件时,将资源文件的缓存状态更新至客户端的Cookie中。图9演示了算法的大体流程。
图9 “缓存感知Server Push”算法的大体流程
固然,Cookie的空间十分宝贵,Server Push又容许存在有必定的“多推”和“漏推”。具体实现的时候,通常不会把全部的资源和hash(或者版本号)直接放进去。好比,H2O使用 Golomb-compressed sets算法生成指纹,编码为base64以后存入Cookie。
这种机制能够在必定程度上减小“多推”的状况,不过也存在一些问题:
须要使用Cookie,占用Cookie必定的空间;
不能自动遵循Cache-Control,须要自行实现缓存策略;
难以彻底避免“多推”的状况,还可能会出现“漏推”。
所以,使用Server Push推送资源依然存在一些问题。在选择要推送的资源时,应当考虑这些问题。最保守的作法是,只用Server Push推送原先内联的资源,即使Server Push存在“多推”的问题,也比内联资源来得好。固然,若是不太在乎流量,也可没必要太过担忧“多推”的问题,由于页面速度的瓶颈每每不在于带宽,而是延迟。
考虑到国内CDN对Server Push的支持和“多推”问题,目前咱们不使用Server Push推送静态资源,而是推送动态资源(API 响应)。与静态资源相比较,推送动态资源有如下区别:
更难被浏览器发现,浏览器只有在接收和解析完JavaScript文件,执行到相关语句的时候,才会发送请求;
不须要缓存,也就不存在“多推”问题。
Server Push只能推送不带请求体的GET和HEAD方法的请求,不过这也能够知足咱们的需求了。由于自动发起的API请求,大可能是GET方法的。咱们的目的是提高页面加载速度,只须要推送这类API便可。
在使用Server Push以前,咱们测试了一下使用Server Push推送API对页面加载速度的影响。咱们选取了PC站的餐厅列表页来测试。为了让结果更准确,咱们写了一个反向代理服务器,反向代理线上的页面和API。除此以外,咱们禁用了浏览器的缓存功能,来模拟用户首次访问的情形。
咱们分别比较了不使用Server Push和使用Server Push推送4个接口的状况。从Chrome开发者工具的Timeline面板中能够看到,使用Server Push后页面的总体加载时间变短了,其中减小最明显的是空闲时间。这与咱们的想法不谋而合,Server Push大大缩减了等待浏览器发起请求的时间。
图10 使用Server Push前、后,页面加载时间统计结果
测试的结果令咱们满意,但随即咱们意识到推送API比推送静态资源复杂得多。API是须要带参数的。这些参数可能源于请求的path、query string、Cookie甚至自定义的HTTP头。这意味着咱们很难使用现成的解决方案来推送API。
为此,咱们开发了一个带基本路由功能的HTTP/2服务器——Sopush。Sopush的目的不是取代Nginx或者Caddy之类的HTTP服务器,做为最外层,它的主要职责是反向代理和使用Server Push推送资源。它能够像Express、Koa那样定义路由规则,解析来自path和query string的参数,也能够自由地设置PUSH_PROMISE中的请求头以知足API的需求。
目前,饿了么已经有一些业务使用Server Push了,包括PC站。用Chrome打开PC站的餐厅列表页,便可在Network面板中看到“Push”字样。
做为HTTP/2的一个重要特性,Server Push有着明显的优点和不足。一方面,Server Push 可以提高在高延迟环境下页面的加载速度。这种延迟不只包括网络延迟,在复杂的SPA下也把首个XHR请求的发起时间做为考量之一。另外一方面,Server Push的支持依然不算使人满意,主要表如今目前国内各大CDN都不支持Server Push,大多数移动端的浏览器也不支持 Server Push。
就目前而言,国内使用Server Push的网站比较少。主要可能仍是因为CDN对Server Push的支持不足,使你们面临使用Server Push和使用CDN之间的抉择,对比优劣后天然是选择使用CDN了。咱们使用Server Push推送API多是现阶段能够绕开这种抉择、效果还不错的少数实践之一。
最后,衷心但愿这篇文章让你对Server Push有了进一步的了解。
本文首发于:「前端开发者说」公众号(ID:bigfrontend)。本公众号专一前端开发领域,播报热点新闻、共享同行高手钻研成果、展示企业最佳实践及研发历程,帮广大前端开发者走好技术成长中的每一步。