什么是 node 的 Buffer

231 阅读6分钟

前言

之前的文章地址,这篇呢,简单聊下 Buffer,这个也不是前端范畴的,但也可以学习。

Buffer 的结构

一句话来介绍,Buffer 是一个像 Arrey 的对象,主要用于操作字节。
之前有提到过,Buffer 不属于 V8 分配的堆内存里,属于堆外内存,node 的 C++ 层面实现的内存申请。
简单来说,就是 C++ 层面向操作系统申请内存后,js 层面来做内存分配。
我们怎么使用 Buffer 呢,真的就像 Array 对象,例如申请一个 100 字节的 Buffer 对象

var buf = new Buffer(100);

里面每一位都可以存储 0 到 256 的数值,十六进制的两位数。

为了高效使用申请而来的内存,node 采用了 slab 分配机制。
就是申请好固定大小的内存,内存有三种状态

  1. full 完全分配
  2. partial 部分分配
  3. empty 没有分配

在 Buffer 中,会区分大对象还是小对象,界限就是 8KB,只要超过 8KB,就是 Buffer 大对象,而 slab 也表示一个内存分配的单位,大小就是 8KB。

在使用 Buffer 时,如果指定 Buffer 小于或等于 8KB,那么就会按照小对象方式分配,分配过程中会有一个中间变量 pool,处于分配状态的 slab 都指向它。
那么当开始使用时,我们执行一行代码

new Buffer(1024);

此时先会去检查有没有中间变量 pool,如果没有就去创建,有了之后,开始创建一个 slab 单元指向它,slab 创建的时候是 empty 状态,指向 pool 并且开始存储了部分数据的时候是 partial 状态,例如我们执行完上面那行代码,那么当前 slab 单元最后是 partial 状态,使用了 1024 字节。
那如果再执行一行代码

new Buffer(1024 * 7);

这个 slab 单元将会刚好填满,处于 full 状态,下次再创建 Buffer 对象时,将会创建新的 slab 单元。

当然这是理想情况,正常情况肯定不会那么刚好,举个极端的例子,这样使用的情况下

new Buffer(1);
new Buffer(1024 * 8);

第一个 slab 单元没有使用完,也会创建第二个 slab 单元来存储新的 Buffer 对象。那么从代码层面看,我们是有 8KB 加 1 字节的内存使用,实际内存试使用情况是使用了 16KB。

那如果是大 Buffer 对象呢,将会直接分配一个 SlowBuffer 对象作为 slab 单元,这个 slab 单元将被这个大 Buffer 对象独占,SlowBuffer 对象是 C++ 层面定义的,我也不是很了解,虽然可以通过引用 Buffer 模块然后访问到它,但不推荐直接使用,正常情况下还是直接使用 Buffer 即可。

我们前面提到的 Buffer 对象都是 js 层面,都会被 V8 的垃圾回收标记回收,但是内部的 parent 属性指向的 SlowBuffer 对象确来自于 node 自身的 C++ 定义,属于 C++ 层面的内存,不属于 V8 的堆内存。

Buffer 的转换

Buffer 和字符串可以互相转化,目前支持的字符串编码有这几种

  • ASCII
  • UTF-8
  • UTF-16LE/UCS-2
  • Base64
  • Binary
  • Hex

中文常用的 GBK、GB2312 不支持,对此可以借助一些第三方插件 iconv、iconv-lite 等。

Buffer 的拼接

先看一段代码

var fs = require('fs');

var rs = fs.createReadStream('test.md');
var data = '';
rs.on("data", function (chunk){
    data += chunk; 
});
rs.on("end", function () { 
    console.log(data);
});

这是一段通过流读取文件的简单代码,里面的 chunk 其实就是 Buffer 对象,但是里面在做

data += chunk; 

拼接操作时,发现他其实是用字符串的操作方式操作 Buffer,当然 Buffer 是可以和字符串互相转化的,这样操作时,Buffer 会自动隐式转化成字符串,看起来好像没也啥问题。
但我们前面说过,Buffer 在和字符串的转化中,是不支持中文常用的一些编码方式,隐式转化一般默认的编码方式是 UTF-8,但 UTF-8 也支持中文啊,是的没错,UTF-8 是支持中文,但是中文在 UTF-8 下占三个字符,那么会有什么问题呢,我们改造一下上面第二行代码,方便让问题更好的暴露

var rs = fs.createReadStream('test.md', {highWaterMark: 11});

将文件流每次读取的 chunk 长度限制为 11 字节,然后我们先看看 test.md 里的内容

image.png

我们查看执行程序后得到的输出

image.png

淦,这就乱码了。
我们把每个 chunk 的长度限制在 11 字节以后,每次都读取 11 字节的 Buffer 对象,而一个汉字是三字节,一开始 床前明 读取完后,还加上 的前两个字节,这时候执行拼接,隐式转换成字符串,默认 UTF-8 编码方式,这俩字节就变成了对应的乱码,接下来是 的前一个字节,加 光,疑 九个字节(句号是中文句号),在加 的前一个字节,这时候又把 字分割了,所以又是乱码。

那么怎么处理呢,其实加一行代码可以解决

rs.setEncoding('utf8');

执行代码看下结果

image.png

为什么执行 setEncoding 就解决了呢?
其实执行了 setEncoding 后,流在内部设置了一个 decoder 对象,具体主要做了,当发现 字的前两个字节时,知道这是汉字的宽字符串,会保留下来,跟下次提供过来的 字的最后一个字节合并,再进行 UTF-8 的解析,所以没有乱码。
那现在我们是解决了乱码问题了吗,其实也没有,这个方法主要有两个问题

  1. 支持的编码方式只有 UTF-8、Base64、UCS-2/UTF-16LE 这三种编码
  2. 而且还需要手动设置,未免不太方便

目前的最好的解决方法,其实是采用数组来处理

var chunks = [];
var size = 0;
res.on('data', function (chunk) {
    chunks.push(chunk);
    size += chunk.length; 
});

res.on('end', function () {
    var buf = Buffer.concat(chunks, size); 
    var str = iconv.decode(buf, 'utf8'); 
    console.log(str);
});

用数组把 chunk 都存下来,先不做解析,然后调用 Buffer.concat 方法,最终生成一个合并的 Buffer 对象,再来进行 UTF-8 的解析,这样就不会出现乱码。

Buffer 的性能

在应用中,我们经常操作字符串,但是一旦在网络中传输,都会转换成 Buffer,以二进制的数据传输。

总结

其实 Buffer 跟字符串很像,但 Buffer 是二进制数据,性能上会比字符串好很多,所以利用好这一点,可以很大程度上提升性能,但一般也就是使用在字符串的处理方面,像文件的网络传输,其实也是二进制,所以不会用到 Buffer。
不过一般应用里,字符串的场景非常多,所以 Buffer 的使用频率还是不少的,对于前端想入门 node 的人来说,还是很有了解的必要。
好了,就到这了~