Buffer是什么?
Js语言自身只有字符串数据类型,没有二进制的数据类型。但是当需要处理类似于TCP流或文件流时,必须使用二进制数据。因此在Node.js中。引入了Buffer类,该类是创建一个专门用来储存二进制数据的缓冲区。
Buffer的创建:
创建指定长度的Buffer
var buf= new Buffer(10);
通过给定的数组创建Buffer
var buf=new Buffer([10,20,30,40,50]);
通过字符串创建Buffer
var buf=new Buffer('luyaoguang','utf-8');
uft-8是默认的编码方式,还支持:[ascii,utf8,utf16le,base64,hex]
缓冲区的写入:
buf.write(string[,offset[,length]][,encoding]);
* string:写入缓冲区的字符串
* offset:缓冲区开始写入的索引值,默认值是0
* length:写入的字节数,默认为'buffer.length'
* encoding:使用的编码,默认为utf-8
* 返回值:返回实际写入的大小,如果buffer空间不足,则只写入部分字符串
eg:buf.write('luyaoguang',0,5,'utf-8');
缓冲区的读取:
* buf.toString([encoding[,start[,end]]]);
* encoding:使用的编码格式,默认utf-8
* start:指定开始读取的索引位置,默认是0
* end:结束位置,默认为缓冲区末尾
*/
//buf=new Buffer(26);
//for(var i=0;i<26;i++){
//buf[i]=i+97;
//}
//console.log(buf.toString('ascii'));
//将Buffer转换为JSON对象
/**
* 将Node.Buffer转换为JSON对象的函数语法格式
* buf.toString();
* 返回JSON对象
*/
//var buf=new Buffer('luyaoguang');
//var json=buf.toJSON();
//console.log(json);//{ type: 'Buffer',data: [ 108, 117, 121, 97, 111, 103, 117, 97, 110, 103 ] }
//缓冲区合并 返回多个成员合并的新的Buffer对象
//var buffer1=new Buffer('my name is ');
//var buffer2=new Buffer('luyaoguang');
//var buffer3=Buffer.concat([buffer1,buffer2]);
//console.log(buffer3.toString());//my name is luyaoguang
//缓冲区比较
/**
* compare
* 参数:
* otherBuffer:与buf对象比较的另一个Buffer对象
* 返回值:返回一个数字,表示buf在otherBuffer之前,之后或相同
*/
//var buffer1=new Buffer('abc');
//var buffer2=new Buffer('abcd');
//var result=buffer1.compare(buffer2);
//if(result<0){
//console.log(`${buffer1}在${buffer2}之前`);
//}else if(result>0){
//console.log(`${buffer1}在${buffer2}之后`);
//}else{
//console.log(`${buffer1}在${buffer2}相同`);
//}
//拷贝缓冲区
/**
* buf.copy(targetBuffer[,targetStart[,sourceStart[,sourceEnd]]]);
* targetBuffer:需要拷贝的Buffer对象
* targetStart:数字,可选,默认0
* sourceStart:被拷贝对象拷贝开始下标,默认0
* sourceEnd:被拷贝对象拷贝结束下标,默认buffer.length
*/
//var buffer1=new Buffer('abc');
//var buffer2=new Buffer(3);
//buffer1.copy(buffer2,1);
//console.log('buffer2 content:'+buffer2.toString());
//缓冲区剪切
/**
* buf.slice([start[,end]]);
* start:开始位置
* end:结束位置
* 返回值:返回一个新的缓冲区,它与旧的缓冲区指向同一块内存,但是从索引start到end位置剪切*
*/
//var buffer1=new Buffer('luyaoguang','utf-8');
//var buffer2=buffer1.slice(0,2);
//console.log(buffer2.toString());//lu
//缓冲区长度
/**
* 返回Buffer对象所占据的内存长度
*/
//var buffer=new Buffer('luyaoguang');
//console.log(buffer.length);//10
/**
* Buffer是一个类似于Array的对象,但主要用于字节操作
* 因为JavaScript自有的字符串远远无法满足处理大量二进制数据的需求
* Buffer是一个典型的JavaScript与C++结合的模块
* 它将性能相关部分用C++实现,将非性能部分用JavaScript实现
* Buffer对象的元素为16进制的两位数,0-255
* 给元素赋值:
* 如果小于0,就将该值逐次加256,直到得到一个0-255之间的整数,如果得到的值大于256,则逐次减256,直到获得0-255之间整数
* 如果是小数,则之间省去小数部分
*/
varbuf=newBuffer(100);
buf[20]=100;
console.log(buf[20]);//100
buf[30]=-100;
console.log(buf[30]);//-100+256=156
buf[40]=300;
console.log(buf[40]);//300-256=44
buf[50]=3.11;
console.log(buf[50]);//3
/**
* Buffer的内存配置
* Buffer对象的内存分配不是在V8的堆内存中,而是在Node的C++层面实现内存的申请
* 因为处理大量的数据字节不能采用需要一点内存就向操作系统申请一点内存的方式,这可能会造成大量的内存申请的系统调用,对操作系统造成压力
* 为此Node在内存的使用上应用的实在C++层面申请内存,在JavaScript中分配内存的策略
*
* 为了高效的使用申请下来的内存,Node采用了slab分配机制
* 简单而言,slab就是一块申请好的固定大小的内存区域,slab状态有3种状态
* full:完全分配状态
* partial:部分分配状态
* empty:没有被分配状态
*
* 当我们需要一个Buffer对象时,需要通过以下方式分配指定大小的Buffer对象
* new Buffer(size)
* Node以8kb为界限来区分Buffer是大对象还是小对象
* Buffer.pollSize=8*1024 这个8kb就是每个slab的大小值
*
*
* 如果指定的Buffer的大小小于8kb,Node会按照小对象的方式进行分配
* Buffer的分配过程中主要使用一个全局变量pool作为中间处理对象,处于分配状态的slab单元都指向它
*
* var pool;
* function allocPool(){
* pool=new SlowBuffer(Buffer.poolSize);
* pool.used=0;
* --------------------------------------
* | 8kb | used:0
* --------------------------------------
* 此时,slab处于empty状态,没有被分配
* 当我们创建新的Buffer
* var buf=new Buffer(1024);
* 这次构造将去检查pool对象,如果pool没有被创建,将会去创建一个新的slab单元指向它
* if(!pool||pool.length-pool.used
* allocPool();
* }
* 同时当前Buffer对象的parent属性指向该slab,并记录下是从这个slab的哪个位置(offset)开始使用的,slab对象自身也被记录使用了多少字节
* this.parent=pool;
* this.offset=pool.used;
* pool.used=this.length;
* if(pool.used & 7)pool.used=(pool.used + 8)&~7
*
* //从一个新的slab单元中初次分配一个Buffer对象
* offset:0
* |
* --------------------------------------
* | Buffer1 | |
* --------------------------------------
* |used:1024
* 这个时候,slab的状态从empty变成了partial
* 如果再创建一个Buffer对象,构造过程中将会判断slab剩余的空间是否足够,如果足够使用,则使用剩余空间,并更新slab的分配状态
*
* new Buffer(3000);
* offset:1024
* |
* -------------------------------------
* | Buffer1 | Buffer2 | |
* -------------------------------------
* |
* offset:4024
* 如果剩余的空间不够,则会构造新的slab,原slab中剩余的空间会造成浪费
* 例如,第一次构造1个字节的Buffer对象,第二次构造8192个字节的Buffer对象,由于第二次分配时slab中空间不够
* 所以创建并使用新的slab,第一个slab的8kb将会被第一个1字节的Buffer独占
* new Buffer(1);
* new Buffer(8192);
* 由于同一个slab可能分配给多个Buffer对象使用,只有这些小的Buffer对象在作用域释放并都可以回收时,slab的8kb空间才会被回收
* 尽管创建了1个字节的Buffer对象,但是如果不释放它,实际上就是8kb的内存没有被释放
**/
/**
* 分配大Buffer对象
如果超过8kb的Buffer对象,将会直接分配一个SlowBuffer对象作为slab单元,这个slab单元将会被这个大Buffer对象独占
this.parent=new SlowBuffer(this.length);
this.offset=0;
这里的SlowBuffer类是在C++中定义的,虽然引用Buffer模块可以访问它,但是不推荐直接操作它,而是用Buffer替代
上面提到的Buffer对象都是JavaScript层面的,能够被V8的垃圾回收标记回收的.但是其内部的parent属性指向的SlowBuffer对象却来自于Node自身C++中定义
在C++层面上的Buffer对象,所用的内存不在V8的堆中
简单而言,真正的内存是在Node的C++层面提供的,JavaScript只是使用它
当进行小而频繁的Buffer操作时,采用slab机制进行预先申请和事后分配,使得JavaScript到操作系统之间不必有过多的内存申请方面的系统调用
对于大块的Buffer而言,则之间使用Buffer提供的内存,而无需细腻的分配操作
*/
/**
* 编码格式问题
* 目前Buffer支持的编码格式:[ascii,utf-8,utf-16le/usc-2,base64,binary,hex];
* 一个Buffer对象可以储存多种编码格式的字符串转码的值,调用write()方法可以实现
* 由于每种编码格式的字节长度不同,将Buffer反转为字符串时,需要注意指定长度
* buf.toString(string[,offset[,length[,encoding]]]);
* 判断Buffer是否支持某种编码格式
* Buffer.isEncoding('ascii');
*
* 对于不支持的编码格式,可以借助Node生态圈中的模块进行转换
* iconv和iconv-lite两个模块可以支持更多编码类型转换
* 包括[windows125,gbk,gb2312,iso-8859,ibm/dos,macintosh,ko18,latin1,us-ascii]
* iconv-lite采用纯JavaScript实现,iconv通过C++调用libiiconv库完成
* 前者比后者更轻量,无需编译和处理环境依赖直接使用
* 在性能方面,由于转码都耗用CPU,在V8的高性能下,少了C++到JavaScript的层次转换,纯JavaScript更高效
*
* var iconv=require('iconv-lite');
* //Buffer转字符串
* var str=iconv.decode(buf,'win1251');
* //字符串转Buffer
* var buf=iconv.encoed('hello','win2151');
*
* 转码乱码问题
* var fs=require('fs');
* var read=fs.createReadStream('input.txt');
* var data='';
* read.on('data',function(chunk){
* data+=chunk
* })
* read.on('end',function(){
* console.log(data);
* })
* 对于data+=chunk这句话 隐藏了toString()操作 ====> data.toString()+chunk.toString()
* 对于英文情境下的toString(),没有任何问题
* 但是对于中文等宽字节的转码,会出现乱码的问题
* var rs=fs.createReadStream('input.txt',{highWaterMark:11});
* 我们将可读流的Buffer长度限制为11 会出现乱码 床前明��光,疑是地上霜,���头望明月,低头���故乡
*
*
* b8 be e5 a4 b4 e6 9c 9b e6 98 8e e6 9c 88 2c e4 bd 8e e5 a4 b4 e6 80 9d e6 95 85 e4 b9 a1>
* 由于限制了长度为11,因此只读流每次只能读11个字符长度
*
*
* toString()默认使用utf-8编码格式,中文占3个字符,所以第一个Buffer对象输出时,只能正常输出9个字符长度,剩下两个字符乱码
* 第二个Buffer对象输出时,第一个字节也不能正常编译 会乱码 于是出现了 床前明��光
*
* 解决办法
* rs.setEncoding('utf-8');
* 无论如何设置编码格式,触发data事件的次数依旧相同,这意味着设置编码并未改变按段读取的基本方式
* 事实上,在调用setEncoding()时,可读流对象在内部设置了一个decoder对象,每次data事件都通过decoder对象进行Buffer到字符串的解码,然后传递给调用者
* 设置编码后,data不再收到原始的Buffer对象
* decoder原理:
* var StringDecoder=require('string_decoder').StringDecoder;
* var decoder=new StringDecoder('utf8');
* var buf1=new Buffer([0xE5,0xBA,0x8A,0xE5,0x89,....]);
* console.log(decoder.write(buf1)) ==>床前明
* var buf2=new Buffer([0x88,0xE5,0x85......]);
* conosle.log(decoder.write(buf2)) ==>月光,疑
*
* 关于'月'字的转码并没有像之前一样乱码的原因在于
* 当我们设置了utf-8的编码格式化后,decoder知道是3个字节储存的方式,当读取到'床前'后,它将剩余的两个字节保留在StringDecoder实例内部
* 第二次write()时,会将剩余2个字符与第一个字符组合成完整的3个字符进行编译 从而不会乱码
*
* 虽然string-decoder模块很奇妙,但是目前它只能处理utf-8,base64,ucs-2/utf-16le这三种编码格式
*
* 正确的Buffer拼接方式 是用一个数组来储存接收到的所有Buffer片段并记录下所有片段的总长度,然后调用Buffer.concat()方法生成一个合并的Buffer对象
* 然后通过调用iconv-lite一类的模块来转码
* var chunks=[];
* var size=0;
* res.on('data',function(chunk){
* chunks.push(chunk);
* size+=chunk.length;
* })
* res.on('end',function(){
* var buf=Buffer.concat(chunks,size);
* var str=iconv.encode(buf,'utf-8');
* console.log(str);
* })
*/