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() 的范围是 00xffffffff。业务 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_INTEGER2^53 - 1)。writeBigUInt64BE()/readBigUInt64BE() 处理范围 0n2^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 的重复复制,可设计分段队列,但先保证正确性。