将二进制的NodeJS缓冲区转换为JavaScript的ArrayBuffer

217

如何将NodeJS二进制缓冲转换为JavaScript ArrayBuffer?


1
我很好奇为什么你需要这样做? - Chris Biscardi
23
一个很好的例子是编写一个可以在浏览器中使用 File 并在 NodeJS 文件中也能使用的库。 - fbstj
3
在NodeJS中使用浏览器库,或者使用一个浏览器库在NodeJS中。 - OrangeDog
1
另一个原因是,当存储在“Array”中时,浮点数占用太多的RAM字节。因此,要存储许多浮点数,您需要使用Float32Array,其中它占用4个字节。如果您想快速将这些浮点数序列化到文件中,则需要使用Buffer,因为将其序列化为JSON需要很长时间。 - nponeccop
1
const file = fs.readFileSync(filePath);,那么我该怎么使用呢?... 30分钟后,哇,我想念C语言。 - William Entriken
显示剩余2条评论
14个回答

236

在Node.js 4.x及以上版本中,Buffer实例同时也是Uint8Array实例。因此,最高效的解决方案是直接访问缓冲区自身的.buffer属性,如https://dev59.com/w2oy5IYBdhLWcg3wWss_#31394257所述。如果需要向另一个方向转换,Buffer构造函数还可以使用ArrayBufferView参数。

请注意,这不会创建副本,这意味着对任何ArrayBufferView的写入都将写入到原始的Buffer实例中。


在旧版Node.js中,ArrayBuffer作为v8的一部分,但是Buffer类提供了更灵活的API。要读取或写入ArrayBuffer,您只需要创建视图并复制即可。

BufferArrayBuffer:

function toArrayBuffer(buffer) {
  const arrayBuffer = new ArrayBuffer(buffer.length);
  const view = new Uint8Array(arrayBuffer);
  for (let i = 0; i < buffer.length; ++i) {
    view[i] = buffer[i];
  }
  return arrayBuffer;
}

从ArrayBuffer到Buffer:

function toBuffer(arrayBuffer) {
  const buffer = Buffer.alloc(arrayBuffer.byteLength);
  const view = new Uint8Array(arrayBuffer);
  for (let i = 0; i < buffer.length; ++i) {
    buffer[i] = view[i];
  }
  return buffer;
}

7
我建议您通过使用DataView在可能的情况下复制整数来进行优化。在 size&0xfffffffe 之前,复制32位整数,然后,如果还剩1个字节,请复制8位整数,如果还剩2个字节,请复制16位整数,如果还剩3个字节,请复制16位和8位整数。 - Triang3l
3
请参考kraag22的答案,他提供了一个更简单的实现方法。 - OrangeDog
第一个例子,你返回的是 ab 而不是 view - jcalfee314
5
为什么会返回 abab 没有被使用过吗?我一直得到的结果是 {} - Andi Giga
2
slice() 方法返回一个新的 ArrayBuffer,其内容是从起始位置(含)到结束位置(不含)的字节副本。 - Константин Ван
显示剩余11条评论

120

"从ArrayBuffer到Buffer"可以通过以下方式完成:

var buffer = Buffer.from( new Uint8Array(arrayBuffer) );

59
这与OP所想的相反。 - Alexander Gonchiy
130
但这正是我想要的,通过搜索我的问题,我很高兴找到了解决方案。 - Maciej Krawczyk

103

无依赖项,最快速,适用于 Node.js 4.x 及更高版本

BufferUint8Array,因此您只需要切片(复制)支持的 ArrayBuffer 区域即可。

// Original Buffer
let b = Buffer.alloc(512);
// Slice (copy) its segment of the underlying ArrayBuffer
let ab = b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength);

slice和偏移量的设置是必需的,因为小型Buffer(默认情况下小于4 kB,等于池大小的一半)可以是共享ArrayBuffer的视图。如果不进行切片,可能会得到包含另一个Buffer数据的ArrayBuffer。请参见文档中的解释

如果您最终需要TypedArray,可以创建一个而无需复制数据:

// Create a new view of the ArrayBuffer without copying
let ui32 = new Uint32Array(b.buffer, b.byteOffset, b.byteLength / Uint32Array.BYTES_PER_ELEMENT);

无依赖,速度适中,适用于任何版本的Node.js

使用Martin Thomson的答案,其运行时间为O(n)。 (请参见我对他的回答中关于非优化的评论。使用DataView会很慢。即使您需要翻转字节,也有更快的方法。)

依赖项,速度快,适用于Node.js ≤ 0.12或iojs 3.x

您可以使用https://www.npmjs.com/package/memcpy在两个方向(Buffer到ArrayBuffer和反向)之间进行转换。它比此处发布的其他答案更快,并且是一个编写良好的库。 Node 0.12到iojs 3.x需要ngossen的分支(请参见此处)。


1
使用ngossen的分支:https://github.com/dcodeIO/node-memcpy/pull/6。如果您正在使用node 4+,请参阅我的新答案。 - ZachB
.byteLength”和“.byteOffset”的文档在哪里可以找到? - Константин Ван
1
@K._ 这些属性是从 TypedArray 继承而来的:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/byteLength 和 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/byteOffset - ZachB
1
var ab = b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength); 保存了我的一天 - Alexey Sh.
使用 slice() 不是 O(1) 而是 O(n),因为它会创建一个副本:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray/slice - Mike Frysinger
显示剩余2条评论

43

更快的写法

var arrayBuffer = new Uint8Array(nodeBuffer).buffer;

然而,对于包含1024个元素的缓冲区,这似乎比建议使用的toArrayBuffer函数运行得慢大约4倍。


3
追加内容:@trevnorris 说道,“从[V8] 4.3开始,缓冲区由Uint8Array支持”,所以现在可能更快了... - ChrisV
请查看我的答案,以了解安全的方法。 - ZachB
3
使用v5.6.0进行测试,速度最快。 - daksh_019
1
这仅适用于在Node.js 4.x及更高版本中,Buffer实例也是Uint8Array实例的情况。对于较低版本的Node.js,您必须实现一个toArrayBuffer函数。 - Benny Code

23

1. 一个Buffer只是查看ArrayBuffer的一种方式。

实际上,一个Buffer是一个FastBuffer,它扩展(继承自)Uint8Array,这是实际内存的八位元组视图(“部分访问器”),即ArrayBuffer

/lib/buffer.js#L65-L73 Node.js 9.4.0
class FastBuffer extends Uint8Array {
  constructor(arg1, arg2, arg3) {
    super(arg1, arg2, arg3);
  }
}
FastBuffer.prototype.constructor = Buffer;
internalBuffer.FastBuffer = FastBuffer;

Buffer.prototype = FastBuffer.prototype;

2. ArrayBuffer的大小和其视图的大小可能会有所不同。

原因1:Buffer.from(arrayBuffer[, byteOffset[, length]])

使用Buffer.from(arrayBuffer[, byteOffset[, length]]),您可以创建一个Buffer,指定其底层ArrayBuffer以及视图的位置和大小。

const test_buffer = Buffer.from(new ArrayBuffer(50), 40, 10);
console.info(test_buffer.buffer.byteLength); // 50; the size of the memory.
console.info(test_buffer.length); // 10; the size of the view.

原因2: FastBuffer 的内存分配。

FastBuffer 根据大小采用两种不同的内存分配方式。

  • 如果大小小于内存池大小的一半且不为0(“小型”):它将使用内存池来准备所需的内存。
  • 否则:它会创建一个专用的ArrayBuffer,恰好适合所需的内存。
/lib/buffer.js#L306-L320 Node.js 9.4.0
function allocate(size) {
  if (size <= 0) {
    return new FastBuffer();
  }
  if (size < (Buffer.poolSize >>> 1)) {
    if (size > (poolSize - poolOffset))
      createPool();
    var b = new FastBuffer(allocPool, poolOffset, size);
    poolOffset += size;
    alignPool();
    return b;
  } else {
    return createUnsafeBuffer(size);
  }
}
/lib/buffer.js#L98-L100 Node.js 9.4.0
function createUnsafeBuffer(size) {
  return new FastBuffer(createUnsafeArrayBuffer(size));
}

什么是“内存池”?

内存池是一个固定大小的预分配内存块,用于保存小型内存块以供Buffer使用。使用它可以将小型内存块紧密地放在一起,因此防止了由于对小型内存块进行单独管理(分配和释放)而导致的碎片化

在这种情况下,内存池是默认大小为8 KiB的ArrayBuffer,这在Buffer.poolSize中指定。当需要为Buffer提供小型内存块时,它会检查最后一个内存池是否有足够的可用内存来处理此操作;如果有,它将创建一个Buffer,该Buffer“查看”内存池中给定部分的内存块,否则,它将创建一个新的内存池,以此类推。


您可以访问Buffer的底层ArrayBufferbuffer属性 (即从Uint8Array继承) 保存它。 “小型” Bufferbuffer 属性是表示整个内存池的ArrayBuffer 因此,在这种情况下,ArrayBufferBuffer 的大小不同。
const zero_sized_buffer = Buffer.allocUnsafe(0);
const small_buffer = Buffer.from([0xC0, 0xFF, 0xEE]);
const big_buffer = Buffer.allocUnsafe(Buffer.poolSize >>> 1);

// A `Buffer`'s `length` property holds the size, in octets, of the view.
// An `ArrayBuffer`'s `byteLength` property holds the size, in octets, of its data.

console.info(zero_sized_buffer.length); /// 0; the view's size.
console.info(zero_sized_buffer.buffer.byteLength); /// 0; the memory..'s size.
console.info(Buffer.poolSize); /// 8192; a memory pool's size.

console.info(small_buffer.length); /// 3; the view's size.
console.info(small_buffer.buffer.byteLength); /// 8192; the memory pool's size.
console.info(Buffer.poolSize); /// 8192; a memory pool's size.

console.info(big_buffer.length); /// 4096; the view's size.
console.info(big_buffer.buffer.byteLength); /// 4096; the memory's size.
console.info(Buffer.poolSize); /// 8192; a memory pool's size.

3. 因此,我们需要提取它“views”的内存。

ArrayBuffer 的大小是固定的,因此我们需要通过复制部分来提取它。为此,我们使用 BufferbyteOffset 属性length 属性,这些属性都是从 Uint8Array 继承而来的,以及 ArrayBuffer.prototype.slice 方法,该方法可以复制 ArrayBuffer 的一部分。本文中的 slice() 方法受到了@ZachB的启发。

const test_buffer = Buffer.from(new ArrayBuffer(10));
const zero_sized_buffer = Buffer.allocUnsafe(0);
const small_buffer = Buffer.from([0xC0, 0xFF, 0xEE]);
const big_buffer = Buffer.allocUnsafe(Buffer.poolSize >>> 1);

function extract_arraybuffer(buf)
{
    // You may use the `byteLength` property instead of the `length` one.
    return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.length);
}

// A copy -
const test_arraybuffer = extract_arraybuffer(test_buffer); // of the memory.
const zero_sized_arraybuffer = extract_arraybuffer(zero_sized_buffer); // of the... void.
const small_arraybuffer = extract_arraybuffer(small_buffer); // of the part of the memory.
const big_arraybuffer = extract_arraybuffer(big_buffer); // of the memory.

console.info(test_arraybuffer.byteLength); // 10
console.info(zero_sized_arraybuffer.byteLength); // 0
console.info(small_arraybuffer.byteLength); // 3
console.info(big_arraybuffer.byteLength); // 4096

4. 性能改进

如果您只需将结果用作只读,或者可以修改输入缓冲区的内容,则可以避免不必要的内存复制。

const test_buffer = Buffer.from(new ArrayBuffer(10));
const zero_sized_buffer = Buffer.allocUnsafe(0);
const small_buffer = Buffer.from([0xC0, 0xFF, 0xEE]);
const big_buffer = Buffer.allocUnsafe(Buffer.poolSize >>> 1);

function obtain_arraybuffer(buf)
{
    if(buf.length === buf.buffer.byteLength)
    {
        return buf.buffer;
    } // else:
    // You may use the `byteLength` property instead of the `length` one.
    return buf.subarray(0, buf.length);
}

// Its underlying `ArrayBuffer`.
const test_arraybuffer = obtain_arraybuffer(test_buffer);
// Just a zero-sized `ArrayBuffer`.
const zero_sized_arraybuffer = obtain_arraybuffer(zero_sized_buffer);
// A copy of the part of the memory.
const small_arraybuffer = obtain_arraybuffer(small_buffer);
// Its underlying `ArrayBuffer`.
const big_arraybuffer = obtain_arraybuffer(big_buffer);

console.info(test_arraybuffer.byteLength); // 10
console.info(zero_sized_arraybuffer.byteLength); // 0
console.info(small_arraybuffer.byteLength); // 3
console.info(big_arraybuffer.byteLength); // 4096

6
这一切都很好...但你实际上回答了OP的问题吗?如果回答了,那就被埋没了... - Tustin2121
1
很棒的答案!在 obtain_arraybuffer 函数中:buf.buffer.subarray 似乎不存在。你是不是想用 buf.buffer.slice - Marcin Czenko
@everydayproductive 谢谢。如您在编辑历史记录中所见,我实际上使用了 ArrayBuffer.prototype.slice,后来将其修改为 Uint8Array.prototype.subarray。哦,我做错了。可能当时有点困惑。现在多亏了您,一切都很好。 - Константин Ван

11

使用以下优秀的npm包:to-arraybuffer

或者,您可以自己实现。如果您的缓冲区称为buf,请执行以下操作:

buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength)

21
Joyent把它拿掉了。 - aleclarson

4

BufferArrayBuffer 的一种 view。您可以使用 buffer 属性访问内部封装的 ArrayBuffer

这是共享内存,无需复制。

const arrayBuffer = theBuffer.buffer

如果您想要数据的副本,请从原始的Buffer(而不是包装的ArrayBuffer)创建另一个Buffer,然后引用其包装的ArrayBuffer

const newArrayBuffer = Buffer.from(theBuffer).buffer

作为参考,从一个 ArrayBuffer 转换成一个 Buffer

const arrayBuffer = getArrayBuffer()
const sharedBuffer = Buffer.from(arrayBuffer)

const copiedBuffer = Buffer.from(sharedBuffer)
const copiedArrayBuffer = copiedBuffer.buffer

2
你可以将ArrayBuffer视为一种类型化的Buffer
因此,ArrayBuffer总是需要一个类型(被称为“Array Buffer View”)。通常,Array Buffer View的类型为Uint8ArrayUint16Array
Renato Mangini在这篇文章中介绍了如何在ArrayBuffer和字符串之间进行转换。我在代码示例中总结了其中的关键部分(适用于Node.js)。它还展示了如何在类型化的ArrayBuffer和非类型化的Buffer之间进行转换。
function stringToArrayBuffer(string) {
  const arrayBuffer = new ArrayBuffer(string.length);
  const arrayBufferView = new Uint8Array(arrayBuffer);
  for (let i = 0; i < string.length; i++) {
    arrayBufferView[i] = string.charCodeAt(i);
  }
  return arrayBuffer;
}

function arrayBufferToString(buffer) {
  return String.fromCharCode.apply(null, new Uint8Array(buffer));
}

const helloWorld = stringToArrayBuffer('Hello, World!'); // "ArrayBuffer" (Uint8Array)
const encodedString = new Buffer(helloWorld).toString('base64'); // "string"
const decodedBuffer = Buffer.from(encodedString, 'base64'); // "Buffer"
const decodedArrayBuffer = new Uint8Array(decodedBuffer).buffer; // "ArrayBuffer" (Uint8Array)

console.log(arrayBufferToString(decodedArrayBuffer)); // prints "Hello, World!"

1

0

1
虽然这个链接可能回答了问题,但最好在此处包含答案的基本部分并提供参考链接。仅有链接的答案如果链接页面更改可能会变得无效。 - koceeng
2
我的措辞可能不太正式,但它提供了足够的信息来重新创建解决方案。该解决方案依赖于JavaScript代理对象,用于包装本机NodeJS缓冲区,并使用TypedArrays使用的getter和setter。这使得缓冲区实例与需要Typed Array接口的任何库兼容。这是原始帖子希望得到的答案,但如果它不符合您的学术/企业术语,请随意忽略它。看看我在乎不在乎。 - Dlabz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接