1.Buffer要解决什么问题
JavaScript早期只有字符串和数字,没有原生的二进制数据类型。浏览器里可以用ArrayBuffer和TypedArray处理二进制,但Node.js需要面对的是文件系统、TCP流、加密运算、图像处理这些场景,操作对象是字节序列而不是文本。Buffer就是为这个需求设计的。
Buffer的内存分配在V8堆之外。它底层是一段连续的原始内存,可以按字节读写,也可以和字符串之间做编码转换。因为不在V8堆内,Buffer不受堆大小限制的直接影响,处理大文件时比普通JavaScript对象更合适。
Buffer是全局类,不需要require('node:buffer')就能使用。但从Node.js的模块规范角度,显式引入node:buffer在阅读上更清晰,尤其是在大型项目里,能避免读者猜测Buffer来自哪里。
一个需要先建立的认知:Buffer存的是字节,不是字符。一个中文字符在UTF-8下占3个字节,一个英文字母占1个字节。buffer.length返回的是字节数,不是字符数。这个区别在计算文件大小、分片、截断字符串时经常导致错误。
2.Buffer.from:从已有数据创建
Buffer.from是最常用的创建方式,支持字符串、数组、ArrayBuffer、另一个Buffer。
const buf1 = Buffer.from('Hello, Node.js');
console.log(buf1);
console.log(buf1.toString());
const buf2 = Buffer.from([50, 55, 60, 65, 70]);
console.log(buf2);
console.log(buf2.toString());
const buf3 = Buffer.from(buf2);
console.log(buf3);
输出:
<Buffer 48 65 6c 6c 6f 2c 20 4e 6f 64 65 2e 6a 73>
Hello, Node.js
<Buffer 32 37 3c 41 46>
27<AF
<Buffer 32 37 3c 41 46>
第一段把字符串按UTF-8编码成字节。第二段用整数数组创建,50对应字符2,55对应7,60对应<,65对应A,70对应F,所以toString输出27<AF。第三段从已有Buffer复制一份新内存。
从字符串创建时可以指定编码:
Buffer.from('hello', 'utf8');
Buffer.from('aGVsbG8=', 'base64');
Buffer.from('68656c6c6f', 'hex');
Base64和Hex在传输和存储场景常见。JWT的payload是Base64URL编码,文件校验值常用Hex表示。
从ArrayBuffer创建时,Buffer.from(arrayBuffer)会共享同一段内存,不是复制。修改Buffer会影响原ArrayBuffer,反之亦然。需要独立副本时用Buffer.from(arrayBuffer.slice(0))或者Buffer.copy。
3.Buffer.alloc与Buffer.allocUnsafe
Buffer.alloc(size)分配指定字节数并用零填充。
const buf = Buffer.alloc(20);
console.log(buf);
输出:
<Buffer 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>
Buffer.allocUnsafe(size)分配内存但不初始化,速度更快,内容是内存里残留的数据。
const buf = Buffer.allocUnsafe(5);
console.log(buf);
buf.fill(0);
console.log(buf);
输出示例:
<Buffer 3c 45 a2 01 7f>
<Buffer 00 00 00 00 00>
第一次输出每次运行都可能不同,因为读的是未清理的内存。如果这段内存之前存过密钥、token、用户数据,就可能泄露。
选择逻辑:不确定内容是否会被完整覆盖时,用Buffer.alloc。只有在分配后立即完整写入、且性能敏感的路径上才考虑allocUnsafe。Node.js内部在流处理、网络包解析里会用allocUnsafe,因为后续操作会覆盖全部字节。业务代码里,除非有明确的性能压测数据支持,否则不必要冒这个风险。
项目经验:在文件上传的中间件里,我曾经见过用allocUnsafe分配缓冲区但只写入了部分数据,剩余字节来自上一次请求。后来改成alloc并加了一层断言校验,问题消失。这类bug不容易复现,排查成本高。
4.编码转换
Buffer和字符串之间的转换依赖编码。Node.js支持的编码包括utf8、utf16le、latin1、base64、base64url、hex、ascii、binary、ucs2。
const text = '你好,Node.js';
const utf8Buf = Buffer.from(text, 'utf8');
const base64Str = utf8Buf.toString('base64');
const hexStr = utf8Buf.toString('hex');
console.log('UTF-8 字节数:', utf8Buf.length);
console.log('Base64:', base64Str);
console.log('Hex:', hexStr);
const restored = Buffer.from(base64Str, 'base64').toString('utf8');
console.log('还原:', restored);
输出:
UTF-8 字节数: 15
Base64: 5L2g5aW977yMTm9kZS5qcw==
Hex: e4bda0e5a5bdefbc8c4e6f64652e6a73
还原: 你好,Node.js
Base64把二进制转成只含可打印字符的字符串,适合放进JSON、URL、HTTP头部。Hex每个字节用两个十六进制字符表示,适合做摘要展示。UTF-8是默认编码,处理文本时不需要显式指定。
一个容易出错的点:用错误的编码解码。比如把UTF-8字节按latin1解码,中文会变成乱码。反过来,如果数据本身是latin1,用UTF-8解码也会出错。编码必须和写入时一致。
Buffer.byteLength(string,encoding)返回字符串在指定编码下的字节数,比string.length更准确。
console.log('你好'.length);
console.log(Buffer.byteLength('你好', 'utf8'));
输出是2和6。字符数是2,字节数是6。做分片或限制请求体大小时,应该用byteLength。
5.常用方法与实践
concat合并多个Buffer:
const a = Buffer.from('Hello ');
const b = Buffer.from('Node.js');
const merged = Buffer.concat([a, b]);
console.log(merged.toString());
输出HelloNode.js。第二个参数可以指定总长度,用于预分配和截断。
slice与subarray返回共享内存的视图:
const buf = Buffer.from('Hello, Node.js');
const part = buf.subarray(0, 5);
part[0] = 0x4a; // 'J'
console.log(buf.toString()); // Jello, Node.js
subarray不复制数据,修改会影响原Buffer。需要独立副本用Buffer.from(buf.subarray(0,5))。slice在Node.js较新版本里行为与subarray一致。
copy把数据复制到目标Buffer:
const source = Buffer.from('Hello');
const target = Buffer.alloc(5);
source.copy(target, 0, 0, source.length);
console.log(target.toString());
equals与compare用于比较:
const x = Buffer.from('abc');
const y = Buffer.from('abc');
console.log(x.equals(y)); // true
console.log(Buffer.compare(x, y)); // 0
compare按字典序返回负数、零、正数,适合排序。
indexOf、includes用于查找:
const buf = Buffer.from('Hello, Node.js');
console.log(buf.indexOf('Node')); // 7
console.log(buf.includes('Node')); // true
write与read系列用于结构化数据。处理二进制协议时经常用到:
const buf = Buffer.alloc(8);
buf.writeUInt32BE(0x12345678, 0);
buf.writeUInt32LE(0x12345678, 4);
console.log(buf);
大端和小端的区分在网络协议、文件格式解析里很关键。同一个数值在不同字节序下解析结果不同,读错端序会得到错误的值。
6.Buffer与ArrayBuffer的区别
两者都是二进制数据容器,但设计目标和API不同。
Buffer是Node.js特有的类,继承自Uint8Array,提供了字符串编码、读写整数、拼接等便利方法。ArrayBuffer是ECMAScript标准的一部分,浏览器和Node.js都有,表示一段固定大小的原始内存,本身不能直接读写,需要通过TypedArray或DataView操作。
| 特性 | Buffer | ArrayBuffer |
|---|---|---|
| 定义 | Node.js二进制数据类 | 标准原始内存块 |
| 环境 | Node.js | 浏览器与Node.js |
| 数据访问 | 直接读写,内置编码方法 | 通过TypedArray或DataView |
| 编码支持 | utf8、base64、hex等 | 不直接提供 |
| 典型用途 | 文件、流、网络、加密 | 底层内存操作、跨平台数据传输 |
在WebAPI与Node.js之间传递数据时,经常需要转换:
const buf = Buffer.from('hello');
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
反过来:
const ab = new ArrayBuffer(5);
const buf = Buffer.from(ab);
buf.buffer返回底层ArrayBuffer,但可能比Buffer本身大,因为Buffer是视图。所以切片时要加上byteOffset。
7.项目实例:图片上传校验
假设一个接口接收图片上传,需要校验文件头判断真实类型,而不是相信客户端传来的Content-Type。
function detectImageType(buffer) {
if (buffer.length < 4) return 'unknown';
const hex = buffer.subarray(0, 4).toString('hex');
if (hex.startsWith('ffd8ff')) return 'jpeg';
if (hex.startsWith('89504e47')) return 'png';
if (hex.startsWith('47494638')) return 'gif';
if (hex.startsWith('52494646')) return 'webp';
return 'unknown';
}
const buf = Buffer.from([0xff, 0xd8, 0xff, 0xe0]);
console.log(detectImageType(buf));
输出jpeg。JPEG文件头是FFD8FF,PNG是89504E47,GIF是47494638。通过读取前几个字节判断格式,比信任客户端声明的类型可靠。
实际项目里还要限制文件大小,在流式读取时累计字节数,超过阈值就中断。Buffer在这里的角色是承载已读取的片段,配合流处理避免一次性把整个文件读进内存。
8.流与Buffer的配合
Node.js的流默认以Buffer形式传输数据。data事件拿到的就是Buffer。
const fs = require('node:fs');
const stream = fs.createReadStream('large-file.bin');
let total = 0;
stream.on('data', (chunk) => {
total += chunk.length;
});
stream.on('end', () => {
console.log('总字节数:', total);
});
chunk是Buffer,chunk.length是字节数。如果处理文本,需要chunk.toString('utf8'),但要注意一个多字节字符可能被切在两个chunk之间,直接逐块toString会产生乱码。正确做法是用StringDecoder或设置流的编码。
const { StringDecoder } = require('node:string_decoder');
const decoder = new StringDecoder('utf8');
stream.on('data', (chunk) => {
process.stdout.write(decoder.write(chunk));
});
StringDecoder会缓存不完整的字节序列,等下一个chunk到达再拼接。这是处理文本流的常见模式。
9.现在框架与库中的用法
Express的express.raw()中间件把请求体解析成Buffer,用于webhook签名校验。Stripe、GitHub的回调都需要原始字节来做HMAC验证,JSON解析会改变字节顺序,所以必须用raw。
数据库驱动(mysql2、pg)返回的BLOB、BYTEA字段是Buffer。处理时需要显式调用toString或写入文件。
加密模块的输出和输入都是Buffer。crypto.randomBytes()返回Buffer,cipher.update()返回Buffer,hash.digest()不带参数时返回Buffer。
WebSocket库的二进制帧对应Buffer。自定义协议解析时,通常按固定长度读取字段,用readUInt16BE、readUInt32BE等方法。
10.本节课程知识要点
-
Buffer存储原始字节,内存分配在V8堆之外。
-
Buffer.from从字符串、数组、ArrayBuffer创建;Buffer.alloc零填充;allocUnsafe不初始化。
-
不确定内容是否被完整覆盖时,使用Buffer.alloc。
-
buffer.length是字节数,不是字符数。
-
编码转换必须读写一致,UTF-8、Base64、Hex是最常用的三种。
-
slice和subarray共享内存,需要副本时用Buffer.from。
-
Buffer与ArrayBuffer转换时注意byteOffset和byteLength。
-
流处理文本时用StringDecoder避免多字节字符被截断。
-
文件类型校验读文件头字节,比信任Content-Type可靠。
11.练习与思考
-
用Buffer.from和Buffer.alloc分别创建10字节的Buffer,比较初始内容差异。
-
把一段中文分别按UTF-8、Base64、Hex编码,再还原,验证结果一致。
-
用subarray切出一个Buffer的子视图,修改子视图的第一个字节,观察原Buffer是否变化。
-
写一个函数,接收Buffer,判断它是否是PNG文件,并返回宽度和高度(提示:PNG的宽高在IHDR块中,偏移16和20字节处,大端32位整数)。
-
用流读取一个文本文件,分别用直接toString和StringDecoder处理,观察多字节字符边界处的差异。
Buffer是Node.js处理二进制数据的基础设施。文件、网络、加密、流,这些场景的数据最终都以Buffer形式流转。掌握创建方式的安全差异、编码转换的一致性、共享内存与副本的区别,能避开不少隐蔽的bug。实际项目里,Buffer很少单独出现,它总是和流、加密、协议解析、文件系统配合使用,理解它在这些组合中的角色,比记住API列表更有意义。