1,HTTP2 的新特性。
关于 HTTP2 的新特性,读着可以参看我之前的文章,这里就不在多说了,本篇文章主要讲一下 server push 这个特性。
HTTP,HTTP2.0,SPDY,HTTPS 你应该知道的一些事
2,Server Push 是什么。
简单来讲就是当用户的浏览器和服务器在建立链接后,服务器主动将一些资源推送给浏览器并缓存起来,这样当浏览器接下来请求这些资源时就直接从缓存中读取,不会在从服务器上拉了,提升了速率。举一个例子就是:
假如一个页面有 3 个资源文件 index.html,index.css,index.js, 当浏览器请求 index.html 的时候,服务器不仅返回 index.html 的内容,同时将 index.css 和 index.js 的内容 push 给浏览器,当浏览器下次请求这 2 两个文件时就可以直接从缓存中读取了。
3,Server Push 原理是什么。
要想了解 server push 原理,首先要理解一些概念。我们知道 HTTP2 传输的格式并不像 HTTP1 使用文本来传输,而是启用了二进制帧 (Frames) 格式来传输,和 server push 相关的帧主要分成这几种类型:
- HEADERS frame(请求返回头帧): 这种帧主要携带的 http 请求头信息,和 HTTP1 的 header 类似。
- DATA frames(数据帧) : 这种帧存放真正的数据 content,用来传输。
- PUSH_PROMISE frame(推送帧): 这种帧是由 server 端发送给 client 的帧,用来表示 server push 的帧,这种帧是实现 server push 的主要帧类型。
- RST_STREAM(取消推送帧): 这种帧表示请求关闭帧,简单讲就是当 client 不想接受某些资源或者接受 timeout 时会向发送方发送此帧,和 PUSH_PROMISE frame 一起使用时表示拒绝或者关闭 server push。
Note:HTTP2.0 相关的帧其实包括 10 种帧,正是因为底层数据格式的改变,才为 HTTP2.0 带来许多的特性,帧的引入不仅有利于压缩数据,也有利于数据的安全性和可靠传输性。
了解了相关的帧类型,下面就是具体 server push 的实现过程了:
- 由多路复用我们可以知道 HTTP2 中对于同一个域名的请求会使用一条 tcp 链接而用不同的 stream ID 来区分各自的请求。
- 当 client 使用 stream 1 请求 index.html 时,server 正常处理 index.html 的请求,并可以得知 index.html 页面还将要会请求 index.css 和 index.js。
- server 使用 stream 1 发送 PUSH_PROMISE frame 给 client 告诉 client 我这边可以使用 stream 2 来推送 index.js 和 stream 3 来推送 index.css 资源。
- server 使用 stream 1 正常的发送 HEADERS frame 和 DATA frames 将 index.html 的内容返回给 client。
- client 接收到 PUSH_PROMISE frame 得知 stream 2 和 stream 3 来接收推送资源。
- server 拿到 index.css 和 index.js 便会发送 HEADERS frame 和 DATA frames 将资源发送给 client。
- client 拿到 push 的资源后会缓存起来当请求这个资源时会从直接从从缓存中读取。
下图表示了整个流程:
4,Server Push 怎么用。
既然 server push 这么神奇,那么我们如何使用呢?怎么设置服务器 push 哪些文件呢?
首先并不是所有的服务器都支持 server push,nginx 目前还不支持这个特性,可以在 nginx 的官方博客上得到证实 https://www.nginx.com/blog/http2-r7/,但是 Apache 和 nodejs 都已经支持了 server push 这一个特性,需要说明一点的是 server push 这个特性是基于浏览器和服务器的,所以浏览器并没有提供相应的 js api 来让用户直接操作和控制 push 的内容,所以只能是通过 header 信息和 server 的配置来实现具体的 push 内容,本文主要以 nodejs 来说明具体如何使用 server push 这一特性。
准备工作:下载 nodejs http2 支持,本地启动 nodejs 服务。
1. 首先我们使用 nodejs 搭建基本的 server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
var http2 = require('http2'); var url=require('url'); var fs=require('fs'); var mine=require('./mine').types; var path=require('path'); var server = http2.createServer({ key: fs.readFileSync('./zs/localhost.key'), cert: fs.readFileSync('./zs/localhost.crt') }, function(request, response) { var pathname = url.parse(request.url).pathname; var realPath = path.join("my", pathname); //这里设置自己的文件名称; var pushArray = []; var ext = path.extname(realPath); ext = ext ? ext.slice(1) : 'unknown'; var contentType = mine[ext] || "text/plain"; if (fs.existsSync(realPath)) { response.writeHead(200, { 'Content-Type': contentType }); response.write(fs.readFileSync(realPath,'binary')); } else { response.writeHead(404, { 'Content-Type': 'text/plain' }); response.write("This request URL " + pathname + " was not found on this server."); response.end(); } }); server.listen(443, function() { console.log('listen on 443'); }); |
这几行代码就是简单搭建一个 nodejs http2 服务,打开 chrome,我们可以看到所有请求都走了 http2,同时也可以验证多路复用的特性。
这里需要注意几点:
- 创建 http2 的 nodejs 服务必须时基于 https 的,因为现在主流的浏览器都要支持 SSL/TLS 的 http2,证书和私钥可以自己通过 OPENSSL 生成。
- node http2 的相关 api 和正常的 node httpserver 相同,可以直接使用。
2. 设置我们的 server push:
1 2 3 4 5 6 7 8 9 |
var pushItem = response.push('/css/bootstrap.min.css', { request: { accept: '*/\*' }, response: { 'content-type': 'text/css' } }); pushItem.end(fs.readFileSync('/css/bootstrap.min.css','binary')); |
我们设置了 bootstrap.min.css 来通过 server push 到我们的浏览器, 我们可以在浏览器中查看:
可以看到,启动 server push 的资源 timelime 非常快,大大加速了 css 的获取时间。
这里需要注意下面几点:
- 我们调用 response.push(), 就是相当于 server 发起了 PUSH_PROMISE frame 来告知浏览器 bootstrap.min.css 将会由 server push 来获取。
- response.push() 返回的对象时一个正常的 ServerResponse,end(),writeHeader() 等方法都可以正常调用。
- 这里一旦针对某个资源调用 response.push() 即发起 PUSH_PROMISE frame 后,要做好容错机制,因为浏览器在下次请求这个资源时会且只会等待这个 server push 回来的资源,这里要做好超时和容错即下面的代码:
-
12345678910111213141516try {pushItem.end(fs.readFileSync('my/css/bootstrap.min.css','binary'));} catch(e) {response.writeHead(404, {'Content-Type': 'text/plain'});response.end('request error');}pushItem.stream.on('error', function(err){response.end(err.message);});pushItem.stream.on('finish', function(err){console.log('finish');});
上面的代码你可能会发现许多和正常 nodejs 的 httpserver 不一样的东西,那就是 stream,其实整个 http2 都是以 stream 为单位,这里的 stream 其实可以理解成一个请求,更多的 api 可以参考:node-http2。
- 最后给大家推荐一个老外写的专门服务 http2 的 node server 有兴趣的可以尝试一下。https://gitlab.com/sebdeckers/http2server
5,Server Push 相关问题。
- 我们知道现在我们 web 的资源一般都是放在 CDN 上的,那么 CDN 的优势和 server push 的优势有何区别呢,到底是哪个比较快呢?这个问题笔者也一直在研究,本文的相关 demo 都只能算做一个演示,具体的线上实践还在进行中。
- 由于 HTTP2 的一些新特性例如多路复用,server push 等等都是基于同一个域名的,所以这可能会对我们之前对于 HTTP1 的一些优化措施例如 (资源拆分域名,合并等等) 不一定适用。
- server push 不仅可以用作拉取静态资源,我们的 cgi 请求即 ajax 请求同样可以使用 server push 来发送数据。
- 最完美的结果是 CDN 域名支持 HTTP2,web server 域名也同时支持 HTTP2。
参考资料:
Jacks gong 2017 年 2 月 14 日
感谢你的分享,我想了解: server 可以在收到一个 stream 1 的情况下,发起多个 PUSH_PROMISE frame,以及分批发资源吗。两种情况,看看成立不: 情况一:1. server 使用 stream 1 正常的发送 HEADERS frame 和 DATA frames 将 index.html 的内容返回给 client。2. client 接收到 PUSH_PROMISE frame 得知 stream 2 和 stream 3 来接收推送资源 3. server 拿到 index.css 资源,通过 stream2 发送 HEADERS frame 和 DATA frames 将 index.css 资源发送给 client4. client 拿到 push 的资源后会缓存起来当请求这个资源时会从直接从从缓存中读取。5. server 拿到 index.js 资源,通过 stream3 发送 HEADERS frame 和 DATA frames 将 index.css 资源发送给 client6. client 拿到 push 的资源后会缓存起来当请求这个资源时会从直接从从缓存中读取。情况二:1. server 使用 stream 1 正常的发送 HEADERS frame 和 DATA frames 将 index.html 的内容返回给 client。2. client 接收到 PUSH_PROMISE frame 得知 stream 2 来接收推送资源 3. server 拿到 index.css 资源,通过 stream2 发送 HEADERS frame 和 DATA frames 将 index.css 资源发送给 client4. client 拿到 push 的资源后会缓存起来当请求这个资源时会从直接从从缓存中读取。5. client 接收到 PUSH_PROMISE frame 得知 stream 3 来接收推送资源 6. server 拿到 index.js 资源,通过 stream3 发送 HEADERS frame 和 DATA frames 将 index.css 资源发送给 client*7. client 拿到 push 的资源后会缓存起来当请求这个资源时会从直接从从缓存中读取。
Jacks gong 2017 年 2 月 14 日
server 可否通过非 stream1(request 的 stream) 发送 PUSH_PROMISE。
Jacks gong 2017 年 2 月 14 日
啃了看了官方文档了,不行。只能通过请求的 stream 发 PUSH_PROMISE。
◕‿◕ME 2017 年 1 月 10 日
push 的资源和浏览器请求 index.html 会同时到达客户端嘛?
吕鸣 2017 年 1 月 10 日
不是同时的 1server 使用 stream 1 正常的发送 HEADERS frame 和 DATA frames 将 index.html 的内容返回给 client。2client 接收到 PUSH_PROMISE frame 得知 stream 2 和 stream 3 来接收推送资源。3server 拿到 index.css 和 index.js 便会发送 HEADERS frame 和 DATA frames 将资源发送给 client。4client 拿到 push 的资源后会缓存起来当请求这个资源时会从直接从从缓存中读取。