在学习nodejs的二进制之前,先回顾下基本知识。
基本概念
字节
字节是计算机里最小的可操作单元,1 字节 = 8 个比特(bit),1 个比特就是 1 个 0 或 1。
所以:1 字节 = 8 位二进制,范围:00000000 ~ 11111111,对应十进制:0 ~ 255。
这就是为什么:
Uint8Array每个值 0~255Buffer每个字节也是 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→ 80001→ 1
合起来:0x81
大端 / 小端
对于多字节数字,在内存里是高位先存还是低位先存。
大端(BE = Big Endian):高字节 放在 前面(低地址)。
比如数字:0x1234,拆成两个字节:
- 高字节:0x12
- 低字节:0x34
内存顺序:[12, 34],就像我们正常写字:从左到右,高位在前。
小端(LE = Little Endian):低字节 放在 前面(低地址)。
内存顺序:[34, 12],反过来存。
网络协议(WebSocket、TCP、IP、自定义协议)全部用大端 BE!
所以:writeUInt16BE,readUInt16BE才是正确的。
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 的范围了,会作为两个数字。
可以看到 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 一般有比较多的类型:
不同数字类型数组的长度都不同。
length 是取 TypedArray 的长度,而 byteLength 是取 ArrayBuffer 的长度:
当然,如果你觉得对同一个 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,你需要告诉它在哪个字节开始读写,然后读写的是什么类型。
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);
})();
当你指定字符集的时候,才会根据编码转为字符串:
在 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 的格式打印。
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 做的事情:
Buffer 内置了 DataView 这些灵活读写字节数组的 api。
此外,这个 Buffer 对象被挂到了全局,就算不引入 node:buffer 模块也可以用:
但最好还是显式引入。
Blob
此外,Buffer 因为有 readUint8、writeUint8 这种读写的 api,所以是可变的。
有的时候,我们要求二进制数据不可变,不可以读写,这时候就要用 Blob 的 api 了。
涉及到多线程的数据,基本都是用 Blob 而不是 Buffer。
为什么呢?
如果多个线程并发的改一个可变的数据,那你怎么知道读取的数据是不是被哪个线程改过了的?
所以都是用 Blob,不让它变。
在 Node.js 文档里,你可以看到 buffer 有这么多读写方法:
而 Blob 没有:
之前用 node:workder-threads 包写多线程案例的时候,我们用过 MessageChannel。
它有 port1、port2 两个端口,在一边 postMessage、另一边可以通过 message 事件拿到消息:
当传递二进制数据的时候,最好用 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);
可以通过 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。