【深入浅出Node.js系列六】Buffer那些事儿

来源:互联网 发布:linux大棚命令百篇上 编辑:程序博客网 时间:2024/05/29 17:45

Javascript是为浏览器而设计的,能很好的处理unicode编码的字符串,但对于二进制或非unicode编码的数据就显得无能为力。Node.js继承Javascript的语言特性,同时又扩展了Javascript语言,为二进制的数据处理提供了Buffer类,让Node.js可以像其他程序语言一样,能处理各种类型的数据了。

1 Buffer介绍

在Node.js中,Buffer类是随Node内核一起发布的核心库。Buffer库为Node.js带来了一种存储原始数据的方法,可以让Nodejs处理二进制数据,每当需要在Nodejs中处理I/O操作中移动的数据时,就有可能使用Buffer库。原始数据存储在 Buffer 类的实例中。一个 Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。

Buffer 和 Javascript 字符串对象之间的转换需要显式地调用编码方法来完成。以下是几种不同的字符串编码:

‘ascii’ – 仅用于 7 位 ASCII 字符。这种编码方法非常快,并且会丢弃高位数据。

‘utf8’ – 多字节编码的 Unicode 字符。许多网页和其他文件格式使用 UTF-8。

‘ucs2’ – 两个字节,以小尾字节序(little-endian)编码的 Unicode 字符。它只能对 BMP(基本多文种平面,U+0000 – U+FFFF) 范围内的字符编码。

‘base64’ – Base64 字符串编码。

‘binary’ – 一种将原始二进制数据转换成字符串的编码方式,仅使用每个字符的前 8 位。这种编码方法已经过时,应当尽可能地使用 Buffer 对象。Node 的后续版本将会删除这种编码。

Buffer官方文档:http://nodejs.org/api/buffer.html

2 你该小心Buffer啦

像许多计算机的技术一样,都是从国外传播过来的。那些以英文作为母语的传 道者们应该没有考虑过英文以外的使用者,所以你有可能看到如下这样一段代码在向你描述如何在data事件中连接字符串。

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

如果这个文件读取流读取的是一个纯英文的文件,这段代码是能够正常输出的。但是如果我们再改变一下条件,将每次读取的buffer大小变成一个奇数,以模拟一个字符被分配在两个trunk中的场景

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

我们将会得到以下这样的乱码输出:

事件循���和请求���象构成了Node.js���异步I/O模 型的���个基本���素,这也是典���的消费���生产者场景。

造成这个问题的根源在于data += trunk语句里隐藏的错误,在默认的情况下,trunk是一个Buffer对象。这句话的实质是隐藏了toString的变换的:

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

由于汉字不是用一个字节来存储的,导致有被截破的汉字的存在,于是出现乱码。解决这个问题有一个简单的方案,是设置编码集:

var rs = fs.createReadStream('testdata.md', {encoding: 'utf-8', bufferSize: 11});

这将得到一个正常的字符串响应:

事件循环和请求对象构成了Node.js的异步I/O模型的两个基本元素 ,这也是典型的消费者生产者场景。

遗憾的是目前Node.js仅支持hex、utf8、ascii、binary、base64、ucs2几种编码的转换。对于那些因为历史遗留问题依旧还生存着的GBK,GB2312等编码, 该方法是无能为力的

3 有趣的string_decoder

在这个例子中,如果仔细观察,会发现一件有趣的事情发生在设置编码集之后。我们提到data += trunk等价于data = data.toString() + trunk.toString()。通过以下的代码可以测试到一个汉字占用三个字节,而我们按11个字节来截取trunk的话,依旧会存在一个汉字被分割在两个trunk中的情景。

console.log("事件循环和请求对象".length); console.log(new Buffer("事件循环和请求对象").length);

按照猜想的toString()方式,应该返回的是事件循xxx和请求xxx象才对,其 中“环”字应该变成乱码才对,但是在设置了encoding(默认的utf8)之后,结果却正常显示了,这个结果十分有趣。

输入图片说明

在好奇心的驱使下可以探查到data事件调用了string_decoder来进行编码补足的行为。通过string_decoder对象输出第一个截取Buffer(事件循xx)时,只返回事件循这个字符串,保留xx。第二次通过string_decoder对象输出时检测到上次保留的xx,将上次剩余内容和本次的Buffer进行重新拼接输出。于是达到正常输出的目的。

string_decoder,目前在文件流读取和网络流读取中都有应用到,一定程度上避免了粗鲁拼接trunk导致的乱码错误。但是,遗憾在于string_decoder目前只支持utf8编码。它的思路其实还可以扩展到其他编码上,只是最终是否会支持目前尚不可得知。

4 连接Buffer对象的正确方法

那么万能的适应各种编码而且正确的拼接Buffer对象的方法是什么呢?我们从 Node.js在github上的源码中找出这样一段正确读取文件,并连接buffer对象的方法:

var buffers = [];var nread = 0;readStream.on('data', function (chunk) {    buffers.push(chunk);    nread += chunk.length;});readStream.on('end', function () {     var buffer = null;     switch(buffers.length) {        case 0:             buffer = new Buffer(0);             break;        case 1:             buffer = buffers[0];             break;        default:            buffer = new Buffer(nread);            for (var i = 0, pos = 0, l = buffers.length; i < l; i++) {                var chunk = buffers[i];                chunk.copy(buffer, pos);                 pos += chunk.length;            }             break;    }});

在end事件中通过细腻的连接方式,最后拿到理想的Buffer对象。这时候无论是在支持的编码之间转换,还是在不支持的编码之间转换(利用iconv模块转换),都不会导致乱码。

5 简化连接Buffer对象的过程

上述一大段代码仅只完成了一件事情,就是连接多个Buffer对象,而这种场景需求将会在多个地方发生,所以,采用一种更优雅的方式来完成该过程是必要的。笔者基于以上的代码封装出一个bufferhelper模块,用于更简洁地处理Buffer对象。可以通过NPM进行安装:

npm install bufferhelper

下面的例子演示了如何调用这个模块。与传统data += trunk之间只是bufferHelper.concat(chunk)的差别,既避免了错误的出现,又使得代码可以得到简化而有效地编写。

var http = require('http');var BufferHelper = require('bufferhelper'); http.createServer(function (request, response) {    var bufferHelper = new BufferHelper();     request.on("data", function (chunk) {         bufferHelper.concat(chunk);    });    request.on('end', function () {        var html = bufferHelper.toBuffer().toString() ;        response.writeHead(200);         response.end(html);    }); }).listen(8001);

所以关于Buffer对象的操作的最佳实践是:

保持编码不变,以利于后续编码转换

使用封装方法达到简洁代码的目的

6 Buffer的基本使用

Buffer的基本使用,主要就是API所提供的操作,主要包括3个部分创建Buffer类、读Buffer、写Buffer

6.1 创建Buffer类

要创建一个Buffer的实例,我们要通过new Buffer来创建。新建文件buffer_new.js。

~ vi buffer_new.js// 长度为0Buffer实例var a = new Buffer(0);console.log(a);> <Buffer >// 长度为0Buffer实例相同,a1,a2是一个实例var a2 = new Buffer(0);console.log(a2);> <Buffer >// 长度为10Buffer实例var a10 = new Buffer(10);console.log(a10);> <Buffer 22 37 02 00 00 00 00 04 00 00>// 数组var b = new Buffer(['a','b',12])console.log(b);> <Buffer 00 00 0c>// 字符编码var b2 = new Buffer('你好','utf-8');console.log(b2);> <Buffer e4 bd a0 e5 a5 bd>

Buffer类有5个类方法,用于Buffer类的辅助操作。

  1. 编码检查,上文中提到Buffer和Javascript字符串转换时,需要显式的设置编码,那么这几种编码类型是Buffer所支持的。像中文处理只能使用utf-8编码,对于几年前常用的gbk,gb2312等编码是无法解析的。

    // 支持的编码console.log(Buffer.isEncoding('utf-8'))console.log(Buffer.isEncoding('binary'))console.log(Buffer.isEncoding('ascii'))console.log(Buffer.isEncoding('ucs2'))console.log(Buffer.isEncoding('base64'))console.log(Buffer.isEncoding('hex'))  # 16制进> true//不支持的编码console.log(Buffer.isEncoding('gbk'))console.log(Buffer.isEncoding('gb2312'))> false
  2. Buffer检查,很多时候我们需要判断数据的类型,对应后续的操作。

    // 是Buffer类console.log(Buffer.isBuffer(new Buffer('a')))> true// 不是Bufferconsole.log(Buffer.isBuffer('adfd'))console.log(Buffer.isBuffer('\u00bd\u00bd'))> false
  3. 字符串的字节长度,由于字符串编码不同,所以字符串长度和字节长度有时是不一样的。比如,1个中文字符是3个字节,通过utf-8编码输出就是4个中文字符,占12个字节。

    var str2 = '粉丝日志';console.log(str2 + ": " + str2.length + " characters, " + Buffer.byteLength(str2, 'utf8') + " bytes");> 粉丝日志: 4 characters, 12 bytesconsole.log(str2 + ": " + str2.length + " characters, " + Buffer.byteLength(str2, 'ascii') + " bytes");> 粉丝日志: 4 characters, 4 bytes
  4. Buffer的连接,用于连接Buffer的数组。我们可以手动分配Buffer对象合并后的Buffer空间大小,如果Buffer空间不够了,则数据会被截断。

    var b1 = new Buffer("abcd");var b2 = new Buffer("1234");var b3 = Buffer.concat([b1,b2],8);console.log(b3.toString());> abcd1234var b4 = Buffer.concat([b1,b2],32);console.log(b4.toString());console.log(b4.toString('hex'));//16进制输出> abcd1234   乱码....> 616263643132333404000000000000000000000000000000082a330200000000var b5 = Buffer.concat([b1,b2],4);console.log(b5.toString());> abcd
  5. Buffer的比较,用于Buffer的内容排序,按字符串的顺序。

    var a1 = new Buffer('10');var a2 = new Buffer('50');var a3 = new Buffer('123');// a1小于a2console.log(Buffer.compare(a1,a2));> -1// a2小于a3console.log(Buffer.compare(a2,a3));> 1// a1,a2,a3排序输出console.log([a1,a2,a3].sort(Buffer.compare));> [ <Buffer 31 30>, <Buffer 31 32 33>, <Buffer 35 30> ]// a1,a2,a3排序输出,以utf-8的编码输出console.log([a1,a2,a3].sort(Buffer.compare).toString());> 10,123,50

6.2 写入Buffer

把数据写入到Buffer的操作,新建文件buffer_write.js。

~ vi buffer_write.js//////////////////////////////// Buffer写入//////////////////////////////// 创建空间大小为64字节的Buffervar buf = new Buffer(64);// 从开始写入Buffer,偏移0var len1 = buf.write('从开始写入');// 打印数据的长度,打印Buffer的0到len1位置的数据console.log(len1 + " bytes: " + buf.toString('utf8', 0, len1));// 重新写入Buffer,偏移0,将覆盖之前的Buffer内存len1 = buf.write('重新写入');console.log(len1 + " bytes: " + buf.toString('utf8', 0, len1));// 继续写入Buffer,偏移len1,写入unicode的字符串var len2 = buf.write('\u00bd + \u00bc = \u00be',len1);console.log(len2 + " bytes: " + buf.toString('utf8', 0, len1+len2));// 继续写入Buffer,偏移30var len3 = buf.write('从第30位写入', 30);console.log(len3 + " bytes: " + buf.toString('utf8', 0, 30+len3));// Buffer总长度和数据console.log(buf.length + " bytes: " + buf.toString('utf8', 0, buf.length));// 继续写入Buffer,偏移30+len3var len4 = buf.write('写入的数据长度超过Buffer的总长度!',30+len3);// 超过Buffer空间的数据,没有被写入到Buffer中console.log(buf.length + " bytes: " + buf.toString('utf8', 0, buf.length));

输入图片说明

Node.js的节点的缓冲区,根据读写整数的范围,提供了不同宽度的支持,使从1到8个字节(8位、16位、32位)的整数、浮点数(float)、双精度浮点数(double)可以被访问,分别对应不同的writeXXX()函数,使用方法与buf.write()类似。

buf.write(string[, offset][, length][, encoding])buf.writeUIntLE(value, offset, byteLength[, noAssert])buf.writeUIntBE(value, offset, byteLength[, noAssert])buf.writeIntLE(value, offset, byteLength[, noAssert])buf.writeIntBE(value, offset, byteLength[, noAssert])buf.writeUInt8(value, offset[, noAssert])buf.writeUInt16LE(value, offset[, noAssert])buf.writeUInt16BE(value, offset[, noAssert])buf.writeUInt32LE(value, offset[, noAssert])buf.writeUInt32BE(value, offset[, noAssert])buf.writeInt8(value, offset[, noAssert])buf.writeInt16LE(value, offset[, noAssert])buf.writeInt16BE(value, offset[, noAssert])buf.writeInt32LE(value, offset[, noAssert])buf.writeInt32BE(value, offset[, noAssert])buf.writeFloatLE(value, offset[, noAssert])buf.writeFloatBE(value, offset[, noAssert])buf.writeDoubleLE(value, offset[, noAssert])buf.writeDoubleBE(value, offset[, noAssert])

另外,关于Buffer写入操作,还有一些Buffer类的原型函数可以操作。Buffer复制函数 buf.copy(targetBuffer[, targetStart][, sourceStart][, sourceEnd])

// 新建两个Buffer实例var buf1 = new Buffer(26);var buf2 = new Buffer(26);// 分别向2个实例中写入数据for (var i = 0 ; i < 26 ; i++) {    buf1[i] = i + 97; // 97是ASCII的a    buf2[i] = 50; // 50是ASCII的2}// 把buf1的内存复制给buf2buf1.copy(buf2, 5, 0, 10); // 从buf2的第5个字节位置开始插入,复制buf1的从0-10字节的数据到buf2中console.log(buf2.toString('ascii', 0, 25)); // 输入buf2的0-25字节> 22222abcdefghij2222222222

Buffer填充函数 buf.fill(value[, offset][, end])。

// 新建Buffer实例,长度20节节var buf = new Buffer(20);// 向Buffer中填充数据buf.fill("h");console.log(buf)> <Buffer 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68 68>console.log("buf:"+buf.toString())> buf:hhhhhhhhhhhhhhhhhhhh// 清空Buffer中的数据buf.fill();console.log("buf:"+buf.toString())> buf:

Buffer裁剪,buf.slice([start][, end])。返回一个新的缓冲区,它和旧缓冲区指向同一块内存,但是从索引 start 到 end 的位置剪裁

var buf1 = new Buffer(26);for (var i = 0 ; i < 26 ; i++) {    buf1[i] = i + 97;}// 从剪切buf1中的0-3的位置的字节,新生成的buf2是buf1的一个切片。var buf2 = buf1.slice(0, 3);console.log(buf2.toString('ascii', 0, buf2.length));> abc// 当修改buf1时,buf2同时也会发生改变buf1[0] = 33;console.log(buf2.toString('ascii', 0, buf2.length));> !bc

6.3 读取Buffer

我们把数据写入Buffer后,我们还需要把数据从Buffer中读出来,新建文件buffer_read.js。我们可以通过readXXX()函数获得对应该写入时编码的索引值,再转换原始值取出,有这种方法操作中文字符就会变得麻烦,最常用的读取Buffer的方法,其实就是toString()

~ vi buffer_read.js//////////////////////////////// Buffer 读取//////////////////////////////var buf = new Buffer(10);for (var i = 0 ; i < 10 ; i++) {    buf[i] = i + 97;}console.log(buf.length + " bytes: " + buf.toString('utf-8'));> 10 bytes: abcdefghij// 读取数据for (ii = 0; ii < buf.length; ii++) {    var ch = buf.readUInt8(ii); // 获得ASCII索引    console.log(ch + ":"+ String.fromCharCode(ch));}> 97:a98:b99:c100:d101:e102:f103:g104:h105:i106:j

写入中文数据,以readXXX进行读取,会3个字节来表示一个中文字。

var buf = new Buffer(10);buf.write('abcd')buf.write('数据',4)for (var i = 0; i < buf.length; i++) {    console.log(buf.readUInt8(i));}>979899100230  // 230,149,176 代表“数”149176230  // 230,141,174 代表“据”141174

如果想输出正确的中文,那么我们可以用toString(‘utf-8’)的函数来操作。

console.log("buffer :"+buf); // 默认调用了toString()的函数> buffer :abcd数据console.log("utf-8  :"+buf.toString('utf-8'));> utf-8  :abcd数据console.log("ascii  :"+buf.toString('ascii'));//有乱码,中文不能被正确解析> ascii  :abcdf 0f.console.log("hex    :"+buf.toString('hex')); //16进制> hex    :61626364e695b0e68dae

对于Buffer的输出,我们用的最多的操作就是toString(),按照存入的编码进行读取。除了toString()函数,还可以用toJSON()直接Buffer解析成JSON对象

var buf = new Buffer('test');console.log(buf.toJSON());> { type: 'Buffer', data: [ 116, 101, 115, 116 ] }

7 Buffer的性能测试

7.1 8K的创建测试

每次我们创建一个新的Buffer实例时,都会检查当前Buffer的内存池是否已经满,当前内存池对于新建的Buffer实例是共享的,内存池的大小为8K

如果新创建的Buffer实例大于8K时,就把Buffer交给SlowBuffer实例存储如果新创建的Buffer实例小于8K,同时小于当前内存池的剩余空间,那么这个Buffer存入当前的内存池如果Buffer实例不大于0,则统一返回默认的zerobuffer实例

下面我们创建2个Buffer实例,第一个是以4k为空间,第二个以4.001k为空间,循环创建10万次。

var num = 100*1000;console.time("test1");for(var i=0;i<num;i++){    new Buffer(1024*4);}console.timeEnd("test1");> test1: 132msconsole.time("test2");for(var j=0;j<num;j++){    new Buffer(1024*4+1);}console.timeEnd("test2");> test2: 163ms

第二个以4.001k为空间的耗时多23%,这就意味着第二个,每二次循环就要重新申请一次内存池的空间。这是需要我们非常注意的。

7.2 多Buffer还是单一Buffer

当我们需要对数据进行缓存时,创建多个小的Buffer实例好,还是创建一个大的Buffer实例好?比如我们要创建1万个长度在1-2048之间不等的字符串。

var max = 2048;     //最大长度var time = 10*1000; //循环1万次// 根据长度创建字符串function getString(size){    var ret = ""    for(var i=0;i<size;i++) ret += "a";    return ret;}// 生成字符串数组,1万条记录var arr1=[];for(var i=0;i<time;i++){    var size = Math.ceil(Math.random()*max)    arr1.push(getString(size));}//console.log(arr1);// 创建1万个小Buffer实例console.time('test3');var arr_3 = [];for(var i=0;i<time;i++){    arr_3.push(new Buffer(arr1[i]));}console.timeEnd('test3');> test3: 217ms// 创建一个大实例,和一个offset数组用于读取数据。console.time('test4');var buf = new Buffer(time*max);var offset=0;var arr_4=[];for(var i=0;i<time;i++){    arr_4[i]=offset;    buf.write(arr1[i],offset,arr1[i].length);    offset=offset+arr1[i].length;}console.timeEnd('test4');> test4: 12ms

读取索引为2的数据:

console.log("src:[2]="+arr1[2]);console.log("test3:[2]="+arr_3[2].toString());console.log("test4:[2]="+buf.toString('utf-8',arr_4[2],arr_4[3]));

运行结果如图所示:

输入图片说明

对于这类的需求来说,提前生成一个大的Buffer实例进行存储,要比每次生成小的Buffer实例高效的多,能提升一个数量级的计算效率。所以,理解并用好Buffer是非常重要的!!

7.3 string VS Buffer

有了Buffer我们是否需求把所有String的连接,都换成Buffer的连接?那么我们就需要测试一下,String和Buffer做字符串连接时,哪个更快一点?

下面我们进行字符串连接,循环30万次:

//测试三,Buffer VS stringvar time = 300*1000;var txt = "aaa"var str = "";console.time('test5')for(var i=0;i<time;i++){    str += txt;}console.timeEnd('test5')> test5: 24msconsole.time('test6')var buf = new Buffer(time * txt.length)var offset = 0;for(var i=0;i<time;i++){    var end = offset + txt.length;    buf.write(txt,offset,end);    offset=end;}console.timeEnd('test6')> test6: 85ms

从测试结果,我们可以明显的看到,String对字符串的连接操作,要远快于Buffer的连接操作。所以我们在保存字符串的时候,该用string还是要用string。那么只有在保存非utf-8的字符串以及二进制数据的情况,我们才用Buffer

0 0
原创粉丝点击