网络协议进阶之 chunked 编码

网络协议进阶之 chunked 编码

技术杂谈小彩虹2021-08-18 12:30:08230A+A-

在 HTTP 的头部字段中,大家对 Content-Length 肯定不陌生,它表示包体的长度,目的是方便 HTTP 应用层从 TCP 层正确快速地读取到包体数据。而当包体长度未知或者我们想把包体拆成几个小块传输时, Transfer-Encoding: chunked 就可以派上用场了。

包体格式

当服务器返回 Transfer-Encoding: chunked 时,表明此时服务器会对返回的包体进行 chunked 编码,每个 chunk 的格式如下所示:

${chunk-length}\r\n${chunk-data}\r\n

其中,${chunk-length} 表示 chunk 的字节长度,使用 16 进制表示,${chunk-data} 为 chunk 的内容。

当 chunk 都传输完,需要额外传输 0\r\n\r\n 表示结束。

下面是一个例子:

HTTP/1.1 200 OK
Date: Wed, 01 Jan 2020 08:46:31 GMT
Connection: keep-alive
Transfer-Encoding: chunked

1\r\n
a\r\n
2\r\n
bc\r\n
3\r\n
def\r\n
14\r\n
ghijklmnopqrstuvwxyz\r\n
0\r\n\r\n

nodejs 实战 chunked 编码

说什么都不如动手实战一把,于是写了一个 DEMO 测试一下:

var http = require('http')

const chunks = ['a', 'bc', 'def', 'ghijklmnopqrstuvwxyz']

http
  .createServer(async function(req, res) {
    res.writeHead(200, {
      'Transfer-Encoding': 'chunked'
    })
    for (let index = 0; index < chunks.length; index++) {
      res.write(`${chunks[index].length.toString(16)}\r\n${chunks[index]}\r\n`)
    }
    res.write('0\r\n\r\n')
    res.end()
  })
  .listen(3000)

打开浏览器,发现结果不正确:

从结果中发现,传入 res.write 的参数都被当做了 chunk 的内容,通过 telnet 调试发现确实如此:

难道 res.write 自动帮我们进行了 chunked 编码?带着这个问题翻阅了一下 nodejs 的源码,结果发现确实如此。顺着这个路径 lib/_http_outgoing.js -> OutgoingMessage.prototype.write -> write_ 我们可以得到答案:

    if (typeof chunk === 'string')
      len = Buffer.byteLength(chunk, encoding);
    else
      len = chunk.length;

    msg._send(len.toString(16), 'latin1', null);
    msg._send(crlf_buf, null, null);
    msg._send(chunk, encoding, null);
    ret = msg._send(crlf_buf, null, callback);

chunked 编码格式还是比较简单的,那么它到底有啥用呢?

使用场景1-大文件下载

当从服务器下载一个比较大的文件时,可以使用 chunked 编码来节省内存等资源。

不使用 chunked 编码

代码

不使用 chunked 编码时需要将文件一次性读到内存中然后返回给用户:

const http = require('http')
const fs = require('fs')

const dir = process.env.dir || '/data'
const filename = `${dir}/movie.mkv`

http
  .createServer(async function(req, res) {
    try {
      const data = fs.readFileSync(filename)
      console.log('length', data.length)
      res.end(data)
    } catch (error) {
      console.error('error', error)
    }
  })
  .listen(3001)

使用 Docker 模拟服务器内存不足

Dockerfile

FROM node:10-alpine

WORKDIR /usr/src/app

VOLUME /data

COPY . .

EXPOSE 3001

CMD [ "node", "index" ]

构建镜像

docker build -t chunk-demo1 .

启动容器

docker run \
-it \
-m 512m \
--rm \
-v /Users/youxingzhi/ayou/net-advance/http-advance/range/demo/server/files:/data \
-p 3001:3001 \
--name chunk-demo1 \
--oom-kill-disable \
chunk-demo1

其中 oom-kill-disable 表示当启动的容器的内存不足时不关闭容器

验证结果

通过浏览器访问 http://localhost:3001,发现浏览器一直处于 loading 的状态:

通过 docker stats chunk-demo1 监控容器的运行状态,发现内存使用率处于 100% 左右:

使用 chunked 编码

代码

这里使用到了流的 pipe 方法,“管道”的这头源源不断从文件中读取数据,“管道”的那头通过 chunked 编码将数据返回给客户端。

const http = require('http')
const fs = require('fs')

const dir = process.env.dir || '/data'
const filename = `${dir}/movie.mkv`

http
  .createServer(async function(req, res) {
    const readStream = fs.createReadStream(filename)
    readStream.pipe(res)
  })
  .listen(3001)

使用 Docker 模拟服务器内存不足

这一步跟上文一样,只需要把 chunk-demo1 换成 chunk-demo2 即可

验证结果

通过浏览器访问 http://localhost:3001, 发现可以正常访问:

通过 docker stats chunk-demo2 监控容器的运行状态,发现内存使用率一直维持在一个较小的水平:

使用场景2-Bigpipe

Bigpipe 想要实现的效果是:用户打开网站,可以快速的看到网站的框架,其他细节内容会逐渐呈现给用户。借助 ajax 技术,这种效果我们很早就实现了,不过 ajax 技术是通过发送额外的 HTTP 请求来实现的,而通过 chunked 编码我们可以在一个请求之内达到我们的目的。下面用一个简单的例子来演示一下这种技术:

前端代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <script> var Bigpipe = function() { this.callbacks = {} } Bigpipe.prototype.ready = function(key, callback) { if (!this.callbacks[key]) { this.callbacks[key] = [] } this.callbacks[key].push(callback) } Bigpipe.prototype.set = function(key, data) { var callbacks = this.callbacks[key] || [] for (var i = 0; i < callbacks.length; i++) { callbacks[i].call(this, data) } } </script>
    <script> var bigpipe = new Bigpipe() bigpipe.ready('personInfo', function(data) { for (let k in data) { const $ele = document.querySelector('#' + k) $ele.innerHTML = data[k] } }) bigpipe.ready('articles', function(data) { const $ele = document.querySelector('#articles') let html = '' data.forEach(article => { html += `<li>${article.title}</li>` }) $ele.innerHTML = html }) </script>
  </head>
  <body>
    <h1>个人信息</h1>
    <p>姓名:<span id="name"></span></p>
    <p>年龄:<span id="age"></span></p>
    <p>性别:<span id="gender"></span></p>
    <h1>文章列表</h1>
    <ul id="articles"></ul>
  </body>
</html>

这里的 Bigpipe 其实实现的是一个“发布/订阅”系统,代码事先订阅了两个事件,当事件发生时将数据渲染到页面中。而返回的页面只是一个简单的框架,没有任何实际内容。

后端代码

const http = require('http')
const fs = require('fs')

function sendPersonInfo(res) {
  return new Promise(resolve => {
    setTimeout(() => {
      const data = {
        name: 'ayou',
        age: 18,
        gender: '男'
      }
      res.write(`<script>bigpipe.set('personInfo', ${JSON.stringify(data)})</script>`)
      resolve()
    }, 1000)
  })
}

function sendArticles(res) {
  return new Promise(resolve => {
    setTimeout(() => {
      const list = []
      for (let i = 0; i < 10; i++) {
        list.push({
          title: `网络协议进阶${i + 1}`
        })
      }
      res.write(`<script>bigpipe.set('articles', ${JSON.stringify(list)})</script>`)
      resolve()
    }, 3000)
  })
}

http
  .createServer(async function(req, res) {
    res.writeHead(200, {
      'Content-Type': 'text/html;charset=utf-8'
    })
    const html = fs.readFileSync('./index.html')
    res.write(html)
    await Promise.all([sendPersonInfo(res), sendArticles(res)])
    res.end()
  })
  .listen(3000)

后端代码通过 chunked 编码(上文已经说过,res.write 会自动进行 chunked 编码)返回了 html,然后模拟获取数据的耗时操作,当数据获取到后返回一段 javascript 脚本给客户端,当脚本执行时会触发上文所说的事件,并传递所获取到的数据,最终将内容呈现给用户。

结语

本文介绍了 HTTP 中的 chunked 编码格式及使用方法,并通过两个 Demo 介绍了它的使用场景,不过应对这些场景使用其他技术也可以,比如前端轮询、websocket等,这里只是多提供一种思路。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们