Buffer 与二进制数据处理
一句话概述:Buffer 原理、编码转换、性能优化、TypedArray 关系——掌握 Node.js 处理二进制数据的核心 API
什么是 Buffer?
定义:Buffer 是 Node.js 中用于处理二进制数据的类,类似于整数数组,但专门用于表示固定长度的字节序列。
涉及场景:
- 文件操作:读写二进制文件
- 网络传输:TCP/UDP 数据包
- 加密解密:crypto 模块
- 图像处理:图片编解码
作用:
- 高效处理二进制数据
- 字符串编码转换
- 与 Stream 配合使用
一、Buffer 基础
创建 Buffer
Node.js 提供三种创建 Buffer 的方法:
1. Buffer.from() - 从现有数据创建
// 从字符串创建
const buf1 = Buffer.from('Hello World', 'utf8');
// 从数组创建
const buf2 = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
// 从另一个 Buffer 创建(拷贝)
const buf3 = Buffer.from(buf1);
// 从 ArrayBuffer 创建
const arr = new Uint8Array(2);
const buf4 = Buffer.from(arr.buffer);2. Buffer.alloc() - 创建已初始化的 Buffer
// 创建 10 字节的 Buffer,填充 0
const buf = Buffer.alloc(10);
// 创建 10 字节的 Buffer,填充 1
const buf2 = Buffer.alloc(10, 1);
// 创建 10 字节的 Buffer,填充 'a'
const buf3 = Buffer.alloc(10, 'a');3. Buffer.allocUnsafe() - 创建未初始化的 Buffer(性能更高)
// 创建 10 字节的 Buffer,内容未初始化(可能包含旧数据)
const buf = Buffer.allocUnsafe(10);
// 必须手动填充数据
buf.fill(0);三种方法的区别:
Buffer.from(): 从现有数据创建,最常用Buffer.alloc(): 创建并初始化为 0,安全但稍慢Buffer.allocUnsafe(): 不初始化,速度最快但可能泄露旧数据
Buffer 与字符串转换
Buffer 转字符串
const buf = Buffer.from('Hello 世界', 'utf8');
// 转为 UTF-8 字符串(默认)
console.log(buf.toString()); // 'Hello 世界'
// 指定编码
console.log(buf.toString('hex')); // '48656c6c6f20e4b896e7958c'
console.log(buf.toString('base64')); // 'SGVsbG8g5LiW55WM'
// 指定范围
console.log(buf.toString('utf8', 0, 5)); // 'Hello'支持的编码类型
utf8: 默认编码,多字节 Unicode 字符utf16le: 2 或 4 字节,小端编码的 Unicode 字符latin1: 单字节编码(ISO-8859-1)base64: Base64 编码hex: 十六进制编码ascii: 7 位 ASCII 编码binary:latin1的别名
中文字符处理
// 一个中文字符在 UTF-8 中占 3 字节
const buf = Buffer.from('你好');
console.log(buf.length); // 6
// 截断可能导致乱码
const buf2 = Buffer.from('你好世界');
console.log(buf2.toString('utf8', 0, 4)); // '你�' (乱码)
// 使用 StringDecoder 避免乱码
const { StringDecoder } = require('string_decoder');
const decoder = new StringDecoder('utf8');
console.log(decoder.write(buf2.slice(0, 4))); // '你' (正确处理)二、Buffer 操作
读写操作
读取数据
const buf = Buffer.from([0x12, 0x34, 0x56, 0x78]);
// 读取无符号整数
console.log(buf.readUInt8(0)); // 18 (0x12)
console.log(buf.readUInt16BE(0)); // 4660 (0x1234)
console.log(buf.readUInt32BE(0)); // 305419896 (0x12345678)
// 读取有符号整数
const buf2 = Buffer.from([0xff, 0xff]);
console.log(buf2.readInt16BE(0)); // -1
// 读取浮点数
const buf3 = Buffer.allocUnsafe(4);
buf3.writeFloatBE(3.14, 0);
console.log(buf3.readFloatBE(0)); // 3.14写入数据
const buf = Buffer.allocUnsafe(6);
// 写入整数
buf.writeUInt8(0x12, 0);
buf.writeUInt16BE(0x3456, 1);
buf.writeUInt8(0x78, 3);
// 写入字符串
buf.write('AB', 4, 'utf8');
console.log(buf); // <Buffer 12 34 56 78 41 42>slice 和 copy
const buf1 = Buffer.from('Hello World');
// slice 创建视图(共享内存)
const buf2 = buf1.slice(0, 5);
buf2[0] = 0x48; // 修改 buf2 会影响 buf1
console.log(buf1.toString()); // 'Hello World'
// subarray 是 slice 的别名(推荐使用)
const buf3 = buf1.subarray(0, 5);
// copy 创建副本(独立内存)
const buf4 = Buffer.allocUnsafe(5);
buf1.copy(buf4, 0, 0, 5);
buf4[0] = 0x41; // 修改 buf4 不影响 buf1Buffer 拼接
使用 Buffer.concat()
const buf1 = Buffer.from('Hello ');
const buf2 = Buffer.from('World');
// 拼接多个 Buffer
const buf3 = Buffer.concat([buf1, buf2]);
console.log(buf3.toString()); // 'Hello World'
// 指定总长度(性能优化)
const buf4 = Buffer.concat([buf1, buf2], 11);性能陷阱:避免 += 拼接
// ❌ 错误:每次都创建新 Buffer,性能极差
let result = Buffer.alloc(0);
for (let i = 0; i < 1000; i++) {
result = Buffer.concat([result, Buffer.from('data')]);
}
// ✅ 正确:先收集,最后一次性拼接
const buffers = [];
for (let i = 0; i < 1000; i++) {
buffers.push(Buffer.from('data'));
}
const result = Buffer.concat(buffers);处理流式数据
const chunks = [];
let totalLength = 0;
readableStream.on('data', (chunk) => {
chunks.push(chunk);
totalLength += chunk.length;
});
readableStream.on('end', () => {
const result = Buffer.concat(chunks, totalLength);
console.log(result.toString());
});三、编码与解码
支持的编码类型
常用编码对比
| 编码 | 用途 | 特点 |
|---|---|---|
| utf8 | 文本存储、网络传输 | 变长编码,兼容 ASCII,默认编码 |
| utf16le | Windows 文件系统 | 固定 2/4 字节,小端序 |
| base64 | 邮件、URL、数据嵌入 | 文本安全,体积增加 33% |
| hex | 调试、哈希值显示 | 可读性好,体积翻倍 |
| latin1 | 二进制数据 | 单字节,与 Buffer 一一对应 |
| ascii | 纯英文文本 | 7 位编码,不支持中文 |
编码转换示例
const text = 'Hello 世界';
// UTF-8 编码(默认)
const utf8Buf = Buffer.from(text, 'utf8');
console.log(utf8Buf.length); // 12 字节
// Base64 编码
const base64 = utf8Buf.toString('base64');
console.log(base64); // 'SGVsbG8g5LiW55WM'
// Hex 编码
const hex = utf8Buf.toString('hex');
console.log(hex); // '48656c6c6f20e4b896e7958c'
// 解码回原文
console.log(Buffer.from(base64, 'base64').toString()); // 'Hello 世界'
console.log(Buffer.from(hex, 'hex').toString()); // 'Hello 世界'Base64 编解码
图片转 Base64
const fs = require('fs');
// 读取图片并转为 Base64
const imageBuffer = fs.readFileSync('image.png');
const base64Image = imageBuffer.toString('base64');
// 用于 HTML img 标签
const dataUrl = `data:image/png;base64,${base64Image}`;
// Base64 转回图片
const imageData = base64Image.replace(/^data:image\/\w+;base64,/, '');
const buffer = Buffer.from(imageData, 'base64');
fs.writeFileSync('output.png', buffer);性能对比
const data = Buffer.alloc(1024 * 1024); // 1MB
console.time('base64 encode');
const base64 = data.toString('base64');
console.timeEnd('base64 encode'); // ~5ms
console.time('base64 decode');
const decoded = Buffer.from(base64, 'base64');
console.timeEnd('base64 decode'); // ~3ms
console.time('hex encode');
const hex = data.toString('hex');
console.timeEnd('hex encode'); // ~8msBase64 的优缺点
- ✅ 文本安全,可嵌入 JSON/XML
- ✅ 无需额外文件,减少 HTTP 请求
- ❌ 体积增加 33%
- ❌ 不适合大文件(>100KB)
四、Buffer 与 TypedArray
关系与区别
Buffer 是 Uint8Array 的子类
const buf = Buffer.from([1, 2, 3]);
console.log(buf instanceof Uint8Array); // true
console.log(buf instanceof Buffer); // true
// Buffer 继承了 TypedArray 的所有方法
console.log(buf.map(x => x * 2)); // <Buffer 02 04 06>共享 ArrayBuffer
// Buffer 底层使用 ArrayBuffer 存储数据
const buf = Buffer.from([1, 2, 3, 4]);
console.log(buf.buffer); // ArrayBuffer { byteLength: 8192 }
// 注意:Buffer 可能使用 Buffer Pool,buffer 属性返回的是整个池
console.log(buf.byteOffset); // 池中的偏移量
console.log(buf.byteLength); // 4(实际使用的字节数)
// 获取独立的 ArrayBuffer
const arrayBuffer = buf.buffer.slice(
buf.byteOffset,
buf.byteOffset + buf.byteLength
);Buffer vs TypedArray
| 特性 | Buffer | TypedArray |
|---|---|---|
| 环境 | Node.js 专有 | 浏览器和 Node.js |
| 编码 | 支持多种字符编码 | 仅二进制 |
| 方法 | write/read/toString | set/subarray |
| 性能 | 针对 I/O 优化 | 通用数组操作 |
互操作
ArrayBuffer 转 Buffer
const arrayBuffer = new ArrayBuffer(8);
const uint8Array = new Uint8Array(arrayBuffer);
uint8Array[0] = 255;
// 从 ArrayBuffer 创建 Buffer(共享内存)
const buf1 = Buffer.from(arrayBuffer);
console.log(buf1[0]); // 255
// 从 TypedArray 创建 Buffer(拷贝数据)
const buf2 = Buffer.from(uint8Array);
uint8Array[0] = 0;
console.log(buf2[0]); // 255(不受影响)Buffer 转 TypedArray
const buf = Buffer.from([1, 2, 3, 4]);
// 方法 1:直接使用(Buffer 本身就是 Uint8Array)
const uint8 = buf;
// 方法 2:创建新的 TypedArray 视图
const uint16 = new Uint16Array(
buf.buffer,
buf.byteOffset,
buf.byteLength / 2
);
console.log(uint16); // Uint16Array [ 513, 1027 ]
// 方法 3:拷贝到新的 ArrayBuffer
const newBuf = new Uint8Array(buf);跨环境数据传输
// Node.js 发送数据
const buf = Buffer.from('Hello');
const arrayBuffer = buf.buffer.slice(
buf.byteOffset,
buf.byteOffset + buf.byteLength
);
// 发送 arrayBuffer 到浏览器
// 浏览器接收数据
const uint8Array = new Uint8Array(arrayBuffer);
const text = new TextDecoder().decode(uint8Array);
console.log(text); // 'Hello'五、性能优化
Buffer Pool
Buffer Pool 机制
Node.js 使用 8KB 的预分配内存池来优化小 Buffer 的创建性能。
// 小于 4KB 的 Buffer 会从池中分配
const buf1 = Buffer.allocUnsafe(100); // 从池中分配
const buf2 = Buffer.allocUnsafe(100); // 从同一个池中分配
// 大于等于 4KB 的 Buffer 独立分配
const buf3 = Buffer.allocUnsafe(4096); // 独立分配
// 查看 Buffer 的底层 ArrayBuffer
console.log(buf1.buffer.byteLength); // 8192 (8KB 池)
console.log(buf3.buffer.byteLength); // 4096 (独立)allocUnsafe 的性能优势
const iterations = 1000000;
// Buffer.alloc() - 安全但慢
console.time('alloc');
for (let i = 0; i < iterations; i++) {
Buffer.alloc(100);
}
console.timeEnd('alloc'); // ~150ms
// Buffer.allocUnsafe() - 快但不安全
console.time('allocUnsafe');
for (let i = 0; i < iterations; i++) {
Buffer.allocUnsafe(100);
}
console.timeEnd('allocUnsafe'); // ~50ms (快 3 倍)安全隐患
// ⚠️ allocUnsafe 可能泄露旧数据
const buf1 = Buffer.allocUnsafe(10);
console.log(buf1); // <Buffer 00 00 a8 3f 00 00 00 00 ...> (随机数据)
// 必须手动初始化
buf1.fill(0);
// 或者立即写入数据
buf1.write('Hello');
// ✅ alloc 自动初始化为 0
const buf2 = Buffer.alloc(10);
console.log(buf2); // <Buffer 00 00 00 00 00 00 00 00 00 00>使用建议
- 性能敏感场景:使用
allocUnsafe+fill() - 一般场景:使用
alloc保证安全 - 从现有数据创建:使用
Buffer.from()
避免频繁创建
复用 Buffer
// ❌ 错误:频繁创建销毁
function processData(data) {
const buf = Buffer.from(data); // 每次都创建新 Buffer
// 处理 buf
return buf.toString('base64');
}
// ✅ 正确:复用 Buffer
class DataProcessor {
constructor() {
this.buffer = Buffer.allocUnsafe(1024); // 预分配
}
process(data) {
const length = this.buffer.write(data);
return this.buffer.toString('base64', 0, length);
}
}预分配策略
// 场景:批量处理固定大小的数据
class BatchProcessor {
constructor(batchSize = 100, itemSize = 256) {
// 预分配整个批次的 Buffer
this.buffer = Buffer.allocUnsafe(batchSize * itemSize);
this.itemSize = itemSize;
}
processItem(index, data) {
const offset = index * this.itemSize;
this.buffer.write(data, offset, this.itemSize);
}
getResult() {
return this.buffer;
}
}对象池模式
class BufferPool {
constructor(size = 10, bufferSize = 1024) {
this.pool = [];
this.bufferSize = bufferSize;
// 预创建 Buffer 池
for (let i = 0; i < size; i++) {
this.pool.push(Buffer.allocUnsafe(bufferSize));
}
}
acquire() {
return this.pool.pop() || Buffer.allocUnsafe(this.bufferSize);
}
release(buffer) {
buffer.fill(0); // 清空数据
this.pool.push(buffer);
}
}
const pool = new BufferPool();
const buf = pool.acquire();
// 使用 buf
pool.release(buf);面试高频题
1. Buffer.alloc() 和 Buffer.allocUnsafe() 的区别?
核心区别:
Buffer.alloc(size): 创建指定大小的 Buffer,并自动填充 0,安全但较慢Buffer.allocUnsafe(size): 创建指定大小的 Buffer,不初始化内存,可能包含旧数据,快但不安全
性能差异:
// allocUnsafe 比 alloc 快约 2-3 倍
const buf1 = Buffer.alloc(1024); // ~100ns
const buf2 = Buffer.allocUnsafe(1024); // ~30ns安全隐患:
const unsafe = Buffer.allocUnsafe(10);
console.log(unsafe); // 可能包含敏感数据(密码、token 等)
// 使用前必须初始化
unsafe.fill(0);使用场景:
alloc: 默认选择,适合大多数场景allocUnsafe: 性能关键路径 + 立即写入数据的场景
Buffer Pool: 小于 4KB 的 Buffer 会从 8KB 的预分配池中分配,进一步提升性能。
2. Buffer 如何处理中文字符?
中文字符的字节占用:
const buf = Buffer.from('你好世界');
console.log(buf.length); // 12 字节(每个中文 3 字节)
console.log('你好世界'.length); // 4 字符截断导致的乱码问题:
const buf = Buffer.from('你好世界');
// ❌ 直接截断会乱码
console.log(buf.toString('utf8', 0, 4)); // '你�'
console.log(buf.toString('utf8', 0, 7)); // '你好�'
// ✅ 按字符边界截断
console.log(buf.toString('utf8', 0, 6)); // '你好'使用 StringDecoder 避免乱码:
const { StringDecoder } = require('string_decoder');
const decoder = new StringDecoder('utf8');
// 处理不完整的 UTF-8 序列
const buf1 = Buffer.from([0xe4, 0xbd]); // '你' 的前 2 字节
const buf2 = Buffer.from([0xa0]); // '你' 的最后 1 字节
console.log(decoder.write(buf1)); // '' (等待完整字符)
console.log(decoder.write(buf2)); // '你' (输出完整字符)流式处理中文:
const decoder = new StringDecoder('utf8');
let result = '';
readableStream.on('data', (chunk) => {
result += decoder.write(chunk); // 自动处理边界
});
readableStream.on('end', () => {
result += decoder.end(); // 输出剩余字符
console.log(result);
});3. Buffer 和 TypedArray 的关系?
继承关系:
const buf = Buffer.from([1, 2, 3]);
console.log(buf instanceof Uint8Array); // true
console.log(buf instanceof Buffer); // true
// Buffer 是 Uint8Array 的子类
Object.getPrototypeOf(Buffer.prototype) === Uint8Array.prototype; // true共享 ArrayBuffer:
const buf = Buffer.from([1, 2, 3, 4]);
// Buffer 底层使用 ArrayBuffer
console.log(buf.buffer); // ArrayBuffer
console.log(buf.byteOffset); // 偏移量
console.log(buf.byteLength); // 实际长度
// 注意:小 Buffer 使用池,buffer 可能很大
const small = Buffer.allocUnsafe(10);
console.log(small.buffer.byteLength); // 8192 (池大小)核心区别:
| 特性 | Buffer | TypedArray |
|---|---|---|
| 环境 | Node.js 专有 | 浏览器 + Node.js |
| 编码 | 支持 utf8/base64/hex 等 | 仅二进制 |
| 方法 | toString/write/slice | set/subarray |
| 创建 | Buffer.from/alloc | new Uint8Array |
| 性能 | I/O 优化 | 通用数组操作 |
互操作:
// TypedArray → Buffer
const uint8 = new Uint8Array([1, 2, 3]);
const buf = Buffer.from(uint8); // 拷贝数据
// Buffer → TypedArray
const buf2 = Buffer.from([1, 2, 3]);
const uint8_2 = new Uint8Array(buf2); // 拷贝数据
const uint8_3 = buf2; // 直接使用(Buffer 本身就是 Uint8Array)4. 如何高效拼接多个 Buffer?
使用 Buffer.concat():
const buf1 = Buffer.from('Hello ');
const buf2 = Buffer.from('World');
const buf3 = Buffer.from('!');
// 一次性拼接
const result = Buffer.concat([buf1, buf2, buf3]);
console.log(result.toString()); // 'Hello World!'
// 指定总长度(性能优化)
const result2 = Buffer.concat([buf1, buf2, buf3], 12);性能陷阱:
// ❌ 错误:O(n²) 复杂度
let result = Buffer.alloc(0);
for (let i = 0; i < 1000; i++) {
result = Buffer.concat([result, Buffer.from('data')]);
}
// ✅ 正确:O(n) 复杂度
const chunks = [];
for (let i = 0; i < 1000; i++) {
chunks.push(Buffer.from('data'));
}
const result = Buffer.concat(chunks);流式数据拼接:
class BufferCollector {
constructor() {
this.chunks = [];
this.length = 0;
}
add(chunk) {
this.chunks.push(chunk);
this.length += chunk.length;
}
toBuffer() {
return Buffer.concat(this.chunks, this.length);
}
}
const collector = new BufferCollector();
stream.on('data', chunk => collector.add(chunk));
stream.on('end', () => {
const result = collector.toBuffer();
});预分配优化:
// 已知总大小时,预分配更快
const chunks = [buf1, buf2, buf3];
const totalLength = chunks.reduce((sum, buf) => sum + buf.length, 0);
const result = Buffer.allocUnsafe(totalLength);
let offset = 0;
for (const chunk of chunks) {
chunk.copy(result, offset);
offset += chunk.length;
}5. Buffer 的内存是如何管理的?
内存分配策略:
- 小 Buffer(< 4KB):从 8KB 的预分配池中分配
- 大 Buffer(≥ 4KB):直接分配独立的 ArrayBuffer
// 小 Buffer 使用池
const small = Buffer.allocUnsafe(100);
console.log(small.buffer.byteLength); // 8192 (池大小)
// 大 Buffer 独立分配
const large = Buffer.allocUnsafe(4096);
console.log(large.buffer.byteLength); // 4096 (精确大小)Buffer Pool 机制:
// Node.js 内部维护一个 8KB 的池
let poolOffset = 0;
const poolSize = 8192;
let allocPool = createPool();
function createPool() {
poolOffset = 0;
return new ArrayBuffer(poolSize);
}
function allocate(size) {
if (size >= poolSize >>> 1) {
// 大 Buffer:独立分配
return new ArrayBuffer(size);
}
if (poolOffset + size > poolSize) {
// 池空间不足:创建新池
allocPool = createPool();
}
// 从池中分配
const offset = poolOffset;
poolOffset += size;
return { buffer: allocPool, offset };
}垃圾回收:
// Buffer 的内存由 V8 垃圾回收器管理
function createLargeBuffer() {
const buf = Buffer.alloc(100 * 1024 * 1024); // 100MB
return buf;
}
let buf = createLargeBuffer();
buf = null; // 标记为可回收
// 强制 GC(仅用于测试)
if (global.gc) {
global.gc();
}内存泄漏风险:
// ⚠️ slice 共享内存,可能导致大 Buffer 无法释放
const large = Buffer.alloc(100 * 1024 * 1024);
const small = large.slice(0, 100); // 引用整个 100MB
// ✅ 使用 Buffer.from 创建副本
const small2 = Buffer.from(large.slice(0, 100));
// 现在 large 可以被回收监控内存使用:
const used = process.memoryUsage();
console.log({
rss: `${Math.round(used.rss / 1024 / 1024)}MB`, // 总内存
heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)}MB`, // 堆总量
heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)}MB`, // 堆使用
external: `${Math.round(used.external / 1024 / 1024)}MB`, // C++ 对象(Buffer)
});