
【仓颉语言入门 · 第二篇 第 6 课】二进制数据处理跟字节序死磕std.binary上一课哈希把字节搅碎成指纹用了异或、移位、与或非——这些位运算只是操作单个数值内部的比特。这一课的std.binary管的是另一件事一个数值在内存/文件里到底占几个字节、这些字节怎么排。为什么同一张 BMP 图片Windows 和 Mac 读出来一样为什么网络协议里写先传高字节答案是同一个词——字节序Endianness。本课把std.binary的三个接口大端、小端、字节反转讲透最后动手解析一张 BMP 图片的文件头看看真实世界的二进制格式长什么样。本文所有代码、输出与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行。目录第二篇导航第一篇《仓颉语言入门》共 30 课已完结环境搭建 → 语法 → 函数 → 类型系统 → 工程化 → 并发 → 两场实战。第二篇在更高的起点上继续共17 课、4 个模块模块课次内容一、补遗篇0106正则、时间日期、命令行参数、随机数、加密哈希、二进制处理二、数据库篇0710std.database.sql 入门与 CRUD、预编译与事务、手写 DAO、综合小实战三、进阶硬核篇1114反射、宏与 deriving、C 互操作 FFI、并发容器与对象池四、实战项目篇1517HTTP 服务端、多线程日志分析器、简易爬虫正则表达式std.regex时间日期处理std.time命令行参数解析std.argopt随机数与概率std.random加密与哈希std.crypto二进制数据处理本文数据库入门std.database.sql 与 CRUD预编译、事务与防注入手写 DAO 数据访问层数据库综合小实战反射在运行时打量类型宏与 deriving让编译器帮你写代码与 C 互操作FFI并发容器与对象池实战三HTTP 服务端实战四多线程日志分析器实战五简易爬虫一、为什么需要二进制处理1.1 一个数占几个字节仓颉里Int32是 32 位整数——也就是 4 个字节。把0x12345678这个数存进文件或发过网络物理上就是 4 个字节依次排开。但哪 4 个字节、什么顺序0x12345678拆成 4 个字节0x12、0x34、0x56、0x78。其中0x12是最高位字节MSBMost Significant Byte0x78是最低位字节LSB。写进内存时谁放低地址、谁放高地址两种主流约定字节序低地址 → 高地址谁先用大端序Big-Endian高位在前12 34 56 78网络协议TCP/IP、Java、PowerPC小端序Little-Endian低位在前78 56 34 12x86/x64 CPU、Windows、ARM多数模式名字来自《格列佛游记》里吃鸡蛋先敲大头还是小头的荒唐争论——没有谁对谁错只是约定。麻烦在于不同约定的系统之间传数据必须先讲清楚用哪种字节序否则同一段字节读出来是完全不同的数。1.2 不处理字节序会怎样假设你用 x86 电脑小端把0x12345678写进文件字节是78 56 34 12。另一个程序或另一台机器按大端序读回来得到的是0x78563412——同一个文件两个数。这就是所有跨平台二进制格式图片、视频、网络包必须在规范里写死字节序的原因。std.binary就是管这件事的给你一套统一接口显式指定按大端还是小端读写把约定写进代码而不是靠默契。1.3 先认人std.binary 的家底std.binary只提供三个接口各司其职接口作用关键方法BigEndianOrderT大端序转换readBigEndian/writeBigEndianLittleEndianOrderT小端序转换readLittleEndian/writeLittleEndianSwapEndianOrderT翻转字节顺序swapBytes注意它的设计思路不是给你一个ByteBuffer类那在std.io里第 5 课用过而是用接口扩展的方式直接给所有数值类型装上读/写字节序列的能力。Int32、Float64、Bool……统统自带writeBigEndian。这种用接口扩展内建类型的写法本身就值得学。二、大端序BigEndianOrder2.1 接口形状public interface BigEndianOrderT { func writeBigEndian(buffer: ArrayByte): Int64 static func readBigEndian(buffer: ArrayByte): T }writeBigEndian把当前值按大端序写进buffer返回实际写入的字节数Int64 写 8 字节Int16 写 2 字节。readBigEndian从buffer开头按大端序读出一个 T 值。已实现的类型Bool、Int8/16/32/64、UInt8/16/32/64、Float16/32/64。import std.binary.*后直接可用。2.2 写值 → 字节数组import std.binary.* main(): Int64 { let buf ArrayByte(8, repeat: 0) // 分配 8 字节缓冲区桌面 SDK 用 repeat: let n 0x1234i16.writeBigEndian(buf) println(写入字节数: ${n}) println(字节内容: ${buf[..n]}) 0 }写入字节数: 2 字节内容: [18, 52]0x1234 十进制 4660拆成大端序两个字节就是0x12(18)、0x34(52)——高位在前和你脑子里读十六进制的顺序一致。坑官方在线文档鸿蒙 V5 版里数组分配写的是ArrayByte(8, item: 0)桌面版 SDK 1.2.0 不支持item:命名参数实测报错unknown named argument prefix item:。桌面版统一用repeat:ArrayByte(8, repeat: 0)。本文所有代码均用桌面版写法。2.3 读字节数组 → 值let buf2: ArrayByte [0x12, 0x34] let v Int16.readBigEndian(buf2) println(读回的值: ${v})读回的值: 4660readBigEndian是静态方法挂在类型上调用Int16.readBigEndian(...)不是在实例上。写出来的[18, 52]原样读回4660往返一致。2.4 多字节类型Int64 与 Float64Int64 占 8 字节Float64 也占 8 字节看大端序怎么排let val 0x0102030405060708i64 let beBuf ArrayByte(8, repeat: 0) val.writeBigEndian(beBuf) println(Int64 BE: ${beBuf}) let f 12.5f64 let fBuf ArrayByte(8, repeat: 0) f.writeBigEndian(fBuf) println(Float64 BE: ${fBuf}) println(Float64 读回: ${Float64.readBigEndian(fBuf)})Int64 BE: [1, 2, 3, 4, 5, 6, 7, 8] Float64 BE: [64, 41, 0, 0, 0, 0, 0, 0] Float64 读回: 12.500000Int64 的字节01 02 03 04 05 06 07 08一眼就能看出高位在前。Float64 的64 41 00 00 00 00 00 00是 IEEE 754 双精度浮点的大端编码——人脑算不出来没关系只要读写用同一种字节序就能无损还原这就是接口的价值。2.5 缓冲区不够会怎样Int32需要 4 字节只给 2 字节的缓冲区读写都会抛异常实测原文let small: ArrayByte [0x12, 0x34] Int32.readBigEndian(small)Exception in thread main java.lang.IllegalArgumentException: Buffer size(2) is too small.写也是一样的报错Buffer size(2) is too small.。所以分配缓冲区时大小要 ≥ 目标类型的字节数Int32→4Int64→8Float32→4……。嫌算麻烦就直接开 8 字节用buf[..n]切出实际写入的部分。三、小端序LittleEndianOrder3.1 接口形状和大端序一模一样只是方法名换了public interface LittleEndianOrderT { func writeLittleEndian(buffer: ArrayByte): Int64 static func readLittleEndian(buffer: ArrayByte): T }实现的类型也完全相同Bool、Int8/16/32/64、UInt8/16/32/64、Float16/32/64。3.2 小端序写入低位在前let buf ArrayByte(8, repeat: 0) let n 0x12345678i32.writeLittleEndian(buf) println(Int32 LE 写入字节: ${buf[..n]})Int32 LE 写入字节: [120, 86, 52, 18]0x12345678的 4 个字节按小端序排列0x78(120)、0x56(86)、0x34(52)、0x12(18)——低位在前正好和大端序反过来。读回来验证let buf2: ArrayByte [0x78, 0x56, 0x34, 0x12] let v Int32.readLittleEndian(buf2) println(读回: ${v})读回: 305419896305419896就是0x12345678。读写配对顺序一致值就对。3.3 大端 vs 小端同一张图把同一个Int64值分别用大端、小端写出来对比一目了然let val 0x0102030405060708i64 let beBuf ArrayByte(8, repeat: 0) val.writeBigEndian(beBuf) let leBuf ArrayByte(8, repeat: 0) val.writeLittleEndian(leBuf) println(大端: ${beBuf}) println(小端: ${leBuf})大端: [1, 2, 3, 4, 5, 6, 7, 8] 小端: [8, 7, 6, 5, 4, 3, 2, 1]同样 8 个字节顺序完全相反。这就是字节序这三个字的全部含义。四、翻转字节序SwapEndianOrder4.1 接口与用途public interface SwapEndianOrderT { func swapBytes(): T }swapBytes()返回一个字节顺序完全翻转的新值。实现的类型Int8/16/32/64、UInt8/16/32/64浮点和 Bool 没有——翻转浮点字节没有意义。它的典型用途从文件里读出来后才发现字节序不对就地翻转一下。比如某格式文档写的是大端序你误用了小端序的readLittleEndian得到一个乱码数值swapBytes()一下就救回来了——等价于换一种读法重算。4.2 实测let val 0x0102030405060708i64 let swapped val.swapBytes() let swapBuf ArrayByte(8, repeat: 0) swapped.writeBigEndian(swapBuf) println(翻转后大端字节: ${swapBuf})翻转后大端字节: [8, 7, 6, 5, 4, 3, 2, 1]原值大端是[1,2,3,4,5,6,7,8]swapBytes()后再按大端写出就是[8,7,6,5,4,3,2,1]——正好等于原值的小端写法。换句话说val.swapBytes()的大端表示 val的小端表示。UInt 类型也一样let u 0x1234u16 println(0x1234 翻转后: ${u.swapBytes()})0x1234 翻转后: 133300x1234翻转字节得到0x3412 13330。五、实战解析 BMP 图片文件头光看接口太干来个真实格式。BMP 是 Windows 原生图片格式文件头全部用小端序因为 Windows 跑在 x86 上。我们用LittleEndianOrder把一个 BMP 文件头拆开看看。5.1 BMP 文件头结构BMP 文件头14 字节 DIB 头40 字节BITMAPINFOHEADER的关键字段偏移长度字段类型说明02signature字符“BM” 魔数24fileSizeUInt32 LE文件总字节数64reserved—保留通常 0104dataOffsetUInt32 LE像素数据起始偏移144dibHeaderSizeUInt32 LEDIB 头大小40184widthInt32 LE图像宽度像素224heightInt32 LE图像高度像素262planesUInt16 LE颜色平面数1282bitsPerPixelUInt16 LE每像素位数24真彩色5.2 构造 解析为了不依赖外部图片文件我们在代码里手动构造一个 54 字节的 BMP 头再用LittleEndianOrder逐字段读出来——写和读都练到import std.binary.* func hexOf(bytes: ArrayByte): String { let hex [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f] var s for (b in bytes) { s s hex[Int64(b 4)] hex[Int64(b 0xF)] } return s } main(): Int64 { // 构造 BMP 文件头54 字节 let bmp ArrayByte(54, repeat: 0) bmp[0] 0x42 // B bmp[1] 0x4D // M 54u32.writeLittleEndian(bmp[2..6]) // fileSize 54u32.writeLittleEndian(bmp[10..14]) // dataOffset 40u32.writeLittleEndian(bmp[14..18]) // dibHeaderSize 800i32.writeLittleEndian(bmp[18..22]) // width 600i32.writeLittleEndian(bmp[22..26]) // height 1u16.writeLittleEndian(bmp[26..28]) // planes 24u16.writeLittleEndian(bmp[28..30]) // bitsPerPixel println(原始头 (前 30 字节): ${hexOf(bmp[..30])}) // 解析 let sig String.fromUtf8(bmp[0..2]) let fileSize UInt32.readLittleEndian(bmp[2..6]) let dataOffset UInt32.readLittleEndian(bmp[10..14]) let dibSize UInt32.readLittleEndian(bmp[14..18]) let width Int32.readLittleEndian(bmp[18..22]) let height Int32.readLittleEndian(bmp[22..26]) let bpp UInt16.readLittleEndian(bmp[28..30]) println(签名: ${sig}) println(文件大小: ${fileSize} 字节) println(像素数据偏移: ${dataOffset}) println(DIB 头大小: ${dibSize}) println(宽度: ${width} px) println(高度: ${height} px) println(色深: ${bpp} bpp) 0 }原始头 (前 30 字节): 424d36000000000000003600000028000000200300005802000001001800 签名: BM 文件大小: 54 字节 像素数据偏移: 54 DIB 头大小: 40 宽度: 800 px 高度: 600 px 色深: 24 bpp几个要点切片直接当缓冲区bmp[2..6]是从第 2 字节开始、长度 4 的切片writeLittleEndian/readLittleEndian直接吃切片不用额外拷数组。这是处理固定偏移格式最顺手的写法。魔数用字符串读前两字节0x42 0x4D是 ASCII 的BM用String.fromUtf8还原成人话。宽度 800 的小端字节是20 03 00 000x00000320 800小端序低位在前 →20 03 00 00。看输出第三行...2003000058020000...20030000是宽 80058020000是高 6000x00000258 600。真实读.bmp文件时把构造那段换成File.readFrom(path)读到bmp数组即可解析代码完全不变。六、常用 API 速查API说明val.writeBigEndian(buf)把val按大端序写入buf返回写入字节数Type.readBigEndian(buf)从buf按大端序读出一个Type值静态方法val.writeLittleEndian(buf)按小端序写入返回写入字节数Type.readLittleEndian(buf)按小端序读出静态方法val.swapBytes()返回字节顺序翻转后的新值仅整数类型ArrayByte(N, repeat: 0)分配 N 字节零填充缓冲区桌面 SDK 用repeat:一张表分清三个接口接口方向实现的类型BigEndianOrderT高位在前Bool, Int8/16/32/64, UInt8/16/32/64, Float16/32/64LittleEndianOrderT低位在前同上SwapEndianOrderT翻转Int8/16/32/64, UInt8/16/32/64无浮点、无 Bool七、常见问题 FAQQ1writeBigEndian返回的Int64是干嘛的为什么不直接返回字节数组返回的是实际写入的字节数。接口设计成调用方管缓冲区——你自己开数组、自己决定大小方法只负责往里写并告诉你写了几个字节。好处是可以复用同一个大缓冲区连续写多个字段像 BMP 头那样不用反复分配。要取实际写入的部分用buf[..n]切片。Q2readBigEndian为什么是静态方法Int32.readBigEndian(...)而不是实例方法因为读之前你手里还没有Int32实例——读的目的就是造出一个实例。所以它必须挂在类型上作为静态方法。写则相反你已经有一个值了在值上调writeBigEndian。一读一写一个静态一个实例对称得很。Q3为什么swapBytes()没有 Float64/Float32 版本翻转浮点数的字节在数学上没有意义——IEEE 754 浮点的符号位、指数、尾数是按位划分的翻转字节会把它们全打乱得到的是一个几乎随机的浮点数没有应用场景。所以SwapEndianOrder只给整数类型实现。Q4缓冲区太小的报错文案是什么怎么避免实测报错Buffer size(N) is too small.N 是你给的实际大小。避免方法分配缓冲区时大小 ≥ 类型字节数Int8/UInt8→1Int16/UInt16→2Int32/UInt32/Float32→4Int64/UInt64/Float64→8。图省事就统一开 8 字节。Q5官方文档里ArrayByte(8, item: 0)为什么我这里编译不过官方在线文档是鸿蒙OpenHarmony版 SDK 的写法桌面版 SDK 1.2.0 的数组构造命名参数是repeat:不是item:。写ArrayByte(8, item: 0)实测报unknown named argument prefix item:。桌面版统一用ArrayByte(8, repeat: 0)。Q6std.binary有没有 Base64、十六进制编码之类的功能1.2.0 的std.binary只做字节序转换大端/小端/翻转没有 Base64、Hex 编码。第 5 课的toHex是我们手写的十六进制编码目前需要自己实现或找第三方库。Base64 同理。八、课后练习规矩不变前 4 题必做第 5 题选做。每题只考本课 12 个知识点题面给全函数签名和 API 提示先看懂题再动笔。第 1 题必做大端读整数用Int32.readBigEndian从字节数组[0x00, 0x00, 0x01, 0xF4]读出一个 Int32 值并打印。提示直接Int32.readBigEndian(buf)0x000001F4 500。import std.binary.* main(): Int64 { let buf: ArrayByte [0x00, 0x00, 0x01, 0xF4] // 补全读出并打印 0 }期望输出500第 2 题必做小端写 Int16把0x0100i16按小端序写入 2 字节缓冲区打印写入的字节内容。提示writeLittleEndian返回写入字节数0x0100小端序是00 01低位0x00在前。import std.binary.* main(): Int64 { let buf ArrayByte(2, repeat: 0) // 补全写入并打印 buf 内容 0 }期望输出[0, 1]第 3 题必做翻转字节序对0x12345678u32调用swapBytes()打印结果的十进制值。提示0x12345678翻转后是0x78563412直接println(val.swapBytes())。import std.binary.* main(): Int64 { let v 0x12345678u32 // 补全打印翻转后的值 0 }期望输出2018915346第 4 题必做大端读 Float32字节数组[0x41, 0x48, 0x00, 0x00]是12.5f32的大端编码。用Float32.readBigEndian读出来并打印。提示Float32 也是BigEndianOrder的实现类型调用方式和整数一样。import std.binary.* main(): Int64 { let buf: ArrayByte [0x41, 0x48, 0x00, 0x00] // 补全读出 Float32 并打印 0 }期望输出12.500000第 5 题选做小端读 BMP 宽度给定一个 22 字节的 BMP 头片段用小端序读出其中的宽度字段偏移 18长度 4Int32。提示宽度在偏移 18 处用切片buf[18..22]读Int32.readLittleEndian。import std.binary.* main(): Int64 { // 一个 BMP 头片段宽 10240x00000400高 7680x00000300 let buf: ArrayByte [ 0x42, 0x4D, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x36, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00 ] // 补全读出宽度并打印 0 }期望输出1024下节预告字节序解决了数值怎么变成字节的问题但真实世界的数据格式远不止整数——数据库里一行行的结构化记录怎么存、怎么查、怎么改下一课进入数据库篇从std.database.sql开始把数据持久化从文件升级到真正的数据库。系列说明本系列基于 Windows 平台 CIDE 仓颉 SDK1.2.0编写所有代码均已实际编译运行通过。如遇 SDK 版本差异导致的细节出入以你本地版本为准欢迎评论区交流。 遇到问题扫码联系作者跟着课程练习时如果在 SDK 安装、环境变量配置、编译报错或调试上卡住欢迎扫码加作者企业微信直接咨询请备注仓颉课程离线环境下图片可能加载不出来也可以在 CIDE 菜单Help ▸ 联系作者 / Contact中查看同一张二维码应用内置兜底图无需联网。 工具下载本系列全程使用的仓颉 IDE ——CIDE免费开源、社区版GitCode 仓库 / 安装包下载https://gitcode.com/wp_upala/cide打开页面后进入发行版Releases两种包任选其一安装版下载CIDE-版本-x64-Setup.exe双击安装适合日常长期使用免安装版Portable下载CIDE-版本-x64-Portable.zip解压到任意目录即用不写注册表、不留安装痕迹拷到 U 盘也能在别的电脑直接运行包内附《使用说明.txt》。适合先试用、或在受限电脑上学习本系列课程。仓颉 SDK 请前往仓颉编程语言官网下载https://cangjie-lang.cn