第4章:数据编码与序列化
好,咱们今天聊聊数据编码和序列化。说实话,这是自定义协议里最基础也最容易踩坑的地方。我见过太多项目,协议设计得天花乱坠,结果编码方式没选对,后面调试起来欲哭无泪。
4.1 基本数据类型编码
先说说最基础的东西——int、float、string 怎么编码。你可能会想,这有什么好讲的?直接存二进制不就行了?嗯,还真没那么简单。
4.1.1 整数编码
整数编码,我个人习惯用大端序(Big-Endian)。为什么?因为 Modbus、DNP3 这些老牌协议都用大端,兼容性好。我在项目中遇到过一个小弟,非要用小端,结果跟第三方系统对接时,数据全反了,排查了一整天。
举个例子,一个 32 位无符号整数 0x12345678:
// 大端序(网络字节序)
内存地址: [0x12] [0x34] [0x56] [0x78]
// 小端序(主机字节序)
内存地址: [0x78] [0x56] [0x34] [0x12]
避坑指南:我曾经在 ARM 和 x86 混合的系统中做协议,没注意字节序,结果数据解析出来全是错的。后来我强制规定:所有自定义协议统一用大端序,谁用别的谁负责改代码。
4.1.2 浮点数编码
浮点数,说白了就是 IEEE 754 标准。单精度 4 字节,双精度 8 字节。这里有个坑——有些老设备用自定义的浮点格式,比如西门子的 S5 系列。我建议你直接上 IEEE 754,别搞特殊。
// 单精度浮点数 3.14 的 IEEE 754 编码
// 二进制: 0x4048F5C3
// 大端序存储: [0x40] [0x48] [0xF5] [0xC3]
小技巧:如果你不确定设备支持哪种浮点格式,可以在协议里加一个版本号字段,标明浮点编码标准。这样以后升级也方便。
4.1.3 字符串编码
字符串编码,我强烈建议用 UTF-8。为什么?因为 UTF-8 兼容 ASCII,而且没有字节序问题。你想想看,如果用了 UTF-16,还得操心是大端还是小端,多麻烦。
字符串长度怎么表示?我见过两种方式:
- 固定长度:比如 32 字节,不够的补 0。简单粗暴,但浪费空间。
- 变长+长度前缀:先写 2 字节长度,再写字符串内容。灵活,但解析时多一步。
我个人习惯用变长方式。记得有一次,一个设备上报的字符串固定 256 字节,结果 90% 都是空字符,浪费带宽不说,还影响实时性。
4.2 TLV 格式设计
TLV,就是 Type-Length-Value。说白了,就是给每个数据字段加个标签和长度。这玩意儿在自定义协议里特别好用,尤其是当你的数据字段不确定时。
4.2.1 TLV 的基本结构
// TLV 格式示例
+--------+--------+--------+
| Type | Length | Value |
| (1字节)| (2字节)| (N字节)|
+--------+--------+--------+
Type 字段用来标识数据类型,Length 字段表示 Value 的长度。我建议 Type 用 1 字节,Length 用 2 字节,这样最多支持 65535 字节的 Value,够用了。
4.2.2 TLV 的设计原则
设计 TLV 时,有几个原则要记住:
- Type 要唯一:每个 Type 值只能对应一种数据类型。我曾经见过一个协议,Type 0x01 既表示温度又表示湿度,结果解析时全靠猜。
- Length 要准确:Length 只包含 Value 的长度,不包括 Type 和 Length 本身。这个坑我踩过,当时 Length 包含了整个 TLV 的长度,导致解析错位。
- 支持嵌套:Value 里可以再包含 TLV。比如一个设备信息 TLV,里面可以嵌套设备 ID、设备类型、设备状态等子 TLV。
实战经验:我在做一个电力监控项目时,用 TLV 格式设计了一个遥测数据包。Type 0x01 表示三相电压,Type 0x02 表示三相电流。后来客户要求增加谐波数据,我直接加了个 Type 0x03,完全不影响原有解析逻辑。
4.3 JSON/XML vs 二进制协议
这个选择题,说白了就是「可读性」和「效率」的权衡。我两种都用过,说说我的感受。
4.3.1 JSON/XML 的优缺点
优点:
- 可读性好,调试方便。你直接用文本编辑器就能看。
- 跨平台,各种语言都有现成的解析库。
- 扩展性强,加个字段不影响老版本。
缺点:
- 数据量大。一个简单的温度值,JSON 可能要几十字节,二进制只要 4 字节。
- 解析慢。尤其是 XML,DOM 解析那叫一个慢。
- 不适合实时性要求高的场景。
4.3.2 二进制协议的优缺点
优点:
- 数据量小,带宽利用率高。
- 解析快,适合实时控制。
- 安全性好,不容易被篡改。
缺点:
- 可读性差,调试时需要专门的工具。
- 扩展性差,加字段可能影响协议版本。
- 开发成本高,需要自己写编解码逻辑。
4.3.3 怎么选?
我个人的经验是:
- 配置类数据:用 JSON。比如设备参数、系统配置,这些数据不频繁传输,可读性更重要。
- 实时数据:用二进制。比如遥测、遥信,这些数据要求毫秒级响应,带宽也有限。
- 日志类数据:用 JSON 或 XML。方便后续分析和归档。
折中方案:我做过一个项目,用 JSON 做配置下发,用二进制做实时数据采集。这样既保证了配置的可读性,又保证了实时数据的效率。你想想看,是不是挺合理的?
4.4 实战:设计一个简单的二进制协议
好了,理论讲完了,咱们来点实际的。假设我们要设计一个温度传感器协议,上报温度数据。
4.4.1 协议定义
// 温度数据包格式
+--------+--------+--------+--------+
| Type | Length | Temp | CRC |
| (1字节)| (2字节)| (4字节)| (2字节)|
+--------+--------+--------+--------+
// Type: 0x01 表示温度数据
// Length: 0x0004 表示 Temp 字段长度为 4 字节
// Temp: IEEE 754 单精度浮点数,单位摄氏度
// CRC: CRC16-CCITT,校验整个包
4.4.2 编码示例
// 温度值: 25.5°C
// 编码过程:
// 1. Type = 0x01
// 2. Length = 0x0004
// 3. Temp = 0x41CC0000 (25.5 的 IEEE 754 编码)
// 4. CRC = 计算整个包的 CRC16
// 最终数据包:
// [0x01] [0x00] [0x04] [0x41] [0xCC] [0x00] [0x00] [0x??] [0x??]
注意:CRC 校验一定要加。我曾经在一个项目中偷懒没加 CRC,结果现场电磁干扰导致数据错误,设备误动作,差点出事故。从那以后,我再也不敢省这一步了。
4.5 总结
数据编码和序列化,看似简单,但细节决定成败。我建议你:
- 整数用大端序,统一标准。
- 浮点数用 IEEE 754,别搞特殊。
- 字符串用 UTF-8,省心省力。
- TLV 格式灵活,适合不确定字段。
- JSON/XML 和二进制各有优劣,按场景选。
好了,这一章就到这里。下一章咱们聊聊协议帧结构设计,包括帧头、帧尾、校验等。到时候我会分享一些我在实际项目中用过的帧结构模板,保证实用。