#0 系列目录#html
Javascript是为浏览器而设计的,能很好的处理unicode编码的字符串,但对于二进制或非unicode编码的数据就显得无能为力。Node.js继承Javascript的语言特性,同时又扩展了Javascript语言,为二进制的数据处理提供了Buffer类,让Node.js能够像其余程序语言同样,能处理各类类型的数据了。node
#1 Buffer介绍# 在Node.js中,Buffer类是随Node内核一块儿发布的核心库。Buffer库为Node.js带来了一种存储原始数据的方法,可让Nodejs处理二进制数据,每当须要在Nodejs中处理I/O操做中移动的数据时,就有可能使用Buffer库
。原始数据存储在 Buffer 类的实例中。一个 Buffer 相似于一个整数数组
,但它对应于 V8 堆内存以外的一块原始内存。git
Buffer 和 Javascript 字符串对象之间的转换须要显式地调用编码方法来完成
。如下是几种不一样的字符串编码:github
‘ascii’ – 仅用于 7 位 ASCII 字符。这种编码方法很是快,而且会丢弃高位数据。npm
‘utf8’ – 多字节编码的 Unicode 字符。许多网页和其余文件格式使用 UTF-8。api
‘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、utf八、ascii、binary、base6四、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 // 长度为0的Buffer实例 var a = new Buffer(0); console.log(a); > <Buffer > // 长度为0的Buffer实例相同,a1,a2是一个实例 var a2 = new Buffer(0); console.log(a2); > <Buffer > // 长度为10的Buffer实例 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类的辅助操做。
// 支持的编码 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
// 是Buffer类 console.log(Buffer.isBuffer(new Buffer('a'))) > true // 不是Buffer console.log(Buffer.isBuffer('adfd')) console.log(Buffer.isBuffer('\u00bd\u00bd')) > false
var str2 = '粉丝日志'; console.log(str2 + ": " + str2.length + " characters, " + Buffer.byteLength(str2, 'utf8') + " bytes"); > 粉丝日志: 4 characters, 12 bytes console.log(str2 + ": " + str2.length + " characters, " + Buffer.byteLength(str2, 'ascii') + " bytes"); > 粉丝日志: 4 characters, 4 bytes
var b1 = new Buffer("abcd"); var b2 = new Buffer("1234"); var b3 = Buffer.concat([b1,b2],8); console.log(b3.toString()); > abcd1234 var b4 = Buffer.concat([b1,b2],32); console.log(b4.toString()); console.log(b4.toString('hex'));//16进制输出 > abcd1234 乱码.... > 616263643132333404000000000000000000000000000000082a330200000000 var b5 = Buffer.concat([b1,b2],4); console.log(b5.toString()); > abcd
var a1 = new Buffer('10'); var a2 = new Buffer('50'); var a3 = new Buffer('123'); // a1小于a2 console.log(Buffer.compare(a1,a2)); > -1 // a2小于a3 console.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字节的Buffer var buf = new Buffer(64); // 从开始写入Buffer,偏移0 var 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,偏移30 var 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+len3 var 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的内存复制给buf2 buf1.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:a 98:b 99:c 100:d 101:e 102:f 103:g 104:h 105:i 106: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)); } >97 98 99 100 230 // 230,149,176 表明“数” 149 176 230 // 230,141,174 表明“据” 141 174
若是想输出正确的中文,那么咱们能够用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: 132ms console.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 string var time = 300*1000; var txt = "aaa" var str = ""; console.time('test5') for(var i=0;i<time;i++){ str += txt; } console.timeEnd('test5') > test5: 24ms console.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
。