Node.js 模块之 Buffer

后端2016-10-080 篇评论 Node.js

在 TypedArray (ES6) 之前,JavaScript 语言还不能直接从二进制数据流中读取数据。Buffer 模块就被用来作为 Node 的核心 API,负责一些如 TCP 流和文件操作的功能。

在 ES6 的 TypedArray 出现后,Buffer 模块也继承了 Uint8Array,并且让它更适用于 Node.js 的场景。

Buffer 模块是 Node.js 中的全局对象,所以不需要像其它模块那样通过 require('buffer').Buffer 这样来导入 Buffer 模块,直接就能作为已存在的全局对象够使用。

Buffer 的实例与整数数组很相似,但是却有着固定的长度,一旦被创建就不能够改变大小。Buffer 在内存分配的位置上也有所不同,它并不是被 V8 分配在堆上,所以不存在受到 V8 内存限制的问题。它是由 Node 的 C++ 层面上来实现对内存的申请的,然后通过 JavaScript 来对内存进行分配。

创建 Buffer

在 Node.js V6 之后,形如 new Buffer(10) 这样创建一个 Buffer 的用法已经被废弃了,因为这样创建出来的 Buffer 对象实例并没有被初始化,而是保留了原先内存地址上一些随机的值,并且有可能包含一些敏感数据。那样的话,就必须通过 buf.fill(0) 或者向其完全写满数据这样的方式来手动初始化 Buffer 的值。

建议通过 Buffer.from(), Buffer.alloc(), Buffer.allocUnsafe()Buffer.allocUnsafeSlow() 方法来创建一个 Buffer:

  • Buffer.from(): 通过已有 String 或者 Array 转换成 Buffer 对象
  • Buffer.alloc(): 从内存中分配指定长度的内存空间,并且默认使用 0 值对分配的内存进行填充,可也手动指定用以初始化的填充值。
  • Buffer.allocUnsafe()Buffer.allocUnsafeSlow(): 从内存中分配指定长度的内存空间后,不对内存进行填充操作。

可使用 --zero-fill-buffers 命令行启动参数来强制对所有的内存分配操作进行 0 值填充。

为什么需要在分配内存后进行填充操作?

使用 new Buffer() 或者 Buffer.allocUnsafe() 来创建 Buffer 时,所分配的内存空间并不会被初始化,这样使得分配内存的速度较快,然而在被分配的内存区间段,可能会包含一些旧的敏感数据。如果不对这些数据进行填充操作的话,可能会让这些数据泄露出去。为了避免应用的一些漏洞产生,避免使用 Buffer.allocUnsafe() 来分配内存。

使用 Buffer

Buffer 与字符串间的相互转换

在使用 Buffer.from() 从字符串转换到 Buffer 对象的时候,可以指定字符串的编码来获取正确的二进制编码,并且也支持与字符串各编码的直接相互转换。支持的编码有:ascii, utf8, utf16le, ucs2, base64, latin1, binaryhex。使用方式如下:

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

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

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

Buffer 拼接的正确姿势

请使用 Buffer.concat(list[, totalLength]) 来实现多个 Buffer 对象的拼接操作。参数中 list 指待拼接的 Buffer 数组,totalLength 指待拼接的全部 Buffer 的总长度。 虽说这里 totalLength 是可选的,可以自动的从 list 中的每一个 Buffer 实例中计算出来,但这可能会导致额外的计算,所以最好的方式还是提供 totalLength 参数,使用方式如下:

const buf1 = Buffer.alloc(10);
const buf2 = Buffer.alloc(14);
const buf3 = Buffer.alloc(18);
const totalLength = buf1.length + buf2.length + buf3.length;
const bufA = Buffer.concat([buf1, buf2, buf3], totalLength);

console.log(bufA);
// <Buffer 00 00 00 00 ...>

console.log(bufA.length);
// 42

其它一些小 Tips

  • 使用 buf.slice() 不会返回拷贝后的 Buffer,而是直接在原有内存地址上进行截取,修改会影响原 Buffer 的值。
  • 使用 buf.swap16(), buf.swap32(), buf.swap64() 等能够将字节大小端翻转,很实用的一个功能。

深入 Buffer 的内存分配机制

在 Node.js v6 版本之后,new Buffer(size) 方法被弃用了,取而代之的与内存分配直接相关的方法有三个,分别是

  • Buffer.alloc()
  • Buffer.allocUnsafe()
  • Buffer.allocUnsafeSlow()

这三者对于内存单次分配的空间大小都有限制,取决于 kMaxLength 这个值,其在 32 位平台上的值是 2^30 - 1(约 1GB),在 64 位平台上的值是 2^31 - 1(约 2GB)。

Buffer.allocUnsafe() (与之前的 new Buffer(size) 机制类似)是三者中分配内存速度最快的方式,它采用了共享内存池(shared internal memory pool)这一方式,通过预先分配一定大小的一段内存,从中再向 JavaScript 分配相应大小的片段,避免频繁的向系统申请内存分配,来达到较高的效率。共享内存池的默认值 poolSize 为 8KB(可重新赋值),只有当需要分配的内存小于等于 poolSize 的一半时,Buffer.allocUnsafe() 才会从共享内存池从分配空间。

Note that the Buffer module pre-allocates an internal Buffer instance of size Buffer.poolSize that is used as a pool for the fast allocation of new Buffer instances created using Buffer.allocUnsafe() (and the deprecated new Buffer(size) constructor) only when size is less than or equal to Buffer.poolSize >> 1 (floor of Buffer.poolSize divided by two).

Buffer.alloc()Buffer.allocUnsafeSlow() 的区别只是在于是否会在分配内存后对内存进行填充,但与 Buffer.allocUnsafe() 都不一样的是,他们都绝不会从共享内存池中分配内存,而是直接在内存上开辟一块相应大小的空间。这和之前 Node.js 版本中的 SlowBuffer() 是一样的。这对于需要长期存储几段不连续的 Buffer 来说是很合适的,不会因为共享内存上有这些长期存在的变量而导致整个内存池无法进行垃圾回收,占用过量的内存空间。比如说下面这种情况使用 Buffer.allocUnsafe() 就是很合适的:

// 需要长期存储一些小的 Buffer 片段
const store = [];

socket.on('readable', () => {
  const data = socket.read();

  // 为数据分配单独的内存空间
  const sb = Buffer.allocUnsafeSlow(10);

  // 将数据拷贝到新分配的内存空间中去
  data.copy(sb, 0, 0, 10);

  store.push(sb);
});

所以针对实际情况,选择最合适的内存分配方式,是提升性能与空间利用率的最佳途径,掌握好 Buffer 的内存分配原理是必不可少的。

评论区

发表评论
用户名
(必填)
电子邮箱
(必填)
个人网站
(选填)
评论内容
Copyright © 2017 dremy.cn
皖ICP备16015002号