前言
高级应用开发迟早会碰到"二进制"这堵墙:解析一个自定义网络协议、读取一个二进制文件头、把结构化数据打包成紧凑的字节流、计算校验和、处理图片/音频的原始数据……这些场景里,你面对的不再是字符串,而是一个个字节(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 从字符串创建
constbuf=buffer.from('Hi鸿蒙','utf8');buffer.from(string, encoding)把字符串按指定编码转成缓冲区。encoding支持'utf8'、'hex'、'base64'、'ascii'、'latin1'等。
2.2 分配指定大小
constbuf=buffer.alloc(4);// 4 字节,全部填 0constbuf2=buffer.alloc(4,0xff);// 4 字节,全部填 0xffbuffer.alloc(size, fill?)分配固定长度的缓冲区并可选填充。写入数值前通常先用它按类型大小开辟空间。
2.3 常用属性与转换
buf.length// 字节数buf.toString('hex')// 转十六进制字符串buf.toString('base64')// 转 base64buf.toString('utf8')// 转文本三、实战:文本 ↔ 缓冲区
实验室第一个 Tab 演示文本与缓冲区的双向转换。
3.1 文本 → 缓冲区
@Statetext:string='Hi鸿蒙';@StatehexOut:string='';@Statebase64Out:string='';@StatelenOut:number=0;// 文本 → 缓冲区,输出 hex / base64 / 字节数privateencodeText():void{constbuf=buffer.from(this.text,'utf8');this.hexOut=this.formatHex(buf.toString('hex'));this.base64Out=buf.toString('base64');this.lenOut=buf.length;}输入Hi鸿蒙,你会看到:
- 字节数 8:
H、i各 1 字节,鸿、蒙各 3 字节(UTF-8); - hex:
48 69 E9 B8 BF E8 92 99——前两个是 ASCII 的 H(0x48)、i(0x69),后六个是两个汉字的 UTF-8 编码; - base64:
SGnpuL/oktk=。
为了让 hex 更易读,我们把连续的十六进制串每字节插一个空格:
privateformatHex(hex:string):string{letout:string='';for(leti=0;i<hex.length;i+=2){out+=hex.substring(i,i+2).toUpperCase();if(i+2<hex.length){out+=' ';}}returnout;}3.2 缓冲区 → 文本(反向解码)
@StatehexIn:string='48 69';@StatedecodeOut:string='';@StatedecodeError:string='';// hex 字节串 → 缓冲区 → 文本privatedecodeHex():void{this.decodeError='';try{constclean:string=this.hexIn.replace(/\s/g,'');if(clean.length%2!==0){this.decodeError='hex 长度必须为偶数';return;}constbuf=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,拆成两个字节就是01和2C。但问题来了:这两个字节谁在前?
- 大端序(Big-Endian, BE):高位字节在前 →
01 2C; - 小端序(Little-Endian, LE):低位字节在前 →
2C 01。
这就是"字节序"(Endianness)。不同 CPU 架构、不同协议规定了不同的字节序。网络协议(TCP/IP)约定用大端(所以大端又叫"网络字节序"),而 x86、ARM 等主流 CPU 内部用小端。如果发送方和接收方对字节序的理解不一致,同一段字节会被解读成完全不同的数值——这是二进制协议里最经典的 bug 来源。
4.2 多类型 + 双字节序编码
实验室让你选择数据类型和输入数值,同时用大端和小端两种方式编码,直观对比:
interfaceNumType{label:string;size:number;}privatenumTypes: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}];// 同一数值分别用大端 / 小端写入,对比字节序privateencodeNumber():void{this.numError='';constt:NumType=this.numTypes[this.typeIndex];constisFloat:boolean=t.label==='Float';constv:number=isFloat?parseFloat(this.numInput):parseInt(this.numInput,10);if(isNaN(v)){this.numError='请输入有效数值';return;}try{constbe=buffer.alloc(t.size);constle=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@Componentstruct BufferLabPage{@StateactiveTab: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:
- 文本 → 缓冲区:输入文本实时显示 hex / base64 / 字节数;
- hex → 文本:输入 hex 字节串反向解码回文本;
- 数值编码:选择类型 + 输入数值,同时显示大端和小端的字节表示。
页面在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是打开二进制世界的钥匙。掌握它,你就能从容应对协议开发、文件解析、硬件通信等所有"和字节打交道"的场景,而不必再用脆弱的字符串拼接去硬扛二进制数据。