在普通的JS程序中,字符串的操作十分普遍,但由于Node的应用范围还涉及到网络传输,文件系统等,所以对二进制文件的支持就需要用到所谓的Buffer这个概念了。

Buffer与Array有点相似,但它主要用于操作字节(即值的范围为0~255),基本的使用示例如下:

var str = "Node的Buffer对象剖析",
    buf = new Buffer(str);
foo(buf)

new Buffer用于初始化Buffer对象,接收的参数主要包括:

new Buffer(size);
new Buffer(str, [encoding])
new Buffer(array)

其中,对于第二个构造函数,Buffer的构造函数第二个可选参数可以用于指定编码方式,如utf-8asciibase64等,默认为utf-8,而给定Buffer对象以后,可以使用buf.toString([encoding])方法返回解码后的字符串。

当然,我们也是可以将Buffer对象转成JSON对象,这时,得到的实际上是一个0~255范围的整数数组,可以使用new Buffer(JSON.parse(buf_json_data))恢复出其Buffer结构。

前面我们说过,Buffer与Array很像,[]取元素和length属性自然是不可或缺的了。另外类似的方法还有:

Buffer.concat(bufList, [totalLength])
Buffer.copy(targetBuffer, [targetStart], [sourceStart], [sourceEnd])
Buffer.slice([start], [end])    // 返回新的对象

Buffer中一类非常重要的方法集是从Buffer对象读取/写入常见数字类型(虽然JS只有Number类型,但我们实际应用的环境中,还是会涉及到float, int, uint8等类型的数字),接口符合一定的规则,如buf.readInt16BE(offset, [noAssert])就是从Buffer对象的指定位置读一个16字节的int数据,由于平台的不同可能会有大端和小端的方式,这个接口名最后的BE即指定了是大端的读取方法。写入的接口规则也类似,buf.writeInt16BE(value, offset, [noAssert])显然就是按大端法向Buffer对象的offset处写入一个Int16类型的数据value。注:noAssert可选参数主要用于指定是否对offset进行验证,如offset越界,默认是false。

Buffer的fill方法非常实用,用于将Buffer对象的内容全部重置为一个指定值。如buf.fill(value, [offset], [end])将buf的空间内容。

Buffer对象常用于输入输出文件流和网络传输,一个常见的误解如下:

var fs = require('fs'),
    rs = fs.createReadStream('file.txt'),
    data = "";
rs.on('data', function (chunk) {
    // 注意:这并不意味着chunk是字符串,chunk本身是Buffer对象,但这里隐含着 data = data.toString() + chunk.toString()
    data += chunk;
});

上面这段代码在处理宽字节的字符串时,可能会存在隐患问题(中间的字节截断转换成字符串),另外它还会有一定的性能问题。更好的处理方式是:

var fs = require('fs'),
    rs = fs.createReadStream('file.txt', {highWaterMark: 10 * 1024}),   // 每次读取10KB数据
    data = [];
rs.on('data', function (chunk) {
    // 注意:这并不意味着chunk是字符串,chunk本身是Buffer对象,但这里隐含着 data = data.toString() + chunk.toString()
    data.push(chunk);
});
res.on('end', function (chunk) {
    Buffer.concat(data) // 一次性拼接所有的Buffer对象,再转化为字符串对象
});

Buffer的底层与性能相关的部分主要使用C++来实现,而将其它性能无关的接口使用JS实现。需要值得注意的一点是,Node中Buffer的内存分配并不是由V8来进行的,而是使用Node本身C++层面的代码来实现,使用的是堆外内存,因此V8的内存回收并不影响到Buffer里的内存。

具体而言,Node使用slab分配方法实现高效的内存管理,当初始化申请一定大小的内存时,Node会根据申请的大小(8KB)将内存分为小的Buffer对象和大的Buffer对象。小对象的分配策略是:

var pool;
function allocPool() {
    pool = new SlowBuffer(Buffer.poolSize);  // poolSize即 8KB 这个阈值
    pool.used = 0;       // 偏移
}

如果pool中未使用的部分足够当前的申请大小,直接返回这个偏移量;否则需要调用allocPool申请一个新的内存块:

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

这意味着,如果第一次申请一个字节的Buffer对象,第二次申请8KB大小的Buffer对象(需要重新调用allocPool申请新的内存块)后,第一次申请的内存块有效区域只有第一个字节块,而后的内存块都将被浪费。另外,如果多个Buffer对象用到了同一片内存块,那么需要等到这些对象都被回收,这个内存块才会被释放掉。

而对于大对象,则直接调用new SlowBuffer()分配内存。