这个系列是翻译文章,虽然是翻译,但有些地方还是做了一些修改。如果你想要看原版,那可以根据这个链接去查找。
这篇文章介绍了 Node 中另一个重要的概念:Buffer
。为了理解它,我们还会解释什么是二进制数据和为什么我们需要 character encodings(字符编码) 。所有这些信息在深入研究 Node.js 的其他部分,例如stream
时非常重要。
Buffer
Buffer
是 Node 中的一个全局对象,它不需要require
就可以直接使用。它的主要作用是帮助我们处理二进制数据。
因为服务端不像客户端只需要做一些简单的字符操作或 DOM 操作,服务端需要处理网络协议
、数据库操作
、图片处理
、文件处理
、加解密
等,在这些操作中,需要处理大量的二进制数据。
但是它究竟是什么?
计算机以二进制(0 和 1)表示数据。要存储一个数字,计算机首先将其转换为二进制表示。数字转换通常相对简单,在大多数情况下,不会对其二进制形式产生任何疑问。
但是数字并不是我们处理的唯一数据类型:我们还有图像、文本、视频等等。为了表示这些数据,我们需要制定一些约定,因为所有数据都是用数字来表示的。当涉及到文本时,有多种字符编码,定义字符集以及如何使用数字来表示它们。其中一个非常流行的编码是UTF-8,我们在本文中使用它。
buffer 是一个数字数组
buffer 对象,类似于 number 数组,它的每个元素为 16 进制的两位数(存储的时候还是二进制,显示为 16 进制是因为可读性并且可以减少字符输出),表示一个字节。由于单个字节上保存的最大数字为 255 (因为一个字节可以表示的二进制位数是 8 位(1 个字节 = 8 个二进制位)。而每一位的值只有 0 或 1 两种可能,所以 8 位的二进制数可以表示 2^8 种不同的数值,即 256 种(从 0 到 255)。因此,一个字节最多只能表示到 255。) 因此 buffer 元素不能包含更大的数字:
const buffer = Buffer.alloc(5);
buffer[0] = 255;
console.log(buffer[0]); // 255
buffer[1] = 256;
console.log(buffer[1]); // 0
buffer[2] = 260;
console.log(buffer[2]); // 4
console.log(buffer[2] === 260 % 256); // true
buffer[3] = 516;
console.log(buffer[3]); // 4
console.log(buffer[3] === 516 % 256); // true
buffer[4] = -50;
console.log(buffer[4]); // 206
如上所示,如果你尝试分配一个大于 255的值,256就会除以这个数,并将余数分配给该值。
而负数的处理比较不同。如果你尝试将一个负数赋给一个字节,它将使用二进制补码系统进行转换。
我们可以看到最后一个例子buffer[4] = -50
,输出的是 206
它的计算过程如下:
$-50_{(10)} = 11001110_{(U2)}$
上面的公式表示把十进制数-50 转化成二进制补码表示,补码表示方式为取反加一。具体过程如下:
- 取绝对值,即 50,转化为二进制数:00110010
- 取反:11001101
- 加一:11001110
最终得到的11001110是 -50 在二进制补码表示方式下的值。
然后在 JavaScript 中输出一个数字的时候,会默认将这个二进制数转换回正常的十进制展示。也就是类似如下,所以输出 206
parseInt('11001110', 2); // 206
当你创建一个 buffer,你也可以使用一个值来填充它
// Creates a Buffer of length 5, filled with 1
const buffer = Buffer.alloc(5, 1);
// Creates a Buffer containing 1, 2, 3
const buffer = Buffer.from([1, 2, 3]);
字符串 Buffer
由于 Buffer 是存储字节数据, 你也可以使用 Buffer 来操作字符串
const buffer = Buffer.from("Hello world!");
默认情况下,我们需要记住它使用的是UTF-8
编码。你可以使用传递给from
函数的第二个参数来更改它。
例如 Buffer 可以使用 toString 方法来轻松读取
const buffer = Buffer.from("Hello world!");
console.log(buffer.toString()); // Hello world!
当然也并不总是那么简单!有许多 UTF-8 字符需要多个字节来表示,这可能会给你带来一些麻烦。让我们看看这个字符串:
Hello 🌎 world!
中间有一个 emoji,由四个字节组成: 11110000
10011111
10001100
10001110
我们把这些数据保存在多个 buffer:
const buffers = [
Buffer.from("Hello "),
Buffer.from([0b11110000, 0b10011111]),
Buffer.from([0b10001100, 0b10001110]),
Buffer.from(" world!"),
];
0b
表示是在 JavaScript 中写一个二进制数据
如果你按块解析一个大的文本文件,然后逐块解析,其中一个块可能只包含字符的一部分,就像上面的例子一样。
可以看我们下面的输出:
let result = "";
buffers.forEach((buffer) => {
result += buffer.toString();
});
console.log(result); // Hello ��� world!
它并没有达到我们想要的效果。这是因为每个 buffer 都被单独处理。我们可以使用StringDecoder
进行改进。它提供了一种 API,用于将 Buffer 对象解码为字符串,同时保留多字节字符。
import { StringDecoder } from "string_decoder";
const decoder = new StringDecoder("utf8");
const buffers = [
Buffer.from("Hello "),
Buffer.from([0b11110000, 0b10011111]),
Buffer.from([0b10001100, 0b10001110]),
Buffer.from(" world!"),
];
const result = buffers.reduce(
(result, buffer) => `${result}${decoder.write(buffer)}`,
""
);
console.log(result); // Hello 🌎 world!
StringDecoder
可以确保解码后的字符串不包含任何不完整的多字节字符,它通过将不完整的字符保留在内部 buffer 中,直到下一次调用decoder.write()
方法。
读取一个文件
在该系列的第一部分,我们读取一个指定编码的文件。
export default async function cat(path: string) {
try {
const content = await fs.promises.readFile(path, { encoding: "utf-8" });
console.log(content);
} catch (err) {
console.log(err);
}
}
正是因为指定了编码格式,所以我们会接收到一个字符串。如果我们不提供一个编码格式,接收到的就是原始的 buffer。需要通过toString
来转换
import * as fs from "fs";
async function readFile() {
try {
const content = await fs.promises.readFile("./file.txt");
console.log(content instanceof Buffer); // true
console.log(content.toString());
} catch (err) {
console.log(err);
}
}
readFile();
readFile
方法会一次性读取整个文件的内容。因此,即使文件非常大,它也只会在整个文件处理完后调用一次回调函数。如果想在整个文件内容被加载之前对文件的部分内容执行操作,需要使用createReadStream
函数返回一个流。
总结
buffer
是一个字节数组
,其中每个元素的值范围从 0 到 255。由于所有类型的数据(如图像和文本)都必须表示为数字,因此我们还解释了字符编码
的概念。在讨论即将涉及到的流时,所有这些信息都是很重要的。