背景知识
- ArrayBuffer
ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。 ArrayBuffer 不能直接操作,而是要通过类型数组对象 或 DataView 对象来操作,它们会将缓冲区中的数据表示为特定的格式,并通过这些格式来读写缓冲区的内容。
你可以把它理解为一块内存, 具体存什么需要其他的声明。
1new ArrayBuffer(length)23// 参数:length 表示要创建的 ArrayBuffer 的大小,单位为字节。4// 返回值:一个指定大小的 ArrayBuffer 对象,其内容被初始化为 0。5// 异常:如果 length 大于 Number.MAX_SAFE_INTEGER(>= 2 ** 53)或为负数,则抛出一个 RangeError 异常。
ex. 比如这段代码, 可以执行一下看看输出什么
1var buffer = new ArrayBuffer(8);2var view = new Int16Array(buffer);34console.log(buffer);5console.log(view);
- Unit8Array
Uint8Array 数组类型表示一个 8 位无符号整型数组,创建时内容被初始化为 0。 创建完后,可以以对象的方式或使用数组下标索引的方式引用数组中的元素。
1// 来自长度2var uint8 = new Uint8Array(2);3uint8[0] = 42;4console.log(uint8[0]); // 425console.log(uint8.length); // 26console.log(uint8.BYTES_PER_ELEMENT); // 178// 来自数组9var arr = new Uint8Array([21,31]);10console.log(arr[1]); // 311112// 来自另一个 TypedArray13var x = new Uint8Array([21, 31]);14var y = new Uint8Array(x);15console.log(y[0]); // 21
- ArrayBuffer 和 TypedArray的关系
TypedArray: Unit8Array, Int32Array这些都是TypedArray, 那些 Uint32Array 也好,Int16Array 也好,都是给 ArrayBuffer 提供了一个 “View”,MDN上的原话叫做 “Multiple views on the same data”,对它们进行下标读写,最终都会反应到它所建立在的 ArrayBuffer 之上。
ArrayBuffer 本身只是一个 0 和 1 存放在一行里面的一个集合,ArrayBuffer 不知道第一个和第二个元素在数组中该如何分配。
为了能提供上下文,我们需要将其封装在一个叫做 View 的东西里面。这些在数据上的 View 可以被添加进确定类型的数组,而且我们有很多种确定类型的数据可以使用。
3.1 例如,你可以使用一个 Int8 的确定类型数组来分离存放 8 位二进制字节。
3.2 例如,你可以使用一个无符号的 Int16 数组来分离存放 16 位二进制字节。
(是不是和8分音符和16分音符有异曲同工之妙?)
- 总结
总之, ArrayBuffer 基本上扮演了一个原生内存的角色.
NodeJs Buffer
Buffer 类以一种更优化、更适合 Node.js 用例的方式实现了 Uint8Array API.
Buffer 类的实例类似于整数数组,但 Buffer 的大小是固定的、且在 V8 堆外分配物理内存。 Buffer 的大小在被创建时确定,且无法调整。
基本使用
1// 创建一个长度为 10、且用 0 填充的 Buffer。2const buf1 = Buffer.alloc(10);34// 创建一个长度为 10、且用 0x1 填充的 Buffer。5const buf2 = Buffer.alloc(10, 1);67// 创建一个长度为 10、且未初始化的 Buffer。8// 这个方法比调用 Buffer.alloc() 更快,9// 但返回的 Buffer 实例可能包含旧数据,10// 因此需要使用 fill() 或 write() 重写。11const buf3 = Buffer.allocUnsafe(10);1213// 创建一个包含 [0x1, 0x2, 0x3] 的 Buffer。14const buf4 = Buffer.from([1, 2, 3]);1516// 创建一个包含 UTF-8 字节 的 Buffer。17const buf5 = Buffer.from('tést');
tips
- 当调用 Buffer.allocUnsafe() 时,被分配的内存段是未初始化的(没有用 0 填充)。
虽然这样的设计使得内存的分配非常快,但已分配的内存段可能包含潜在的敏感旧数据。 使用通过 Buffer.allocUnsafe() 创建的没有被完全重写内存的 Buffer ,在 Buffer内存可读的情况下,可能泄露它的旧数据。 虽然使用 Buffer.allocUnsafe() 有明显的性能优势,但必须额外小心,以避免给应用程序引入安全漏洞。
Buffer 与字符编码
Buffer 实例一般用于表示编码字符的序列,比如 UTF-8 、 UCS2 、 Base64 、或十六进制编码的数据。 通过使用显式的字符编码,就可以在 Buffer 实例与普通的 JavaScript 字符串之间进行相互转换。
1const buf = Buffer.from('hello world', 'ascii');23console.log(buf)45// 输出 68656c6c6f20776f726c646console.log(buf.toString('hex'));78// 输出 aGVsbG8gd29ybGQ=9console.log(buf.toString('base64'));
Node.js 目前支持的字符编码包括:
ascii
- 仅支持 7 位 ASCII 数据。如果设置去掉高位的话,这种编码是非常快的。utf8
- 多字节编码的 Unicode 字符。许多网页和其他文档格式都使用 UTF-8 。utf16le
- 2 或 4 个字节,小字节序编码的 Unicode 字符。支持代理对(U+10000 至 U+10FFFF)。ucs2
- ‘utf16le’ 的别名。base64
- Base64 编码。当从字符串创建 Buffer 时,按照 RFC4648 第 5 章的规定,这种编码也将正确地接受 “URL 与文件名安全字母表”。latin1
- 一种把 Buffer 编码成一字节编码的字符串的方式(由 IANA 定义在 RFC1345 第 63 页,用作 Latin-1 补充块与 C0/C1 控制码)。binary
- ‘latin1’ 的别名。hex
- 将每个字节编码为两个十六进制字符。
Buffer 内存管理
在介绍 Buffer 内存管理之前,我们要先来介绍一下 Buffer 内部的 8K 内存池。
8K 内存池
- 在 Node.js 应用程序启动时,为了方便地、高效地使用 Buffer,会创建一个大小为 8K 的内存池。
1Buffer.poolSize = 8 * 1024; // 8K2var poolSize, poolOffset, allocPool;34// 创建内存池5function createPool() {6 poolSize = Buffer.poolSize;7 allocPool = createUnsafeArrayBuffer(poolSize);8 poolOffset = 0;9}1011createPool();
- 在 createPool() 函数中,通过调用 createUnsafeArrayBuffer() 函数来创建 poolSize(即8K)的 ArrayBuffer 对象。createUnsafeArrayBuffer() 函数的实现如下:
1function createUnsafeArrayBuffer(size) {2 zeroFill[0] = 0;3 try {4 return new ArrayBuffer(size); // 创建指定size大小的ArrayBuffer对象,其内容被初始化为0。5 } finally {6 zeroFill[0] = 1;7 }8}
这里你只需知道 Node.js 应用程序启动时,内部有个 8K 的内存池即可。
- 那接下来我们要介绍哪个对象呢?在前面的预备知识部分,我们简单介绍了 ArrayBuffer 和 Unit8Array 相关的基础知识,而 ArrayBuffer 的应用在 8K 的内存池部分的已经介绍过了。那接下来当然要轮到 Unit8Array 了,我们再来回顾一下它的语法:
1Uint8Array(length);2Uint8Array(typedArray);3Uint8Array(object);4Uint8Array(buffer [, byteOffset [, length]]);
其实除了 Buffer 类外,还有一个 FastBuffer 类,该类的声明如下:
1class FastBuffer extends Uint8Array {2 constructor(arg1, arg2, arg3) {3 super(arg1, arg2, arg3);4 }5}
是不是知道 Uint8Array 用在哪里了,在 FastBuffer 类的构造函数中,通过调用 Uint8Array(buffer [, byteOffset [, length]]) 来创建 Uint8Array 对象。
- 那么现在问题来了,FastBuffer 有什么用?它和 Buffer 类有什么关系?带着这两个问题,我们先来一起分析下面的简单示例:
1const buf = Buffer.from('semlinker');2console.log(buf); // <Buffer 73 65 6d 6c 69 6e 6b 65 72>
为什么输出了一串数字, 我们创建的字符串呢? 来看一下源码
1/**2 * Functionally equivalent to Buffer(arg, encoding) but throws a TypeError3 * if value is a number.4 * Buffer.from(str[, encoding])5 * Buffer.from(array)6 * Buffer.from(buffer)7 * Buffer.from(arrayBuffer[, byteOffset[, length]])8 **/9Buffer.from = function from(value, encodingOrOffset, length) {10 if (typeof value === "string") return fromString(value, encodingOrOffset);11 // 处理其它数据类型,省略异常处理等其它代码12 if (isAnyArrayBuffer(value))13 return fromArrayBuffer(value, encodingOrOffset, length);14 var b = fromObject(value);15};
可以看出 Buffer.from() 工厂函数,支持基于多种数据类型(string、array、buffer 等)创建 Buffer 对象。对于字符串类型的数据,内部调用 fromString(value, encodingOrOffset) 方法来创建 Buffer 对象。
是时候来会一会 fromString() 方法了,它内部实现如下:
1function fromString(string, encoding) {2 var length;3 if (typeof encoding !== "string" || encoding.length === 0) {4 if (string.length === 0) return new FastBuffer();5 // 若未设置编码,则默认使用utf8编码。6 encoding = "utf8";7 // 使用 buffer binding 提供的方法计算string的长度8 length = byteLengthUtf8(string);9 } else {10 // 基于指定的 encoding 计算string的长度11 length = byteLength(string, encoding, true);12 if (length === -1)13 throw new errors.TypeError("ERR_UNKNOWN_ENCODING", encoding);14 if (string.length === 0) return new FastBuffer();15 }1617 // 当字符串所需字节数大于4KB,则直接进行内存分配18 if (length >= Buffer.poolSize >>> 1)19 // 使用 buffer binding 提供的方法,创建buffer对象20 return createFromString(string, encoding);2122 // 当剩余的空间小于所需的字节长度,则先重新申请8K内存23 if (length > poolSize - poolOffset)24 // allocPool = createUnsafeArrayBuffer(8K); poolOffset = 0;25 createPool();26 // 创建 FastBuffer 对象,并写入数据。27 var b = new FastBuffer(allocPool, poolOffset, length);28 const actual = b.write(string, encoding);29 if (actual !== length) {30 // byteLength() may overestimate. That's a rare case, though.31 b = new FastBuffer(allocPool, poolOffset, actual);32 }33 // 更新pool的偏移34 poolOffset += actual;35 alignPool();36 return b;
所以我们得到这样的结论
- 当未设置编码的时候,默认使用 utf8 编码;
- 当字符串所需字节数大于4KB,则直接进行内存分配;
- 当字符串所需字节数小于4KB,但超过预分配的 8K 内存池的剩余空间,则重新申请 8K 的内存池;
- 调用 new FastBuffer(allocPool, poolOffset, length) 创建 FastBuffer 对象,进行数据存储,数据成功保存后,会进行长度校验、更新 poolOffset 偏移量和字节对齐等操作。