前阵子,在Media看到一篇文章《Node.js can HTTP/2 push!》。看到push这个字眼时,我想到的是WebSocket消息推送。难不成HTTP/2还能像WebSocket那样能够服务端主动推送消息?好厉害,我就一会儿来了兴趣。javascript
然而阅读完文章以后,发现理想与现实略有差距。简单的说,HTTP/2 所谓的server push实际上是当服务器接收一个请求时,能够响应多个资源。举个栗子:浏览器向服务器请求index.html,服务器不只把index.html返回了,还能够把index.js,index.css等一块儿推送给客户端。最直观的好处就是,浏览器不用解析页面再发起请求来获取数据,节约了页面加载时间。css
虽然略有差距,但看起来仍是挺有意思的,值得去尝试一下。毕竟纸上得来终觉浅,绝知此事要躬行!html
我以前并未使用过HTTP/2,在进行实践以前,总要先了解一下。关于HTTP/2,网上也有不少资料,我这里就简单说一下它最大的优势:快!。这里的快是相比HTTP 1.x 而言的,那为何它会更快呢?前端
这里的头部指的是http请求头headers。你们可能会想请求头能有多大呢,跟资源相比算不上啥。其实否则,随着互联网的发展,请求头里携带的数据愈来愈多了,随随便一个“user-agent”就一长串。另外cookie也会被存放愈来愈多的信息。更烦的是,一个页面全部的请求,都会带上这些重复的请求头数据。java
因此HTTP/2采用HPACK算法,能极大压缩头部数据,减小整体资源请求大小。大体的原理就是维护两本字典,一本静态字典,维护比较常见的头部名称。一本动态字典,维护不一样请求的公共的头数据。node
咱们知道,在HTTP 1.x中,咱们是能够并行请求的。可是,浏览器对于同一个域名的并行请求是有上限的(FireFox, Chrome上限6个 )。因此不少网站的静态资源站可能会有多个。并且每次请求都要从新创建TCP链接,想必大部分web工程师都了解过TCP三次握手,这个握手的代价也是比较高的。git
虽然http1.x里有keep-alive能够避免TCP三次握手,可是keep-alive又是串行的。因此要么并行多握手,要么串行不握手,都不是最好的结果,咱们但愿的是并行也不握手。程序员
幸运的是HTTP/2解决了这个问题。当客户端与服务端创建链接后,就会在双方创建一个双向流通道。这个流通道,能够同时包含多个消息(http请求),不一样消息各自的数据帧在流里能够乱序并行的发送,不会互相影响与堵塞,从而实现了一个TCP连接,并发执行N个http请求。经过提升并发,减小TCP链接开销,HTTP/2的速度获得了很大提高,尤为是在网络延迟比较高的状况下。github
这里用展示两张网络请求时间瀑布流对比图:web
HTTP 1.1
HTTP/2
上文中,咱们描述了HTTP/2的链接会创建一个双向流通道。Server Push就是在某次流中,能够返回客户端并无主动要的数据。
上述的头部压缩、多路复用,并不须要开发人员作什么操做,只要开启HTTP/2,浏览器也支持就能够了。可是Server Push就须要开发人员编写代码去操做了。那咱们就动手,在Node上玩玩看。
在Node 8.4.0版本时,就对HTTP/2实验性的支持了。2018年4月24日晚,Node v10终于发布了,然而对于HTTP/2,仍是实验性的支持。。。不过社区已经对HTTP/2移除实验性进行讨论了,相信在不远的未来应该能看到Node对HTTP/2更好的支持。所以在这以前,咱们能够先去掌握这个知识,作一些实践。
咱们先根据Node文档,建立一个HTTP/2服务。这里须要提的一点就是,目前流行的浏览器都不支持未加密的、不安全的HTTP/2。因此咱们必须生成下证书与秘钥,而后经过http2.createSecureServer
建立安全的HTTP/2连接。
想本身实践,生成本地证书的同窗能够参考这里:传送门。
// server.js const http2 = require('http2') const fs = require('fs') const streamHandle = require('./streamHandle/sample') const options = { key: fs.readFileSync('./ryans-key.pem'), cert: fs.readFileSync('./ryans-cert.pem'), } const server = http2.createSecureServer(options) server.on('stream', streamHandle) server.listen(8125) 复制代码
而后咱们再照着文档,编写对流的处理,并推送一个url路径为 '/' 的数据。
// streamHandle/sample.js module.exports = stream => { stream.respond({ ':status': 200 }) stream.pushStream({ ':path': '/' }, (err, pushStream, headers) => { if (err) throw err pushStream.respond({ ':status': 200 }) pushStream.end('some pushed data') pushStream.on('close', () => console.log('close')) }) stream.end('some data') } 复制代码
而后咱们打开chrome,访问https://127.0.0.1:8125 发现页面显示的一直是 some data。some pushed data这个主动推送的数据不知在哪里。打开网路请求面板,也没有任何其余请求。
百思不得其解阿,但我又不想止步于此,怎么办呢?
我决定先写一个正常的HTTP/2业务请求,代码以下:
module.exports = (stream, headers) => { const path = headers[':path'] if (path.indexOf('api') >= 0) { // 请求api stream.respond({ 'content-type': 'application/json', ':status': 200 }) stream.end(JSON.stringify({ success: true })) } else if (path.indexOf('static') >= 0) { // 请求静态资源 const fileType = path.split('.').pop() const contentType = fileType === 'js' ? 'application/javascript' : 'text/css' stream.respondWithFile(`./src${path}`, { 'content-Type': contentType }) } else { // 请求html stream.respondWithFile('./src/index.html') } } 复制代码
代码大意就是,判断请求连接,当请求地址带有api
字眼时就返回一个json,当请求地址带有static
时,就返回对应路径的静态资源。其余状况就返回一个html
文件。
html文件内容为:
<!DOCTYPE html> <html> <head> <meta charset=utf-8> <title>HTTP/2 Server Push</title> <link rel="shortcut icon" type=image/ico href=/static/favorite.ico> <link href=/static/css/app.css rel=stylesheet> </head> <body> <h1>HTTP/2 Server Push</h1> <script type=text/javascript src=/static/js/test.js></script> </body> </html> 复制代码
运行后咱们再打开chrome,访问https://127.0.0.1:8125 ,咱们能看到页面正常渲染了,查看网络面板,发现协议也已是HTTP/2。
这样咱们就开发了一个很是简单的HTTP/2应用。下一步,咱们再加上server push的功能,当访问index.html
的请求时,咱们主动将js
的资源返回,看看浏览器是怎么样的响应状况。
module.exports = (stream, headers) => { const path = headers[':path'] if (path.indexOf('api') >= 0) { // 请求api部分代码-略 } else if (path.indexOf('static') >= 0) { // 请求静态资源部分代码-略 } else { // 请求html时 主动推送js文件 stream.pushStream( { ':path': '/static/js/test.js' }, (err, pushStream, headers) => { if (err) throw err pushStream.respondWithFile('./src/static/js/test2.js', { 'content-type:': 'application/javascript' }) pushStream.on('error', console.error) } ) stream.respondWithFile('./src/index.html') } } 复制代码
代码大意就是,当客户端请求index.html
时,服务端除了返回index.html
文件,顺便把test2.js
这个文件推给服务端,客户端若是再次请求 https://127.0.0.1:8125/static/js/test.js
时,就会直接获取到test2.js
。
这里我用test2.js
的目的是为了方便的知道,客户端请求的究竟是服务端推送的test2.js
文件,仍是直接经过服务器再次请求获取到的test.js
文件。
其中test.js
会在页面打印:This is normal js. test2.js
会在页面打印:This is server push js.
按照指望,应该是后者。而后咱们打开chrome,访问 https://127.0.0.1:8125,展示以下结果:
!!!!掀桌!!!!
这个展现结果并非意料中的打印出This is server push js
,页面请求的js文件仍是正常网络请求的,并不是是我主动推送的test2.js
。我翻山越岭搜遍祖国内外,终于在Node的一条issue下看到相似的问题:http2 pushStream not providing files for :path entries (CHROME 65, works in FF) 。
Works in FireFox ??????? Chrome的bug ??????
你照着文档写代码,结果却不像文档所展现,各类排查没有用,最终发现是一些非主观的缘由,程序员最大的痛苦莫过于此....而后我夹杂着痛苦心塞和峰回路转的心情,打开了本身的Firefox,访问页面,展示以下结果:
这回终于对了!能够看到,页面中打印的是test2.js
文件的输出结果。
最开始依葫芦画瓢没用,其实也是由于Chrome的bug。无论怎么样,咱们仍是往前迈进了巨大的一步。
ps: 本人chrome版本66.0.3359.117,依旧有此bug
虽然咱们前进了一大步,但是面临了一个很尴尬的问题:咱们的静态资源更可能是托管在cdn上的。那咱们实际场景就会遇到以下状况:
这么一说,这就是个鸡肋啊!到头来竹篮打水一场空?
作人仍是不能轻易的放弃治疗。再仔细想一想,仍是有一些应用场景的---初始化的API请求。
如今不少单页应用,每每有不少的初始化请求,获取用户信息、获取页面数据等等。而这些都是须要html加载完,而后js加载完,而后再去执行的。并且不少时候,这些数据不加载完,页面都只能空白显示。但是单页应用的js资源每每又很大。一个vendor包好几兆也很常见。等浏览器加载并解析完这么大的包,可能已经不少时间消耗了。这时候再去请求一些初始化API,若是这些API又比较费时的话,页面就要多空白很长时间。
但若是能在请求html时,咱们就把初始化的api数据推送给客户端,当js解析完再去请求时,就能立刻获取到数据,这就能节省宝贵的白屏时间。说干就干,咱们再次动手实践!
module.exports = (stream, headers) => { const path = headers[':path'] if (path.indexOf('api') >= 0) { // 请求api stream.respond({ 'content-type': 'application/json', ':status': 200 }) stream.end(JSON.stringify({ apiType: 'normal' })) } else if (path.indexOf('static') >= 0) { // 请求静态资源代码-略 } else { // 请求html stream.pushStream({ ':path': '/api/getData' }, (err, pushStream, headers) => { if (err) throw err pushStream.respond({ ':status': 200 , 'content-type': 'application/json'}); pushStream.end(JSON.stringify({ apiType: 'server push' })) }); stream.respondWithFile('./src/index.html') } } 复制代码
一样的,我让正常请求api与服务端推送的api数据作一些差别,以便于更直观的判断是否获取了服务端推送的数据。而后在前端的js文件中写以下请求,并打印出请求结果:
window.fetch('/api/getData').then(result => result.json()).then(rs => { console.log('fetch:', rs) }) 复制代码
使人遗憾的是,咱们的到的是以下的结果:
请求的结果表示这并非server push的数据。吃一堑长一智,这会不会又是浏览器的什么bug?亦或者是否是fetch
不支持获取server push的数据?我立刻用XMLHttpRequest又写了一版:
window.fetch('/api/getData').then(result => result.json()).then(rs => { console.log('fetch:', rs) }) const request = new XMLHttpRequest(); request.open('GET', '/api/getData', true) request.onload = function(result) { console.log('ajax:', JSON.parse(this.responseText)) }; request.send(); 复制代码
结果以下:
!!!!掀桌!!!!
居然还真的是fetch
不支持http2 server push!
其实除了fetch
不支持外,还有一个比较致命的问题,就是这个server push,在当下的node服务器上,不能对服务端推送资源的url进行模糊匹配。也就是说,若是一个请求有url动态参数的话,实际上是匹配不到的。像我例子中的stream.pushStream({ ':path': '/api/getData' }, pushHandle)
,若是前端请求的接口是 /api/getData?param=1
,那就得不到server push的数据了。
另外,它仅支持GET请求与HEAD请求,POST、PUT这些也是不支持的。
针对fetch
这个问题,我又了搜了下祖国内外,也没得出个因此然来。这也变相的说明,目前社区里针对server push这个特性使用的还不多,遇到问题时,很难快速的去定位与解决问题。
因此,彷佛在推送api上,它的应用场景又局限了,仅适用于推送固定URL的初始化GET请求。
综上所述,我得出的结论就是:目前在Node上,使用server push,极大的状况与几率是不合适的,是付出大于收益的。主要因为以下缘由:
注:上述内容仅局限在Node服务,其余服务器本人未有研究,不必定有上述问题
虽然server push我目前以为很差用,可是HTTP/2仍是个好东西的,除了我文章开头讲的那些好处外,HTTP/2还有不少新奇的有用的特性,诸如流优先级、流控制等一些特性,本文并未讲到。你们能够去了解了解,对咱们将来开发高性能的web应该确定有不少帮助!
本文所涉及源码:https://github.com/wuomzfx/http2-test
原文连接:https://yuque.com/wuomzfx/article/eh551s