[转]nodejs深入学理解Buffer
Posted 牛顿的小脑
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[转]nodejs深入学理解Buffer相关的知识,希望对你有一定的参考价值。
原文: https://www.jianshu.com/p/e3f14cdf78f1
-----------------------------------------------------------
前言
因为在node中需要处理网络协议、操作数据库、处理图片、接受上传文件,因此,需要大量操作二进制数据,虽然js对于字符串支持良好,但是由于需要对于字符串进行序列化,因此,就有必要了解一下Buffer,对,没错,Buffer其实是二进制数据模块。
另外,本章将不是ES的范畴,本章定义的内容都源于commonjs(二进制部分)。因此,这也不是前端开发工程师所涉及过的场景,因此,由前端转到node的工程师很有必要来看看这一章。
buffer结构
buffer与array很像,主要用于操作字节。
buffer模块结构
buffer是一个典型的js与c++结合的模块,将性能相关的部分用c++实现,将非性能相关的部分用js实现。同时buffer也是node的核心模块,可以直接使用,并且,第五章我们已经知道buffer属于堆外内存,可以通过自己管理其垃圾回收。当然,buffer对象的管理还是在堆内,再由这个对象去管理堆外的内存。
buffer对象
buffer对象类似于数组,他的元素都是16进制的两位数,即0~255的数值,我们看一下示例代码:
var str = "深入浅出node.js";
var buf = new Buffer(str, ‘utf-8‘);
console.log(buf);
// => <Buffer e6 b7 b1 e5 85 a5 e6 b5 85 e5 87 ba 6e 6f 64 65 2e 6a 73>
不同编码的字符串,占用的元素个数也不相同,中文字在UTF-8下占用3个元素,字母和半角标点符号占用1个元素。同时,我们还可以调用length属性,得到buffer对象的长度,还可以通过下标访问元素。
var buf = new Buffer(100);
console.log(buf.length); // => 100
console.log(buf[10]);
//我们给buffer元素赋值
buf[10] = 100;
console.log(buf[10]); // => 100
buf[20] = -100;
console.log(buf[20]); // 156
buf[21] = 300;
console.log(buf[21]); // 44
buf[22] = 3.1415;
console.log(buf[22]); // 3
我们看到,给元素的赋值如果小于0,就将该值逐次加256,直到得到一个0255之间的整数,如果得到赋值大于255,就逐次减256,直到得到0255区间内的数值。如果是小数,则舍弃小数部分,只保留整数部分。
buffer内存分配
buffer不同v8申请内存,它通过node的c++模块申请内存。因此,buffer的内存策略是由c++申请内存,然后,在js中分配内存。因为,处理大量的字节数据不能采用需要一点内存就向操作系统申请一点内存的方式,这可能造成大量的内存申请的系统调用,对操作系统有一定压力。
node采用了slab的分配机制,slab其实就是一块申请好的固定内存区域,它有3种状态:
1.full:完全分配状态
2.partial:部分分配状态
3.empty:没有被分配状态
当我们需要一个buffer对象时,可以通过:new Buffer(size);来申请内存和内存的大小,另外还有大内存和小内存的区分,例如,以buffer.poolsize = 8 *1024来分配,这样就得到了一个8kb的内存。node其实就是以8KB为界限来区分Buffer是大对象还是小对象的。底层的代码是Buffer.poolSize = 8 * 1024;
这个8kb的值也是每个slab的大小值,在js层面以他作为单位单元进行内存分配。
1.如果指定的buffer的大小小于8kb,node会按照小对象的方式进行分配。buffer的分配过程中主要使用一个局部变量pool作为中间处理对象,处于分配状态的slab单元都会指向他,以下是分配一个全新的slab单元的操作,他会将新申请的SlowBuffer对象指向它:
var pool;
function allocPool() {
pool = new SlowBuffer(Buffer.poolSize);
pool.used = 0;
}
我们看个图,来看看这段代码都干了什么(这段代码其实就是为一个新构造的slab单元分配了空间和指针)
此时,这个slab处于empty状态,然后,我们构造一个小buffer对象,也就是小于8kb的buffer。构造小buffer对象的代码为new Buffer(1024)
这次构造会去检查pool对象,如果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单元中初次分配一个Buffer对象
这个时候的slab状态为partial,当再次创建一个Buffer对象时,构造过程中将会判断这个slab的剩余空间是否足够,如果足够,使用剩余空间,并更新slab的分配状态,例如new Buffer(3000),就会再次引起slab分配:我们看一下示意图
如果slab的剩余空间不够本次分配,则会构造一个新的slab,原slab中剩余的空间将会造成浪费。例如:
new Buffer(1);
new Buffer(8192);
此时将会创建两个slab空间,第一个slab空间的8kb会被1个字节Buffer对象独占。因此,需要注意这种浪费的发生。
2.分配大Buffer对象
大于8kb的buffer对象,会被分配一个SlowBuffer对象作为slab单元,这个slab单元将被这个大的Buffer对象独占。
// Big buffer, just alloc one
this.parent = new SlowBuffer(this.length);
this.offset = 0;
这里的SlowBuffer类是在C++中定义的,虽然引用buffer模块可以访问到它,但是不推荐直接操纵它,而是用buffer替代。上面提到的buffer对象都是js层面的,能够被v8标记回收,但是其内部的parent属性指向的SlowBuffer对象却来自Node的c++模块,是c++层面的buffer对象,所用的这部分内存不在v8的堆中。
综上所述,真正的buffer内存是在node的c++层面提供的,js层面只是使用它。当进行小而频繁的buffer操作时,采用slab的机制进行预先申请和事后分配,使得js到操作系统之间不必有过多的内存申请方面的系统调用。对于大块的buffer而言,直接使用c++层面提供的内存,无需频繁的分配操作。
Buffer的转换
Buffer对象可以和字符串进行相互转换,支持的编码类型有:ASCII、UTF-8、UTF-16LE/UCS-2、Base64、Binary、Hex
字符串转Buffer
通过构造函数来完成,new Buffer(str,[encoding]);encoding默认为utf-8类型的编码和存储。同时,因此buffer记录的都是一个一个的二进制元素,或者说是汇编码,因此,可以将不同类型的buffer写入到buffer对象内,但是,因为是不同类型转为的buffer,因此,再转回字符串的时候也需要使用相同的编码规范,否则就会出现乱码的情况,因此,不建议将不同类型的编码写入到一个buffer对象中。(写入的方法是:buf.write(string,[offset],[length],[encoding])
)
Buffer转字符串
只需要toString()即可。
buf.toString([encoding], [start], [end])
start和end是转换时候的起始位置,之前通过写入不同编码的那种方式写入到buffer对象里的二进制元素,就可以通过这个方式重新读出了,不过,还是不要这样使用为好。
Buffer不支持的编码类型
我们可以通过调用Buffer.isEncoding(encoding)
来看是否支持某种编码。对于不支持的编码格式,可以使用iconv和iconv-lite来解决。
其中,iconv-lite采用纯js实现,iconv通过c++调用libiconv库实现。在性能方面,iconv-lite由于少了c++到js层次的转换,因此,消耗更少的cpu,效率更高一点。我们卡一下例子:
var iconv = require(‘iconv-lite‘);
// Buffer转字符串
var str = iconv.decode(buf, ‘win1251‘);
// 字符串转Buffer
var buf = iconv.encode("Sample input string", ‘win1251‘);
对于无法转换的内容,iconv和iconv-lite会有不同的处理。iconv-lite对于无法转换的单字节输出?,多字节输出■(里边应该有个问号的,可是我不会打....)
iconv则由三级降级策略,会尝试翻译无法转换的内存,或者忽略这些内容,如果不设置忽略,iconv可能会报EILSEQ异常。如下是iconv的示例代码,我们来感受一下
var iconv = new Iconv(‘UTF-8‘, ‘ASCII‘);
iconv.convert(‘ça va‘); // throws EILSEQ
var iconv = new Iconv(‘UTF-8‘, ‘ASCII//IGNORE‘);
iconv.convert(‘ça va‘); // returns "a va"
var iconv = new Iconv(‘UTF-8‘, ‘ASCII//TRANSLIT‘);
iconv.convert(‘ça va‘); // "ca va"
var iconv = new Iconv(‘UTF-8‘, ‘ASCII//TRANSLIT//IGNORE‘);
iconv.convert(‘ça va ? ‘); // "ca va "
Buffer的拼接
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);
});
data事件中获取的chunk对象其实就是buffer对象。这里需要注意的是data += chunk;这句话,也就拼接buffer。其实质是data = data.toString() + chunk.toString();。这里其实对于中文的支持就会存在问题。因为,英语环境下,不需要转码,直接拼接转换就行。对于宽字符的中文,就很有问题了,例如李白的静夜思进行读取和转换:
var rs = fs.createReadStream(‘test.md‘, {highWaterMark: 11});
我们限定可读流的每次读取的buffer长度限制为11,则很有可能出现如下情况:
这个乱码为啥产生呢?我们来看看我们读的过程:这首诗原始的buffer应该是这样的
<Buffer e5 ba 8a e5 89 8d e6 98 8e e6 9c 88 e5 85 89 ef bc 8c e7 96 91 e6 98 af e5 9c b0 e4 b8 8a e9
9c 9c ef bc 9b e4 b8 be e5 a4 b4 e6 9c 9b e6 98 8e e6 9c 88 ...>
由于,我们限定了长度,因此,每个buffer对象的长度都为11,7次读完,每次的结果就变成了这样:
<Buffer e5 ba 8a e5 89 8d e6 98 8e e6 9c>
<Buffer 88 e5 85 89 ef bc 8c e7 96 91 e6>
...
因为,默认为utf-8的读取,因此,第四个字,只能显示一半。也就造成了乱码的产生。这个问题值得注意。
setEncoding()和string_decoder()
为了解决上文中的乱码问题,我们应该设置一些编解码格式:
readable.setEncoding(encoding)
var rs = fs.createReadStream(‘test.md‘, { highWaterMark: 11});
rs.setEncoding(‘utf8‘);
通过这个方法,我们传递的不再是buffer对象,而是编码后的字符串了,这样做之后就可以得到正确的输出了:
这个过程中,也就是调用setEncoding(),可读流在内部设置了decoder对象,这个对象来自于string_decoder模块的StringDecoder对象实例,我们来感受一下:
var StringDecoder = require(‘string_decoder‘).StringDecoder;
var decoder = new StringDecoder(‘utf8‘);
var buf1 = new Buffer([0xE5, 0xBA, 0x8A, 0xE5, 0x89, 0x8D, 0xE6, 0x98, 0x8E, 0xE6, 0x9C]);
console.log(decoder.write(buf1));
// =>床前明
var buf2 = new Buffer([0x88, 0xE5, 0x85, 0x89, 0xEF, 0xBC, 0x8C, 0xE7, 0x96, 0x91, 0xE6]);
console.log(decoder.write(buf2));
// => 月光,疑
这个过程中,因为基于StringDecoder得到的编码,直到utf-8的宽字符是3个字节,因此会将前3个汉字先输出,也就是先输出9个字节,然后将月字的前两个字节保留在StringDecoder实例内部,再和后续的字节进行拼接。它目前支持utf-8、base64、ucs-2、utf-16le等,其他的没有支持的编解码格式,还是需要字节手工控制。
正确拼接Buffer
我们来看一下这个例子,它对拼接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.concat()合成一个buffer对象。concat还实现了从小对象buffer向大对象buffer复制的过程,我们来看一下源代码:
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 (var i = 0; i < list.length; i++) {
var buf = list[i];
length += buf.length;
}
}
var buffer = new Buffer(length);
var pos = 0;
for (var i = 0; i < list.length; i++) {
var buf = list[i];
buf.copy(buffer, pos);
pos += buf.length;
}
return buffer;
};
Buffer与性能
buffer在文件io和网络io中具有广泛应用,不管是什么对象,一旦进入到网络传输中,都需要转换为buffer,然后以二进制进行数据传输。因此,提供io效率,可以从buffer转换入手。
var http = require(‘http‘);
var helloworld = "";
for (var i = 0; i < 1024 * 10; i++) {
helloworld += "a";
}
// helloworld = new Buffer(helloworld);
http.createServer(function (req, res) {
res.writeHead(200);
res.end(helloworld);
}).listen(8001);
我们用ab发起200个并发
ab -c 200 -t 100 http://127.0.0.1:8001/
测试结果如下:
这里QPS(每秒查询次数)是2527.64,传输率是25370.16kb/s
然后,我们取消掉注释,也就是不进行转换了,直接发送buffer
var http = require(‘http‘);
var helloworld = "";
for (var i = 0; i < 1024 * 10; i++) {
helloworld += "a";
}
helloworld = new Buffer(helloworld);
http.createServer(function (req, res) {
res.writeHead(200);
res.end(helloworld);
}).listen(8001);
我们再来测试:
我们看到qps提升了进1倍。这个问题也是我们为啥要做动静分离的原因。
在构建web服务时,将页面的动态内容和静态内容进行分离,静态内容可以通过先转换为buffer的方式,提升传输性能。
接下来,我们再看看文件读取:
文件读取
文件读取时需要设置好highWaterMark参数。也就是我们在fs.createReadStream(path,opts)时,可以传入一些参数:
{
flags: ‘r‘,
encoding: null,
fd: null,
mode: 0666,
highWaterMark: 64 * 1024
}
还可以设置start和end来指定读取文件的位置范围:
{start: 90, end: 99}
fs.createReadStream()的工作方式是在内存中准备一段buffer,然后,通过fs.read()读取时,逐步从磁盘中将字节复制到buffer中。完成一次读取,则从这个buffer中通过slice()取出部分数据作为一个小buffer对象,再通过data事件传递给调用方。如果buffer用完,则重新分配一个,如果还有剩余,则继续使用。
var pool;
function allocNewPool(poolSize) {
pool = new Buffer(poolSize);
pool.used = 0;
}
在理想状态下,每次读取的长度都是用户指定的highWaterMark,剩余的还可分配给下一次。pool是常驻内存的,只有当pool单元神域数量小于128(kMinPoolSpace)字节时,才会重新分配一个buffer对象,我们来看一下源代码:
if (!pool || pool.length - pool.used < kMinPoolSpace) {
// discard the old pool
pool = null;
allocNewPool(this._readableState.highWaterMark);
}
此处注意两点:
1.highWaterMark设置对buffer内存的分配和使用有一定影响
2.highWaterMark设置过小,可能导致系统调用次数过多
文件读取基于buffer分配,buffer基于Slowbuffer分配,如果文件过小,则可能造成slab的浪费。
另外,fs.createReadStream()内部使用了fs.read()实现,会多次调用系统磁盘,如果文件过大的话,highWaterMark将会决定出发系统调用的次数和data事件的次数。
以下是node自带的基准测试:在benchmark/fs/read-stream-throughput.js下:
function runTest() {
assert(fs.statSync(filename).size === filesize);
var rs = fs.createReadStream(filename, {
highWaterMark: size,
encoding: encoding
});
rs.on(‘open‘, function () {
bench.start();
});
var bytes = 0;
rs.on(‘data‘, function (chunk) {
bytes += chunk.length;
});
rs.on(‘end‘, function () {
try { fs.unlinkSync(filename); } catch (e) { }
// MB/sec
bench.end(bytes / (1024 * 1024));
});
}
//执行结果
fs/read-stream-throughput.js type=buf size=1024: 46.284
fs/read-stream-throughput.js type=buf size=4096: 139.62
fs/read-stream-throughput.js type=buf size=65535: 681.88
fs/read-stream-throughput.js type=buf size=1048576: 857.98
我们可以看出,highWaterMark的值越大,读取速度越快。
作者:白昔月
链接:https://www.jianshu.com/p/e3f14cdf78f1
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
以上是关于[转]nodejs深入学理解Buffer的主要内容,如果未能解决你的问题,请参考以下文章