Buffer 练习答案(二):17~34 题
适用环境:Node.js 22+、TypeScript。本册从复制、数值读写进入长度前缀协议、文件与 TCP 数据流。
17. 克隆 Buffer
function cloneBuffer(buffer: Buffer): Buffer {
return Buffer.from(buffer);
}
const source = Buffer.from("hello");
const cloned = cloneBuffer(source);
cloned[0] = 0x48;
console.log(source.toString()); // hello
console.log(cloned.toString()); // Hello
不要用 subarray() 实现克隆,它只是共享内存的视图。
18. 仅将 ASCII 小写转大写
function asciiUppercase(input: Buffer): Buffer {
const output = Buffer.from(input);
for (let index = 0; index < output.length; index += 1) {
const byte = output[index];
if (byte >= 0x61 && byte <= 0x7a) {
output[index] = byte - 0x20;
}
}
return output;
}
console.log(asciiUppercase(Buffer.from("Hello, node.js 中文")).toString());
// HELLO, NODE.JS 中文
这只适用于 ASCII。不要用“逐字节减 32”的方式处理 UTF-8 中文或 Unicode 大小写;应转字符串后用 toUpperCase(),并理解语言规则。
19. 大端和小端读取
const buffer = Buffer.alloc(4);
buffer.writeUInt32BE(300);
console.log(buffer.toString("hex")); // 0000012c
console.log(buffer.readUInt32BE()); // 300
console.log(buffer.readUInt32LE()); // 738263040
同一段字节按不同字节序解释会得到不同数字。协议必须明确规定端序;网络协议通常采用大端序(network byte order)。
20. 0x1234 的底层字节
const be = Buffer.alloc(2);
const le = Buffer.alloc(2);
be.writeUInt16BE(0x1234);
le.writeUInt16LE(0x1234);
console.log(be.toString("hex")); // 1234
console.log(le.toString("hex")); // 3412
BE 先写高位字节;LE 先写低位字节。
21. 读写 32 位请求 ID
const requestId = 4_000_000_000;
const buffer = Buffer.alloc(4);
buffer.writeUInt32BE(requestId, 0);
console.log(buffer.readUInt32BE(0)); // 4000000000
writeUInt32BE() 的范围是 0 到 0xffffffff。业务 ID 超过 32 位、或需要与数据库 BIGINT UNSIGNED 对齐时,应改用 64 位 BigInt API。
22. 编码“4 字节长度 + body”
function encodeFrame(text: string): Buffer {
const body = Buffer.from(text, "utf8");
const frame = Buffer.allocUnsafe(4 + body.length);
frame.writeUInt32BE(body.length, 0);
body.copy(frame, 4);
return frame;
}
console.log(encodeFrame("hello").toString("hex"));
// 0000000568656c6c6f
这里 allocUnsafe 是安全的:4 个头字节和所有 body 字节都立即被写满。真实协议还应限制 body 的最大长度,防止伪造长度导致内存耗尽。
23. 解析完整帧
const MAX_BODY_BYTES = 1024 * 1024;
function decodeCompleteFrame(frame: Buffer): string {
if (frame.length < 4) throw new Error("消息头不足 4 字节");
const bodyLength = frame.readUInt32BE(0);
if (bodyLength > MAX_BODY_BYTES) throw new Error("消息体过大");
if (frame.length !== 4 + bodyLength) throw new Error("不是恰好一个完整帧");
return frame.subarray(4).toString("utf8");
}
toString("utf8") 不会自动证明内容是有效业务数据;如果 body 是 JSON,还需要 JSON.parse 错误处理和 schema 校验。
24. 编码带头部的二进制包
type PacketInput = {
version: number;
type: number;
requestId: number;
body: Buffer;
};
function encodePacket(input: PacketInput): Buffer {
if (!Number.isInteger(input.version) || input.version < 0 || input.version > 0xff) throw new RangeError("version");
if (!Number.isInteger(input.type) || input.type < 0 || input.type > 0xff) throw new RangeError("type");
if (!Number.isInteger(input.requestId) || input.requestId < 0 || input.requestId > 0xffffffff) throw new RangeError("requestId");
const headerLength = 10;
const packet = Buffer.allocUnsafe(headerLength + input.body.length);
packet.writeUInt8(input.version, 0);
packet.writeUInt8(input.type, 1);
packet.writeUInt32BE(input.requestId, 2);
packet.writeUInt32BE(input.body.length, 6);
input.body.copy(packet, headerLength);
return packet;
}
字段偏移量要定义为常量会更易维护;生产协议通常还包括 magic number、校验和、压缩标志或序列化格式版本。
25. 解码二进制包
type DecodedPacket = {
version: number;
type: number;
requestId: number;
body: Buffer;
};
function decodePacket(packet: Buffer): DecodedPacket {
const headerLength = 10;
if (packet.length < headerLength) throw new Error("包头不足");
const bodyLength = packet.readUInt32BE(6);
if (bodyLength > 1024 * 1024) throw new Error("body 超过协议上限");
if (packet.length !== headerLength + bodyLength) throw new Error("包长度不匹配");
return {
version: packet.readUInt8(0),
type: packet.readUInt8(1),
requestId: packet.readUInt32BE(2),
body: Buffer.from(packet.subarray(headerLength)),
};
}
这里复制 body,调用方可安全长期保存或修改它;若明确只在同步流程读取,可返回 subarray() 降低复制成本。
26. 超出字节范围的数组元素
const buffer = Buffer.from([200, 300, -1, 256]);
console.log([...buffer]); // [200, 44, 255, 0]
console.log(buffer.toString("hex")); // c82cff00
数组形式会将每个元素转换为无符号 8 位值,相当于对 256 取模。它不会替你发现输入错误;构造协议字段时先自行做范围校验。
27. 8 字节整数:number 与 bigint
const unsafeNumber = Number.MAX_SAFE_INTEGER + 1;
console.log(unsafeNumber === unsafeNumber + 1); // true:精度已经丢失
const buffer = Buffer.alloc(8);
const id = 9_007_199_254_740_993n;
buffer.writeBigUInt64BE(id);
console.log(buffer.readBigUInt64BE()); // 9007199254740993n
JavaScript number 仅能精确表示到 Number.MAX_SAFE_INTEGER(2^53 - 1)。writeBigUInt64BE()/readBigUInt64BE() 处理范围 0n 到 2^64 - 1n;带符号数使用 writeBigInt64BE()。不要把 BigInt 和 number 直接算术混用。
28. readFile() 为什么返回 Buffer
import { readFile } from "node:fs/promises";
const bytes = await readFile("./example.txt");
console.log(Buffer.isBuffer(bytes)); // true
console.log(bytes.toString("utf8"));
文件本质是字节序列,Node 不应擅自猜测编码,因此默认返回 Buffer。确认文件是 UTF-8 文本后才解码;二进制文件(图片、ZIP)绝不能随意 toString()。
29. 字节数与字符数
import { readFile } from "node:fs/promises";
const bytes = await readFile("./example.txt");
const text = bytes.toString("utf8");
console.log("字节数", bytes.length);
console.log("UTF-16 code unit 数", text.length);
console.log("UTF-8 重新编码字节数", Buffer.byteLength(text, "utf8"));
最后两个字节数在“源文件有效 UTF-8 且未包含 BOM/未做转换”时通常一致。无效 UTF-8 会被替换字符修复后改变内容,因此日志和文件格式应明确编码。
30. Base64 上传体积
const fileBytes = Buffer.from("这是一个模拟文件内容", "utf8");
const base64 = fileBytes.toString("base64");
const base64Bytes = Buffer.byteLength(base64, "ascii");
console.log({ original: fileBytes.length, base64Bytes, ratio: base64Bytes / fileBytes.length });
Base64 长度公式为 4 * ceil(n / 3),通常增加约 33%,小文件因 = 填充比例可能更高。上传文件优先 multipart/form-data 或二进制请求体,不要为了 JSON 方便而把大文件转 Base64。
31. JSON HTTP 响应体长度
const payload = { message: "你好", ok: true };
const text = JSON.stringify(payload);
const body = Buffer.from(text, "utf8");
console.log(text);
console.log(body.length); // 可直接作为 Content-Length
Content-Length 是字节数。不可用 text.length 替代,因为 JSON 中可能有中文或 emoji。
32. TCP 粘包:一次拿到两帧
const combined = Buffer.concat([encodeFrame("first"), encodeFrame("second")]);
function decodeAllCompleteFrames(input: Buffer): { messages: string[]; rest: Buffer } {
const messages: string[] = [];
let offset = 0;
while (input.length - offset >= 4) {
const length = input.readUInt32BE(offset);
if (length > 1024 * 1024) throw new Error("非法长度");
const end = offset + 4 + length;
if (input.length < end) break;
messages.push(input.subarray(offset + 4, end).toString("utf8"));
offset = end;
}
return { messages, rest: Buffer.from(input.subarray(offset)) };
}
console.log(decodeAllCompleteFrames(combined).messages); // ["first", "second"]
TCP 是字节流,所谓“粘包”不是错误:一次 data 回调可包含 0、1 或多条应用消息。应用层需要自己的分帧规则。
33. TCP 半包:数据不足
const complete = encodeFrame("hello");
const partial = complete.subarray(0, 6); // 4 字节长度 + 2 字节 body
function hasCompleteFrame(input: Buffer): boolean {
if (input.length < 4) return false;
const length = input.readUInt32BE(0);
return length <= 1024 * 1024 && input.length >= 4 + length;
}
console.log(hasCompleteFrame(partial)); // false
数据不足不是异常;保存当前数据,等待下一次 data。非法长度才是协议错误,应断开或报告。
34. 可累积解析的帧解码器
class FrameAccumulator {
private pending = Buffer.alloc(0);
private static readonly maxBodyBytes = 1024 * 1024;
push(chunk: Buffer): string[] {
this.pending = Buffer.concat([this.pending, chunk]);
const messages: string[] = [];
let offset = 0;
while (this.pending.length - offset >= 4) {
const length = this.pending.readUInt32BE(offset);
if (length > FrameAccumulator.maxBodyBytes) {
this.pending = Buffer.alloc(0);
throw new Error("非法帧长度,已清空连接缓冲区");
}
const end = offset + 4 + length;
if (this.pending.length < end) break;
messages.push(this.pending.subarray(offset + 4, end).toString("utf8"));
offset = end;
}
this.pending = Buffer.from(this.pending.subarray(offset));
return messages;
}
}
const decoder = new FrameAccumulator();
const frame = encodeFrame("hello");
console.log(decoder.push(frame.subarray(0, 2))); // []
console.log(decoder.push(frame.subarray(2))); // ["hello"]
生产实现还应设置每个连接的累计缓冲上限、空闲超时和错误后关闭 socket。对于高吞吐协议,避免每次 Buffer.concat 的重复复制,可设计分段队列,但先保证正确性。