浅析node的buffer模块(三读取)

2013-05-08 23:37

浅析node的buffer模块(三读取)

by snoopyxdy

at 2013-05-08 15:37:41

original http://snoopyxdy.blog.163.com/blog/static/601174402013480524058

最后我们看一下buffer的读取是如何进行的,buffer的读取主要包括以下几个api
1、buf = new Buffer(),然后就可以直接读取buf
2、buf.toString([encoding], [start], [end])
3、buf.toJSON()
4、buf[index]
5、buf.slice([start], [end])
6、buf.readUInt8(offset, [noAssert])等其他read操作

1、buf = new Buffer() 和 buf[index]
创建一个buf实例,会返回一个buf数组给这个实例,就像我们直接打印buf会出现以下内容:

var buf = new Buffer(100)
console.log(buf)
//输出随机的一些16进制内存单元
<Buffer 8b 42 0f 4d 8b 55 08 4c 39 50 ff 0f 85 61 00 00 00 48 3b 48 07 0f 83 26 01 00 00 48 8b d9 48 c1 fb 1d 48 8b 5c 18 0f 49 3b 5d b0 0f 84 10 01 00 00 48 8b ...>

通过 Buffer::MakeFastBuffer 这个方法来关联起来的,我们看主要代码:

Handle<Value> Buffer::MakeFastBuffer(const Arguments &args) {
HandleScope scope;

if (!Buffer::HasInstance(args[0])) {
return ThrowTypeError("First argument must be a Buffer");
}

Buffer *buffer = ObjectWrap::Unwrap<Buffer>(args[0]->ToObject());
Local<Object> fast_buffer = args[1]->ToObject();;
uint32_t offset = args[2]->Uint32Value();
uint32_t length = args[3]->Uint32Value();

// ..省略了一些验证代码
 

//最主要是下面这句,将this关联索引属性
fast_buffer->SetIndexedPropertiesToExternalArrayData(buffer->data_ + offset,
kExternalUnsignedByteArray,
length);

return Undefined();
}

这样我们的buf实例看起来就像是一个数组了

2、buf.toString([encoding], [start], [end])
这个是我们读取buf最常用的方法,我们可以将一些字符串存入buf,然后使用toString方法将他们取出来。

// toString(encoding, start=0, end=buffer.length)
Buffer.prototype.toString = function(encoding, start, end) {
  encoding = String(encoding || 'utf8').toLowerCase(); //将encoding参数小写

  if (typeof start !== 'number' || start < 0) { //将start参数合法化
    start = 0;
  } else if (start > this.length) {
    start = this.length;
  }

  if (typeof end !== 'number' || end > this.length) { //将end参数合法化
    end = this.length;
  } else if (end < 0) {
    end = 0;
  }

  start = start + this.offset; //加上偏移量,这个offset很重要,代表共享buffer的偏移
  end = end + this.offset;

  switch (encoding) {
    case 'hex':
      return this.parent.hexSlice(start, end);

    case 'utf8':
    case 'utf-8':
      return this.parent.utf8Slice(start, end); //我们主要看这个,utf8用的比较多

    case 'ascii':
      return this.parent.asciiSlice(start, end);

    case 'binary':
      return this.parent.binarySlice(start, end);

    case 'base64':
      return this.parent.base64Slice(start, end);

    case 'ucs2':
    case 'ucs-2':
    case 'utf16le':
    case 'utf-16le':
      return this.parent.ucs2Slice(start, end);

    default:
      throw new TypeError('Unknown encoding: ' + encoding);
  }
};

C++代码

Handle<Value> Buffer::Utf8Slice(const Arguments &args) {
  HandleScope scope;
  Buffer *parent = ObjectWrap::Unwrap<Buffer>(args.This());
  SLICE_ARGS(args[0], args[1])
  char *data = parent->data_ + start;
//static V8EXPORT Local<String> New (const char *data, int length=-1) V8手册上的说明
//分配一个新的string对象
  Local<String> string = String::New(data, end - start); //将char* 转为Local<string>
  return scope.Close(string);
}

这样我们就拿到了utf-8格式的字符串了

3、buf.slice([start], [end])
这个是对buf进行剪切的功能,我们看下代码:

// slice(start, end)
Buffer.prototype.slice = function(start, end) {
var len = this.length; //合法性验证
start = clamp(start, len, 0); //clamp函数也是做合法性验证的,会返回合适的start和end值
end = clamp(end, len, len);
return new Buffer(this.parent, end - start, start + this.offset); //最后slice是依靠重新创建一个buffer实例来完成剪切工作的
};

然后可以参考buffer的构造函数来追踪slice的创建

4、总结
我们大致了解了buffer的工作机制之后,我们的日常工作有了一些注意,什么时候该用buffer,什么时候不该用,使用buffer应该注意哪些问题?
1、什么时候该用buffer,什么时候不该用buffer
写入速度的测试:

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进制等等其他格式的时候,我们就必须得使用了。

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载体中,并且更新剩余的空间。

之前我的博客中有模拟过一个比较严重的buffer内存泄露情况,所以一定要注意,就算只有1byte的buffer空间没释放掉,整个8KB的内存都不会被V8释放。

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.fill