4、数据序列化:JSON与MessagePack对比、Protocol Buffers入门、自定义二进制协议设计

数据序列化,说白了就是给数据「打包」。

机器人控制器要跟远程监控端通信,数据不能裸奔。你得把它变成一串字节流,发过去,那边再拆开。这个「打包-拆包」的过程,就是序列化和反序列化。

我做了这么多年机器人通信,见过太多因为序列化选型翻车的案例。有的嫌JSON太慢,有的嫌Protobuf太麻烦,最后自己手撸一个协议,结果踩坑踩到怀疑人生。今天咱们就把这几个主流方案掰开揉碎,讲清楚。

4.1 JSON:最通用的选择,但别滥用

JSON的优点不用我多说:人类可读、生态丰富、几乎所有语言都支持。但它在机器人场景下有个致命伤——效率低

核心问题:JSON是文本协议,解析开销大,数据体积也大。对于高频控制指令(比如100Hz的关节角度下发),JSON基本扛不住。

举个例子,一个简单的电机控制指令:

{
  "motor_id": 3,
  "target_position": 45.6,
  "speed": 120.5,
  "acceleration": 500
}

这段文本占了差不多80个字节。但实际有效数据只有4个数值。你想想看,80%的带宽都浪费在花括号和引号上了。

我在项目中遇到过,用JSON传机器人状态数据,1ms周期根本跑不满,CPU全花在解析字符串上了。后来换成二进制协议,CPU占用直接降了60%。

我的建议:JSON适合做配置下发、日志记录、调试接口。别用它做高频实时控制。

4.2 MessagePack:JSON的二进制表亲

MessagePack(简称MsgPack)本质上就是JSON的二进制版本。它保留了JSON的数据结构(map、array、string、number),但用二进制格式存储。

同样的数据,用MsgPack编码后:

0x84 0xA8 "motor_id" 0x03 0xB0 "target_position" 0xCB 0x40 0x46 0xCC 0xCC ...

体积大概能缩小30%-50%。解析速度也比JSON快不少,因为不需要处理字符串匹配。

特性 JSON MessagePack
数据体积 大(文本格式) 较小(二进制格式)
解析速度 慢(字符串解析) 快(二进制解析)
人类可读 否(需工具查看)
跨语言支持 极好

但MsgPack有个尴尬的地方——它没有类型约束。你发过去一个字段,接收方不知道它是int还是float,得靠约定。这就埋下了隐患。

我曾经踩过的坑:用MsgPack传传感器数据,发送端把温度字段当int32发了,接收端按float32解析,结果温度直接飙到几万度。排查了半天才发现是类型不匹配。

4.3 Protocol Buffers:工业级的选择

Protobuf是Google搞的,现在基本成了工业机器人通信的事实标准。它的核心思路是:先定义数据结构,再生成代码

你写一个.proto文件:

syntax = "proto3";

message MotorCommand {
  int32 motor_id = 1;
  float target_position = 2;
  float speed = 3;
  float acceleration = 4;
}

然后运行protoc编译器,自动生成C++/Python/Java等语言的代码。序列化和反序列化直接调用生成好的函数。

Protobuf的优势很明显:

  • 体积小:比JSON小3-10倍
  • 速度快:解析速度是JSON的10-100倍
  • 类型安全:编译时就检查类型,不会出现MsgPack那种问题
  • 向前/向后兼容:加字段不影响旧版本

我个人习惯在机器人控制器里用Protobuf做核心通信。尤其是多关节机器人,一个控制周期要下发几十个关节指令,Protobuf的编码效率非常关键。

注意:Protobuf不是银弹。它的学习曲线比JSON陡,而且生成的代码体积较大。对于资源极度受限的MCU(比如只有几KB RAM),Protobuf可能跑不动。

4.4 自定义二进制协议:终极性能方案

当Protobuf都满足不了你的时候——比如你需要纳秒级的解析速度,或者你的MCU只有几百字节RAM——那就得自己设计二进制协议了。

我设计过一个简单的自定义协议,结构如下:

| 帧头(2B) | 长度(1B) | 类型(1B) | 数据(NB) | 校验(1B) |
| 0xAA 0x55 |   N+1    |  0x01    |  payload  |  XOR     |

帧头用来同步,长度字段告诉接收方要读多少字节,类型字段区分不同指令,数据段放实际内容,校验位保证数据完整性。

数据段内部也可以设计紧凑格式。比如一个电机指令:

| 电机ID(1B) | 位置(4B, float) | 速度(4B, float) | 加速度(4B, float) |
|    0x03     |  0x40 0x46 ...  |  0x42 0xF1 ...  |  0x43 0xFA ...    |

总共13个字节,比JSON少了80%,比Protobuf还少30%。解析就是直接memcpy,零开销。

但是!自定义协议坑非常多。字节序(大端/小端)、对齐、版本兼容、扩展性……每一个都能让你debug到怀疑人生。我曾经花了两周时间,就为了修复一个字节对齐导致的解析错误。

所以我的建议是:

  • 能用Protobuf就用Protobuf
  • 实在不行再用自定义协议
  • 自定义协议一定要写详细的文档,不然三个月后你自己都看不懂

4.5 选型决策指南

最后,我整理了一个选型表,方便你快速决策:

场景 推荐方案 理由
配置下发、日志记录 JSON 人类可读,调试方便
中等频率通信(10-50Hz) MessagePack 比JSON快,实现简单
高频控制(100Hz以上) Protocol Buffers 性能好,类型安全
资源极度受限的MCU 自定义二进制协议 极致性能,最小体积

嗯,数据序列化这块就讲这么多。记住一句话:没有最好的协议,只有最合适的协议。选型的时候,先想清楚你的场景——带宽、CPU、内存、开发成本,哪个是你的瓶颈?想明白了,答案自然就有了。