nodeJS的buffer详解

JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。

但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。

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

所谓缓冲区Buffer,就是 “临时存贮区” 的意思,是暂时存放输入输出数据的一段内存。

JS语言自身只有字符串数据类型,没有二进制数据类型,因此NodeJS提供了一个与String对等的全局构造函数Buffer来提供对二进制数据的操作。除了可以读取文件得到Buffer的实例外,还能够直接构造

var buffer = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]) ; 

Buffer与字符串类似,除了可以用.length属性得到字节长度外,还可以用[index]方式读取指定位置的字节,例如:

buffer[0] ; // 0x68;

“`

1. 什么是Buffer #

  • 缓冲区Buffer是暂时存放输入输出数据的一段内存。
  • JS语言没有二进制数据类型,而在处理TCP和文件流的时候,必须要处理二进制数据。
  • NodeJS提供了一个Buffer对象来提供对二进制数据的操作
  • 是一个表示固定内存分配的全局对象,也就是说要放到缓存区中的字节数需要提前确定
  • Buffer好比由一个多位字节元素组成的数组,可以有效的在javascript中表示二进制数据

2. 什么是字节 #

  • 字节(Byte)是计算机存储时的一种计量单位,一个字节等于8位二进制数
  • 一个位就代表一个0或1,每8个位(bit)组成一个字节(Byte)
  • 字节是通过网络传输信息的单位
  • 一个字节最大值十进制数是255
var sum =0;
for(var i=0;i<8;i++){
  sum += Math.pow(2,i);
}

3. 定义buffer的三种方式 #

3.1 通过长度定义buffer #

new Buffer(size);

3.2 通过数组定义buffer #

new Buffer(array);

正常情况下为0-255之间;

3.3 字符串创建 #

new Buffer(str,[encoding]);

4.buffer常用方法 #

4.1 fill方法 #

手动初始化,擦干净桌子,将buffer内容清0

buffer.fill(0);

4.2 write方法 #

string, offset, length, encoding

buffer.write('珠',0,3,'utf8');
buffer.write('峰',3,3,'utf8'); //珠峰

4.3 toString方法 #

将buffer转换成字符串类型 start end 是截取的buffer的长度

buffer.toString('utf8',3,6)

4.4 slice方法 #

buffer.slice(0,4);

截取乱码问题

var StringDecoder  = require('string_decoder').StringDecoder;
var sd = new StringDecoder;
var buffer = new Buffer('珠峰');
console.log(sd.write(buffer.slice(0,4)));
console.log(sd.write(buffer.slice(4)));

4.5 copy方法 #

复制Buffer 把多个buffer拷贝到一个大buffer上

sourceBuffer.copy(targetBuffer,targetstart,sourcestart,sourceend);

4.6 concat方法 #

Buffer.concat([buf1,buf2],length);

实现concat方法

4.7 isBuffer #

判断是否是buffer

Buffer.isBuffer

4.8 length #

获取字节长度(显示是字符串所代表buffer的长度)

Buffer.byteLength("珠峰");
buffer.length;

5.进制转换 #

  • 将任意进制字符串转换为十进制
    • parseInt(“11”, 2); // 3 2进制转10进制
    • parseInt(“77”, 8); // 63 8进制转10进制
    • parseInt(“e7”, 16); //175 16进制转10进制
  • 将10进制转换为其它进制字符串
    • (3).toString(2)) // “11” 十进制转2进制
    • (17).toString(16) // “11” 十进制转16进制
    • (33).toString(32) // “11” 十提制转32进制

base64的转换

什么时候该用buffer,什么时候不该用

我看一下如下的测试代码,分别是拼接各种不同长度的字符串,最后直接拼接了10MB的字符串

var string,string2,string3;
var bufstr,bufstr2,bufstr3;
var j;

console.time('write 100 string')
for(j=0;j<1000;j++){
	var x = j+'';
	string += x;
}
console.timeEnd('write 100 string')

console.time('write 100 buffer')
bufstr = new Buffer(100)
for(j=0;j<1000;j++){
	var x = j+'';
	bufstr.write(x,j);
}
console.timeEnd('write 100 buffer')


console.time('write 100000 string')
for(j=0;j<100000;j++){
	var x = j+'';
	string2 += x;
}
console.timeEnd('write 100000 string')

console.time('write 100000 buffer')
bufstr2 = new Buffer(100000)
for(j=0;j<100000;j++){
	var x = j+'';
	bufstr2.write(x,j);
}
console.timeEnd('write 100000 buffer')

console.time('write 1024*1024*10 string')
for(j=0;j<1024*1024*10;j++){
	var x = j+'';
	string3 += x;
}
console.timeEnd('write 1024*1024*10 string')

console.time('write 1024*1024*10 buffer')
bufstr3 = new Buffer(1024*1024*10)
for(j=0;j<1024*1024*10;j++){
	var x = j+'';
	bufstr3.write(x,j);
}
console.timeEnd('write 1024*1024*10 buffer')

接着是输出结果:

write 100 string: 0ms
write 100 buffer: 6ms
write 100000 string: 37ms
write 100000 buffer: 150ms
write 1024*1024*10 string: 4262ms
write 1024*1024*10 buffer: 8904ms

读取速度都不需要测试了,肯定string更快,buffer还需要toString()的操作。所以我们在保存字符串的时候,该用string还是要用string,就算大字符串拼接string的速度也不会比buffer慢。那什么时候我们又需要用buffer呢?没办法的时候,当我们保存非utf-8字符串,2进制等等其他格式的时候,我们就必须得使用了。

buffer不得不提的8KB

buffer著名的8KB载体,举个例子好比,node把一幢大房子分成很多小房间,每个房间能容纳8个人,为了保证房间的充分使用,只有当一个房间塞满8个人后才会去开新的房间,但是当一次性有多个人来入住,node会保证要把这些人放到一个房间中,比如当前房间A有4个人住,但是一下子来了5个人,所以node不得不新开一间房间B,把这5个人安顿下来,此时又来了4个人,发现5个人的B房间也容纳不下了,只能再开一间房间C了,这样所有人都安顿下来了。但是之前的两间房A和B都各自浪费了4个和3个位置,而房间C就成为了当前的房间。

具体点说就是当我们实例化一个新的Buffer类,会根据实例化时的大小去申请内存空间,如果需要的空间小于8KB,则会多一次判定,判定当前的8KB载体剩余容量是否够新的buffer实例,如果够用,则将新的buffer实例保存在当前的8KB载体中,并且更新剩余的空间。

我们做个简单的实验,模拟一个比较严重的内存泄露情况:

第一次我们将内存泄漏点那行代码注释掉,运行4分钟后,得到如下打印信息,V8已经自动把我分配的内存释放掉了,free men又回到了开始的数值,第二次我们将泄漏点那行代码放开,让全局变量 leak_buf_ary 始终引用着buffer,同样执行10分钟

var os = require('os');
var leak_buf_ary = [];
var show_memory_usage = function(){ //打印系统空闲内存
	console.log('free mem : ' + Math.ceil(os.freemem()/(1024*1024)) + 'mb');
}

var do_buf_leak = function(){
	var leak_char = 'l'; //泄露的几byte字符
	var loop = 100000;//10万次
	var buf1_ary = []
	while(loop--){
		buf1_ary.push(new Buffer(4096)); //申请buf1,占用4096byte空间,会得到自动释放

		//申请buf2,占用几byte空间,将其引用保存在外部数据,不会自动释放
		//*******
		leak_buf_ary.push(new Buffer(loop+leak_char));
		//*******
	}
	console.log("before gc")
	show_memory_usage();
	buf1_ary = null;
	return;
}


console.log("process start")
show_memory_usage()

do_buf_leak();

var j =10000;
setInterval(function(){
	console.log("after gc")
	show_memory_usage()
},1000*60)

第一次结果:

process start
free mem : 5362mb
before gc
free mem : 5141mb
after gc
free mem : 5163mb
after gc
free mem : 5151mb
after gc
free mem : 5148mb
after gc
free mem : 5556mb

第二次结果:

process start
free mem : 5692mb
before gc
free mem : 4882mb
after gc
free mem : 4848mb
after gc
free mem : 4842mb
after gc
free mem : 4843mb
after gc
free mem : 4816mb
after gc
free mem : 4822mb
after gc
free mem : 4816mb
after gc
free mem : 4809mb
after gc
free mem : 4810mb
after gc
free mem : 4831mb
after gc
free mem : 4830mb

虽然我们释放了4096byte的buffer,但是由于那几byte的字节没有释放掉,将会造成整个8KB的内存都无法释放,如果继续执行循环最终我们的系统内存将耗尽,程序将crash。同样由于我们是依次循环分配 4096+几 byte内存的,所以每块8KB的内存空间都将浪费409Xbyte,在执行循环之后,我们明显发现第二次的内存占用比第一次要大很多。这里我们将近多出了300MB左右的内存消耗。

3、buffer字符串的连接 我们接受post数据时,node是以流的形式发送上来的,会触发ondata事件,所以我们见到很多代码是这样写的:

var http = require('http');
 http.createServer(function (req, res) {
  
  var body = '';
  req.on('data',function(chunk){
	//console.log(Buffer.isBuffer(chunk))
	body +=chunk
  })
  req.on('end',function(){
	 console.log(body)
	 res.writeHead(200, {'Content-Type': 'text/plain'});
         res.end('Hello World\n');
  })
  
 
}).listen(8124);

console.log('Server running at http://127.0.0.1:8124/');

下面我们比较一下两者的性能区别,测试代码:

var buf = new Buffer('nodejsv0.10.4&nodejsv0.10.4&nodejsv0.10.4&nodejsv0.10.4&');
console.time('string += buf')
var s = '';
for(var i=0;i<10000;i++){
	s += buf;
}
s;
console.timeEnd('string += buf')


console.time('buf concat')
var list = [];
var len=0;
for(var i=0;i<10000;i++){
	list.push(buf);
	len += buf.length;
}
var s2 = Buffer.concat(list, len).toString();
console.timeEnd('buf concat')

输出结果,相差近一倍:

string += buf: 15ms
buf concat: 8ms

在1000次拼接过程中,两者的性能几乎相差一倍,而且当客户上传的是非UTF8的字符串时,直接+=还容易出现错误。

4、独享的空间 如果你想创建一个独享的空间,独立的对这块内存空间进行读写,有两种办法,1是实例化一个超过8KB长度的buffer,另外一个就是使用slowbuffer类。

5、buffer的释放 很遗憾,我们无法手动对buffer实例进行GC,只能依靠V8来进行,我们唯一能做的就是解除对buffer实例的引用。

6、清空buffer 刷掉一块buffer上的数据最快的办法是buffer.fill

四,总结一下

(1),JavaScript适合处理Unicode编码数据,但对二进制数据的处理并不友好。
(2),所以处理TCP流或文件系统时,对八位字节流的处理很有必要。
(3),Node有几个用于处理,创建和消耗八位字节流的方法。
(4),原始数据存放在一个Buffer实例中,一个Buffer类似一个整数数组,但是它的内存,分配在V8堆栈外。一个Buffer的大小是不能更改的。
(5),处理的编码类型有:ascii,utf8,utf16le,ucs2(utf16le的别名),base64,binary,hex。
(6),Buffer为全局元素,直接new Buffer()就得到一个Buffer实例。


~~~

未经允许不得转载:WEB前端开发 » nodeJS的buffer详解

赞 (1)