人人都会AI编程

25.1 Buffer 与二进制数据处理

更新时间:2026-07-10

在 Node.js 中,JavaScript 原生擅长处理 Unicode 字符串和对象,但当数据以二进制形式流入时(例如读取一个图像文件、解析网络协议报文,或者处理加密后的字节流),字符串和普通数组就显得力不从心。为此,Node.js 在全局作用域中提供了 Buffer 类——一个专门用来处理原始二进制数据的工具。它不在 V8 堆内分配,而是直接操作系统内存,是大规模数据处理的基石。

25.1.1 为什么需要 Buffer

JavaScript 最初的定位是浏览器脚本,主要处理 HTML 文本和用户交互,因此原生只提供了一组以 UTF-16 编码操作为主的字符串方法。但是服务端开发不可避免地要面对二进制协议(TCP、UDP、HTTP/2 帧)、文件系统读写(图片、视频、压缩包)以及加密/解密(哈希、签名)等场景,这些数据都是以字节为单位组织,而非字符。

Buffer 正是为填补这个空缺而生的。它能让你直接操控内存中的原始字节,并且与 JavaScript 的 Uint8Array 共享底层 ArrayBuffer,但在设计上提供了更多面向 Node.js 核心能力的便捷方法。

25.1.2 Buffer 的创建与基本操作

创建 Buffer

Node.js 提供了三种主要方式创建 Buffer

// 1. 指定大小并初始化为零(安全但略慢)
const buf1 = Buffer.alloc(10);  // 长度为 10 字节,全部填 0

// 2. 指定大小但不初始化(性能更高,但可能包含旧数据,需立即覆写)
const buf2 = Buffer.allocUnsafe(10);

// 3. 从已有数据创建
const buf3 = Buffer.from('Hello 世界', 'utf-8');
const buf4 = Buffer.from([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
const buf5 = Buffer.from(buf1); // 深拷贝

allocUnsafealloc 稍快,因为它不会清零内存,但这也意味着它会暴露之前进程残留的敏感数据。除非你马上用完整数据覆盖它,否则应该优先使用 allocallocSafe(在较新版本中更名为 Buffer.alloc,并总是清零)。

读写与字节序

你可以像操作数组一样通过下标读写单个字节,Buffer 中每个元素都是一个 0~255 的无符号整数:

const buf = Buffer.alloc(4);
buf[0] = 0xff;          // 255
buf[1] = 0xab;          // 171
console.log(buf[0]);    // 255

对于多字节数值的读写(如 16 位整数、32 位浮点数等),Buffer 提供了一系列带字节序后缀的方法——LE(小端)和 BE(大端):

const buf = Buffer.alloc(4);
buf.writeUInt16LE(256, 0);   // 写入小端序 16 位无符号整数
console.log(buf.readUInt16LE(0)); // 256

这在进行网络协议解析(通常使用大端序)或读取系统文件(通常与本机字节序一致)时至关重要。

25.1.3 编码与字符串互转

Buffer 与字符串之间的转换需要指定字符编码。Node.js 支持众多编码,最常用的是 utf8,其他还有 hexbase64latin1 等:

const str = 'Hello 中文';
const buf = Buffer.from(str, 'utf8');
console.log(buf.toString('hex'));      // 输出十六进制串
console.log(buf.toString('base64'));   // 转换为 Base64

处理 Web 文件上传时经常要面对 Base64 格式的数据,例如将 <img>src 前缀去除后转为 Buffer 存入磁盘:

const dataURL = 'data:image/png;base64,iVBORw0KGgo...';
const base64Data = dataURL.replace(/^data:image\/\w+;base64,/, '');
const imgBuffer = Buffer.from(base64Data, 'base64');

反之,将二进制数据编码为 Base64 后可以直接嵌入 HTML 或 JSON 中。

25.1.4 实际应用场景

1. 文件处理:读取图片并转换为缩略图

虽然 Node.js 的 fs.readFile 默认返回 Buffer,配合 sharp 等图像库,可以用流或 Buffer 实现:

const fs = require('fs');
const sharp = require('sharp');

fs.readFile('./input.jpg', (err, data) => {
  if (err) throw err;
  sharp(data)
    .resize(200, 200)
    .toBuffer()
    .then(outputBuf => {
      fs.writeFile('./thumb.jpg', outputBuf, err => { /* ... */ });
    });
});

这里 data 是一个 Buffer,sharp 能直接接受 Buffer 输入。

2. 网络协议解析

假设我们自定义一个简单的二进制协议:前 4 字节表示消息长度,后面跟着消息体。

const net = require('net');

net.createServer(socket => {
  let headerBuf = Buffer.alloc(4);
  let headerRead = 0;

  socket.on('data', chunk => {
    // 粘包/拆包处理,此处仅演示思路
    if (headerRead < 4) {
      const remaining = 4 - headerRead;
      chunk.copy(headerBuf, headerRead, 0, remaining);
      headerRead += Math.min(chunk.length, remaining);
      // 如果读完头部,开始读 body
    }
  });
});

Buffer 的方法如 copyslice(注意浅拷贝)、indexOf 等能让网络数据处理变得灵活。

3. 流式管道中的 Buffer 操作

Node.js 的 Stream 模块中,当调用 readable.read()writeable.write() 时,操作的对象往往是 Buffer(或者字符串,流会自动编码)。自定义 Transform 流可以逐块处理二进制数据,例如计算文件的 MD5:

const crypto = require('crypto');
const { Transform } = require('stream');

const hash = crypto.createHash('md5');
const transform = new Transform({
  transform(chunk, encoding, callback) {
    hash.update(chunk);
    this.push(chunk); // 原样传递数据
    callback();
  },
  flush(callback) {
    this.push(hash.digest('hex'));
    callback();
  }
});

process.stdin.pipe(transform).pipe(process.stdout);

这里的 chunk 本身就是 Buffer。

25.1.5 内存模型与性能考量

Buffer 的内存是在 V8 堆外分配的。这意味着它不受 V8 的常规垃圾回收直接管理,而是由 Node.js 底层的 C++ 层进行分配和释放。对于处理 GB 级的文件或长期缓存大量二进制数据,这可以显著减少 GC 暂停和内存膨胀。

  • 创建 Buffer 的代价allocUnsafe 只是简单获取内存的粗略操作;alloc 会额外清零,代价稍高。在极端场景中应优先重用已有 Buffer。
  • 切片是浅拷贝buf.slice() 返回的 Buffer 与原 Buffer 共享同一块内存,修改切片会影响原始数据。如果需要独立数据,请使用 Buffer.from(slicedBuf) 深拷贝。
  • 与 TypedArray 的关系Buffer 继承自 Uint8Array,因此你可以将 Buffer 传入接受 TypedArray 的函数(例如 WebSocketsend)。不过某些旧版 Node.js 中 Buffer 的行为与标准 Uint8Array 有细微差异(如 slice 行为),需要留意。

25.1.6 常见“坑”与最佳实践

  1. 不要使用过时的构造函数

避免 new Buffer(size)new Buffer(string),它们不安全且已在 Node.js 10+ 中废弃。始终使用 Buffer.allocBuffer.from 等工厂方法。

  1. 注意编码一致性

当你从流中读取字节并转为字符串时,确保编码与发送端一致,否则会产生乱码。如果处理的是多字节字符(如 UTF-8),不要强行将单个字节乱切,应该按完整字符边界解析,或使用 StringDecoder 辅助。

   const { StringDecoder } = require('string_decoder');
   const decoder = new StringDecoder('utf8');
   // 即使 chunk 被截断在字符中间,decoder 也能正常累积处理
   console.log(decoder.write(chunk1));
   console.log(decoder.end(chunk2));
   
  1. 大文件不应用 readFile 直接全部加载到 Buffer

如果一个文件几百 MB,直接 fs.readFile 会生成一个巨大的 Buffer 常驻内存,容易导致 OOM。应改用流式处理:fs.createReadStream + 管道或 Transform

  1. 安全与隐私

allocUnsafe 的回收内存可能包含密码、私钥等敏感数据。在处理加解密和认证信息时,务必使用 alloc 或者在用完后手动覆盖原 Buffer(例如用 buf.fill(0))。

  1. 性能敏感场景重用 Buffer

如果你在一个高频事件循环中需要频繁创建小块 Buffer(例如解析网络包),可以考虑使用 Buffer 池技术或复用预先分配的 Buffer,以避免额外 GC 压力。Node.js 内部也在一定程度上缓存了小 Buffer。

总结

Buffer 是 Node.js 突破 JavaScript 语言限制、深入二进制世界的关键工具。无论是文件处理、网络协议、加密算法还是流数据管道,Buffer 都提供了直接、高效的操作接口。掌握 Buffer 的创建、编码、切片、内存模型以及与流的结合,是走向高阶 Node.js 开发者的必经之路。在下一节中,我们将继续深入流的高级应用,看 Buffer 如何在管道中发挥最大效能。