自从 2015 年 HTTP/2 标准正式发布以来,各大主要 Web 服务器以及各大主要浏览器已经陆续完成了对终版 HTTP/2 协议的支持。对于各位站长来讲,这项技术也已经不是什么新鲜玩意。然而 HTTP/2 的一项子功能“服务器推送”——尽管当初被吹得很火——却没有跟上 HTTP/2 推广的步伐。css
HTTP/2 服务器推送是一种提高首屏加载速度的技术,它容许 Web 服务器在收到浏览器的请求以前提早发送一些资源给客户端。好比说某个页面 index.html
使用了 a.css
和 b.js
两个子资源,Web 服务器在返回 index.html
的内容后表示“你可能还须要这两个文件”将 a.css
和 b.js
的内容一并发送给了客户端浏览器,因而浏览器就不须要另外去单独请求这两个文件。html
看起来一切都是这么美好,然而现实状况却没有这么简单。首要问题就是 Web 服务器怎样知道客户端须要什么,若是推送了没必要要的资源——好比某个资源已经被浏览器缓存——不只不能提高加载速度还会形成网络带宽的浪费。nginx
固然还有另一个更重要缘由:我大 Nginx 根本不支持 HTTP/2 服务器推送,你想体验一把都不行。。。git
这里得夸奖一下咱们的老大哥 Apache Httpd,在很早以前就支持了,但 Apache 并未能(彻底)解决过度推送的问题。在这里笔者给你们安利的是另一款 Web 服务器:H2Ogithub
H2O 是一款新的 Web 服务器,它在 HTTP/2 正式标准化的那年发布稳定版 1.0 版本,口号就是“可彻底利用 HTTP/2 的特性”,并且号称比 Nginx 还快。这里笔者并不打算比较二者的性能,但对 HTTP/2 的支持的确是 H2O 走在了前面。apache
注:Nginx 有个第三方模块用于实现 HTTP/2 服务器推送,实测还不能正常使用。数组
H2O 安装的话很简单。笔者是 macOS 用户能够 brew install h2o
直接安装,Linux 用户可使用官方的 rpm 镜像包安装。promise
要启用 HTTP/2 首先要启用 SSL,要启用 SSL 就首先得有个证书。笔者这里直接建立了一个指向本机(127.0.0.1)的域名而后用 Let's engypt
生成了有效证书;若是读者没有此条件也能够用 openssl 生成一个自签名证书而后将其加入系统钥匙串(网上资料不少,也有专门生成自签名证书的网站)。浏览器
H2O 的配置文件是含有少许扩展的 YAML 文件,简单配置以下:缓存
hosts: "test.eoitek.net:80": # 你的域名 listen: port: 80 # 监听 80 端口 paths: /: redirect: https://test.eoitek.net/ # 重定向至 HTTPS access-log: /dev/stdout # 测试时简单的把 log 输出至控制台 "test.eoitek.net:443": # 域名 listen: port: 443 # 监听 443 端口 ssl: certificate-file: /Users/Carter/.acme.sh/test.eoitek.net_ecc/fullchain.cer # 公钥(彻底证书链,用于自动 OCSP stapling) key-file: /Users/Carter/.acme.sh/test.eoitek.net_ecc/test.eoitek.net.key # 私钥 minimum-version: TLSv1.2 # 最小支持 TLS 版本(想支持 IE 8 须要设置为 TLSv1.0) dh-file: dhparam.pem # DH秘钥(openssl dhparam -out dhparam.pem 2048) cipher-preference: server # 让服务器决定使用的加密套件 cipher-suite: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256" # 支持的加密套件 paths: /: mruby.handler: | # 注入 mruby 脚本 lambda do |env| [399, {"link" => "</test.css>; rel=preload; as=style"}, []] end file.dir: . # 映射的服务器路径 access-log: /dev/stdout # 将 log 输出至控制台 http2-casper: ON # 启用 [cache-aware server-push](https://h2o.examp1e.net/configure/http2_directives.html#http2-casper) compress: ON # 启用即时压缩(同时会启用 brotli 压缩支持)
其中的 mruby 脚本是启用 HTTP/2 服务器推送的重点。注入的脚本是一个 lambda 表达式,env 是函数参数,包括了客户端请求信息。返回值是一个数组,格式为 [HTTP 状态码, { 返回头 }, [返回体]]
。特别的,当 HTTP 状态码为 399 时表示将本次请求交给其余处理程序处理(即把这段 mruby 脚本当作中间件使用)。因此这段脚本的意思即“将全部的请求添加返回头 link: </test.css>; rel=preload; as=style
”。
H2O 会识别 mruby 脚本输出的 link
返回头,当基本条件成立时就会将对应的文件(示例中为“/test.css”)推送给客户端。
启动 H2O(由于监听了 80 和 443 端口,因此须要 sudo 权限),能够看到 H2O 帮咱们自动搞定了 OCSP stapling
$ sudo h2o -c h2o.conf [INFO] raised RLIMIT_NOFILE to 10240 h2o server (pid:15880) is ready to serve requests fetch-ocsp-response (using OpenSSL 1.1.0g 2 Nov 2017) fetch-ocsp-response (using OpenSSL 1.1.0g 2 Nov 2017) sending OCSP request to http://ocsp.int-x3.letsencrypt.org sending OCSP request to http://ocsp.int-x3.letsencrypt.org /Users/Carter/.acme.sh/test.eoitek.net_ecc/fullchain.cer: good This Update: Nov 25 14:00:00 2017 GMT Next Update: Dec 2 14:00:00 2017 GMT verifying the response signature /Users/Carter/.acme.sh/test.eoitek.net_ecc/fullchain.cer: good This Update: Nov 25 14:00:00 2017 GMT Next Update: Dec 2 14:00:00 2017 GMT verifying the response signature verify OK (used: -VAfile /tmp/pszPkNSxUe/issuer.crt) [OCSP Stapling] successfully updated the response for certificate file:/Users/Carter/.acme.sh/test.eoitek.net_ecc/fullchain.cer verify OK (used: -VAfile /tmp/S5a00hOcQ6/issuer.crt) [OCSP Stapling] successfully updated the response for certificate file:/Users/Carter/.acme.sh/test.eoitek.net_ecc/fullchain.cer
在 Chrome 浏览器中测试,Network 中带 Push / XXX
字样的“伪”请求即为由服务器推送到客户端的文件。
若是看得不很清楚,可使用 nghttp
调试 HTTP/2
流量:
$ nghttp -nv https://test.eoitek.net # -n 表示丢弃返回体,-v 表示输出冗余调试信息 [ 0.008] Connected The negotiated protocol: h2 [ 0.010] send SETTINGS frame <length=12, flags=0x00, stream_id=0> (niv=2) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535] [ 0.010] send PRIORITY frame <length=5, flags=0x00, stream_id=3> (dep_stream_id=0, weight=201, exclusive=0) [ 0.010] send PRIORITY frame <length=5, flags=0x00, stream_id=5> (dep_stream_id=0, weight=101, exclusive=0) [ 0.010] send PRIORITY frame <length=5, flags=0x00, stream_id=7> (dep_stream_id=0, weight=1, exclusive=0) [ 0.010] send PRIORITY frame <length=5, flags=0x00, stream_id=9> (dep_stream_id=7, weight=1, exclusive=0) [ 0.010] send PRIORITY frame <length=5, flags=0x00, stream_id=11> (dep_stream_id=3, weight=1, exclusive=0) [ 0.010] send HEADERS frame <length=38, flags=0x25, stream_id=13> ; END_STREAM | END_HEADERS | PRIORITY (padlen=0, dep_stream_id=11, weight=16, exclusive=0) ; Open new stream :method: GET :path: / :scheme: https :authority: test.eoitek.net accept: */* accept-encoding: gzip, deflate user-agent: nghttp2/1.28.0 [ 0.010] recv SETTINGS frame <length=6, flags=0x00, stream_id=0> (niv=1) [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100] [ 0.010] recv WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0> (window_size_increment=16711681) [ 0.010] recv SETTINGS frame <length=0, flags=0x01, stream_id=0> ; ACK (niv=0) [ 0.010] recv (stream_id=13) :method: GET [ 0.010] recv (stream_id=13) :scheme: https [ 0.010] recv (stream_id=13) :authority: test.eoitek.net [ 0.010] recv (stream_id=13) :path: /test.css [ 0.010] recv (stream_id=13) accept: */* [ 0.010] recv (stream_id=13) accept-encoding: gzip, deflate [ 0.010] recv (stream_id=13) user-agent: nghttp2/1.28.0 [ 0.010] recv PUSH_PROMISE frame <length=44, flags=0x04, stream_id=13> ; END_HEADERS (padlen=0, promised_stream_id=2) [ 0.010] recv (stream_id=2) :status: 200 [ 0.010] recv (stream_id=2) server: h2o/2.3.0-DEV [ 0.010] recv (stream_id=2) link: </test.css>; rel=preload; as=style [ 0.010] recv (stream_id=2) date: Sun, 26 Nov 2017 13:47:14 GMT [ 0.010] recv (stream_id=2) date: Sun, 26 Nov 2017 13:47:14 GMT [ 0.010] recv (stream_id=2) content-type: text/css [ 0.010] recv (stream_id=2) last-modified: Sat, 25 Nov 2017 15:02:22 GMT [ 0.010] recv (stream_id=2) etag: "5a1985fe-8a88" [ 0.010] recv (stream_id=2) accept-ranges: none [ 0.010] recv (stream_id=2) x-content-type-options: nosniff [ 0.010] recv (stream_id=2) content-encoding: gzip [ 0.010] recv (stream_id=2) vary: accept-encoding [ 0.010] recv (stream_id=2) x-http2-push: pushed [ 0.010] recv HEADERS frame <length=177, flags=0x04, stream_id=2> ; END_HEADERS (padlen=0) ; First push response header [ 0.010] recv (stream_id=13) :status: 200 [ 0.010] recv (stream_id=13) server: h2o/2.3.0-DEV [ 0.010] recv (stream_id=13) link: </test.css>; rel=preload; as=style [ 0.010] recv (stream_id=13) date: Sun, 26 Nov 2017 13:47:14 GMT [ 0.010] recv (stream_id=13) date: Sun, 26 Nov 2017 13:47:14 GMT [ 0.010] recv (stream_id=13) content-type: text/html [ 0.010] recv (stream_id=13) last-modified: Sat, 29 Jul 2017 13:49:11 GMT [ 0.010] recv (stream_id=13) etag: "597c9257-28a" [ 0.010] recv (stream_id=13) accept-ranges: none [ 0.010] recv (stream_id=13) x-content-type-options: nosniff [ 0.010] recv (stream_id=13) content-encoding: gzip [ 0.010] recv (stream_id=13) vary: accept-encoding [ 0.010] recv (stream_id=13) set-cookie: h2o_casper=AAAAAAADoA; Path=/; Expires=Tue, 01 Jan 2030 00:00:00 GMT; Secure [ 0.010] recv HEADERS frame <length=113, flags=0x04, stream_id=13> ; END_HEADERS (padlen=0) ; First response header [ 0.010] recv DATA frame <length=3837, flags=0x01, stream_id=2> ; END_STREAM [ 0.010] recv DATA frame <length=355, flags=0x01, stream_id=13> ; END_STREAM [ 0.011] send GOAWAY frame <length=8, flags=0x00, stream_id=0> (last_stream_id=2, error_code=NO_ERROR(0x00), opaque_data(0)=[])
上面输出中能够很清楚的看到:HTTP/2 是经过发送数据帧来达成双端通讯的。服务端给客户端推送了一个 PUSH_PROMISE
帧,表示服务器推送的文件,它同样带有返回头,只是没有请求头。
特别的,服务器给客户端发送了一个名为 h2o_casper 的 Cookie,这个 Cookie 就是用来标识客户端缓存的。H2O 经过这个 Cookie 识别已经给客户端推送过哪些文件(客户端缓存了哪些文件),从而最大限度避免浪费带宽。读者可能会发现再次刷新浏览器就算禁用缓存服务器也不会向客户端推送文件,就是这个 Cookie 在起做用。