nodejs api 学习3:二进制ArrayBuffer

0 阅读8分钟

在学习nodejs的二进制之前,先回顾下基本知识。

基本概念

字节

字节是计算机里最小的可操作单元1 字节 = 8 个比特(bit),1 个比特就是 1 个 0 或 1。

所以:1 字节 = 8 位二进制,范围:00000000 ~ 11111111,对应十进制:0 ~ 255

这就是为什么:

  • Uint8Array 每个值 0~255
  • Buffer 每个字节也是 0~255

二进制

只有 0 和 1,逢 2 进 1

比如:

  • 0 → 0
  • 1 → 1
  • 2 → 10
  • 3 → 11
  • 4 → 100
  • 255 → 11111111

1 个字节 = 固定 8 位二进制不足 8 位前面补 0。

十六进制

数字:0 ~ 9 + A~F,A=10, B=11, C=12, D=13, E=14, F=15,逢 16 进 1

为什么要用它?

因为 1 位十六进制 = 4 位二进制,刚好完美对应!

怎么用十六进制表示二进制?

第一步:把 8 位二进制切成两段,每段 4 位:

比如二进制:1000 0001

第二步:每 4 位转成 1 位十六进制

  • 1000 → 8
  • 0001 → 1

合起来:0x81

大端 / 小端

对于多字节数字,在内存里是高位先存还是低位先存。

大端(BE = Big Endian):高字节 放在 前面(低地址)。

比如数字:0x1234,拆成两个字节:

  • 高字节:0x12
  • 低字节:0x34

内存顺序:[12, 34],就像我们正常写字:从左到右,高位在前。

小端(LE = Little Endian):低字节 放在 前面(低地址)。

内存顺序:[34, 12],反过来存。

网络协议(WebSocket、TCP、IP、自定义协议)全部用大端 BE!

所以:writeUInt16BEreadUInt16BE才是正确的。

nodejs如何操作二进制数据

ArrayBuffer、TypedArray、DataView

我们经常用的 string 存储的是字符串,比如 utf8,他可能一个字符占 1-3 个字节。

如果想操作原始的字节数组呢?

这时候就可以用 js 语言内置的 ArrayBuffer 的 api 了。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        const buffer = new ArrayBuffer(10);

        const arr1 = new Uint16Array(buffer);

        arr1[0] = 256;
        console.log(arr1)

        const arr2 = new Uint8Array(buffer);
        console.log(arr2);
    </script>
</body>
</html>

ArrayBuffer 不能直接操作,我们可以通过具体的 TypedArray 来操作。

TypedArray 也就是指定你存的是什么类型的数据,比如 8 位有符号整型、16 位无符号整型等。

有符号需要有一位专门来存储 0 1 代表正负,比如 8 位有符号整数,只有 7 位可以用来存二进制数字,所以范围是 2 的 7 次方:-128 到 127 (还需要存储 0)

那无符号就多了一个位来存储数字,所以范围是 2 的 8 次方,也就是 256。

Uint16Array 是用 16 位也就是 2 个字节来存储数字,而 Uint8Array 只用一个字节存储。

我们用 Uint6Array 存储了一个 256 的数字,那就超出了 Uint8Array 的范围了,会作为两个数字

image.png

可以看到 Uint16Array 是 5 个元素,Uint8 是 10 个元素。因为每个元素的字节数不同, Uint16Array是每个元素两个字节,Uint8每个元素一个字节。

Uint16Array 里的 256,在 Unit8Array 里就作为了两个数字,0 和 1。

那为什么是0和1呢?

我们把256拆成两段8 位二进制,256 = 00000001 00000000。

  • 高 8 位:00000001 → 十进制 = 1

  • 低 8 位:00000000 → 十进制 = 0

它们转化为十进制的时候就是 1 和 0。

有因为JavaScript 使用小端序(Little Endian),所以,内存第1字节:00000000 → 0, 内存第2字节:00000001 → 1。

如果把arr1[0] = 178; 用unit8Array读取是多少?

178 是小于 256 的数,用 16 位表示就是:

  • 高 8 位:00000000 → 0
  • 低 8 位:10110010 → 178

结果是:[178, 0, 0, 0, 0, 0, 0, 0, 0, 0]

TypedArray 一般有比较多的类型:

image.png

不同数字类型数组的长度都不同。

image.png

length 是取 TypedArray 的长度,而 byteLength 是取 ArrayBuffer 的长度:

image.png

当然,如果你觉得对同一个 ArrayBuffer 操作,需要转成不同的 TypedArray 太麻烦,也可以不转,用 DataView 来操作。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        const buffer = new ArrayBuffer(10);

        const dataView = new DataView(buffer);

        dataView.setUint16(0, 256);

        console.log(dataView.getUint16(0));

        console.log(dataView.getUint8(0));
        console.log(dataView.getUint8(8));
    </script>
</body>
</html>

我们之前用 Uint8Array、Uint16Array 读写元素,指定下标就行,会自动算出来在哪个字节读写。

但用 DataView,你需要告诉它在哪个字节开始读写,然后读写的是什么类型。

image.png

image.png

DataView 相比 TypedArray 更加灵活。

如果你需要灵活的读写 ArrayBuffer 里的元素的时候就用 DataView,否则用 TypedArray 更简单。

刚才这些代码在 node 里也一样能跑。

因为 node 也是用 v8 引擎来跑 js 代码的,JS 的 api 在 node 里自然也都可以用。

说回这节要学的 Buffer。

Buffer

它继承了 js 的 ArrayBuffer,是它的子类。

在 node 里非常多的 api 都用到了 buffer,比如你用 fs.readFile 的时候,读出来的就是 buffer:

const fs = require('node:fs/promises');

(async function(){
    const res = await fs.readFile('./package.json');
    console.log(res);
})();

image.png

当你指定字符集的时候,才会根据编码转为字符串:

image.png

在 Node.js 里,操作二进制数据都是用 Buffer

我们来学一下它的 api:

import { Buffer } from 'node:buffer'

// 分配一个 10 个字节的 buffer,用 6 填充
const buf1 = Buffer.alloc(10, 6)

console.log(buf1)

for (let i = 0; i < buf1.length; ++i) {
  console.log(buf1[i])
}

const buf2 = Buffer.from('神说要有光', 'utf-8')

const buf3 = Buffer.from([1, 2, 3])

// hex = 十六进制 1 个字节 = 2 个 hex 字符
console.log(buf1.toString('hex'))

console.log(buf2.toString('utf-8'))
console.log(buf2.toString('base64'))

console.log(buf3.toString('hex'))

console.log(new Uint8Array(buf3))

Buffer.alloc(10, 6) 是分配一个 10 个字节的 buffer,用 6 填充。

Buffer.from('神说要有光', 'utf-8') 是把神说要有光按照 utf-8 转为字节数组,创建的对应的 buffer。

Buffer.from([1, 2, 3]) 是根据传入的字节数组来创建 buffer。

然后我们调用了 toString 方法,分别按照 hex、utf-8、base64 的格式打印。

image.png

buf.toString() 就是把二进制 Buffer 转成人能看懂的字符串,你可以指定编码格式,不同格式出来完全不一样。

  • utf8(默认)转普通字符串、中文、英文

  • hex 把每个字节转成 2 位十六进制

  • base64 转 base64 格式(图片、传输)

  • ascii 转英文字符

默认编码是 utf8

此外,Buffer 自带了 DataView 的 api:

const { Buffer } = require('node:buffer');

const buffer = Buffer.alloc(10);

buffer.writeUint16LE(256, 0)

console.log(buffer.readUInt16LE(0));
console.log(buffer.readUint8(0), buffer.readUint8(1));

打印结果: 256 0 1。采用了小端顺序。

这其实就是我们之前用 DataView 做的事情:

image.png

Buffer 内置了 DataView 这些灵活读写字节数组的 api。

此外,这个 Buffer 对象被挂到了全局,就算不引入 node:buffer 模块也可以用:

image.png

但最好还是显式引入。

Blob

此外,Buffer 因为有 readUint8、writeUint8 这种读写的 api,所以是可变的。

有的时候,我们要求二进制数据不可变,不可以读写,这时候就要用 Blob 的 api 了。

image.png

涉及到多线程的数据,基本都是用 Blob 而不是 Buffer。

为什么呢?

如果多个线程并发的改一个可变的数据,那你怎么知道读取的数据是不是被哪个线程改过了的?

所以都是用 Blob,不让它变。

在 Node.js 文档里,你可以看到 buffer 有这么多读写方法:

image.png

而 Blob 没有:

image.png

之前用 node:workder-threads 包写多线程案例的时候,我们用过 MessageChannel。

它有 port1、port2 两个端口,在一边 postMessage、另一边可以通过 message 事件拿到消息:

image.png

当传递二进制数据的时候,最好用 Blob 而不是 Buffer:

const { Blob } = require('node:buffer');

const blob = new Blob(['神说要有光']);

const { port1, port2 } = new MessageChannel();

port1.onmessage = async ({ data }) => {
  console.log(data);
  console.log(await data.text())
  console.log(await data.arrayBuffer())
};

port2.postMessage(blob);

image.png

可以通过 blob.text、blob.arrayBuffer 来把二进制数据转为文本、ArrayBuffer。

当你想传递不可变二进制数据的时候,就用 Blob。

其实浏览器里也有 Blob 对象,也是用来放不可变数据的,比如 File 对象就是它的子类。

总结

不管是浏览器还是 Node.js 都有操作二进制数据的需求,所以 JS 标准里有 ArrayBuffer、Blob、DataView、TypedArray 等 api。

  • ArrayBuffer 是用来存储可变的二进制数据的,通过 Uint8Array 等 TypedArray 来通过下标读写,或者通过 DataView 的 setUint16、getUint8 等来灵活读写。
  • Blob 是不可变的二进制数据,用来传递一些参数之类的很合适,浏览器里的 File 就是 Blob 的子类,有 text、arrayBuffer 等方法来转换成别的格式。
  • Node.js 里继承 ArrayBuffer 实现了 Buffer 的 api,可以通过 alloc、from 创建 buffer,通过 readUint8、writeUint16LE 等来灵活读写

Node.js 里很多 api 都是基于 Buffer 的,比如 fs.readFile。

但当你需要传输不可变数据的时候,还是用 Blob 更合适一点。

可能大家平时很少会操作二进制数据,但这个是必须掌握的知识点,不管是写页面还是写 Node.js。