Node HTTP/2 Server Push 从了解到放弃

前阵子,在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/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

undefined

HTTP/2

undefined

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 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 datasome 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。

undefined

这样咱们就开发了一个很是简单的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,展示以下结果:

undefined

!!!!掀桌!!!!

这个展现结果并非意料中的打印出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,访问页面,展示以下结果:

undefined

这回终于对了!能够看到,页面中打印的是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) {
    // 请求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)
})
复制代码

使人遗憾的是,咱们的到的是以下的结果:

undefined

请求的结果表示这并非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();
复制代码

结果以下:

undefined

!!!!掀桌!!!!

居然还真的是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/http2-test

原文连接:https://yuque.com/wuomzfx/article/eh551s

相关文章
相关标签/搜索