Buffer

在ECMAScript 2015(ES6)中引入TypedArray之前,JavaScript中没有读取或操作二进制数据流的机制。 Buffer类作为Node.js标准API的一部分被引入,使得可以在诸如TCP流和文件系统操作的上下文中通过8位的字节流进行交互。

现在TypedArray已经添加到ES6中,Buffer类以一种更优化和适用于Node.js的方式实现了Uint8Array的API。

Buffer类的实例似于整数数组,不过是对应V8堆外的固定大小的原始内存进行分配(堆外内存,不受V8引擎控制)。 缓冲区的大小在创建时已经确定,不能调整大小。

Buffer类是Node.js中的一个全局变量,因此不需要使用require('buffer').Buffer

// 创建一个长度为10的『0填充』Buffer实例
const buf1 = Buffer.alloc(10);

// 创建一个长度为10的Buffer,填充0x1
const buf2 = Buffer.alloc(10, 0x1);

// 创建一个长度为10的未初始化缓冲区
// 这个方法比使用Buffer.alloc()快,但是返回的Buffer实例可能包含之前的老数据。
// 需要使用fill()或write()重写。
const buf3 = Buffer.allocUnsafe(10);

// 创建一个包含[0x1, 0x2, 0x3]的缓冲区
const buf4 = Buffer.from([1, 2, 3]);

// 创建一个包含UTF-8字节[0x74, 0xc3, 0xa9, 0x73, 0x74]的缓冲区
const buf5 = Buffer.from('tést');

// 创建一个包含Latin-1字节[0x74, 0xe9, 0x73, 0x74]的缓冲区
const buf6 = Buffer.from('tést', 'latin-1');

Buffer.from(),Buffer.alloc(),Buffer.allocUnsafe()

在v6之前的Node.js版本中,使用Buffer构造函数创建了Buffer实例,该函数根据提供的参数以不同的方式分配返回的Buffer:

  • 将一个数字作为第一个参数传递给Buffer() (例如new Buffer(10)),分配一个指定大小的新的Buffer对象。 为这些缓冲区实例分配的内存不会被初始化,并且可能包含敏感数据。 这种缓冲区实例必须通过使用buf.fill(0)或完全写入缓冲区来手动初始化。 虽然这种行为是为了提高性能,但是开发经验表明,创建一个快速但未初始化的缓冲区和创建一个更慢但更安全的缓冲区之间需要更明确的区分。

  • 传递字符串,数组或Buffer作为第一个参数将传递的对象的数据复制到缓冲区中。

  • 传递ArrayBuffer返回一个与给定的ArrayBuffer共享已分配内存的Buffer。

因为new Buffer()的行为是基于第一个参数传递的值的类型来选择具体操作,如果没有传递正确的参数给给new Buffer()或未能适当地初始化新分配的Buffer中缓冲区内容。那么此类应用将在无意中将安全性和可靠性的相关问题引入到他们的代码中。

为了使缓冲区实例的创建更可靠,出现更少的错误,new Buffer()造函数的这种形式已被弃用,使用Buffer.from()Buffer.alloc()Buffer.allocUnsafe()方法来代替。

开发人员应该将现在使用的new Buffer()函数迁移到以下这些新的API之一。

  • Buffer.from(array)返回一个包含所提供的八位字节的副本的新缓冲区。

  • Buffer.from(arrayBuffer [,byteOffset [,length]])返回一个新的缓冲区,它与给定的ArrayBuffer共享相同的分配内存。

  • Buffer.from(buffer)返回一个包含给定Buffer的内容的副本的新缓冲区。

  • Buffer.from(string [,encoding])返回一个包含所提供字符串副本的新缓冲区。

  • Buffer.alloc(size [,fill [,encoding]])返回指定大小的“已填充”Buffer实例。 此方法虽然明显慢于Buffer.allocUnsafe(size)方法,但它可以确保新创建的Buffer实例不会包含旧的和潜在的敏感数据。

  • Buffer.allocUnsafe(size)Buffer.allocUnsafeSlow(size)都返回一个指定大小的新的缓冲区,其内容必须使用buf.fill(0)初始化或完全写入新的内容以覆盖旧的敏感数据。

Buffer.allocUnsafe()返回的缓冲区实例的大小小于或等于Buffer.poolSize的一半那么可以在内部共享内存池中进行分配。 而Buffer.allocUnsafeSlow()返回的实例不会使用内部共享内存池。


--zero-fill-buffers命令行参数

Node.js可以使用--zero-fill-buffers命令行参数启动,强制所有新分配的Buffer实例在使用new Buffer(size)Buffer.allocUnsafe()Buffer.allocUnsafeSlow()或者new SlowBuffer()在创建时自动0。 使用此参数会更改这些方法的默认行为,并且会对性能产生重大影响。 建议仅在必要时强制使用--zero-fill-buffers选项,以确保新分配的缓冲区实例不会包含潜在敏感数据。

$ node --zero-fill-buffers
> Buffer.allocUnsafe(5);
<Buffer 00 00 00 00 00>

为什么Buffer.allocUnsafe()Buffer.allocUnsafeSlow()不安全?

当调用Buffer.allocUnsafe()Buffer.allocUnsafeSlow()时,分配的内存段未初始化(不归0)。 虽然该设计使得存储器的分配相当快,但是所分配的存储器段可能包含潜在敏感的旧数据。 使用由Buffer.allocUnsafe()创建的缓冲区而不完全重新覆盖的内存可能在读取缓冲区内存时泄漏这些旧数据。

虽然使用Buffer.allocUnsafe()有明显的性能优势,但必须格外小心,以避免将安全漏洞引入应用程序。


Buffers和字符编码

Buffer实例通常用于表示编码字符的序列,例如UTF-8,UCS2,Base64甚至Hex编码数据。 可以通过使用字符编码声明字符串在缓冲区实例和普通JavaScript字符串之间来回转换。

const buf = Buffer.from('hello world', 'ascii');

console.log(buf.toString('hex')); // => 68656c6c6f20776f726c64

console.log(buf.toString('base64')); // => aGVsbG8gd29ybGQ=

Node.js当前支持的字符编码包括:

  • ascii

  • utf8

  • utf16le

  • ucs2

  • base64

  • latin1

  • binary

  • hex

注意:当今的浏览器遵循WHATWG规范,将“latin1”和ISO-8859-1都合并到win-1252。 这意味着,当做类似于http.get()的操作时,如果返回的字符集是在WHATWG规范列出的范围内,服务器可能实际返回的是win-1252的编码数据,此时使用“latin1”编码可能会错误地解码字符,从而出现乱码。


Buffers和TypeArray

Buffer实例也是Uint8Array实例。 但是,与ECMAScript 2015中的TypedArray规范有一点点的不兼容。例如,当ArrayBuffer#slice()建了一个slice的副本时,Buffer#slice()实现创建了一个没有复制的现有Buffer的副本,使得Buffer#slice()更有效率。

Buffer中创建新的TypedArray实例时需要注意以下事项:

  1. Buffer对象的内存是从TypedArray复制过来的,不是共享。

  2. Buffer对象的内存可以解释为不同元素的数组,而不是目标类型的字节数组。 也就是说,new Uint32Array(Buffer.from([1,2,3,4]))是通过数组[1,2,3,4]创建一个长度为4的Uint32Array,而不是通过[0x1020304][0x4030201]创建一个长度为1的Uint32Array

可以通过使用TypeArray对象的.buffer属性创建一个新的Buffer,它与TypedArray实例共享相同的分配内存。

const arr = new Uint16Array(2);

arr[0] = 5000;
arr[1] = 4000;

const buf1 = Buffer.from(arr);

const buf2 = Buffer.from(arr.buffer);

console.log(buf1); // => <Buffer 88 a0>

console.log(buf2); // => <Buffer 88 13 a0 0f>

arr[1] = 6000;

console.log(buf1); // => <Buffer 88 a0>

console.log(buf2); // => <Buffer 88 13 70 17>

注意,当使用TypedArray的.buffer创建一个Buffer时,可以通过传递byteOffsetlength参数来使用ArrayBuffer的一部分内容。

const arr = new Uint16Array(20);
const buf = new Buffer.from(arr.buffer, 0, 16);

console.log(buf.length); // => 16

Buffer.from()TypedArray.from()有不同的函数签名和实现。 具体来说,TypedArray变量接受第二个参数,它是在类型数组的每个元素上调用的映射函数:

  • TypedArray.from(source[, mapFn[, thisArg]])

Float32Array.from([1, 2, 3], x => x + x);      
// Float32Array [ 2, 4, 6 ]

然而,Buffer.from()方法不支持使用映射函数:

  • Buffer.from(array)

  • Buffer.from(buffer)

  • Buffer.from(arrayBuffer[, byteOffset [, length]])

  • Buffer.from(string[, encoding])


Buffers和ES6的迭代器

可以使用ECMAScript 2015(ES6)for..of语法对缓冲区实例进行迭代。

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

for (const b of buf) {
    console.log(b)); // => 1 2 3
}

此外,buf.values()buf.keys()buf.entries()方法都可用于创建迭代器。



张亚涛
5.3k 声望2.8k 粉丝

人首先应该接受现实,承认问题的存在,并且反思它。而不是首先就跳出来回避这个问题,找比我们更差的。质疑甚至抨击提出问题的人,并且一杆子打死。