深入浅出Nodejs学习笔记——第六章 Buffer


二进制的水很深还得深入学习,ArrayBuffer对象 待学习

Buffer的本质是二进制数组的操作对象,无论什么都是二进制,无非是编码不同

Buffer

计算机就是处理 0 和 1,很尴尬的是在引入 TypedArray 之前,JavaScript 没有操作二进制数据流的机制,Buffer 类用一种更适合 Node.js 的方式实现了 Uint8Array API,用于在 TCP 流、文件系统操作等场景处理二进制字节。
JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。
但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。

字符集 && 字符编码

感兴趣看看
字符集是一个系统支持的所有抽象字符的集合。字符是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。我们在计算机屏幕上看到的是实体化的文字,而在计算机存储介质中存放的实际是二进制的比特流。那么在这两者之间的转换规则就需要一个统一的标准。
字符集就是定义数字所代表的字符的一个规则表,同样定义了怎样用二进制存储和表示。那么,用多少位来表示一个数字,这个就叫字符编码(Character Encoding)

字库表和编码字符集看来是必不可少的,那既然字库表中的每一个字符都有一个自己的序号,直接把序号作为存储内容就好了。为什么还要多此一举通过字符编码把序号转换成另外一种存储格式呢?其实原因也比较容易理解:统一字库表的目的是为了能够涵盖世界上所有的字符,但实际使用过程中会发现真正用的上的字符相对整个字库表来说比例非常低。例如中文地区的程序几乎不会需要日语字符,而一些英语国家甚至简单的ASCII字库表就能满足基本需求。而如果把每个字符都用字库表中的序号来存储的话,每个字符就需要3个字节(这里以Unicode字库为例),这样对于原本用仅占一个字符的ASCII编码的英语地区国家显然是一个额外成本(存储体积是原来的三倍)。算的直接一些,同样一块硬盘,用ASCII可以存1500篇文章,而用3字节Unicode序号存储只能存500篇。于是就出现了UTF-8这样的变长编码。在UTF-8编码中原本只需要一个字节的ASCII字符,仍然只占一个字节。而像中文及日语这样的复杂字符就需要2个到3个字节来存储。

乱码 mojibake

简单的说乱码的出现是因为:编码和解码时用了不同或者不兼容的字符集。对应到真实生活中,就好比是一个英国人为了表示祝福在纸上写了bless(编码过程)。而一个法国人拿到了这张纸,由于在法语中bless表示受伤的意思,所以认为他想表达的是受伤(解码过程)。这个就是一个现实生活中的乱码情况。在计算机科学中一样,一个用UTF-8编码后的字符,用GBK去解码。由于两个字符集的字库表不一样,同一个汉字在两个字符表的位置也不同,最终就会出现乱码。

Buffer 特性

Buffer 类的实例类似于 0 到 255 之间的整型数组(其他整数会通过 & 255 操作强制转换到此范围),Buffer 是一个 JavaScript 和 C++ 结合的模块,对象内存不经 V8 分配,而是由 C++ 申请、JavaScript 分配。缓冲区的大小在创建时确定,不能调整。

Buffer 构建

在 Node.js v6 之前都是通过调用构造函数的方式实例化 Buffer,根据参数返回不同结果。处于安全性原因,这种方式在 v6 后的版本中已经被废除,现在提供了三个职责清晰的函数处理实例化 Buffer 的工作

Buffer.from 支持四种参数类型
Buffer.from(string [, encoding]):返回一个包含给定字符串的 Buffer
Buffer.from(buffer):返回给定 Buffer 的一个副本 Buffer
Buffer.from(array):返回一个内容包含所提供的字节副本的 Buffer,数组中每一项是一个表示八位字节的数字,所以值必须在 0 ~ 255 之间,否则会取模
Buffer.from(arrayBuffer[, byteOffset[, length]]):返回一个与给定的 ArrayBuffer 共享内存的新 Buffer
Buffer.from(object[, offsetOrEncoding[, length]]):取 object 的 valueOf 或 Symbol.toPrimitive 初始化

// 16 进制显示值
const buf1 = Buffer.from('test', 'utf-8'); // <Buffer 74 65 73 74>
const buf2 = Buffer.from(buf1); // <Buffer 74 65 73 74>,buf1 副本,修改 buf2 不会影响 buf1
const buf3 = Buffer.from([256, 2, 3]); // <Buffer 00 02 03>,超过 255 会取模
const arr = new Uint16Array(2);
arr[0] = 5000;
arr[1] = 4000;
// 和 arr 共享内存
const buf4 = Buffer.from(arr.buffer);
console.log(buf4); // <Buffer 88 13 a0 0f>
// 修改 arr
arr[1] = 6000;
console.log(buf4); // <Buffer 88 13 70 17> buf4 也受到影响
const buf = Buffer.from(new String('this is a test'));
// <Buffer 74 68 69 73 20 69 73 20 61 20 74 65 73 74>

node v15新增blob。

Buffer.alloc(size[, fill[, encoding]])

  • size 新 Buffer 所需的长度。
  • fill | | | 用于预填充新 Buffer 的值。 默认值: 0。
  • encoding 如果 fill 是字符串,则这就是它的编码。 默认值: 'utf8'。
    分配 size 个字节的新 Buffer。 如果 fill 为 undefined,则 Buffer 将以零填充。

    var buf = Buffer.alloc(5);
    console.log(buf);
    // 打印: <Buffer 00 00 00 00 00>
    var buf = Buffer.alloc(5, 'a');
    console.log(buf);
    // 打印: <Buffer 61 61 61 61 61>

    如果同时指定了 fill 和 encoding,则分配的 Buffer 将通过调用 buf.fill(fill, encoding) 进行初始化。

  • const buf = Buffer.alloc(11, 'aGVsbG8gd29ybGQ=', 'base64');
    console.log(buf);
    // 打印:

    #### 调用 Buffer.alloc() 可能比替代的 Buffer.allocUnsafe() 慢得多,但可确保新创建的 Buffer 实例的内容永远不会包含来自先前分配的敏感数据,包括可能尚未分配给 Buffer 的数据。

Buffer.allocUnsafe(size)

  • size 新 Buffer 所需的长度。

    以这种方式创建的 Buffer 实例的底层内存不会被初始化。 新创建的 Buffer 的内容未知,可能包含敏感数据。 使用 Buffer.alloc() 来用零初始化 Buffer 实例。
    const buf = Buffer.allocUnsafe(10);
    console.log(buf);
    // 打印(内容可能会有所不同): <Buffer a0 8b 28 3f 01 00 00 00 50 32>
    buf.fill(0);
    console.log(buf);
    // 打印: <Buffer 00 00 00 00 00 00 00 00 00 00>

    Buffer 模块预先分配了大小为 Buffer.poolSize 的内部 Buffer 实例作为池,用于快速分配使用 Buffer.allocUnsafe()、Buffer.from(array)、Buffer.concat() 创建的新 Buffer 实例,仅当 size 小于或等于 Buffer.poolSize >> 1(Buffer.poolSize 除以二再向下取整)时才使用弃用的 new Buffer(size) 构造函数。
    使用这个预先分配的内部内存池是调用 Buffer.alloc(size, fill) 与调用 Buffer.alloc(size, fill) 之间的关键区别。 Buffer.allocUnsafe(size).fill(fill). 具体来说,Buffer.alloc(size, fill) 永远不会使用内部 Buffer 池,而如果 size 小于或等于 Buffer.poolSize 的一半,则 Buffer.allocUnsafe(size).fill(fill) 将使用内部 Buffer 池。 当应用程序需要 Buffer.allocUnsafe() 提供的额外性能时,差异很细微,但可能很重要。
    Buffer.allocUnsafeSlow(size)

  • size 新 Buffer 所需的长度。

    以这种方式创建的 Buffer 实例的底层内存不会被初始化。 新创建的 Buffer 的内容未知,可能包含敏感数据。 使用 buf.fill(0) 用零初始化此类 Buffer 实例。
    当使用 Buffer.allocUnsafe() 分配新的 Buffer 实例时,4KB 以下的分配将从单个预先分配的 Buffer 中切分。 这允许应用程序避免创建许多单独分配的 Buffer 实例的垃圾收集开销。 这种方法无需跟踪和清理尽可能多的单个 ArrayBuffer 对象,从而提高了性能和内存使用率。
    但是,在开发人员可能需要在不确定的时间内从池中保留一小块内存的情况下,使用 Buffer.allocUnsafeSlow() 创建未池化的 Buffer 实例然后复制出相关位可能是合适的。
    // 需要保留一些小块内存。
    const store = [];
    socket.on('readable', () =>
    {
      let data;
      while(null !== (data = readable.read()))
      {
          // 为保留的数据分配。
          const sb = Buffer.allocUnsafeSlow(10);
          // 将数据复制到新分配中。
          data.copy(sb, 0, 0, 10);
          store.push(sb);
      }
    });

buffer 内存分配

Buffer对象的内存分配不是在V8的堆内存中,而是在Node的C++层面实现内存的申请的。因为处理大量的字节数据不能采用需要一 点内存就向操作系统申请一点内存的方式, 这可能造成大量的内存申请的系统调用,对操作系统有一定压力。为此Node在内存的使用上应用的是在C++层面申请内存、在JavaScript中 分配内存的策略。为了高效地使用申请来的内存,Node采用了slab分配机制。简单而言,slab就是一块申请好的固定大小的内存区域。slab具有如下3种状态。
full:完全分配状态。
partial: 部分分配状态。
empty:没有被分配状态。
分配小Buffer对象 // to do
分配大Buffer对象 // to do

slab的具体机制、实现 暂不了解

ArrayBuffer、Typed Arrays、DataView

ArrayBuffer对象、TypedArray视图和DataView视图是 JavaScript 操作二进制数据的一个接口。这些对象早就存在,属于独立的规格(2011 年 2 月发布),ES6 将它们纳入了 ECMAScript 规格,并且增加了新的方法。它们都是以数组的语法处理二进制数据,所以统称为二进制数组。
JavaScript 类型数组(Typed Arrays)将实现拆分为缓冲和视图两部分。它是一种处理二进制数据的特殊数组,像C语言那样直接操纵字节,不过得先用ArrayBuffer对象创建数组缓冲区(Array Buffer),再映射到指定格式的视图(view)之后,才能读写其中的数据。总共有两类视图,分别是特定类型的TypedArray和通用类型的DataView。在ES6引入类型化数组之后,大大提升了JavaScript数学运算的性能。

ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。
TypedArray对象:用来生成内存的视图,通过9个构造函数,可以生成9种数据格式的视图
DataView对象:用来生成内存的视图,可以自定义格式和字节序
简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray对象代表确定类型的二进制数据,DataView对象代表不确定类型的二进制数据。它们支持的数据类型一共有9种(DataView对象支持除Unit8c以外的其他8种)
ES6——二进制数组 待学习

Buffer 与 ArrayBuffer 区别

Buffer 类是 JavaScript Uint8Array 类的子类,并使用涵盖额外用例的方法对其进行扩展。 Node.js API 在支持 Buffer 的地方也接受普通的 Uint8Array。
Node.js v14.x
/lib/buffer.js#L120

缓冲区与 TypedArray

Buffer 实例也是 JavaScript Uint8Array 和 TypedArray 实例。 所有 TypedArray 方法都可在 Buffer 上使用。 但是,Buffer API 和 TypedArray API 之间存在细微的不兼容。

参考文章:

  1. http://nodejs.cn/api/buffer.html#buffer_buffer
  2. https://www.yuque.com/sunluyong/node/buffer
  3. https://www.w3.org/International/questions/qa-what-is-encoding
  4. https://www.cnblogs.com/zhaoshujie/p/9594649.html
  5. https://es6.ruanyifeng.com/#docs/arraybuffer
  6. https://www.cnblogs.com/diligenceday/p/5998806.html
  7. https://juejin.cn/post/6844903889364336654
  8. https://blog.csdn.net/qq_38128179/article/details/106012003
  9. https://www.runoob.com/nodejs/nodejs-buffer.html
  10. http://javascript.ruanyifeng.com/stdlib/arraybuffer.html
  11. https://zhuanlan.zhihu.com/p/144381462
  12. https://www.jianshu.com/p/4db4b2633dbe
  13. https://www.cnblogs.com/jixiaohua/p/10714662.html

声明:一个萌新|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 深入浅出Nodejs学习笔记——第六章 Buffer


耳不闻人之非,目不视人之短,口不言人之过。