写点什么

NodeJs 深入浅出之旅:理解 Buffer 🐰

作者:空城机
  • 2021 年 11 月 22 日
  • 本文字数:2947 字

    阅读完需:约 10 分钟

NodeJs深入浅出之旅:理解Buffer 🐰

理解 Buffer

JavaScript对于字符串的操作十分友好


Buffer是一个像Array的对象,主要用于操作字节。




Buffer 结构

Buffer是一个典型的 JavaScript 和 C++结合的模块,将性能相关部分用 C++实现,将非性能相关部分用 JavaScript 实现。


Buffer 所占用的内存不是通过 V8 分配,属于堆外内存。 由于 V8 垃圾回收性能影响,将常用的操作对象用更高效和专有的内存分配回收政策来管理是个不错的思路。


Buffer 在 Node 进程启动时就已经价值,并且放在全局对象(global)上。所以使用 buffer 无需 require 引入



Buffer 对象

Buffer 对象的元素未 16 进制的两位数,即 0-255 的数值

let buf01 = Buffer.alloc(8);console.log(buf01);  // <Buffer 00 00 00 00 00 00 00 00>
复制代码


可以使用fill填充 buf 的值(默认为utf-8编码),如果填充的值超过 buffer,将不会被写入。


如果 buffer 长度大于内容,则会反复填充


如果想要清空之前填充的内容,可以直接fill()

buf01.fill('12345678910')
console.log(buf01); // <Buffer 31 32 33 34 35 36 37 38>console.log(buf01.toString()); // 12345678
复制代码


如果填入的内容是中文,在utf-8的影响下,中文字会占用 3 个元素,字母和半角标点符号占用 1 个元素。

let buf02 = Buffer.alloc(18, '开始我们的新路程', 'utf-8');console.log(buf02.toString());  // 开始我们的新
复制代码


BufferArray类型影响很大,可以访问 length 属性得到长度,也可以通过下标访问元素,也可以通过 indexOf 查看元素位置。

console.log(buf02);  // <Buffer e5 bc 80 e5 a7 8b e6 88 91 e4 bb ac e7 9a 84 e6 96 b0>console.log(buf02.length)  // 18字节console.log(buf02[6])  // 230: e6 转换后就是 230console.log(buf02.indexOf('我'))  // 6:在第7个字节位置console.log(buf02.slice(6, 9).toString())  // 我: 取得<Buffer e6 88 91>,转换后就是'我'
复制代码


如果给字节赋值不是 0~255 之间的整数,或者赋值时小数时,赋值小于 0,将该值逐次加 256.直到得到 0~255 之间的整数。如果大于 255,就逐次减去 255。 如果是小数,舍去小数部分(不做四舍五入)



Buffer 内存分配

Buffer对象的内存分配不是在 V8 的堆内存中,而是在 Node 的 C++层面实现内存的申请。 因为处理大量的字节数据不能采用需要一点内存就向操作系统申请一点内存的方式。为此 Node 在内存上使用的是在 C++层面申请内存,在JavaScript中分配内存的方式


Node采用了slab分配机制slab是以中动态内存管理机制,目前在一些*nix操作系统用中有广泛的应用,比如Linux


slab就是一块申请好的固定大小的内存区域,slab 具有以下三种状态:


  • full:完全分配状态

  • partial:部分分配状态

  • empty:没有被分配状态


Node 以 8KB 为界限来区分 Buffer 是大对象还是小对象

console.log(Buffer.poolSize);  // 8192
复制代码


这个 8KB 的值就额是每个 slab 的大小值,在 JavaScript 层面,以它作为单位单元进行内存的分配

分配小 buffer 对象

如果指定Buffer大小小于 8KB,Node 会按照小对象方式进行分配


  1. 构造一个新的 slab 单元,目前 slab 处于 empty 空状态


  1. 构造小buffer对象 1024KB,当前的slab会被占用 1024KB,并且记录下是从这个slab的哪个位置开始使用的


  1. 这时再创建一个buffer对象,大小为 3072KB。 构造过程会判断当前slab剩余空间是否足够,如果足够,使用剩余空间,并更新slab的分配状态。 3072KB 空间被使用后,目前此 slab 剩余空间 4096KB。


  1. 如果此时创建一个 6144KB 大小的buffer,当前 slab 空间不足,会构造新的slab(这会造成原 slab 剩余空间浪费)


比如下面的例子中:

Buffer.alloc(1)Buffer.alloc(8192)
复制代码


第一个slab中只会存在 1 字节的 buffer 对象,而后一个 buffer 对象会构建一个新的 slab 存放


由于一个 slab 可能分配给多个 Buffer 对象使用,只有这些小 buffer 对象在作用域释放并都可以回收时,slab 的空间才会被回收。 尽管只创建 1 字节的 buffer 对象,但是如果不释放,实际是 8KB 的内存都没有释放


小结:

真正的内存是在 Node 的 C++层面提供,JavaScript 层面只是使用。当进行小而频繁的 Buffer 操作时,采用 slab 的机制进行预先申请和时候分配,使得 JavaScript 到操作系统之间不必有过多的内存申请方面的系统调用。 对于大块的 buffer,直接使用 C++层面提供的内存即可,无需细腻的分配操作。




Buffer 的拼接

buffer 在使用场景中,通常是以一段段的方式进行传输。

const fs = require('fs');
let rs = fs.createReadStream('./静夜思.txt', { flags:'r'});let str = ''rs.on('data', (chunk)=>{ str += chunk;})
rs.on('end', ()=>{ console.log(str);})
复制代码


以上是读取流的范例,data 时间中获取到的 chunk 对象就是 buffer 对象。


但是当输入流中有宽字节编码(一个字占多个字节)时,问题就会暴露。在str += chunk中隐藏了toString()操作。等价于str = str.toString() + chunk.toString()


下面将可读流的每次读取 buffer 长度限制为 11.

fs.createReadStream('./静夜思.txt', { flags:'r', highWaterMark: 11});
复制代码


输出得到:


上面出现了乱码,上面限制了 buffer 长度为 11,对于任意长度的 buffer 而言,宽字节字符串都有可能存在被截断的情况,只不过 buffer 越长出现概率越低。

encoding

但是如果设置了encodingutf-8,就不会出现此问题了。

fs.createReadStream('./静夜思.txt', { flags:'r', highWaterMark: 11, encoding:'utf-8'});
复制代码



原因: 虽然无论怎么设置编码,流的触发次数都是一样,但是在调用setEncoding时,可读流对象在内部设置了一个decoder对象。每次 data 事件都会通过decoder对象进行 buffer 到字符串的解码,然后传递给调用者。


string_decoder 模块提供了用于将 Buffer 对象解码为字符串(以保留编码的多字节 UTF-8 和 UTF-16 字符的方式)的 API

const { StringDecoder } = require('string_decoder');let s1 = Buffer.from([0xe7, 0xaa, 0x97, 0xe5, 0x89, 0x8d, 0xe6, 0x98, 0x8e, 0xe6, 0x9c])let s2 = Buffer.from([0x88, 0xe5, 0x85, 0x89, 0xef, 0xbc, 0x8c, 0x0d, 0x0a, 0xe7, 0x96])console.log(s1.toString());console.log(s2.toString());console.log('------------------');
const decoder = new StringDecoder('utf8');console.log(decoder.write(s1));console.log(decoder.write(s2));
复制代码



StringDecoder在得到编码之后,知道了宽字节字符串在utf-8编码下是以 3 个字节的方式存储的,所以第一次decoder.write只会输出前 9 个字节转码的字符,后两个字节会被保留在StringDecoder内部。




Buffer 与性能

buffer 在文件 I/O 和网络 I/O 中运用广泛,尤其在网络传输中,性能举足轻重。在应用中,通常会操作字符串,但是一旦在网络中传输,都需要转换成 buffer,以进行二进制数据传输。 在 web 应用中,字符串转换到 buffer 是时时刻刻发生的,提高字符串到 buffer 的转换效率,可以很大程度地提高网络吞吐率。


如果通过纯字符串的方式向客户端发送,性能会比发送 buffer 对象更差,因为 buffer 对象无须在每次响应时进行转换。通过预先转换静态内容为 buffer 对象,可以有效地减少 CPU 重复使用,节省服务器资源。


可以选择将页面中动态和静态内容分离,静态内容部分预先转换为 buffer 的方式,使得性能得到提升。


在文件的读取时,highWaterMark设置对性能影响至关重要。在理想状态下,每次读取的长度就是用户指定的highWaterMark


highWaterMark大小对性能有两个影响的点:


  • 对 buffer 内存的分配和使用有一定影响

  • 设置过小,可能导致系统调用次数过多

发布于: 2021 年 11 月 22 日阅读数: 9
用户头像

空城机

关注

曾经沧海难为水,只是当时已惘然 2021.03.22 加入

业余作者,在线水文 主要干前端的活,业余会学学python 欢迎各位关注,互相学习,互相进步

评论

发布
暂无评论
NodeJs深入浅出之旅:理解Buffer 🐰