node buffer 模块常用方法、安全与性能最佳实践

基础术语

  • 编码(Encoding):将字符串转换为 Buffer(二进制数据)。
  • 解码(Decoding):将 Buffer 转换为字符串。
  • 字节序是指计算机在处理多字节数据时,根据预设规则或目标平台的字节序要求,字节的存储或传输的顺序
    • 小端序(Little-Endian):低位字节在后
    • 大端序(Big-Endian):高位字节在前

作用

Buffer 类用于表示 固定长度的字节序列,用于处理二进制数据

该类是 JavaScript 内置的 <Uint8Array> 类的子类,并扩展了适用于更多用例的方法。在 Node.js API 中,任何支持 Buffer 的地方也接受普通的 <Uint8Array> 对象。

使用前提

无前提可直接使用,但是从 buff 模块中引用的话可读性好

ini 复制代码
const Buffer = require('buffer');

buffer 与字符串的转换

Buffer 和字符串之间的转换可通过指定字符编码实现。默认使用 UTF-8 编码,但支持多种其他编码格式。

  • unicode 编码
    • utf8: 多字节编码,支持所有 Unicode 字符,是默认编码。若解码时发现无效字节,会用 U+FFFD (�) 替代错误字符
    • utf16le:每个字符用 2 或 4 字节表示,仅支持小端序
  • 单字节编码
    • latin1: 仅支持 U+0000U+00FF 的字符,超出部分会被截断
  • 二进制到文本编码
    • base64:支持 URL 和文件名安全的 Base64 变种(RFC 4648),忽略空格和换行符
    • base64url:类似 base64,但编码时省略填充符 =
    • hex:每字节编码为两个十六进制字符,解码时若长度非偶数会截断数据
  • 传统编码(不推荐使用)
    • ascii :仅支持 7 位 ASCII,解码时会清除高位比特(等效于 latin1)。
    • ucs-2:已弃用,完全由 utf16le 替代。

可迭代性

可以使用 for of 来进行迭代,也可以使用 values 、 keys 、 entries 创建迭代器

Buffer.from

作用

根据传入值生成二进制数据存储入一个新的 Buffer 中。

语法

整型数组、TypedArray、Buffer 初始化

csharp 复制代码
Buffer.from(dataArr)
  • 接收一个数组,该数组每个元素的字节长度必须为 1 的整数【也就是 0-255】。如果超过长度则会被截断。该数组可以是普通数组或类数组对象
  • 输入是 TypedArray(如 Uint16Array)或 Buffer ,则会直接复制数据而非共享内存。

ArrayBuffer 初始化

csharp 复制代码
Buffer.from(dataArr, [byteOffset, length])
  • dataArr 为 ArrayBufferTypedArray.buffer 【传递底层的 ArrayBuffer 数据】时,会与 dataArr 共享内存
  • byteOffset 表示从下标为多少的字节开始共享空间。默认值为 0
  • length 表示总共共享多少个字节的空间,默认值为 arrayBuffer.byteLength - byteOffset.

对象初始化

csharp 复制代码
 Buffer.from(object[, offsetOrEncoding, length])
  • object :支持 Symbol.toPrimitive自定义对象到原始值(如数字、字符串)的隐式类型转换行为, 参数 hint 表示要转换为什么类型的原始数据:number、default、string 】或 valueOf() 方法的对象,将其转换为字符串后生成 Buffer。【若对象不支持上述方法或无法转换,抛出 TypeError。】
  • offsetOrEncoding:为数字表示偏移量,为字符串表示将字符串按指定编码转换为 Buffer 实例
  • length: 表示转换多少字符

字符串初始化

csharp 复制代码
 Buffer.from(string[, encoding])
  • object :支持 Symbol.toPrimitive自定义对象到原始值(如数字、字符串)的隐式类型转换行为, 参数 hint 表示要转换为什么类型的原始数据:number、default、string 】或 valueOf() 方法的对象,将其转换为字符串后生成 Buffer。【若对象不支持上述方法或无法转换,抛出 TypeError。】
  • encoding:表示将字符串按指定编码转换为 Buffer 实例,默认utf8

Buffer实例.copyBytesFrom() 比较

方法 Buffer.from(typedArray) Buffer实例.copyBytesFrom(typedArray)
操作对象 创建新 Buffer 操作已存在的 Buffer
内存分配 每次调用都分配新内存 复用现有内存
适用场景 初始化一次性数据 高性能数据更新、流式处理
字节序处理 直接复制底层字节 自动转换字节序

二者如何选择:

  • Buffer.from() :需要快速创建一个新 Buffer,且后续不需要修改或复用内存。
  • copyBytesFrom():需要高性能地更新 Buffer 内容(如实时数据处理),或处理非 Uint8Array 类型时不需复用内存【Buffer.alloc()分配空间 实例.copyBytesFrom 数据初始化】。

Buffer.alloc

作用

创建新 Buffer 实例并安全初始化。

安全但是比 Buffer.allocUnsafe 慢

语法

Buffer.alloc(size[, fill, encoding])

  • size 指定新 buffer 的字节长度
  • fill 每个字节填充的值,值的类型可以为:字符串/Buffer/Uint8Array/整数
  • encoding 用于解析 fill 的编码格式

返回 Buffer 新实例

Buffer.allocUnsafe

作用

创建新 Buffer 实例

用于高性能内存分配的静态方法,不会进行内存初始化,size ≤ Buffer.poolSize / 2 时从内存池中分配内存。

适用场景

  • 短期持有小内存的 Buffer 场景
    • 因为长期持有会导致内存池空间碎片化,影响后续分配内存效率
  • 高性能场景

语法

Buffer.allocUnsafe(size)

  • size 指定新 buffer 的字节长度

返回 Buffer 新实例。

Buffer.allocUnsafeSlow

作用

创建新 Buffer 实例

不会进行内存初始化,也不会从内存池中分配内存。独立内存块无共享风险

适用场景

  • 长期持有小内存的 Buffer 场景,防止内存池碎片化

语法

Buffer.allocUnsafeSlow(size)

  • size 指定新 buffer 的字节长度

返回 Buffer 新实例。

Buffer.concat()

作用

用于使用多个 Buffer 实例数据来构造一个新的 Buffer 实例

Buffer.allocUnsafe() 类似,Buffer.concat() 可能复用预分配的内存池,减少系统调用次数,若合并后的总长度超过 Buffer.poolSize(默认 8KB),可能直接分配独立内存块以提高性能

语法

css 复制代码
Buffer.concat(list,[ totalLength])
  • list:为一个 Buffer 或 Uint8Array 实例的数组,表示这些数组中的每一项都会成为合并素材
  • totalLength: 表示再进行多少个字节后停止合并,若是实际数据字节长度小于 totalLength ,则剩余内存使用 0 填充。默认值为 list 所有元素字节长度之和。

cookbook

在需要精确控制内存或高频操作时,优先指定 totalLength 参数以避免自动计算的开销。

Buffer.compare()

作用

用于判断两个 Buffer的大小关系。

语法

scss 复制代码
Buffer.compare(buf1, buf2)

返回值

  • 0:两个 Buffer 内容完全相同。
  • 1buf1 在二进制字典序中大于 buf2,排序时应排在后面。
  • -1buf1 在二进制字典序中小于 buf2,排序时应排在前面。

比较机制

  • 逐个字节比较每个索引位置的十六进制值,直到找到差异。
  • Buffer 的编码(如 UTF-8、Hex)需一致,否则比较结果可能不符合预期
  • 若 Buffer 长度不同,较短 Buffer 的缺失部分视为 0x00

Buffer.copyBytesFrom()

作用

用于从 TypedArray 直接复制底层内存到新 Buffer 的高效方法,适用于需要快速处理二进制数据的场景。

语法

sql 复制代码
Buffer.copyBytesFrom(view[, offset, length])

参数

  • view:必填参数,类型为 TypedArray(如 Uint8ArrayUint16Array)。表示要复制的底层内存数据源。
  • offset(默认 0)起始元素索引(非字节偏移),例如 Uint16Array 每个元素占 2 字节,offset=1 表示从第二个元素开始复制。
  • length(默认 view.length - offset):要复制的元素数量。实际复制的字节数为 length * view.BYTES_PER_ELEMENT

返回值

返回一个包含复制数据的新 Buffer,其长度由复制的元素数量及类型决定。

机制

  • 直接内存复制:直接操作 TypedArray 的底层内存,避免逐元素转换的性能损耗。例如,Uint16Array 的每个元素占 2 字节,复制时会按内存布局直接拷贝字节。
  • 数据独立性:复制后的 Buffer 与原 TypedArray 数据完全独立,后续修改原数据不影响 Buffer 内容。

Buffer实例.fill

作用

用指定值填充 Buffer 实例的指定字节范围

语法

语法

css 复制代码
buffer.fill(value, [offset], [end], [encoding])
  • value | | | 默认值为 0,用于填充 buffer 每个字节的值,多余内容会被截断,截断时有数据泄露风险。
  • offset 从 buffer 的下标为几的字节开始填充。
  • end 表示到下标为 end 的字节停止填充【end 下标本身不会被填充】
  • encoding 表示解析数据时的编码

示例

arduino 复制代码
// 以utf8的格式解析 c ,并且将内容从下标为1的字节内存开始填充,填充完19后停止填充
buffer.fill('c', 1, 20, 'utf8')
// 以utf8的格式解析 c ,并且将内容从下标为1的字节内存开始填充,填充完 buffer 后停止填充
buffer.fill('c', 1, 'utf8')
// 以utf8的格式解析 c ,并且将 buffer 每个字节都填充此值
buffer.fill('c','utf8')

Buffer实例.toString

作用

将 buffer 的内容转换为指定编码的字符串

特性

  1. 多字节字符处理

解码多字节字符(如中文)时需确保 startend 位置对齐字符边界。例如 UTF-8 中一个汉字占 3 字节,若截断位置非 3 的倍数,可能导致部分字符丢失

示例:

arduino 复制代码
javascript


const buf = Buffer.from('今天有沙尘暴');
console.log(buf.toString('utf8', 3, 9)); // 输出:天有
  1. 编码兼容性
    • hex:将每个字节转为两个十六进制字符(如 74c3a9)。
    • base64:生成 Base64 编码字符串(如 dGVzdA==)。
    • utf8:支持 Unicode 字符,自动处理多字节序列。

语法

语法

vbscript 复制代码
buf.toString([encoding, [start], [end]])
  • start(默认 0)从 buf 的下标为 start 字节开始解码。若超出 Buffer 长度,返回空字符串。
  • end (默认 buf.length)解码的结束字节位置(不包含该位置)。若 end > buf.length,自动修正为 Buffer 末尾。

返回值:解码后的字符串。若遇到无效 UTF-8 字节,替换为 U+FFFD(�)

示例

arduino 复制代码
// 以utf8的格式解析 c ,并且将内容从下标为1的字节内存开始填充,填充完19后停止填充
buffer.fill('c', 1, 20, 'utf8')
// 以utf8的格式解析 c ,并且将内容从下标为1的字节内存开始填充,填充完 buffer 后停止填充
buffer.fill('c', 1, 'utf8')
// 以utf8的格式解析 c ,并且将 buffer 每个字节都填充此值
buffer.fill('c','utf8')

Buffer实例.write

作用

用指定值填充 Buffer 实例的指定字节范围,若超出 Buffer 长度,写入操作将被忽略。

语法

语法

css 复制代码
buf.write(string, [offset], [length], [encoding]

返回成功写入的字节个数

编码与字节计算

不同编码的字符串占用字节数不同。例如:

    • UTF-8 编码的 '½'(Unicode U+00BD)占 2 字节(0xC2 0xBD)****。
    • ASCII 编码的 'a' 仅占 1 字节****。
多字节字符处理

写入多字节字符时需确保 Buffer 有足够空间。若空间不足,仅写入完整字符,,避免产生乱码。如:

arduino 复制代码
const buf = Buffer.alloc(5);
buf.write('€', 0, 3, 'utf8');  // '€' UTF-8 编码为 3 字节(0xE2 0x82 0xAC)
console.log(buf.toString('utf8'));  // 输出: €(完整写入)
arduino 复制代码
const buffer = Buffer.alloc(10);
const length = buffer.write('abcd', 8);
console.log(`${length} bytes: ${buffer.toString('utf8', 8, 10)}`);
// 输出: 2 bytes: ab

Buffer实例.toJSON

作用

用于将 Buffer 对象转换为 JSON 格式

语法

css 复制代码
buf.toJSON(): { type: 'Buffer', data: Array<number> }

一个包含 typedata 属性的 JSON 对象

  • type:固定为 'Buffer',标识数据类型。
  • data:数组形式存储的 Buffer 二进制字节(十进制数值)

如何反序列化

通过 JSON.parse() 函数,可自动将 JSON 还原为 Buffer 。

javascript 复制代码
// 序列化(无编码转换)
const buf = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]); // ASCII: Hello
const json = JSON.stringify(buf); // {"type":"Buffer","data":[72,101,108,108,111]}

// 反序列化(直接重建二进制)
const restoredBuf = Buffer.from(JSON.parse(json));
console.log(restoredBuf); // <Buffer 48 65 6c 6c 6f>

当然若是字符串格式与Buffer 转换后的格式一致,也会转换为 Buffer 实例

ini 复制代码
const restoredBuf = Buffer.from(JSON.parse('{"type":"Buffer","data":[72,101,108,108,111]}'));
console.log(restoredBuf); // <Buffer 48 65 6c 6c 6f>

Buffer 实例.length

返回Buffer 的字节长度

Buffer 实例.copy

作用

用于将指定数据覆盖 Buffer 指定区域的数据,支持内存区域重叠的场景

应用场景

  1. 协议数据重组
    在网络通信中,将分片接收的 Buffer 数据合并到目标 Buffer 的指定位置
  2. 内存高效复用
    通过重叠复制实现内存复用,减少内存分配次数。例如,滑动窗口协议中的缓冲区管理
  3. 数据截取与拼接
    提取源 Buffer 的特定片段(如文件头、校验码)并拼接到目标 Buffer 中

语法

css 复制代码
buf.copy(target[, targetStart[, sourceStart[, sourceEnd]]])

参数

  • target,要复制的数据源
  • targetStart:默认值为 0 ,目标 Buffer 的写入起始偏移量(单位:字节)
  • sourceStart: 要覆盖的 Buffer 实例字节区域的起始字节
  • sourceEnd: 要覆盖的 Buffer 实例字节区域的结束字节【不包含】。若 sourceEnd > buf.length,则自动修正为 buf.length

targetStartsourceStart 为负数,或 targetStart + 复制长度 > target.length,会抛出 ERR_INDEX_OUT_OF_RANGE 错误。

返回值

实际复制的字节数,值为 sourceEnd - sourceStart

cookbook

  1. 性能优化
    • 批量复制时,优先使用 Buffer.concat() 代替多次 copy,以减少内存操作次数6
    • 避免在循环中频繁创建新 Buffer,可复用现有内存空间。
  1. 错误处理
    • 使用 try-catch 捕获可能的 ERR_INDEX_OUT_OF_RANGE 错误,确保参数合法性45

安全问题

fill 后由于引擎优化引起的安全问题

虽然 fill 操作的是物理内存,即将内存数据覆盖,但是在 js 引擎优化或是垃圾回收的时候,可能会导致 fill 操作被延迟或合并,覆盖前的数据被缓存到内存中

可以通过二次数据覆盖尽可能减少这种情况的出现。

php 复制代码
const { randomFillSync } = require('crypto');
const sensitiveData = Buffer.from("pass");
// 使用加密安全的随机数据覆盖,破坏内存数据的可预测性,增加攻击者恢复原始数据的难度。
randomFillSync(sensitiveData);
sensitiveData.fill(0); // 二次覆盖

共享内存引发的安全问题

场景

共享 ArrayBuffer 内存时,可能访问到超出预期范围的数据,因为 ArrayBuffer 的实际内存可能比当前视图(如 TypedArray)覆盖的范围更广。

比如 Buffer.from 参数为一个 ArrayBuffer 时,新的 Buffer 实例会与之共享内存,但若是没有手动限制共享的内存长度或是共享内存长度过长,就会新 Buffer 实例可以访问到比当前视图更长的内存空间。

ini 复制代码
// 创建包含敏感数据的 ArrayBuffer
const ab = new ArrayBuffer(8);
const view = new Uint8Array(ab);
view.set([0x73, 0x65, 0x63, 0x72]); // "secr"

// 创建共享内存的 Buffer,但未限制范围
const buf = Buffer.from(ab);
console.log(buf.toString()); 
// 输出 "secr\u0000\u0000\u0000\u0000"(残留内存的零值可能被后续覆盖为其他数据)

解决方案

  1. 限制内存共享的空间长度,比如 Buffer.from 的第三个参数。
  2. 内存隔离 :对高敏感场景建议完全拷贝而非共享(Buffer.from(view.buffer.slice(...))),避免其他视图修改导致数据污染。

使用未初始化的 Buffer 分配方法

场景

Buffer.allocUnsafe()Buffer.allocUnsafeSlow()创建 Buffer 时,这些方法直接从内存池或操作系统的空闲内存块分配内存,跳过了初始化步骤,底层内存可能残留其他进程或应用的历史数据。

arduino 复制代码
// 创建未初始化的 Buffer
const buf = Buffer.allocUnsafe(4);
console.log(buf.toString('hex')); // 可能输出旧数据,如 "a1b2c3d4"

// 假设此前内存中存有密码 "pass"
const sensitiveData = Buffer.from("pass");
sensitiveData.fill(0); // 未正确清空

// 新 Buffer 复用同一内存池
const newBuf = Buffer.allocUnsafe(4);
console.log(newBuf.toString()); // 可能残留 "pass"

解决方案

  1. 每次使用完 Buffer 后及时清空内存
  2. 在进行未初始化的 Buffer 分配方法的调用后,对新内存进行初始化操作。

编码转换或截断时的数据泄露

问题原因

使用 Buffer.from(string, encoding)Buffer.transcode() 转换编码时,若目标编码不支持某些字符,可能因替换字符(如 �)或截断不完整导致敏感数据暴露,因为 编码转换未正确处理的这些字节,残留数据是未被覆盖的。

解决方案

双重 encoding 转换,保证就算数据截断或是解析错误,也会将解析错误的原始数据覆盖掉。

php 复制代码
function safeEncode(str, encoding = 'utf8') {
  const buf = Buffer.from(str, encoding);
  return Buffer.from(buf.toString(encoding), encoding); // 双重转换确保覆盖残留
}

内存池分配导致的安全问题

内存池

Node.js 默认预分配一个 8KB 的内存池(可通过 Buffer.poolSize 调整),用于高效管理小对象的频繁内存请求。内存池分为三种状态:

  • empty:未分配。
  • partial:部分分配。
  • full:完全分配

即使因内存分配释放产生空间碎片,内存池自身会通过 alignPool() 自动对齐管理

何时会使用内存池分配内存

当通过 Buffer.from() 创建 小对象(小于等于 Buffer.poolSize / 2Buffer 时)【内存共享并不属于创建小对象情况】或是 Buffer.allocUnsafe()Buffer.allocUnsafeSlow() 创建内存 时,Node.js 会优先从内存池中分配内存块,避免频繁向操作系统申请内存的开销。

而此时分配的内存并没有进行初始化可能会有原始数据残留。

若是小对象累计内存超过已申请的内存池大小时, 底层会调用createPool() 新建 8KB 内存池

安全隐患场景及解决方案

造成原因

当一个内存池分配的内存被释放掉后,下次请求新的内存时,会进行旧内存的复用。

若未及时清空内存池,新分配的 Buffer 可能读取到旧数据

风险类型

可以直接禁用内存池 Buffer.poolSize = 0; // 禁用内存池(牺牲性能换取安全)

风险类型 示例场景 解决方案
敏感数据泄露 内存池残留旧密码、密钥等 优先使用 Buffer.alloc()
内存内容不可预测 调试时输出乱码或异常数据 创建后立即初始化内存
攻击者利用未初始化内存 通过未初始化内存获取系统信息 严格校验输入参数类型和范围

防范手段总结

  1. 优先使用安全分配方法
    • 使用 Buffer.alloc() 初始化内存。
  1. 显式初始化内存或是在用完后覆盖清理内存
php 复制代码
const { randomFillSync } = require('crypto');
const sensitiveData = Buffer.from("pass");
// 使用加密安全的随机数据覆盖
randomFillSync(sensitiveData);
sensitiveData.fill(0); // 二次覆盖
  1. 限制共享内存范围
    • 通过 Buffer.from(arrayBuffer, offset, length) 明确内存边界。
  1. 解析文本时指定正确的字符编码,并且进行双重 encoding 转换,保证就算数据截断或是解析错误,也会将解析错误的原始数据覆盖掉。

高效场景最佳实践

js 复制代码
    1.  在创建 Buffer 时:
        a.  判断是copy数据还是自主创建数据
            i.  若是copy数据则使用 `Buffer.copyBytesFrom`,直接从 `TypedArray` 底层内存复制,避免逐字节操作的开销
        b.  判断是大内存(≥4KB)还是小内存
            i.  大内存则使用 Buffer.allocUnsafeSlow ,因为比 Buffer.allocUnsafe 快不用判断是否要在内存池中申请内存
            ii.  若是小内存则判断是否长期
                1.  长期 (如缓存池、持久化数据结构) 则使用 Buffer.allocUnsafeSlow ,防止内存池碎片化,导致后续分配效率低下
                2.  短期 (如临时网络包处理) 则使用 Buffer.allocUnsafe 利用内存池切割复用,减少系统调用
            iii.  若是无从得知,则使用 Buffer.allocUnsafe
    2.  在进行内存初始化时:使用 randomFillSync + fill 在双重覆盖保障安全性的同时保障覆盖高效
    3.  在进行数据拷贝的过程:
        a.  批量复制时,优先使用 `Buffer.concat()` 代替多次 `copy`,以减少内存操作次数
        b.  内存区域重叠的场景、将一个Buffer 中的数据片段覆盖另一个 Buffer 中某块数据内容,使用 Buffer实例.copy
        c.  以单个数据源创建新 Buffer 时,Buffer.copyBytesFrom
相关推荐
To_OC6 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
To_OC6 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic6 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy6 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志7 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble8 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长8 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯8 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜9 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司9 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能