前阵子,在Media看到一篇文章《Node.js can HTTP/2 push!》。看到push这个字眼时,我想到的是WebSocket消息推送。难不成HTTP/2还能像WebSocket那样可以服务端主动推送消息?好厉害,我就一下子来了兴趣。

然而阅读完文章之后,发现理想与现实略有差距。简单的说,HTTP/2 所谓的server push其实是当服务器接收一个请求时,可以响应多个资源。举个栗子:浏览器向服务器请求index.html,服务器不仅把index.html返回了,还可以把index.js,index.css等一起推送给客户端。最直观的好处就是,浏览器不用解析页面再发起请求来获取数据,节约了页面加载时间。

虽然略有差距,但看起来还是挺有意思的,值得去尝试一下。毕竟纸上得来终觉浅,绝知此事要躬行!

HTTP/2

我之前并未使用过HTTP/2,在进行实践之前,总要先了解一下。关于HTTP/2,网上也有很多资料,我这里就简单说一下它最大的优点:快!。这里的快是相比HTTP 1.x 而言的,那为什么它会更快呢?

头部压缩

这里的头部指的是http请求头headers。大家可能会想请求头能有多大呢,跟资源相比算不上啥。其实不然,随着互联网的发展,请求头里携带的数据越来越多了,随随便一个“user-agent”就一长串。另外cookie也会被存放越来越多的信息。更烦的是,一个页面所有的请求,都会带上这些重复的请求头数据。

所以HTTP/2采用HPACK算法,能极大压缩头部数据,减少总体资源请求大小。大致的原理就是维护两本字典,一本静态字典,维护比较常见的头部名称。一本动态字典,维护不同请求的公共的头数据。

多路复用

我们知道,在HTTP 1.x中,我们是可以并行请求的。但是,浏览器对于同一个域名的并行请求是有上限的(FireFox, Chrome上限6个 )。所以很多网站的静态资源站可能会有多个。而且每次请求都要重新建立TCP连接,想必大部分web工程师都了解过TCP三次握手,这个握手的代价也是比较高的。

虽然http1.x里有keep-alive可以避免TCP三次握手,但是keep-alive又是串行的。所以要么并行多握手,要么串行不握手,都不是最好的结果,我们希望的是并行也不握手。

幸运的是HTTP/2解决了这个问题。当客户端与服务端建立连接后,就会在双方建立一个双向流通道。这个流通道,可以同时包含多个消息(http请求),不同消息各自的数据帧在流里可以乱序并行的发送,不会互相影响与堵塞,从而实现了一个TCP链接,并发执行N个http请求。通过提高并发,减少TCP连接开销,HTTP/2的速度得到了很大提升,尤其是在网络延迟比较高的情况下。

这里用展现两张网络请求时间瀑布流对比图:

HTTP 1.1

HTTP/2

Server Push

上文中,我们描述了HTTP/2的连接会建立一个双向流通道。Server Push就是在某次流中,可以返回客户端并没有主动要的数据。

上述的头部压缩、多路复用,并不需要开发人员做什么操作,只要开启HTTP/2,浏览器也支持就可以了。但是Server Push就需要开发人员编写代码去操作了。那我们就动手,在Node上玩玩看。

Node HTTP/2 Server Push 实操

Node对HTTP/2支持情况

在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 errpushStream.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 datasome pushed data这个主动推送的数据不知在哪里。打开网路请求面板,也没有任何其他请求。

百思不得其解阿,但我又不想止步于此,怎么办呢?

从娃娃抓起

我决定先写一个正常的HTTP/2业务请求,代码如下:

module.exports = (stream, headers) => {const path = headers[':path']if (path.indexOf('api') >= 0) {// 请求apistream.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 {// 请求htmlstream.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 errpushStream.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上的。那我们实际场景就会遇到如下情况:

  1. 所有网站的资源,包括html/css/js/image等,都是在一台业务服务器上的。抱歉同学,你的业务服务器的带宽本来就低,怕是吃不消这么多静态资源的并发请求,你本来就慢的无可救药了。
  2. 网络路由走后端,即html走后端,其他静态资源托管cdn。抱歉同学,静态资源都在cdn上的,你的业务服务器怎么去推?
  3. 完全的前后端分离,html与其他静态资源都是在cdn上。这种情况下,还是有点用处的,但效果并不会很出色。因为HTTP/2本身就支持多路复用,已经减少了TCP三次握手带来的网络消耗。server push仅仅只是降低了浏览器解析html的时间,对于现代浏览器来说,这太微乎其微了。(ps: 就在我写文章之时,恰好看到某云服务商支持了server push。)

这么一说,这就是个鸡肋啊!到头来竹篮打水一场空?

天生我材必有用

做人还是不能轻易的放弃治疗。再仔细想想,还是有一些应用场景的---初始化的API请求。

现在很多单页应用,往往有很多的初始化请求,获取用户信息、获取页面数据等等。而这些都是需要html加载完,然后js加载完,然后再去执行的。而且很多时候,这些数据不加载完,页面都只能空白显示。可是单页应用的js资源往往又很大。一个vendor包好几兆也很常见。等浏览器加载并解析完这么大的包,可能已经很多时间消耗了。这时候再去请求一些初始化API,如果这些API又比较费时的话,页面就要多空白很长时间。

但如果能在请求html时,我们就把初始化的api数据推送给客户端,当js解析完再去请求时,就能马上获取到数据,这就能节省宝贵的白屏时间。说干就干,我们再次动手实践!

module.exports = (stream, headers) => {const path = headers[':path']if (path.indexOf('api') >= 0) {// 请求apistream.respond({ 'content-type': 'application/json', ':status': 200 })stream.end(JSON.stringify({ apiType: 'normal' }))} else if (path.indexOf('static') >= 0) {// 请求静态资源代码-略} else {// 请求htmlstream.pushStream({ ':path': '/api/getData' }, (err, pushStream, headers) => {if (err) throw errpushStream.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,极大的情况与概率是不合适的,是付出大于收益的。主要由于如下原因:

  1. 截止Node v10.0.0,HTTP/2依旧是一个实验性的模块;
  2. 浏览器支持极差;如上述的Chrome的bug,fetch对server push的不支持;
  3. 推送静态资源的实际场景非常少,而且速度提升在理论上也不会很明显;
  4. 推送API仅支持固定的URL,不能携带任何动态参数。

注:上述内容仅局限在Node服务,其他服务器本人未有研究,不一定有上述问题

虽然server push我目前觉得不好用,但是HTTP/2还是个好东西的,除了我文章开头讲的那些好处外,HTTP/2还有很多新奇的有用的特性,诸如流优先级、流控制等一些特性,本文并未讲到。大家可以去了解了解,对我们未来开发高性能的web应该肯定有很多帮助!

本文所涉及源码:https://github.com/wuomzfx/ht...
原文链接:https://yuque.com/wuomzfx/art...

Node HTTP/2 Server Push 从了解到放弃相关推荐

  1. Node.js Git Server搭建及Git常用操作笔记

    Node.js Git Server搭建及Git常用操作笔记 安装Git工具即可在本地进行Git仓库的管理,如果要实现远程仓库则需要搭建Git Server.通过Node.js搭建Git Server ...

  2. 关于.NET中的Server push技术

    关于.NET中的Server push技术 一般来说方法有2种,一种就是客户端用JS异步定时轮询服务器端,这种是大部分人采用的方法,但是我在想到底可以不可以实现服务器端有改变时才会主动推送到客户端呢? ...

  3. HTTP/2之服务器推送(Server Push)最佳实践

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由mariolu 发表于云+社区专栏 HTTP/1.X出色地满足互联网的普遍访问需求,但随着互联网的不断发展,其性能越来越成为瓶颈.IE ...

  4. COMET彗星(一)SERVER PUSH介绍

    COMET介绍: 还在为AJAX赞叹的时候,COMET竟也悄悄降临,更有甚者已经将COMET比作是AJAX的接班人.暂且不考虑服务性能和维持connection的负担,COMET的日益走红,让SERV ...

  5. HTTP/2 Server Push 详解

    在过去的一年时间,HTTP/2 的出现为关注性能的开发者带来了显著的变化.HTTP/2 已经不再是我们期待中的特性,而是伴着 Server Push(服务端推送)能力已然到来. 除了解决常见的 HTT ...

  6. ZK Server Push实现数据主动推送

    由于业务部门需要将现有系统的WebService中的利率,鉴于C/S客户端的维护性,因此使用了B/S的结构,框架选择了较为熟悉的ZK.主要使用了ZK较早的Server Push技术实现数据的主动推送. ...

  7. Server push(服务器推送技术)

    一.服务器推送技术Server Push详解:        推送技术Server Push的基础思想是将浏览器主动查询信息改为服务器主动发送信息.服务器发送一批数据,浏览器显示这些数据,同时保证与服 ...

  8. 服务器推送技术Server Push详解

    服务器推送技术(Server Push)是最近Web技术中最热门的一个流行术语,它的别名叫Comet(彗星).它是继AJAX之后又一个倍受追捧的Web技术.服务器推送技术最近的流行与AJAX有着密切的 ...

  9. Solution : Cannot add new node – Rule SQL Server Database Services feature state failed.

    原文链接 已测试,如果在主节点中都修复了的话,直接改注册表可以搞定. While deploying SQL Server 2014 cluster in my lab I ran into this ...

最新文章

  1. 深入理解javascript函数系列第二篇——函数参数
  2. 现在的男生真的太惨了
  3. Markdown会干掉Html吗?
  4. 关于android开发时,发生Error infalting classa com.baidu.mapapi.map.MapView的解决办法
  5. 神奇的css3(2)动画
  6. linux系统安全加固
  7. C++句柄类 [ 资深博主 ]
  8. gabor matlab pudn,matlab-Face-recognition 基于Gabor特征提取和人工智能的人脸检测系统 271万源代码下载- www.pudn.com...
  9. 墨刀右键菜单被浏览器右键菜单遮挡导致墨刀右键菜单无法使用
  10. 带你了解“不拘一格去创新,别出心裁入场景”的锐捷
  11. mac os 直接打开html文件,macos – 在Mac OS X上打开磁盘设备文件以进行写访问
  12. 大数据笔记-外存算法
  13. Oracle中rowid的用法(全面)
  14. 利用PhotoSwipe进行完成图片预览功能
  15. 一种快速锁定的 Fractional PLL 设计
  16. 4.19内核SLUB内存分配器
  17. 【自然语言处理】Word2Vec 词向量模型详解 + Python代码实战
  18. 上海悦颜白继平院长助力上海整形科技周第二十届上海国际整形美容外科会议圆满结束!
  19. 8000字长文带你了解真实的山东大学软件园校区
  20. 那些前端的特效(装哈哈神器)

热门文章

  1. oracle 11g dataguard安装出现的错误
  2. [转载]利用@media screen实现网页布局的自适应,@media screen and
  3. hadoop : hdfs的心跳时间设置及心跳检测算法
  4. Rundeck crontab格式
  5. vCloud Automation Center (vCAC) 6.0 (一)
  6. 网络回溯分析技术八大应用之运维评估 网络运维的真正价值
  7. TSP问题由标准格式转成简单格式(我的实验代码需要的格式)
  8. mysql注入 outfile_Mysql注入中的outfile、dumpfile、load_file函数详解
  9. 微信多开txt_微信仅需3步操作,就能多开登录?手把手包教包会
  10. NGUI创建Camera参数为Simple 2D的UI UI对象的结构UI Root(2D)