读书笔记(深入浅出Node.js)—— Node中的Buffer

432 阅读2分钟

Buffer

  • v8内存分配 —— 堆内存
  • 不是通过V8进行分配的内存 —— 堆外内存(如Buffer)

定义

Buffer是一个像Array的对象,但它主要作用于操作字节。

内存分配

Buffer对象的内存分配不是在v8的堆内存中,而是在node的C++层面实现的。

因为要处理大量数据,不能像堆内存一样,需要一点申请一点,所以使用的是在C++层面申请,JS中分配策略。

为了高效使用申请来的内存,Node采用slab分配机制。简而言之slab就是一块申请好的固定大小的内存区域。slab具有如下三种状态:

  • full:完全分配状态
  • partial:部分分配状态
  • empty: 没有被分配状态

当我们需要Buffer对象,可以通过以下方式指定Buffer大小:

new Buffer(size);

Node是以8 KB为界限来区分Buffer是大对象还是小对象,这个8kb也是每个slab的值,在js层面,以它作为单位单元进行内存分配。

分配小Buffer对象

如果制定的size<8kb,Node会按照小对象分方式进行分配。分配过程中主要是用一个局部变量pool作为中间对象,处于分配状态的slab单元都指向它。

var pool;
function allocPool() {
	pool = new SlowBuffer(Buffer.poolSize);
  pool.used = 0;
}

image-20210816113838389.png

此时slab处于empty状态。

构造小Buffer对象的代码如下:

new Buffer(1024);

这次构造将会去检查pool对象,如果没有被创建,将会创建一个新的slab对象指向它:

if (!pool || pool.length - pool.used < this.length) allocPool();

同时当前Buffer对象的parent属性指向该slab,并记录下是从这个slab的哪个位置(offset)开始使用的,slab本身也记录被使用了多少字节。

this.parent = pool;
this.offset = pool.used;
pool.used += this.length;
if (pool.used & 7) pool.used = (pool.used + 8) & ~7;

这时候slab的状态为partial。

再次创建一个Buffer对象时,构造过程中将会判断这个slab的剩余空间是否足够。如果足够,使用剩余空间,并更新slab的分配状态。下面是创建一个新的Buffer对象,将引起slab的再次分配:

new Buffer(3000);

image-20210816114808979.png

如果slab空间不足,将会构造新的slab。原slab中剩余空间将造成浪费。

要注意的是,由于同以slab分配给不同的Buffer使用,只有这些小Buffer对象在作用域释放并都回收时,slab的8KB空间才会被回收。

分配大Buffer对象

如果需要超过8KB的Buffer对象,将会直接分配一个SlowBuffer对象作为slab单元,这个slab单元将会被这个Buffer独占。

// Big buffer, just alloc one
this.parent = new SlowBuffer(this.length);
this.offset = 0;

上面提到的Buffer对象都是js层面的,能够被V8标记回收。但其内部的parent属性指向的SlowBuffer对象却来自于Node自身C++中的定义,是C++层面上的Buffer对象。

小结

真正的内存是在Node的C++层面提供的,js层面只是使用它。当进行小而频繁的Buffer操作时,采用slab的机制进行预先申请和事后分配,使得js到操作系统之间不用有太多的内存申请方面的调用。而对于大块的Buffer而言,则直接使用C++层面提供的内存,无需细腻的分配操作。

Buffer的转换

字符串转Buffer

构造函数转换Buffer对象,存储的只能是一种编码类型。encoding不传递是默认按UTF-8编码进行转码和存储。

new Buffer(str, [encoding]);

一个Buffer对象可以存储多种不同类型的字符串转码的值,但美中编码的长度不同,从Buffer转回字符串时要谨慎处理:

buf.write(string, [offset], [length], [encoding])

Buffer转字符串

buf.toString([encoding], [start], [end])

Buffer不支持的编码类型

提供一个isEncoding()方法来判断编码是否支持转换,支持返回true,否则为false:

Buffer.isEncoding(encoding)

Buffer的拼接

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

以上代码中,data事件中获取的chunk就是Buffer对象 ,这句data拼接中隐藏了toString操作,它等价于:

data = data.toString() + chunk.toString();

注意的是上述方法,在读取宽字节的中文时会产生乱码问题。我们模拟场景,将每次读取的Buffer长度设置为11:

var rs = fs.createReadStream('test.md', {highWaterMark: 11});
// 测试内容为《静夜思》

// 输出 -----> 窗前���光,疑���地上霜;举头���明月,���头思故乡

乱码产生原因

buf.toString()方法默认以UTF-8为编码,中文在UTF-8下占3个字节,而我们每次的读取长度只有11,所以第一个Buffer对象在输出时只能显示3个字符,Buffer中剩下的两个字节不足以显示文字,所以显示为2个�乱码。第二次输出中的第一个字节也不足以表示问题,也显示为1个�乱码。

正确拼接Buffer

正确的方法是,用一个数组来存储接收到的所有Buffer片段,并记录下所有片段的总长度,然后调用Buffer.concat()生成一个合并的Buffer对象。

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);
});

Buffer与性能

Buffer在文件I/O与网络I/O中运用十分广泛。在应用中我们通常会操作字符串,但一旦在网络中传输,都要转换成Buffer,以进行二进制传输。

通过预先转换静态内容为Buffer对象,可以有效的减少CPU的重复使用,节省服务器资源。