Node理论笔记:理解Buffer

2020-06-01 14:41:36 浏览数 (1)

一、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之间的值
  • 如果是小数,就舍弃小数,保留整数部分
代码语言:javascript复制
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~

0 人点赞