Buffer

笔记部分内容摘自 《深入浅出 Node.JS》 —— 朴灵

背景

与前端不同,前端常常只需要处理简单字符串操作及DOM操作,而后端需要处理相对复杂的文件、网络I/O等,JS自由的字符串无法满足需求,于是有了buffer以满足后端的需求。

结构

  • buffer是JS与C++结合的一个模块,性能相关以C++实现,非性能相关以JS实现。
  • buffer由于是C++实现的内建模块,其在node项目进程启动时就已加载进内存,使用时无需require。
  • buffer所使用的内存为堆外内存,非V8分配,而是node在C++层面申请,在使用时node再进行分配。
  • buffer对象类似于Array,形如<Buffer d3 e2 cd 67 2b 21 8c 9a e7 20 1f 2e 7c 6d 4a cf 3c>,其元素皆为16进程两位数。

使用

1
2
var buf = new Buffer(100);
console.log(buf.length); //100

内存分配

采用slab分配机制

slab就是一块申请好的固定大小的内存区域

具有三种状态:

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

slab以8k为界限,区分buffer是大对象还是小对象。

1
Buffer.poolSize = 8 * 1024;

分配小对象 (<=8kb的buffer)

使用局部变量pool作为中间处理对象,处于分配状态的slab都指向该变量。

1
2
3
4
5
6
var pool;

function allocPool() {
pool = new SlowBuffer(Buffer.poolSize);
pool.used = 0;
}
  • 分配buffer对象会先检查是否有pool对象,若没有,则通过创建新的slab对象单元,并指向它。
  • 然后记录下使用的大小used,及此slab使用的偏移量offset。
  • 再次创建buffer会判断此slab单元是否足够存放,够则继续存放,不够则重新创建新的slab单元以存储。
  • 当slab单元不够存放,重新创建单元时,旧的slab单元则会造成浪费。

一个slab单元内所有的buffer小对象全部都已经释放并可以回收时,才能释放此slab单元。
即若一个slab单元中只存放了1个字节的buffer对象,也会造成整个8kb的slab单元内存无法释放。

分配大对象 (>8kb的buffer)

直接分配一个指定大小(大对象大小)的内存空间:

1
2
this.parent = new SlowBuffer(this.length);
this.offset = 0;

小结

这里的slab分配机制,旨在于减少系统调用,以提升性能。不过对于平时的开发来说,我暂时没看出来有什么作用,算是顶多更加深入的了解了buffer。

Buffer的转换

支持的转换编码类型:

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

字符串转Buffer

** 方式: ** 通过构造函数转换。

1
var buf = new Buffer(str, [encoding]);

encoding为可选参数,默认值为UTF-8

一个Buffer对象即可以存储多种编码类型的字符串转码,通过write()方法实现。

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

这种存储多种编码类型转码的Buffer,由于每种编码所有的字节长度不同,再解码转回普通字符串时需要格外谨慎小心!

Buffer转字符串

方式: toString()方法

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

encoding默认UTF-8

不支持编码类型的相互转换

由于Buffer对象支持的编码类型有限,所以借助其它具有相似功能的模块可以更好的完成Buffer的转换。

Buffer拼接

不正确(危险)的Buffer拼接方式

1
2
3
4
5
6
7
8
9
10
11
12
var fs = require('fs');

var rs = fs.createReadStream('test.txt');
var data = '';

rs.on('data', chunk => {
data += chunk;
})

rs.on('end', () => {
console.log(data);
})

注意点:

  • data += chunk; 隐含着buffer转换: data = data.toString() +chunk.toString();
  • 当出现宽字节编码,可能会出现乱码问题。
  • 在处理UTF-8、Base64或UCS-2/UTF-16LE这三种编码时可以采用添加一条语句来解决此问题:rs.setEncoding(encoding)

正确的拼接Buffer

思路: 拼接Buffer对象,再用iconv-lite等转换

1
2
3
4
5
6
7
8
9
10
11
12
13
let chunks = [];
let size = 0;

rs.on('data', (chunk) => {
chunks.push(chunk);
size += chunk.length;
});

rs.on('end', () => {
let buf = Buffer.concat(chunks, size);
let str = iconv.decode(buf, 'utf-8');
console.log(str);
});

其中值得一看的是Buffer.concat()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Buffer.concat = function(list, length) {
if (!Array.isArray(list)) {
throw new Error('Usage: Buffer.concat(list, [length])');
}

if (list.length === 0) {
return new Buffer(0);
} else if (list.length === 1) {
return list[0];
}

if (typeof length !== 'number') {
length = 0;
for (let i = 0; i < list.length; ++i) {
let buf = list[i];
length += buf.length;
}
}

let buffer = new Buffer(length);
let pos = 0;

for (let i = 0; i < list.length; ++i) {
let buf = list[i];
buf.copy(buffer, pos);
pos += buf.length;
}

return buffer;
}

用Buffer提升性能

  • 在web应用中,逻辑代码的编写过程中,通常是在操作字符串,但一旦通过网络传输,都需要进行Buffer转换,以进行二进制数据传输。
  • 在不需要改动读取的文件内容的前提下,可以直接读取Buffer并进行传输,尽量减少转换。