序列化框架对比:JSON vs XML vs Protobuf vs MsgPack vs 自定义格式
做嵌入式开发这些年,我接触过的序列化方案少说也有七八种。每次新项目启动,总有人问我:「到底该用哪种?」说实话,没有银弹。每种方案都有自己的脾气,选对了事半功倍,选错了后期改起来想哭。
今天我就把这五种主流方案拉出来遛遛。JSON、XML、Protobuf、MsgPack,再加上我们自己手写的自定义格式。咱们从实际工程角度,看看它们各自擅长什么、短板在哪。
先看一张全景图
下面这张图是我自己整理的,把五种方案的定位和核心差异画了出来。你一看就明白它们各自在哪个维度上发力。
从图上能看出来,XML和JSON偏左上方,可读性好但性能一般。Protobuf和自定义格式在右下角,性能拉满但可读性差。MsgPack卡在中间,算是个折中方案。
JSON:最通用的选择
JSON现在基本是互联网的事实标准了。我最早用JSON是在做Web后端的时候,前后端通信全靠它。后来做嵌入式,发现很多云平台也支持JSON上报数据。
优点很明显:
- 人类可读——调试的时候直接看字符串,不用额外工具
- 语言支持广泛——几乎每种语言都有现成的解析库
- 自描述——字段名都在数据里,不需要额外的schema文件
但缺点也扎心:
- 体积大——字段名重复出现,比如{"temperature":25.5},光字段名就占了十几个字节
- 解析慢——字符串解析在MCU上很吃资源
- 无类型约束——数字和字符串混在一起,容易踩坑
XML:老当益壮,但有点重
XML我接触得早,刚入行那会儿做工业控制,设备配置全是XML格式。说实话,XML的严谨性确实好,有DTD或XSD约束,结构清晰。
但它的缺点也让人头疼:
- 标签冗余严重——一个字段被开始标签和结束标签包两遍
- 解析开销大——DOM解析吃内存,SAX解析写起来又麻烦
- 在嵌入式领域水土不服——我见过有人在Cortex-M0上跑XML解析器,结果RAM直接爆了
XML现在主要用在配置文件、文档存储这些场景。如果你在做嵌入式,我个人不太推荐XML做通信协议——除非你资源非常充裕,比如跑Linux的高端芯片。
Protobuf:Google出品,性能怪兽
Protobuf是我在做一个车联网项目时开始用的。当时需要把车辆状态数据(几百个字段)以1Hz频率上报,JSON搞不定带宽和延迟,就换成了Protobuf。
它的核心优势:
- 体积极小——用变长编码和字段编号代替字段名,数据量能压缩到JSON的1/3甚至更少
- 解析极快——二进制格式,直接内存拷贝,不需要字符串解析
- 强类型约束——.proto文件定义了每个字段的类型,跨语言通信不会出错
- 向前向后兼容——加字段不影响旧版本解析
但要注意几个坑:
- 需要编译步骤——写.proto文件,然后用protoc生成代码,流程多了几步
- 调试不方便——二进制数据没法直接看,得用protoc --decode_raw转成文本
- 库体积不小——完整Protobuf库在MCU上可能偏大,但有个Nanopb版本专门为嵌入式优化
避坑指南:我曾经在一个项目里直接用完整版Protobuf库,结果Flash占用多了30KB。后来换成Nanopb,只用了不到8KB。所以嵌入式上用Protobuf,记得选Nanopb或者自己裁剪。
MsgPack:JSON的二进制表亲
MsgPack的设计理念很有意思——它保持了JSON的数据模型(数组、map、字符串、数字),但用二进制编码代替文本。说白了,就是JSON的二进制版本。
它的特点:
- 兼容JSON数据结构——JSON能表达的数据,MsgPack都能表达
- 体积比JSON小——省掉了引号、冒号、逗号这些冗余字符
- 解析比JSON快——二进制格式,不需要字符串解析
- 有现成的C库——mpack、msgpack-c都挺好用
但MsgPack也有局限:
- 仍然有字段名开销——不像Protobuf用编号代替字段名,MsgPack还是存字段名字符串
- 生态不如JSON——虽然不少语言支持,但工具链和社区资源少一些
- 类型系统不够强——没有Protobuf那种严格的类型约束
我个人的使用场景:当项目需要比JSON更紧凑,但又不想引入Protobuf那套编译流程时,MsgPack是个不错的中间选择。
自定义格式:极致性能的终极方案
说到自定义格式,可能有人觉得「这不就是自己造轮子吗?」嗯,确实有造轮子的风险。但在某些场景下,自定义格式是唯一的选择。
我举个例子:之前做一个传感器采集节点,MCU只有2KB RAM,Flash 32KB。JSON库放不下,Protobuf库也放不下。最后我手写了一个二进制协议:
// 自定义二进制格式示例
// 协议定义:
// 字节0: 帧头 0xAA
// 字节1: 数据长度
// 字节2: 消息类型
// 字节3-N: 负载数据
// 字节N+1: CRC校验
typedef struct {
uint8_t header; // 0xAA
uint8_t length; // 负载长度
uint8_t msg_type; // 消息类型
uint8_t payload[64]; // 负载
uint8_t crc; // 校验和
} __attribute__((packed)) CustomFrame;
// 序列化
void serialize_temperature(CustomFrame *frame, float temp) {
frame->header = 0xAA;
frame->length = sizeof(float);
frame->msg_type = 0x01; // 温度消息
memcpy(frame->payload, &temp, sizeof(float));
frame->crc = calc_crc(frame, sizeof(CustomFrame) - 1);
}
// 反序列化
float deserialize_temperature(CustomFrame *frame) {
if (frame->header != 0xAA) return -1;
if (frame->msg_type != 0x01) return -1;
if (frame->crc != calc_crc(frame, sizeof(CustomFrame) - 1)) return -1;
float temp;
memcpy(&temp, frame->payload, sizeof(float));
return temp;
}
自定义格式的优势:
- 极致紧凑——没有冗余字节,每个bit都用在刀刃上
- 解析最快——直接内存操作,没有解析开销
- 完全可控——想怎么设计就怎么设计,不受第三方库限制
但风险也大:
- 开发成本高——要自己处理字节序、对齐、校验、版本兼容
- 调试困难——没有现成的工具,得自己写解析脚本
- 跨语言麻烦——如果C端和Python端通信,两边都得实现一遍协议
横向对比:一张表说清楚
| 维度 | JSON | XML | Protobuf | MsgPack | 自定义 |
|---|---|---|---|---|---|
| 数据体积 | 大 | 很大 | 极小 | 较小 | 极小 |
| 解析速度 | 慢 | 很慢 | 极快 | 快 | 极快 |
| 人类可读 | 好 | 好 | 差 | 差 | 差 |
| 跨语言支持 | 极好 | 好 | 好 | 好 | 差 |
| 嵌入式适配 | 一般 | 差 | 好(Nanopb) | 好 | 极好 |
| 开发效率 | 高 | 中 | 中 | 高 | 低 |
| 版本兼容 | 手动处理 | 手动处理 | 内置支持 | 手动处理 | 手动处理 |
我总结的选型建议
说了这么多,到底怎么选?我根据自己的经验,给几个参考:
- 和云端通信、调试阶段——用JSON。方便、通用、生态好。
- 工业控制、配置文件——用XML。虽然重,但严谨性无可替代。
- 高性能通信、跨语言、字段多——用Protobuf。性能好,兼容性强。
- 比JSON紧凑、又不想引入编译流程——用MsgPack。折中方案。
- 资源极度受限、协议固定——用自定义格式。极致性能,但要做好文档。
其实选型没有标准答案。我见过有人用JSON在8位MCU上跑,也见过有人用Protobuf做百万级并发服务器。关键是你得清楚自己的约束条件——RAM多少?Flash多少?带宽多少?开发周期多长?想清楚这些,答案自然就有了。
嗯,序列化框架对比就聊到这儿。每种方案都有它的脾气,选对了就是好工具。
公众号:蓝海资料掘金营,微信deep3321