Buffer 是什么?和字符编码有什么关系?
一句话回答
Buffer 是 Node.js 用来表示一段固定长度的原始字节的对象,是 Uint8Array 的子类,文件、网络、加密这些处理二进制数据的地方都会用到它。字符串和 Buffer 互相转换时要指定字符编码:Buffer.from(str, 'utf8') 把字符串编码成字节,buf.toString('base64') 把字节按指定编码转成字符串。UTF-8 中一个汉字通常占 3 个字节,把 Buffer 从字符中间切开、再分别转成字符串就会出现乱码,流式处理文本时用 StringDecoder 或流的 setEncoding 解决。
详细解析
Buffer 和 Uint8Array
- 每个元素是 0~255 的整数,
length是字节数,创建后长度不能改变 - 继承自
Uint8Array,能用在任何接收Uint8Array的地方;额外提供了按编码转字符串、readUInt32BE这类按格式读写数字的方法 - 数据存放在 V8 堆外,
process.memoryUsage()里的arrayBuffers统计的就是这部分内存 - 字符串的
length是 UTF-16 码元的个数,不是字节数:'中文'.length是 2,Buffer.byteLength('中文')是 6
创建方式
| 方法 | 说明 |
|---|---|
Buffer.from(string, encoding) |
按编码把字符串转成字节,默认 utf8 |
Buffer.from(array)、Buffer.from(buffer) |
复制一份数据 |
Buffer.from(arrayBuffer) |
和这个 ArrayBuffer 共享内存,不复制 |
Buffer.alloc(size, fill) |
分配内存并清零(或填充指定的值),安全 |
Buffer.allocUnsafe(size) |
分配但不初始化,更快;内容可能是这块内存上残留的旧数据,较小的 Buffer 还会从预先分配的内存池里切出来 |
new Buffer() |
已废弃,行为随参数类型变化,容易误用 |
allocUnsafe 适合马上会被完整写满的场景,比如分配好之后立刻把读到的数据填进去。如果没写满就发送出去,残留的旧数据(可能是别的请求的内容、密钥)会一起泄露。
编码转换
Buffer 支持的编码有 utf8、utf16le、latin1、ascii、base64、base64url、hex 等,不支持 GBK 这类中文编码。
const buf = Buffer.from('你好', 'utf8')
console.log(buf) // <Buffer e4 bd a0 e5 a5 bd>:每个汉字 3 个字节
console.log(buf.toString('hex')) // e4bda0e5a5bd
console.log(buf.toString('base64')) // 5L2g5aW9
console.log(Buffer.from('5L2g5aW9', 'base64').toString()) // 你好
要分清两类编码:utf8、utf16le 是字符编码,决定一个字符对应哪几个字节;hex、base64 是把任意字节表示成可打印文本的方式,常用来在 JSON、URL、HTTP 头里传输二进制数据。
为什么会出现乱码
UTF-8 是变长编码:ASCII 字符占 1 个字节,常用汉字占 3 个字节,多数 emoji 占 4 个字节。从文件或网络读数据时,每一块的边界按字节数划分,完全可能落在一个汉字的中间:
import { StringDecoder } from 'node:string_decoder'
const buf = Buffer.from('你好')
const part1 = buf.subarray(0, 4) // "你"的 3 个字节 + "好"的第 1 个字节
const part2 = buf.subarray(4) // "好"的后 2 个字节
// 错误:每块单独解码,不完整的字节被替换成 U+FFFD
console.log(part1.toString() + part2.toString()) // 你���
// 正确:StringDecoder 把末尾不完整的字节先存起来,和下一块拼成完整的字符
const decoder = new StringDecoder('utf8')
let text = decoder.write(part1) // "你"
text += decoder.write(part2) // "好"
text += decoder.end()
console.log(text) // 你好
几种解决方法:
- 数据量不大时,先收集所有块,拼接后再统一解码:
Buffer.concat(chunks).toString('utf8') - 用
StringDecoder逐块解码,如上 - 流式读取时调用
readable.setEncoding('utf8'),或在创建流时传入encoding,内部用的就是 StringDecoder,见 Stream 和背压 - Web 标准的
TextDecoder加上{ stream: true }选项,效果相同
常见的错误写法是 req.on('data', (chunk) => { body += chunk }):+= 会把每个 chunk 单独转成字符串,汉字恰好跨块时就会乱码。正确写法:
const chunks = []
for await (const chunk of req) chunks.push(chunk)
const body = Buffer.concat(chunks).toString('utf8')
面试官可能追问
Buffer、ArrayBuffer、Uint8Array 是什么关系?
ArrayBuffer 是一块原始内存,本身不能直接读写;Uint8Array 等类型化数组是访问这块内存的视图;Buffer 是 Node.js 在 Uint8Array 基础上扩展的子类。buf.buffer 能拿到底层的 ArrayBuffer,但用 Buffer.from、Buffer.allocUnsafe 创建的较小 Buffer 往往是从共享的内存池里切出来的,这时 buf.buffer 是整个内存池,要配合 buf.byteOffset 和 buf.byteLength 才能取到正确的范围。
读取 GBK 编码的文件为什么乱码?怎么处理?
readFile 指定 'utf8' 时按 UTF-8 解码,而 Buffer 本身不支持 GBK。可以先读成 Buffer,再用 new TextDecoder('gbk').decode(buf) 解码(Node.js 官方发布的版本内置了完整的 ICU 数据,支持 GBK、GB18030 等编码),也可以用 iconv-lite 这类库。流式读取时用 TextDecoder 的 stream: true 选项,或者 iconv-lite 提供的解码流。
base64 和 base64url 有什么区别?
base64 用 A-Z、a-z、0-9、+、/ 这 64 个字符,结尾用 = 补齐长度;+、/、= 在 URL 里都有特殊含义。base64url 把 +、/ 换成 -、_,并省略结尾的 =,可以直接放进 URL 和文件名,JWT 的三段用的就是它。两者都是每 3 个字节编码成 4 个字符,体积增加约三分之一。
怎么用 Buffer 解析二进制协议?
用 readUInt32BE、readInt16LE、writeUInt32BE 这类方法按指定的字节序读写数字,用 subarray 截取消息体。TCP 是字节流,还要自己处理消息边界,比如"4 字节长度 + 消息体"的格式,示例见 TCP 和 UDP 的区别。
易错点
buf.subarray()和buf.slice()返回的都是共享同一块内存的视图,改一个另一个也会变;Uint8Array.prototype.slice才会复制。Buffer 的slice已经废弃,需要复制时用Buffer.from(buf)- 拼接流数据时写
body += chunk,汉字跨块就会乱码 Buffer.allocUnsafe的内容可能是旧数据,没写满就发送会泄露内存里的敏感信息- 设置
Content-Length这类按字节计算的值时用Buffer.byteLength(str),不能用str.length
AI 模拟面试官
用自己的话回答,AI 对照参考答案打分、指出遗漏,再追问,最多 3 轮
这道题你掌握了吗?
选一个最接近的状态,没掌握的题会出现在"我的进度 · 待复习"里。
学习记录暂存在本机浏览器。登录后自动同步到账号,换设备也能看到。