基础术语
- 编码(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 字节表示,仅支持小端序
- utf8: 多字节编码,支持所有 Unicode 字符,是默认编码。若解码时发现无效字节,会用
- 单字节编码
-
- latin1: 仅支持
U+0000
至U+00FF
的字符,超出部分会被截断
- latin1: 仅支持
- 二进制到文本编码
-
- 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 为
ArrayBuffer
或TypedArray.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 内容完全相同。1
:buf1
在二进制字典序中大于buf2
,排序时应排在后面。-1
:buf1
在二进制字典序中小于buf2
,排序时应排在前面。
比较机制
- 逐个字节比较每个索引位置的十六进制值,直到找到差异。
- Buffer 的编码(如 UTF-8、Hex)需一致,否则比较结果可能不符合预期
- 若 Buffer 长度不同,较短 Buffer 的缺失部分视为
0x00
。
Buffer.copyBytesFrom()
作用
用于从 TypedArray
直接复制底层内存到新 Buffer 的高效方法,适用于需要快速处理二进制数据的场景。
语法
sql
Buffer.copyBytesFrom(view[, offset, length])
参数
view
:必填参数,类型为TypedArray
(如Uint8Array
、Uint16Array
)。表示要复制的底层内存数据源。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 的内容转换为指定编码的字符串
特性
- 多字节字符处理
解码多字节字符(如中文)时需确保 start
和 end
位置对齐字符边界。例如 UTF-8 中一个汉字占 3 字节,若截断位置非 3 的倍数,可能导致部分字符丢失
示例:
arduino
javascript
const buf = Buffer.from('今天有沙尘暴');
console.log(buf.toString('utf8', 3, 9)); // 输出:天有
- 编码兼容性
-
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 编码的
'½'
(UnicodeU+00BD
)占 2 字节(0xC2 0xBD
)****。 - ASCII 编码的
'a'
仅占 1 字节****。
- UTF-8 编码的
多字节字符处理
写入多字节字符时需确保 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> }
一个包含 type
和 data
属性的 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 指定区域的数据,支持内存区域重叠的场景
应用场景
- 协议数据重组
在网络通信中,将分片接收的 Buffer 数据合并到目标 Buffer 的指定位置 - 内存高效复用
通过重叠复制实现内存复用,减少内存分配次数。例如,滑动窗口协议中的缓冲区管理 - 数据截取与拼接
提取源 Buffer 的特定片段(如文件头、校验码)并拼接到目标 Buffer 中
语法
css
buf.copy(target[, targetStart[, sourceStart[, sourceEnd]]])
参数
- target,要复制的数据源
- targetStart:默认值为 0 ,目标 Buffer 的写入起始偏移量(单位:字节)
- sourceStart: 要覆盖的 Buffer 实例字节区域的起始字节
- sourceEnd: 要覆盖的 Buffer 实例字节区域的结束字节【不包含】。若
sourceEnd > buf.length
,则自动修正为buf.length
若 targetStart
或 sourceStart
为负数,或 targetStart + 复制长度 > target.length
,会抛出 ERR_INDEX_OUT_OF_RANGE
错误。
返回值
实际复制的字节数,值为 sourceEnd - sourceStart
cookbook
- 性能优化
-
- 批量复制时,优先使用
Buffer.concat()
代替多次copy
,以减少内存操作次数6。 - 避免在循环中频繁创建新 Buffer,可复用现有内存空间。
- 批量复制时,优先使用
- 错误处理
-
- 使用
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"(残留内存的零值可能被后续覆盖为其他数据)
解决方案
- 限制内存共享的空间长度,比如 Buffer.from 的第三个参数。
- 内存隔离 :对高敏感场景建议完全拷贝而非共享(
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"
解决方案
- 每次使用完 Buffer 后及时清空内存
- 在进行未初始化的 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 / 2
的 Buffer
时)【内存共享并不属于创建小对象情况】或是 Buffer.allocUnsafe()
、Buffer.allocUnsafeSlow()
创建内存 时,Node.js 会优先从内存池中分配内存块,避免频繁向操作系统申请内存的开销。
而此时分配的内存并没有进行初始化可能会有原始数据残留。
若是小对象累计内存超过已申请的内存池大小时, 底层会调用createPool()
新建 8KB 内存池
安全隐患场景及解决方案
造成原因
当一个内存池分配的内存被释放掉后,下次请求新的内存时,会进行旧内存的复用。
若未及时清空内存池,新分配的 Buffer 可能读取到旧数据
风险类型
可以直接禁用内存池 Buffer.poolSize = 0; // 禁用内存池(牺牲性能换取安全)
风险类型 | 示例场景 | 解决方案 |
---|---|---|
敏感数据泄露 | 内存池残留旧密码、密钥等 | 优先使用 Buffer.alloc() |
内存内容不可预测 | 调试时输出乱码或异常数据 | 创建后立即初始化内存 |
攻击者利用未初始化内存 | 通过未初始化内存获取系统信息 | 严格校验输入参数类型和范围 |
防范手段总结
- 优先使用安全分配方法:
-
- 使用
Buffer.alloc()
初始化内存。
- 使用
- 显式初始化内存或是在用完后覆盖清理内存:
php
const { randomFillSync } = require('crypto');
const sensitiveData = Buffer.from("pass");
// 使用加密安全的随机数据覆盖
randomFillSync(sensitiveData);
sensitiveData.fill(0); // 二次覆盖
- 限制共享内存范围:
-
- 通过
Buffer.from(arrayBuffer, offset, length)
明确内存边界。
- 通过
- 解析文本时指定正确的字符编码,并且进行双重 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