前言

高级应用开发迟早会碰到"二进制"这堵墙:解析一个自定义网络协议、读取一个二进制文件头、把结构化数据打包成紧凑的字节流、计算校验和、处理图片/音频的原始数据……这些场景里,你面对的不再是字符串,而是一个个字节(byte)。用字符串拼接去处理二进制,既低效又极易出错。

在 Node.js 生态里,Buffer 是处理二进制数据的事实标准。HarmonyOS NEXT 的 ArkTS 标准库把这套能力搬了过来——@ohos.buffer 提供了一个与 Node.js Buffer 高度兼容的二进制缓冲区实现:创建、读写各种数值类型、编码转换(hex / base64 / utf8)、切片、拷贝、比较,一应俱全,零权限、纯本地。

本文用一个可交互的"二进制缓冲区实验室"页面,把 buffer 最核心的两块能力讲透:文本与缓冲区的双向转换(hex / base64)、以及数值的多类型编码与大端/小端字节序对比。全文含完整可运行代码,并会把"字节序"这个二进制世界里最容易踩坑的概念讲清楚。适合想进军协议开发、二进制解析的中级开发者。


一、Buffer 是什么

Buffer 本质是一段固定长度的原始字节序列,可以把它理解成一个"字节数组",但它提供了远比数组丰富的读写接口:你可以往里按 Int8、UInt16、Int32、Float 等类型写入数值,也可以把它整体转成 hex 字符串、base64 字符串或 UTF-8 文本。

它和前面介绍过的 TextEncoder/Uint8Array 有什么区别?Uint8Array 只能逐字节存取,而 Buffer 在其之上封装了按数值类型和字节序读写的能力——这正是处理二进制协议的关键。一个 4 字节的整数怎么摆放在内存里、高位在前还是低位在前,Buffer 都用明确的 API 帮你控制。

引入方式:

import { buffer } from '@kit.ArkTS';

二、创建缓冲区

2.1 从字符串创建

const buf = buffer.from('Hi鸿蒙', 'utf8');

buffer.from(string, encoding) 把字符串按指定编码转成缓冲区。encoding 支持 'utf8''hex''base64''ascii''latin1' 等。

2.2 分配指定大小

const buf = buffer.alloc(4);          // 4 字节,全部填 0
const buf2 = buffer.alloc(4, 0xff);   // 4 字节,全部填 0xff

buffer.alloc(size, fill?) 分配固定长度的缓冲区并可选填充。写入数值前通常先用它按类型大小开辟空间。

2.3 常用属性与转换

buf.length              // 字节数
buf.toString('hex')     // 转十六进制字符串
buf.toString('base64')  // 转 base64
buf.toString('utf8')    // 转文本

三、实战:文本 ↔ 缓冲区

实验室第一个 Tab 演示文本与缓冲区的双向转换。

3.1 文本 → 缓冲区

@State text: string = 'Hi鸿蒙';
@State hexOut: string = '';
@State base64Out: string = '';
@State lenOut: number = 0;

// 文本 → 缓冲区,输出 hex / base64 / 字节数
private encodeText(): void {
  const buf = buffer.from(this.text, 'utf8');
  this.hexOut = this.formatHex(buf.toString('hex'));
  this.base64Out = buf.toString('base64');
  this.lenOut = buf.length;
}

输入 Hi鸿蒙,你会看到:

  • 字节数 8:Hi 各 1 字节,鸿 各 3 字节(UTF-8);
  • hex:48 69 E9 B8 BF E8 92 99——前两个是 ASCII 的 H(0x48)、i(0x69),后六个是两个汉字的 UTF-8 编码;
  • base64:SGnpuL/oktk=

为了让 hex 更易读,我们把连续的十六进制串每字节插一个空格:

private formatHex(hex: string): string {
  let out: string = '';
  for (let i = 0; i < hex.length; i += 2) {
    out += hex.substring(i, i + 2).toUpperCase();
    if (i + 2 < hex.length) {
      out += ' ';
    }
  }
  return out;
}

3.2 缓冲区 → 文本(反向解码)

@State hexIn: string = '48 69';
@State decodeOut: string = '';
@State decodeError: string = '';

// hex 字节串 → 缓冲区 → 文本
private decodeHex(): void {
  this.decodeError = '';
  try {
    const clean: string = this.hexIn.replace(/\s/g, '');
    if (clean.length % 2 !== 0) {
      this.decodeError = 'hex 长度必须为偶数';
      return;
    }
    const buf = buffer.from(clean, 'hex');
    this.decodeOut = buf.toString('utf8');
  } catch (e) {
    this.decodeError = '非法的 hex 字符串';
  }
}

这里做了两层健壮性处理:先用正则 replace(/\s/g, '') 去掉用户输入里的空格,再校验长度必须为偶数(每字节两个 hex 字符),最后用 try-catch 兜住非法字符。输入 48 69 会解码回 Hi。这种"hex 转文本"在调试协议报文时特别常用——抓包工具给你一串 hex,你想知道它对应什么内容。


四、实战:数值编码与字节序

这是本文的重点,也是 Buffer 相比普通字节数组最有价值的能力。

4.1 为什么数值需要"编码"

一个整数 300 在内存里怎么存?它超过了一个字节(0~255)的范围,需要 2 个字节。300 的十六进制是 0x012C,拆成两个字节就是 012C。但问题来了:这两个字节谁在前?

  • 大端序(Big-Endian, BE):高位字节在前 → 01 2C;
  • 小端序(Little-Endian, LE):低位字节在前 → 2C 01

这就是"字节序"(Endianness)。不同 CPU 架构、不同协议规定了不同的字节序。网络协议(TCP/IP)约定用大端(所以大端又叫"网络字节序"),而 x86、ARM 等主流 CPU 内部用小端。如果发送方和接收方对字节序的理解不一致,同一段字节会被解读成完全不同的数值——这是二进制协议里最经典的 bug 来源。

4.2 多类型 + 双字节序编码

实验室让你选择数据类型和输入数值,同时用大端和小端两种方式编码,直观对比:

interface NumType {
  label: string;
  size: number;
}

private numTypes: NumType[] = [
  { label: 'Int8', size: 1 },
  { label: 'UInt8', size: 1 },
  { label: 'Int16', size: 2 },
  { label: 'UInt16', size: 2 },
  { label: 'Int32', size: 4 },
  { label: 'UInt32', size: 4 },
  { label: 'Float', size: 4 }
];

// 同一数值分别用大端 / 小端写入,对比字节序
private encodeNumber(): void {
  this.numError = '';
  const t: NumType = this.numTypes[this.typeIndex];
  const isFloat: boolean = t.label === 'Float';
  const v: number = isFloat ? parseFloat(this.numInput) : parseInt(this.numInput, 10);
  if (isNaN(v)) {
    this.numError = '请输入有效数值';
    return;
  }
  try {
    const be = buffer.alloc(t.size);
    const le = buffer.alloc(t.size);
    switch (t.label) {
      case 'Int8':
        be.writeInt8(v, 0); le.writeInt8(v, 0); break;
      case 'UInt8':
        be.writeUInt8(v, 0); le.writeUInt8(v, 0); break;
      case 'Int16':
        be.writeInt16BE(v, 0); le.writeInt16LE(v, 0); break;
      case 'UInt16':
        be.writeUInt16BE(v, 0); le.writeUInt16LE(v, 0); break;
      case 'Int32':
        be.writeInt32BE(v, 0); le.writeInt32LE(v, 0); break;
      case 'UInt32':
        be.writeUInt32BE(v, 0); le.writeUInt32LE(v, 0); break;
      case 'Float':
        be.writeFloatBE(v, 0); le.writeFloatLE(v, 0); break;
      default:
        break;
    }
    this.beHex = this.formatHex(be.toString('hex'));
    this.leHex = this.formatHex(le.toString('hex'));
  } catch (e) {
    this.numError = '数值超出 ' + t.label + ' 的表示范围';
    this.beHex = '';
    this.leHex = '';
  }
}

Buffer 为每种数值类型都提供了成对的 BE/LE 写入方法:

类型 大端方法 小端方法 字节数
Int8 / UInt8 writeInt8 (无字节序之分) 1
Int16 / UInt16 writeInt16BE writeInt16LE 2
Int32 / UInt32 writeInt32BE writeInt32LE 4
Float writeFloatBE writeFloatLE 4
Double writeDoubleBE writeDoubleLE 8

每个方法签名是 writeXxx(value, offset),offset 是写入起始位置,单缓冲区从 0 开始。

4.3 观察实验结果

输入 300、选 Int16,你会看到:

BE 大端:01 2C
LE 小端:2C 01

字节顺序正好相反。切到 Int32,同样是 300:

BE 大端:00 00 01 2C
LE 小端:2C 01 00 00

现在 4 个字节,大端把有效字节放在末尾,小端放在开头。再试 Int8 输入 100,BE 和 LE 都是 64——因为单字节没有"顺序"可言,这也是表格里 Int8/UInt8 不分 BE/LE 的原因。

试试输入超范围的值,比如 Int8 输入 300(超过 127 上限),writeInt8 会抛异常,被 catch 捕获后提示"数值超出 Int8 的表示范围"。这演示了类型的取值边界:Int8 是 -128~127,UInt8 是 0~255,Int16 是 -32768~32767,以此类推。

再试 Float 输入 3.14,你会看到它被编码成 IEEE 754 单精度浮点的 4 个字节,BE 与 LE 同样顺序相反。这解释了浮点数在二进制里的存储方式。


在这里插入图片描述
在这里插入图片描述

五、页面结构与交互

整个实验室是"双 Tab"结构,主体简洁:

@Entry
@Component
struct BufferLabPage {
  @State activeTab: number = 0;

  build() {
    Column() {
      // 标题栏
      Row() { /* 返回 + 标题 + @ohos.buffer 角标 */ }

      // Tab 切换
      Row() {
        this.tabBtn('文本缓冲', 0)
        this.tabBtn('数值编码', 1)
      }

      Scroll() {
        Column() {
          if (this.activeTab === 0) {
            this.textTab()
          } else {
            this.numberTab()
          }
        }
      }
      .layoutWeight(1)
    }
  }
}

三个交互点分布在两个 Tab:

  1. 文本 → 缓冲区:输入文本实时显示 hex / base64 / 字节数;
  2. hex → 文本:输入 hex 字节串反向解码回文本;
  3. 数值编码:选择类型 + 输入数值,同时显示大端和小端的字节表示。

页面在 aboutToAppear 里把三块都先算一遍,保证打开即有内容:

aboutToAppear(): void {
  this.encodeText();
  this.decodeHex();
  this.encodeNumber();
}

数值输入框用 InputType.NUMBER_DECIMAL 限制只能输入数字,减少非法输入:

TextInput({ text: this.numInput })
  .type(InputType.NUMBER_DECIMAL)
  .onChange((v: string) => { this.numInput = v; this.encodeNumber(); })

字节序对比用两张卡片上下并列,分别用不同的强调色(BE 深蓝、LE 青色)区分,hex 用 monospace 等宽字体展示,让字节对齐一目了然。


六、几个实战要点与坑

6.1 字节序必须与协议约定一致

这是二进制开发的头号原则。解析或封装协议前,先查清协议规定的字节序(多数网络协议是大端)。读写时选对 BE/LE 方法,否则数值全错。调试时如果读出来的数值大得离谱或符号错乱,第一反应就该怀疑字节序用反了。

6.2 数值范围会溢出抛异常

writeInt8 写入超过 127 的值、writeUInt8 写入负数,都会抛异常。凡是写入外部来源的数值,都要用 try-catch 兜底,并向用户给出范围提示。

6.3 offset 别越界

writeXxx(value, offset) 的 offset + 类型字节数不能超过缓冲区长度,否则抛 RangeError。往一个 alloc(2) 的缓冲区里 writeInt32BE 会越界。分配大小要与写入类型匹配。

6.4 hex 解码要校验偶数长度

每个字节对应两个 hex 字符,输入的 hex 串长度必须是偶数。去空格后先校验 length % 2 === 0,再解码,能提前拦住大部分非法输入。

6.5 Buffer 与 Uint8Array 的关系

Buffer 底层兼容 Uint8Array,很多需要 Uint8Array 的 API(如加密、压缩模块)可以直接接收 Buffer。反过来,你也可以用 buffer.from(uint8Array) 把已有字节数组包装成 Buffer 以使用其丰富的读写方法。


七、典型应用场景

  • 自定义协议封包/解包:按协议字段顺序 writeUInt16BE(长度)、writeUInt8(类型)、write(负载),或反向 readXxx 解析;
  • 二进制文件头解析:读取 PNG、WAV 等文件头的魔数、宽高、采样率等字段;
  • 校验和计算:遍历缓冲区字节求和、异或,生成 checksum;
  • 数据紧凑存储:把多个数值打包成最小字节流,节省存储或传输;
  • 与硬件/蓝牙通信:BLE 特征值、串口数据往往是裸字节,用 Buffer 组织读写。

八、小结

本文以"二进制缓冲区实验室"为载体,系统讲解了 HarmonyOS NEXT 标准库 @ohos.buffer 的核心用法:

  • 创建:buffer.from(string, encoding) / buffer.alloc(size, fill);
  • 编码转换:toString('hex' / 'base64' / 'utf8'),以及 hex 反向解码;
  • 数值读写:writeInt8 / writeInt16BE/LE / writeInt32BE/LE / writeFloatBE/LE 等成对的大端/小端方法;
  • 核心概念:字节序(大端 vs 小端)——二进制协议里最容易踩坑却最关键的知识点;
  • 工程要点:字节序须与协议一致、数值溢出与 offset 越界的异常处理、hex 偶数长度校验、Buffer 与 Uint8Array 的互通。

Buffer 是打开二进制世界的钥匙。掌握它,你就能从容应对协议开发、文件解析、硬件通信等所有"和字节打交道"的场景,而不必再用脆弱的字符串拼接去硬扛二进制数据。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐