一、Buffer结构
对于JavaScript,无论是宽字节字符串还是单字节字符串,都被认为是一个字符串。
Buffer是一个类Array的对象,主要用于操作字节。
1.1 模块结构
Buffer是一个典型的JavaScript与C 结合的模块,JavaScript核心模块:Buffer/SlowBuffer,C 内建模块:node_buffer。
Buffer所占用的内存不是通过V8分配的,属于堆外内存。
Buffer由于使用频繁,所以node进程启动就已经加载了,不需要通过require()引用。
1.2 Buffer对象
Buffer对象类似于数组,元素为16进制的两位数,即0到255的数值。
代码语言:javascript复制const str = "你好 nodeJs";
const buffer = new Buffer(str,"utf8");
console.log(buffer);
//打印结果
<Buffer e4 bd a0 e5 a5 bd 20 6e 6f 64 65 4a 73>
可以看到不同编码的字符串所占用的元素个数是不同的。在utf8编码下,中文占3个字符,字母和半角符号占用1个字符。
类似Array,length属性可以返回Buffer长度,通过下标可以访问元素。
代码语言:javascript复制const buffer = new Buffer(100);
console.log(buffer.length);//100
console.log(buffer[10]);//0
对于一个空的Buffer,每一个元素的值都为0。
通过下标可以为Buffer赋值,但仅限数字型,且遵循以下几个原则:
- 如果小于0,就将该值逐次加256,直到得到一个0到255之间的值
- 如果大于255,就将该值逐次减256,直到得到一个0到255之间的值
- 如果是小数,就舍弃小数,保留整数部分
const buffer = new Buffer(100);
buffer[10] = -2;//254
buffer[11] = 256;//0
buffer[12] = 3.5;//3
buffer[13] = -3.5;//253
1.3 Buffer内存分配
Buffer的内存分配是在node的C 层面实现的,同时处理大量的字节数据不能采用需要一点就申请一点的方式,为此node在内存的使用上是C 层面申请内存、JavaScript层面分配内存的策略。
node采用了slab分配机制来管理内存,slab有三种状态:
- full:完全分配状态
- partial:部分分配状态
- empty:没有被分配状态
node以8KB为界限来区分Buffer是大对象还是小对象。
代码语言:javascript复制console.log(Buffer.poolSize);//8192
这个8Kb就是每个slab的大小值,在JavaScript层面以此作为单位单元进行内存的分配。
1、分配小Buffer对象
创建新的Buffer会检查上次slab的剩余空间,如果够则从上一次的结束位置存储Buffer对象,如果不够则创建一个新的slab来存储对象。
代码语言:javascript复制new Buffer(1);
new Buffer(8192);
实际创建的是2个slab空间。
对于每个slab空间,只有内部的Buffer全部释放,slab空间才会被释放。
2、分配大Buffer对象
如果需要超过8KB的Buffer对象,将会直接分配一个SlowBuffer对象作为slab单元,这个slab单元将被这个大Buffer对象独占。
这个SlowBuffer是在C 中定义的,通过buffer模块可以访问到,但一般不需要直接操作。
上面提到的Buffer对象都是JavaScript层面的,能够被V8的垃圾回收机制标记回收,但其内部的SlowBuffer对象来自C 层面的,,所以内存不在V8的堆中。简单而言,真正的内存是在node的C 层面提供的,JavaScript层面只是使用它。
二、Buffer的转换
Buffer对象可以与字符串之间相互转换,目前支持的编码类型:
- ASCII
- UTF-8
- UFT-16LE/UCS-2
- Base64
- Binary
- Hex
2.1 字符串转Buffer
主要通过构造函数完成。
语法:new Buffer(str,[encoding])
通过构造函数创建的Buffer,只能存储一种编码类型,如果缺省encoding,则默认为uft8。
同时一个Buffer对象可以存储不同编码类型的字符串,调用write()方法即可。
语法:buf.write(str,[offset],[length],[encoding])
注意默认offset为0,所以重复写入后边会覆盖前边,而不是自动写入空余位置。
2.2 Buffer转字符串
Buffer转字符串比较简单,调用Buffer实例的toString()方法。巧妙的是可以指定encoding、start、end来实现整体或局部的转换。
代码语言:javascript复制const buffer = new Buffer("你好 nodeJs");
console.log(buffer.toString("utf8",3,6));//好
2.3 Buffer不支持的编码类型
Buffer.isEncoding()函数可以判断是否支持某种编码。
代码语言:javascript复制console.log(Buffer.isEncoding("utf8"));//true
console.log(Buffer.isEncoding("ascii"));//true
console.log(Buffer.isEncoding("utf16le"));//true
console.log(Buffer.isEncoding("ucs2"));//true
console.log(Buffer.isEncoding("base64"));//true
console.log(Buffer.isEncoding("binary"));//true
console.log(Buffer.isEncoding("hex"));//true
console.log(Buffer.isEncoding("gbk"));//false
console.log(Buffer.isEncoding("gb2312"));//false
编码是不分大小写的。对于不支持的编码,node内置了iconv-title模块可以解决这个问题。
代码语言:javascript复制const iconv = require("iconv-lite");
const buffer = new Buffer("你好 nodeJs");
console.log(buffer);//浣犲ソ nodeJs
//解码
const str = iconv.decode(buffer,"gb2312");
console.log(str);//浣犲ソ nodeJs
//编码
const buf = iconv.encode("你好 nodeJs","gb2312");
console.log(buf);//浣犲ソ nodeJs
三、Buffer的拼接
代码语言:javascript复制const fs = require("fs");
const rs = fs.createReadStream("./file/test1.txt");
let data = "";
rs.on("data",(chunk)=>{
data = chunk;
});
rs.on("end",()=>{
console.log(data);
});
这段代码其实有问题,关键在 data =chunk,其实实际是这么调用的:
代码语言:javascript复制data = data.toString() chunk.toString();
toString()方法默认是utf8编码,如果我们的文件不是utf8编码且包含一些中文就会有问题。对于无法解码的文件,打印出来就是这样的:
代码语言:javascript复制�������
正确的做法是通过数组来存放:
代码语言:javascript复制const fs = require("fs");
const iconv = require("iconv-lite");
const rs = fs.createReadStream("./file/test1.txt");
let data = [];
let size = 0;
rs.on("data",(chunk)=>{
data.push(chunk);
size = chunk.length;
});
rs.on("end",()=>{
const buf = Buffer.concat(data,size);
const str = iconv.decode(buf,"gbk");
console.log(str);
});
最终使用文件本身的编码来解码Buffer。
四、Buffer与性能
Buffer在文件I/O和网络I/O中运用广泛,特别是网络传输中。在应用中,通常操作的是字符串,但是在网络中传输则都要转化为Buffer,以进行二进制数据传输。
fs模块的createReadStream()方法可以创建一个文件读取流,其工作方式是在内存中准备一段Buffer,然后逐步从磁盘中将字节复制到Buffer中。完成一次读取时,则从这个Buffer中通过slice方法取出部分数据作为一个小buffer对象,再通过data事件传递给调用方。如果Buffer用完则再分配一个,如果还有剩余则继续使用。
highWaterMark参数用于指定每次读取的长度,设置过小会导致系统调用次数过多及频繁触发data事件。实践证明,对于大文件,该值越大,读取速度越快。
具体到文件系统,将在后续的章节介绍。
本章End~