也终于到这章,这章的 Buffer 将非常有用,buffer 的运用从一而钟基本贯穿了所有网络服务的项目,那么对于一个前端人员来说,buffer甚至比写接口更难理解,因为这是在前端完全没有什么概念的东西,我在最初利用文档与实践学习 node 时,虽然有使用 buffer,但是对其结构确是完全不理解,导致有很多时候即使得到结果,但却心中一点底都没有,当你出现这种想法时,其实就暴露了你根本不会这个事实,而结果的输出只是你搬运过来的而已,绝非是你得出的。
在读完下一章网络编程前,我对网络协议的理解更是差劲,并完全无法理解 OSI 七层模型的意义,所以从本章开始,后面的章节都及其重要,因为只有实践过的同学可能才能深切体会到什么叫剖开云雾见光明般的顿悟。
那么,接下来开始本章的讲解,本章内容不多,但本质上非常重要,学习完本章,你至少应该掌握:
- 什么是 Buffer及Buffer 的组成结构
- Buffer 的日常运用、编码格式的转换及拼接方式
- 使用 Buffer 与否的性能差异
Buffer及Buffer 的组成结构
概念总结:Buffer是一个像 Array 的对象,你可以想象成类数组,可以获取到长度,但是单位是字节,所以是以字节为单位来操纵的,从属上是一个 js 与 C++ 结合的模块,性能部分由 C++ 实现,非性能方面由 js 实现,Node 进程启动时就加载了它,所以全局对象上已经存在,可以直接使用。
注意:Buffer 这个类数组上每个元素都是 0 — 255 的随机值,当你手动赋值时,如果赋值数值小于 0,则在结果上 +256,大于255,则 -256,小数则舍去小数点后面的位数(注意是舍去,不是四舍五入)
Buffer 的内存分配:先在 C++ 中申请内存,再在 js 中分配内存,并不是需要一点就申请一点,Node 以 8KB 为界限来区分 buffer 属于 大对象 还是 小对象,小于 8K 的小对象将使用预先在 C++ 中申请的内存,由 js 来分配,而大于 8KB 的大对象将直接使用 C++ 提供的内存,而没有 js 的精细分配,下面是分配的过程
- node 采用 slab 分配机制,slab 就是一块申请好的固定大小的内存区域,当指定 Buffer 小于 8K 时,slab 会指向一个局部变量 pool 作为中间处理对象,而新申请的 slowBuffer 对象是指向这个 slab 单元的;
- slab 会记录本次 Buffer 的长度以及开始及结束的位置,当一个新 Buffer 建立时,会先判断上个未超过 8K 的 slab 单元剩下的内存是否足够存储本次新建的 Buffer,如果够,将会从上次的结束为止拼接上(因为 slab 也记录了开始位置与结束位置),如果不够就会新申请一个 slowBuffer 来指向新的 slab ,而上个 slab 剩下的内存将被浪费;
- 所以以 8K 为界限,我们可以充分利用 8K 空间来建立 Buffer ,尽量将每个 slab 单元都填满 8K,而如果新建的 Buffer 对象是大于 8K 的,那么它将直接新建为 slowBuffer 对象,而不指向 slab,这样就不存在 slab 的这个分配过程了,此时这个 Buffer 类是直接由 C++ 来提供的内存。
Buffer 的日常运用、编码格式的转换及拼接方式
- 新建一个 buffer 特别简单,直接
new Buffer(str, [encoding])
即可,使用buf.write(string, [offset], [length], [encoding])
可控制写入的此 buffer 的位置及编码类型,但是要注意每种编码的字符串所占用的字节可能会不一样; - Buffer 转字符串直接使用
buf.toString([encoding], [start], [end])
,同时支持转换的编码类型,及转换起始位置与终止位置 - Node 的 自带 Buffer 对象暂时只支持以下几种编码类型,如果需要其他类型可以使用 iconv-lite 或 iconv 这两个模块去实现
- ASCII
- UTF-8
- UTF-16LE/UCS-2
- Base64
- Binary
- Hex
Buffer 的拼接,主要运用于日常的开发中,场景比如操纵文件系统、上传文件等都要拼接,因为很多时候文件都是以流的形式传输的,此时我们应当将 buffer 拼接为一个完整的块
当我们利用原生 node 的 http 模块开启 tcp 服务时,我们常会写这样的代码,这里我们将读取的文件流数据块拼接了起来,并且打印
var fs = require('fs') var rs = fs.createReadStream('test.md') var data = '' rs.on('data', function (chunk) { data += chunk }) rs.on('end', function (data) { console.log(data) })
这里便会存在一个问题,当我们的数据块全部为字母时,不会有任何问题,因为一个字母是 1 字节,Buffer 是以字节为单位的,所以怎么都不会存在 被截断 的情况,但是当我在读取流限制每次读取的标记个数并且读取的字符串为中文时,就会出问题了
fs.createReadStream('test.md', { highWaterMark: 11 })
,因为一个中文是 3 个字节,很明显如果一段全为中文的数据块被读取时,每 3 个字就会断层,而导致乱码,因为最后一个本为 3 个字节组成一个中文,但是在第二个字节被截断了,系统无法识别这个 buffer 元素,因为不是一个整体,标点符号为 1 字节,例如
床前明???光,疑???地上霜,举头???明月,???头思故乡。
此时我们的解决方案应利用 Buffer.concat(chunks, length)
去拼接 buffer,暂时将 buffer 的每段存储在一个数组内,并记下总长,最后将数组内的所有 buffer 一次性拼接,代码如下
var chunks = [], len = 0
rs.on('data', (chunk) => {
chunks.push(chunk)
len += chunk.length
})
rs.on('end', (err) => {
res.end(Buffer.concat(chunks, len))
})
在这里大家一定要重点理解这两点,那么其他 api 的问题直接查文档就好
- Buffer 是一个类数组的存在;
- Buffer 的单位是字节;
- 注意 Buffer 编码格式的转换;
Buffer 与性能的关系
书上的例子已经很明显,同样响应给前端时,测试用例的结果是使用普通字符串拼接响应 QPS(每秒查询次数)为 2527.64,而使用后为 4843.28,很明显预先将静态内容转换为 Buffer 对象响应可以大量减少 CPU 的重复使用,节省服务器资源,所以在响应静态内容时,大家可以选择将静态内容都以 buffer 的形式输出,而动态内容与其分离的去正常输出,尤其是文件读取后输出文件的情况。