Skip to content

ArrayBuffer & Buffer #1

@Weiting-Zhang

Description

@Weiting-Zhang

在读《深入浅出 Node.js》时看到 Buffer 这一章,并不是十分理解。工作中也遇到过诸如加解密,处理二进制数据之类的东东。于是从 ES6 的一个新 API ArrayBuffer 开始来捋一捋。

ArrayBuffer 是什么

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区,不能直接操作,需要通过 TypedArray 或者 DataView 视图进行操作。视图的作用是以指定的格式解读二进制数据。

Buffer 是 Node 中以一种更优化、更适合 Node.js 用例的方式对 Uint8Array API 的实现。

Uint8ArrayTypedArray 已有的9种类型之一(其他类型具体可见 TypedArray

为什么需要这种数据类型

性能 当 JS 与操作系统之间发生大量的、实时的数据交换时,它们之间的数据通信必须是二进制的。否则两端的 JavaScript 脚本(浏览器 or Node)和操作系统(显卡 or 文件I/O,网络 I/O等)都要进行转化,将会十分损耗性能。ArrayBuffer 接口的原始设计目的与 WebGL 项目有关。

如何使用

在 C 语言中,我们可以通过数组的下标直接操作内存。而 JavaScript 则提供了 ArrayBuffer 这个与操作系统的原生接口进行二进制通信。需要注意的是,ArrayBuffer 本身只代表一小段缓存区,可以存放以任何格式解读的内容(8位/16位/32位/64位/原码/补码/有符号/无符号),具体的解读和设置需要通过视图 来操作,视图类似于数组(但与实际的数组有区别,比如元素数值只能在某个确定的范围内)。

比如说创建一个有 12 bytes 的内存(也就是 96 bits):

const buffer = new ArrayBuffer(12)

TypedArray 为例,可以建立 32 位带符号整数 Int32Array 构造函数来操作和解读(因此数组的长度会是 3,元素范围在 -2^31~(2^31)-1 之间):

const x1 = new Int32Array(buffer) // => Int32Array(3) [0, 0, 0]
x1[0] = 1;

也可以建立 8 位不带符号整数 Uint8Array 构造函数来操作解读(因此数组长度会是 12 位,元素范围在 0~255 之间):

const x2 = new Int8Array(buffer) // => Int8Array(12) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
x2[0]  = 2;

需要注意的是,由于操作的是同一段内存,一个视图对底层内存的修改会影响到另一个视图。

x1[0] // 2

除了接受 ArrayBuffer 实例作为参数,TypedArray 视图的构造函数还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值。

const typedArray = new Uint8Array([0,1,2]);
typedArray.length // 3

typedArray[0] = 5;
typedArray // [5, 1, 2]

这里只是简单的介绍,细致的 API 可以移步 ArrayBuffer

Buffer(Node.js)

在 ECMAScript 2015 引入 TypedArray 之前,JavaScript 语言没有读取或操作二进制数据流的机制。 Buffer 类被引入作为 Node.js API 的一部分,使其可以在 TCP 流或文件系统操作等场景中处理二进制数据流。

与 TypedArray 的关系

Buffer 实例也是 Uint8Array 实例,但是与 ES6 规范还是有微妙的不同。具体参见 Buffer and TypedArray

如何使用

Uint8Array 相似,Buffer 对象类似于数组,元素为 16 进制的两位数,即 0255 的数值。由于 new Buffer API 已废弃,一般使用 Buffer.from() 来创建新 buffer 对象。Uint8Array 也有 from 方法,只不过参数必须为数组或可迭代的对象;而 Buffer.from() 则提供了如 array,arrayBuffer,buffer, string(需提供 encoding,默认 utf-8) 等多种选项。
例如:

const buf = Buffer.from([0x62, 0x75, 0x66, 0x66, 0x65, 0x72]); // <Buffer 62 75 66 66 65 72>
buf.toString() // 'buffer'
buf[0] // 98 (0x62 的10进制)
buf[0] = 0x61;
buf.toString() // 'auffer'

const buf1 = Buffer.from('this is a tést'); // <Buffer 74 68 69 73 20 69 73 20 61 20 74 c3 a9 73 74>
buf1.toString() // 'this is a tést'

这里只列举了最简单的使用方法,更多操作请移步 Buffers API Encodings

如上,Buffer 对象可以与字符串之间相互转换,目前支持的字符串编码类型有:

  • ascii
  • utf8
  • utf16le
  • ucs2
  • base64
  • latin1
  • hex

详情可参见 Buffers and Character Encodings

注意事项

Buffer 在使用场景中,通常是以一段一段的方式传输,在拼接时宽字节字符串有可能被截断,正确的处理方式应该是将多个小 Buffer 对象拼接为一个 Buffer 对象,例如使用 Buffer.concat(chunks, size)。(chunks 元素为多个小 buffer 对象,size 为 Buffer 总长度)

总结

之前容易搞混的是内存,Buffer/TypedArray,实际内容(比如传入的数组/字符串等)之间的关系。还有 ArrayBuffer 的字节长等等都是什么鬼,这里以自己的方式总结一下:

  • ArrayBuffer 代表的是缓存,按照实例化时传入的 length 向系统申请内存,length 即申请的内存的字节长度
  • 申请完内存后要往内存里存储/操作数据了,就像 C 语言里是通过 * 或者数组下标来操作内存中存储的内容一样,JS 中可以通过与刚才创建的 ArrayBuffer 对应的 TypedArray 或者 DataView 来操作这段内存中存储的内容(本篇文章没有涉及 DataView 的操作,不过也比较相似)
  • TypedArray 的使用就和 C 语言的数组比较类似了,创建实例时内存中就会在对应地址存储相应的值,也可以通过数组下标来改变存储内容
  • Buffer 是对 TypedArrayUint8Array 在 Node 里的实现,当从字符串创建 Buffer 时,实际存储的内容与字符串属于编码关系

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions