在 HTTP 的头部字段中,大家对 Content-Length
肯定不陌生,它表示包体的长度,目的是方便 HTTP 应用层从 TCP 层正确快速地读取到包体数据。而当包体长度未知或者我们想把包体拆成几个小块传输时, Transfer-Encoding: chunked
就可以派上用场了。
包体格式
当服务器返回 Transfer-Encoding: chunked
时,表明此时服务器会对返回的包体进行 chunked
编码,每个 chunk 的格式如下所示:
1 | ${chunk-length}\r\n${chunk-data}\r\n |
其中,${chunk-length}
表示 chunk 的字节长度,使用 16 进制表示,${chunk-data}
为 chunk 的内容。
当 chunk 都传输完,需要额外传输 0\r\n\r\n
表示结束。
下面是一个例子:
1 | HTTP/1.1 200 OK |
nodejs 实战 chunked 编码
说什么都不如动手实战一把,于是写了一个 DEMO 测试一下:
1 | var http = require('http') |
打开浏览器,发现结果不正确:
从结果中发现,传入 res.write
的参数都被当做了 chunk 的内容,通过 telnet 调试发现确实如此:
难道 res.write
自动帮我们进行了 chunked 编码?带着这个问题翻阅了一下 nodejs 的源码,结果发现确实如此。顺着这个路径 lib/_http_outgoing.js -> OutgoingMessage.prototype.write -> write_
我们可以得到答案:
1 | if (typeof chunk === 'string') |
chunked
编码格式还是比较简单的,那么它到底有啥用呢?
使用场景1-大文件下载
当从服务器下载一个比较大的文件时,可以使用 chunked 编码来节省内存等资源。
不使用 chunked 编码
代码
不使用 chunked 编码时需要将文件一次性读到内存中然后返回给用户:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17const 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
1 | FROM node:10-alpine |
构建镜像
docker build -t chunk-demo1 .
启动容器
1 | docker run \ |
其中 oom-kill-disable
表示当启动的容器的内存不足时不关闭容器
验证结果
通过浏览器访问 http://localhost:3001
,发现浏览器一直处于 loading 的状态:
通过 docker stats chunk-demo1
监控容器的运行状态,发现内存使用率处于 100% 左右:
使用 chunked 编码
代码
这里使用到了流的 pipe
方法,“管道”的这头源源不断从文件中读取数据,“管道”的那头通过 chunked
编码将数据返回给客户端。1
2
3
4
5
6
7
8
9
10
11
12const 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 编码我们可以在一个请求之内达到我们的目的。下面用一个简单的例子来演示一下这种技术:
前端代码
1 |
|
这里的 Bigpipe 其实实现的是一个“发布/订阅”系统,代码事先订阅了两个事件,当事件发生时将数据渲染到页面中。而返回的页面只是一个简单的框架,没有任何实际内容。
后端代码
1 | const http = require('http') |
后端代码通过 chunked 编码(上文已经说过,res.write 会自动进行 chunked 编码)返回了 html,然后模拟获取数据的耗时操作,当数据获取到后返回一段 javascript 脚本给客户端,当脚本执行时会触发上文所说的事件,并传递所获取到的数据,最终将内容呈现给用户。
结语
本文介绍了 HTTP 中的 chunked 编码格式及使用方法,并通过两个 Demo 介绍了它的使用场景,不过应对这些场景使用其他技术也可以,比如前端轮询、websocket等,这里只是多提供一种思路。