一、引言:

HTTP/2 是 HTTP 协议的一个升级,它的主要目的是为了解决 HTTP/1.x 中不好实现功能,包括通过支持完整的请求与响应复用来减少延迟,通过有效压缩 HTTP 标头字段将协议开销降至最低,增加对服务器推送的支持。

二、HTTP 1.1 和 HTTP 2.0 的区别

1、二进制协议:
HTTP/1.1 版中,报文的头信息必须是文本(ASCII 编码),数据体可以是文本,也可以是二进制。 HTTP/2 彻底的二进制协议 ,头信息和数据体都是二进制,并且统称为"帧",可以分为头信息帧和数据帧。 帧的概念是它实现多路复用的基础。概念:
:HTTP/2通信的最小单位,包含帧头。
消息 :与逻辑请求或响应消息对应的完整的一系列帧。
数据流 : 已建立连接内的双向字节流,可以承载一条或多条消息。
帧、消息、数据流之间的关系 :一个 TCP 连接上承载任意数量的数据流,每个数据流承载双向消息,每条消息包含一或多个帧,帧是承载特定数据的最小单位。
二进制分帧层 :在应用层新增一个二进制分帧层,用来处理所有 http/2 新增的特性,对于通过 http/2 传输的信息细分为消息和帧,使用二进制格式编码。
2、持久连接:
在 HTTP/1.0 中,一个服务器在发送完一个 HTTP 响应后,会断开 TCP 链接,每次请求都会重新建立和断开 TCP 连接,代价过大。所以虽然标准中没有设定,某些服务器对 Connection: keep-alive 的 Header 进行了支持。这样的好处是连接可以被重新使用,之后发送 HTTP 请求的时候不需要重新建立 TCP 连接,以及如果维持连接,那么 SSL 的开销也可以避免。
既然维持 TCP 连接好处这么多,HTTP/1.1 就把 Connection 头写进标准,并且默认开启持久连接,除非请求中写明 Connection: close,那么浏览器和服务器之间是会维持一段时间的 TCP 连接,不会一个请求结束就断掉。
HTTP/2 的链接都是永久的。
3、多路复用:
HTTP/1.1 存在一个问题: 单个 TCP 连接在同一时刻只能处理一个请求( 串行 )。 意思是说: 客户端可以发起多个请求,可以减少整体的响应时间,但是服务器还是按照顺序回应请求,如果前面的回应特别慢,后面就会有许多请求排队等着。
HTTP2 提供了 Multiplexing 多路传输特性,可以在一个 TCP 连接中同时完成多个 HTTP 请求( 并行
4、数据流
HTTP/2 的消息都是在一个 TCP 连接内完成,通过把消息分成一些列的帧,把这些帧交错传输从而达到复用的目的,每个帧都包含数据流标识符,接收端根据其在重新组装成一条消息。
5、头信息压缩 :
由于 HTTP 1.1 协议不带状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,比如 Cookie 和 User Agent ,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。
HTTP/2 对这一点做了优化,引入了头信息压缩机制。一方面,头信息使用 gzip 或 compress 压缩后再发送;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就能提高速度了。
6、服务器推送:
HTTP/2 允许服务器未经请求,主动向客户端发送资源。使用服务器推送提前给客户端推送必要的资源,这样就可以相对减少一些延迟时间。这里需要注意的是 http2 下服务器主动推送的是静态资源,和 WebSocket 以及使用 SSE 等方式向客户端发送即时数据的推送是不同的。

三、node.js搭建一个http2 server:

1、安装http2

npm i --save http2

2、生成ssl证书
可以查一下如何生成,这里不详细说明,就用别人生成好的证书来测试了。

3、新建项目、初始化项目并安装spdy和express

npm init -y
npm i express spdy -D

当前来说,为 Node.js 主要有两个库实现了 HTTP/2 :

  • http2
  • 两个库都跟 Node.js 核心模块的 http 和 https 模块 api 很相似。这就意味着如果你不使用 Express ,这两个库就没什么区别。然而, spdy 库支持 HTTP/2 和 Express,而 http2 库当前不支持 Express。这就是为什么我们选择使用 spdy , Express 是Node.js 适合搭配的实践标准的服务框架。之所以叫 spdy是来自于 Google 的 SPDY 协议后来升级成 HTTP/2。

    4、搭建项目的目录结构

    5、serve.js文件 项目中最核心的就是server文件,http2.createSecureServer(options, callback),options表示你的证书或者其他有关的配置选项,但是证书是必备的。

    'use strict'
    const fs = require('fs') 
    const path = require('path')
    const http2 = require('http2')
    const helper = require('./helper')
    const PORT = process.env.PORT || 8080
    const PUBLIC_PATH = path.join(__dirname, '../public')
    const publicFiles = helper.getFiles(PUBLIC_PATH)
    // 读取证书
    const options = {
      cert: fs.readFileSync(path.join(__dirname, '../ssl/cert.pem')),
      key: fs.readFileSync(path.join(__dirname, '../ssl/key.pem'))
    // 创建HTTP2服务器 
    const server = http2.createSecureServer(options, onRequest)
    // Request 事件
    function onRequest(req, res) {
      // 路径指向 index.html
      const reqPath = req.url === '/' ? '/index.html' : req.url
      //获取html资源
      const file = publicFiles.get(reqPath)
      // 文件不存在
      if (!file) {
        res.statusCode = 404
        res.end()
        return
      res.stream.respondWithFD(file.fileDescriptor, file.headers)
    server.listen(PORT)
    

    helper.js:可以看到代码中用到了fs读取文件,helper也是获取文件的插件。

    'use strict'
    const fs = require('fs')
    const path = require('path')
    const mime = require('mime')
    function getFiles(baseDir) {
      const files = new Map()
      fs.readdirSync(baseDir).forEach((fileName) => {
        const filePath = path.join(baseDir, fileName)
        const fileDescriptor = fs.openSync(filePath, 'r')
        const stat = fs.fstatSync(fileDescriptor)
        const contentType = mime.lookup(filePath)
        files.set(`/${fileName}`, {
          fileDescriptor,
          headers: {
            'content-length': stat.size,
            'last-modified': stat.mtime.toUTCString(),
            'content-type': contentType
      return files
    module.exports = {
      getFiles
    

    关键点2 html文件
    这里没有引用第三方支持库,用fetch()和for循环的来模拟客户端向服务器发起100个请求,让我们更加直观的看到http2请求多个资源的情况。(了解更多关于:fetch->developer.mozilla.org/en-US/docs/…

    <meta charset="UTF-8"> </head> <h1>Test HTTP2 server</h1> </body> <script src="test1.js"></script> <script src="test2.js"></script> <script> for (var i = 0; i < 100; i++) { fetch('//localhost:8080/network.png'); </script> </html>

    test1.js/test2.js

    'use strict'
    console.log('test 1')
    document.body.innerHTML += '<p>第一个js</p>'
    

    6、启动服务

    浏览器打开页面测试 https://localhost:8080/, 因为我们创建的证书并没有经过CA机构认证,所以会提示页面不安全,点击继续访问即可。 查看网络请求协议可以看到网站使用了HTTP/2(h2)

    $ node index.js
    

    7、测试结果

    从测试结果来看,可以回顾一下http2的知识,非常明显的一点是:同个域名只需要占用一个 TCP 连接

    8、测试一下服务端推送Server Push

    为了改善延迟,HTTP/2引入了 Server Push ,这允许服务器在明确的请求之前将资源推送到浏览器。这就允许服务器充分利用空闲的网络来改善加载时间。 我们新建一个sever1.js来测试服务推送功能。
    服务器推送只需简单的调用 spdy 实现的 res.push ,我们将文件路径名传输进去作为第一个参数,浏览器会使用这个路径名来匹配push promise 资源。res.push() 的第一个参数 /test1.js 一定得跟 HTML 文件中需要的文件名相匹配。而第二个参数是一个可选的对象,设置了该资源的一些资源信息描述。

    const port = 8080
    const spdy = require('spdy')      
    const fs = require('fs') 。
    const express = require('express')
    const app = express() // https://www.it1352.com/1058471.html
    const path = require('path')
    const options = {
      cert: fs.readFileSync(path.join(__dirname, '../ssl/cert.pem')),
      key: fs.readFileSync(path.join(__dirname, '../ssl/key.pem'))
    app.use('/', express.static(path.join(__dirname, 'public2')))
    app.get('/', (req, res) => {
      var stream = res.push('/test1.js', {
        status: 200, // optional
        method: 'GET', // optional
        request: {
          accept: '*/*'
        response: {
          'content-type': 'application/javascript'
      stream.on('error', function () { })
      //stream.end('alert("hello from push stream!");')
      stream.end(fs.readFileSync('./public2/test1.js'))
      res.end(fs.readFileSync('./public2/index.html'))
      .createServer(options, app)
      .listen(port, (error) => {
        if (error) {
          console.error(error)
          return process.exit(1)
        } else {
          console.log('Listening on port: ' + port + '.')
    

    你可以看到,stream 对象有两个方法 on 和 end。前者监听了 error 和 finish 事件,而后者则监听完成传输 end,然后就会main.js 就会触发弹窗。(例子中弹窗代码被我注掉了,可以放开测一下)

    或者,如果你拥有多个数据块,你可以选择使用 res.write() 然后最后使用 res.end(),其中 res.end() 会自动关闭结束response 而 res.write() 则让它保持开启。

    最后,读取 HTTPS 密钥和证书并使用 spdy 启动运转服务器。-->spdy .createServer(options, app)
    该实现的关键就在于,围绕着 streams(从源头到客户端的建立起的数据通道流)

    index.js

    'use strict'
    // require('./src/server')
    require('./src/server1')
    

    9、启动和对比 HTTP/2 Server Push

    可以在浏览器中检测收到服务器端推送的行为。Chrome 启动开发者工具,打开 Network 标签,你可以看到 test1.js 不存在绿色时间条,没有等待时间 TTFB (Time to First Byte)详细。
    使用了http2 server Push截图如下:

    再仔细看,可以看到请求是由 Push 开始发起的(Initiator列查看),没有使用服务器推送的 HTTP/2 服务器或者 HTTP/1,这一列就会显示文件名称,如 index.html 发起的显示就是 index.html。
    不使用server push截图如下:

    实践就结束了,使用了 Express 和 Spdy 简单就实现了push JS 资源,而该资源可以用于后面 HTML 中 script 标签引入的。当然你也可以在脚本中使用 fs 来读取文件资源。
    HTTP/2 Server Push的好处就在于当浏览器请求页面的时候,同时发送必需的资源文件(图片,CSS 样式,JS 文件),而不需要等待客户端浏览器请求这些资源,从而做到更快的第一次渲染时间。

    HTTP/2 库 spdy 让开发者在基于 Express 的应用能更容易的实现服务器推送特性。

    参考:zhuanlan.zhihu.com/p/76302817
    参考:segmentfault.com/a/119000001…

    分类:
    前端
    标签: