3、序列化与反序列化:数据在线的“打包”与“拆包”

大家好,我是你们的老朋友。今天我们来聊聊嵌入式通信里一个绕不开的话题——序列化与反序列化。

说白了,序列化就是把内存里的结构体、对象,变成一串能塞进网线或者无线信道里的字节流。反序列化就是反过来,把收到的字节流再还原成结构体。你想想看,两个设备要聊天,总得约定好“话怎么说”吧?序列化就是那个约定。

我在项目中遇到过不少因为序列化没选对,导致整个系统推倒重来的案例。嗯,今天咱们就把这块彻底讲透。

3.1 什么是序列化?为什么需要它?

先问个问题:你的MCU里有一个结构体,里面存了温度、湿度、风速三个数据。你想把它发给云端服务器。直接memcpy发过去行不行?

答案是不行。原因有三:

  • 字节序不同:ARM Cortex-M一般是小端,x86也是小端,但有些MIPS或PowerPC是大端。直接发内存,对方读出来全是反的。
  • 内存对齐:结构体里如果有uint32_t,编译器可能会在中间塞填充字节。不同编译器、不同优化选项,填充方式不一样。
  • 数据自描述:你发了一串字节,对方怎么知道哪个字节是温度,哪个是湿度?序列化格式里通常会带上字段名或编号。

所以,序列化的核心目的就三个:跨平台、跨语言、自描述

核心观点:序列化不是“把数据变个样子”,而是“让数据在异构系统之间能互相理解”。

3.2 常见序列化格式对比

市面上常见的序列化格式,我按使用场景分成了三类:文本型、二进制型、高性能二进制型。咱们一个一个看。

3.2.1 JSON

JSON 应该是大家最熟悉的。它基于文本,可读性好,调试方便。我在调试初期阶段,特别喜欢用JSON,因为用串口打印出来一眼就能看出问题。

{
  "temperature": 25.6,
  "humidity": 68.2,
  "wind_speed": 3.5
}

但JSON在嵌入式里有个致命伤:解析开销大。你需要一个完整的JSON解析器,内存占用动辄几十KB。而且它没有类型约束,你传个字符串"25.6"和传个数字25.6,解析结果完全不同。我曾经在一个只有64KB RAM的STM32上跑cJSON,结果堆栈直接爆了。

避坑指南:在资源受限的MCU上,尽量避免使用JSON。如果非要用,建议用静态内存分配的解析器,比如JSMN,不要用cJSON这种动态分配内存的库。

3.2.2 XML

XML 在工业控制和配置文件中用得比较多。它的优点是自描述性极强,带标签和属性。但缺点也很明显:冗余数据太多

<sensor>
  <temperature unit="celsius">25.6</temperature>
  <humidity unit="percent">68.2</humidity>
</sensor>

你想想看,传输同样的数据,XML的包体可能是JSON的2-3倍,是Protobuf的5-10倍。在低带宽的LoRa或NB-IoT场景下,这简直是灾难。我个人习惯,只在配置文件和SOAP协议里用XML,实时通信基本不考虑。

3.2.3 Protobuf

Protobuf 是Google推出的二进制序列化格式。它通过一个.proto文件定义数据结构,然后生成各语言的代码。它的核心优势是:体积小、解析快、强类型

syntax = "proto3";
message SensorData {
  float temperature = 1;
  float humidity = 2;
  float wind_speed = 3;
}

编码后的二进制数据大概只有JSON的1/5大小。而且它使用Varint编码,对于小整数能进一步压缩。我在一个智能网关项目里,用Protobuf替换了JSON,网络带宽占用直接降了70%。

但Protobuf也有缺点:需要预定义schema。如果字段经常变,你得频繁更新.proto文件并重新生成代码。另外,它的解码库在MCU上大概要占用8-12KB的Flash,比JSON小,但也不是零开销。

3.2.4 CBOR

CBOR 是另一种二进制格式,它的设计目标是“JSON的二进制等价物”。它不需要预定义schema,可以动态编码任意结构。而且它的编码规则非常简洁,适合在极低资源的设备上实现。

// CBOR编码示例(十六进制)
A3          # map(3)
  69        # text(9)
    74656D7065726174757265  # "temperature"
  FA 41CCCCCD  # float(25.6)
  68        # text(8)
    68756D6964697479  # "humidity"
  FA 42886666  # float(68.2)
  ...

CBOR 在物联网领域用得越来越多,比如CoAP协议就推荐使用CBOR。我个人觉得,如果你不想引入Protobuf那种“代码生成”的流程,又想要二进制的高效,CBOR是个很好的折中方案。

3.3 性能对比:一张表看懂

格式 编码体积 解析速度 内存占用 自描述 强类型 适用场景
JSON 调试、Web API
XML 极大 极慢 极高 配置文件、工业协议
Protobuf 高性能通信、微服务
CBOR 物联网、低功耗设备

我的建议:如果你在做LoRa、ZigBee这类低带宽项目,优先考虑CBOR或Protobuf。如果是Wi-Fi或以太网项目,JSON也够用,但要注意内存。

3.4 自定义序列化实现

有时候,现成的格式都不太合适。比如你只需要传两个uint16_t和一个int8_t,用Protobuf还得引入整个库,太笨重了。这时候,自己手写一个序列化函数反而更高效。

我曾经在一个Bootloader项目里,需要传输固件包的元信息(版本号、长度、校验和)。数据量极小,但要求解析速度极快。我直接定义了一个固定长度的结构体,手动处理字节序。

// 自定义序列化示例
typedef struct {
    uint16_t version;
    uint32_t length;
    uint8_t  checksum;
} __attribute__((packed)) FirmwareMeta;

// 序列化:将结构体转为网络字节序(大端)
void serialize_firmware_meta(const FirmwareMeta *meta, uint8_t *buf) {
    buf[0] = (meta->version >> 8) & 0xFF;
    buf[1] = meta->version & 0xFF;
    buf[2] = (meta->length >> 24) & 0xFF;
    buf[3] = (meta->length >> 16) & 0xFF;
    buf[4] = (meta->length >> 8) & 0xFF;
    buf[5] = meta->length & 0xFF;
    buf[6] = meta->checksum;
}

// 反序列化:从网络字节序恢复结构体
void deserialize_firmware_meta(const uint8_t *buf, FirmwareMeta *meta) {
    meta->version = ((uint16_t)buf[0] << 8) | buf[1];
    meta->length  = ((uint32_t)buf[2] << 24) | 
                    ((uint32_t)buf[3] << 16) | 
                    ((uint32_t)buf[4] << 8)  | buf[5];
    meta->checksum = buf[6];
}

这里有几个关键点:

  • 固定字节序:统一使用大端(网络字节序),避免大小端问题。
  • 紧凑打包:使用__attribute__((packed))禁止编译器填充,保证结构体内存布局和字节流完全一致。
  • 无动态内存:所有操作都在栈上完成,不会产生碎片。

注意:自定义序列化只适合数据结构非常稳定的场景。如果字段经常增删,或者需要兼容多个版本,还是用Protobuf这类带版本管理的格式更稳妥。

3.5 知识体系图:序列化选型决策

下面这张图,是我自己总结的序列化格式选型流程。你可以把它当作一个决策树来用。

序列化格式选型决策树 开始选型 资源是否受限?(RAM < 32KB, Flash < 128KB) 数据结构是否稳定? 自定义序列化 CBOR 是否需要自描述? Protobuf JSON / XML 注:JSON适合调试和Web场景,XML适合工业配置场景 推荐方案 决策节点 条件判断

3.6 总结与个人经验

序列化这件事,说白了就是“沟通的语法”。选对了,系统跑得又快又稳;选错了,后期改起来想哭。

我个人的经验是:

  • 原型阶段:用JSON,调试方便,改起来快。
  • 产品阶段:如果资源够,上Protobuf;资源紧,上CBOR或自定义。
  • 永远不要:直接memcpy结构体到网络。你永远不知道对方的编译器会怎么对齐。

我曾经在一个NB-IoT项目里,因为用了JSON,导致每次上报数据包太大,电池续航从3个月掉到1个月。后来换成CBOR,包体缩小了60%,续航直接回到3个月。嗯,这就是选型的力量。

一句话总结:序列化不是技术问题,是工程权衡问题。理解你的资源边界,选择最合适的格式,而不是最流行的格式。


公众号:蓝海资料掘金营,微信deep3321