浅析node的buffer模块(二写入)

2013-05-08 18:48

浅析node的buffer模块(二写入)

by snoopyxdy

at 2013-05-08 10:48:20

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

好久没更新博客了,本文继续上篇介绍nodejs的buffer类的写入和操作机制。
相关buffer类的写入和操作方法大致有如下几个:

Class Method: Buffer.concat(list, [totalLength])
new Buffer(str, [encoding])
buf.copy(targetBuffer, [targetStart], [sourceStart], [sourceEnd])
buf.write(string, [offset], [length], [encoding])
buf.fill(value, [offset], [end])
还有各种wrtieInt16等方法

1、Class Method: Buffer.concat(list, [totalLength])
源码在lib/buffer.js#473行(node v0.10.4)
大致工作流程:
1.判断list参数是否是数组
2.判断list数组的长度,如果是0则返回空buffer,如果是1则直接返回list的一个内容,这里要保证list中保存的数据类型都是buffer实例
3.如果totalLength参数不是数字,则循环list数组将list数组中的每项的字符长度加起来,所以api上说如果提供了正确的totalLength将会加快concat的速度
4.得到totalLength之后,创建一个临时的buffer实例,大小为totalLength。
5.循环遍历list数组,调用list数组中成员的copy方法,将内容拷贝到目标临时buffer中去,然后记录位置,下次循环从记录的那个位置开始拷贝
6.最后返回这个临时buffer的引用


2、new Buffer(str, [encoding])
当我们创建一个string的buffer实例时,将会去写入字符串,我们看下代码:

if (type === 'string') {
        // We are a string
        this.length = this.write(subject, 0, encoding);
      // if subject is buffer then use built-in copy method
      } 

subject参数表示的就是传入的参数string,调用的是buffer实例的write方法


3、buf.copy(targetBuffer, [targetStart], [sourceStart], [sourceEnd])
buffer实例的copy方法代码如下:

// copy(targetBuffer, targetStart=0, sourceStart=0, sourceEnd=buffer.length)
Buffer.prototype.copy = function(target, target_start, start, end) {
// set undefined/NaN or out of bounds values equal to their default

//各种参数的合理化
if (!(target_start >= 0)) target_start = 0;
if (!(start >= 0)) start = 0;
if (!(end < this.length)) end = this.length;

// Copy 0 bytes; we're done
if (end === start || //如果源的开始 等于 源的结束
target.length === 0 || //如果目标的buffer大小为0,
this.length === 0 || //如果源的大小为0,
start > this.length) //如果源开始的位置大于源的buffer大小
return 0; //即拷贝0,则返回0

if (end < start)//如果源的起始位置大于结束位置
throw new RangeError('sourceEnd < sourceStart');

if (target_start >= target.length) //如果目标的其实拷贝位置大于目标的大小
throw new RangeError('targetStart out of bounds');

if (target.length - target_start < end - start) //如果目标的剩余大小小于要拷贝入的大小
end = target.length - target_start + start; //调整end的位置


return this.parent.copy(target.parent || target, //最后调用buffer实例的parent的copy方法来进行拷贝,这里的parent属性就对应着slowbuffer对象,所以这个copy方法也是对应它的
target_start + (target.offset || 0),
start + this.offset,
end + this.offset);
};

我们看一下node_buffer.cc中是如何实现copy操作的:
// var bytesCopied = buffer.copy(target, targetStart, sourceStart, sourceEnd);
Handle<Value> Buffer::Copy(const Arguments &args) {
  HandleScope scope;

  Buffer *source = ObjectWrap::Unwrap<Buffer>(args.This()); //将buffer实例解除对象的包装

  if (!Buffer::HasInstance(args[0])) { //检查第一个参数是否是Buffer类的实例
    return ThrowTypeError("First arg should be a Buffer");
  }

  Local<Value> target = args[0];
  char* target_data = Buffer::Data(target); //根据inline的两个 Buffer::Data 获得源的内容
  size_t target_length = Buffer::Length(target); //根据inline的两个Buffer::Length函数得到目标buffer的大小
  size_t target_start = args[1]->IsUndefined() ? 0 : args[1]->Uint32Value(); //获得目标buffer写入位置
  size_t source_start = args[2]->IsUndefined() ? 0 : args[2]->Uint32Value(); //获得读取其实位置
  size_t source_end = args[3]->IsUndefined() ? source->length_ //获得源的读取结束位置
                                              : args[3]->Uint32Value();

  if (source_end < source_start) { //和buffer.js一样进行合法性验证,不多解释,下同
    return ThrowRangeError("sourceEnd < sourceStart");
  }

  // Copy 0 bytes; we're done
  if (source_end == source_start) {
    return scope.Close(Integer::New(0));
  }

  if (target_start >= target_length) {
    return ThrowRangeError("targetStart out of bounds");
  }

  if (source_start >= source->length_) {
    return ThrowRangeError("sourceStart out of bounds");
  }

  if (source_end > source->length_) {
    return ThrowRangeError("sourceEnd out of bounds");
  }
//执行两次MIN方法,第一次返回源的开始到结束的空间 和 目标的开始到结束的空间的 想比较小的数值
//第二次比较上述的值和源的长度到起始值的值的较小值,保证将拷贝的数值安全值
  size_t to_copy = MIN(MIN(source_end - source_start,
                           target_length - target_start),
                           source->length_ - source_start);

  // need to use slightly slower memmove is the ranges might overlap
//需要使用稍微慢的memove,因为返回可能重叠
  memmove((void *)(target_data + target_start), //传入指向目标buffer的开始位置之后的指针
          (const void*)(source->data_ + source_start), //传入指向源的开始位置之后的指针
          to_copy); //传入需要拷贝的长度

  return scope.Close(Integer::New(to_copy)); //最后返回整形的拷贝了多少长度
}

注:strcpy只能处理字符串;如果拷贝带有特殊字符的串,就只能用memcpy或memmove。memcpy和memmove功能基本上差不多,但是当源串和目标串有Overlap时,memmove可以正确处理,memcpy则不行,声明如下:

void *memcpy(void *dst, const void *src, size_t count);
void *memmove(void *dst, const void *src, size_t count);

它们都是从src所指向的内存中复制count个字节到dst所指内存中,并返回dst的值。

4、buf.write(string, [offset], [length], [encoding])
这个方法是写入字符串的方法,代码如下:

// write(string, offset = 0, length = buffer.length-offset, encoding = 'utf8')
Buffer.prototype.write = function(string, offset, length, encoding) {
// Support both (string, offset, length, encoding)
// and the legacy (string, encoding, offset, length)

//对参数的各种支持
if (isFinite(offset)) { //如果偏移位置不是数字
if (!isFinite(length)) { //写入buffer的长度如果是数字
encoding = length; //则将参数encoding编码改为长度
length = undefined; //将参数length改为未定义
}
} else { // legacy
var swap = encoding;
encoding = offset;
offset = length;
length = swap;
}

offset = +offset || 0; //初始化offset
var remaining = this.length - offset;
if (!length) {
length = remaining;
} else {
length = +length;
if (length > remaining) {
length = remaining;
}
}
encoding = String(encoding || 'utf8').toLowerCase();

var ret;
switch (encoding) {
case 'hex':
ret = this.parent.hexWrite(string, this.offset + offset, length);
break;

case 'utf8':
case 'utf-8':
ret = this.parent.utf8Write(string, this.offset + offset, length); //如果是utf-8则调用utf-8的接口
break;

case 'ascii':
ret = this.parent.asciiWrite(string, this.offset + offset, length);
break;

case 'binary':
ret = this.parent.binaryWrite(string, this.offset + offset, length);
break;

case 'base64':
// Warning: maxLength not taken into account in base64Write
ret = this.parent.base64Write(string, this.offset + offset, length);
break;

case 'ucs2':
case 'ucs-2':
case 'utf16le':
case 'utf-16le':
ret = this.parent.ucs2Write(string, this.offset + offset, length);
break;

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

Buffer._charsWritten = SlowBuffer._charsWritten; //同步记录多少字符被写入了

return ret;

};

utf8Write的代码如下,其实其他write代码都大同小异的

// var charsWritten = buffer.utf8Write(string, offset, [maxLength]);
Handle<Value> Buffer::Utf8Write(const Arguments &args) {
  HandleScope scope;
  Buffer *buffer = ObjectWrap::Unwrap<Buffer>(args.This());

  if (!args[0]->IsString()) { //有效性检查
    return ThrowException(Exception::TypeError(String::New(
            "Argument must be a string")));
  }

  Local<String> s = args[0]->ToString();

  size_t offset = args[1]->Uint32Value();

  int length = s->Length();

  if (length == 0) {
    constructor_template->GetFunction()->Set(chars_written_sym,
                                             Integer::New(0)); //将写入的字符数设置为0
    return scope.Close(Integer::New(0)); //返回0
  }

  if (length > 0 && offset >= buffer->length_) { //如果偏移大于等于整个buffer的长度则抛出错误
    return ThrowTypeError("Offset is out of bounds");
  }

  size_t max_length = args[2]->IsUndefined() ? buffer->length_ - offset
                                             : args[2]->Uint32Value(); //设置最大写入字符大小
  max_length = MIN(buffer->length_ - offset, max_length); //取 写入buffer的剩余值 和 用户传入的maxlength的小的一个值

  char* p = buffer->data_ + offset;//将指针偏移到写入的位置

  int char_written;

 //调用V8的writeUTF-8写入buffer的接口,返回写入的字符大小
//V8EXPORT int WriteUtf8 (char *buffer, int length=-1, int *nchars_ref=NULL, int options=NO_OPTIONS) const
  int written = s->WriteUtf8(p,
                             max_length,
                             &char_written,
                             (String::HINT_MANY_WRITES_EXPECTED |
                              String::NO_NULL_TERMINATION));

  constructor_template->GetFunction()->Set(chars_written_sym,
                                           Integer::New(char_written));

  return scope.Close(Integer::New(written)); //返回写入的字符数
}


5、buf.fill(value, [offset], [end])
向buffer中填充value,和write有点像,不过区别是fill会将整个范围填入相同的内容,会是value字符串的第一个字符的Unicode 编码。

// fill(value, start=0, end=buffer.length)
Buffer.prototype.fill = function fill(value, start, end) {
//初始化参数

value || (value = 0);
start || (start = 0);
end || (end = this.length); 


//校验字符串有效性
if (typeof value === 'string') {
value = value.charCodeAt(0); //获取字符串第一个Unicode 编码
}


//下面各种合法性检查
if (typeof value !== 'number' || isNaN(value)) {
throw new TypeError('value is not a number');
}

if (end < start) throw new RangeError('end < start');

// Fill 0 bytes; we're done
if (end === start) return 0;
if (this.length == 0) return 0;

if (start < 0 || start >= this.length) {
throw new RangeError('start out of bounds');
}

if (end < 0 || end > this.length) {
throw new RangeError('end out of bounds');
}

//调用slowerbuffer的fill方法,传入参数

//这里的offset还记得吗?就是当前buffer如果共享则会记录offset共享内存的便宜量
return this.parent.fill(value,
start + this.offset,
end + this.offset);
};

最后我们看下Buffer::fill方法:

// buffer.fill(value, start, end);
Handle<Value> Buffer::Fill(const Arguments &args) {
HandleScope scope;


//参数检查

if (!args[0]->IsInt32()) {
return ThrowException(Exception::Error(String::New(
"value is not a number")));
}
int value = (char)args[0]->Int32Value();

Buffer *parent = ObjectWrap::Unwrap<Buffer>(args.This());

//合法性检查
SLICE_ARGS(args[1], args[2])
 

//memset:作用是在一段内存块中填充某个给定的值,它对较大的结构体或数组进行清零操作的一种最快方法。

//void *memset(void *s, int c, size_t n);
memset( (void*)(parent->data_ + start),
value,
end - start);

return Undefined();
}



6、总结
其实node的buffer写入主要是用char * 指针来完成的,我们在使用这些接口时要注意一下小问题:
1、Buffer.concat,第二个参数totallength是所有list数组中的buffer实例的大小总和,不是list的长度
2、最快刷新清零一块buffer的方法是buffer.fill()
其他好像没什么要注意的了,api接口做的很完善了
最后一篇buffer的读取稍后献上