7、MessagePack:MsgPack简介、msgpack-c库的使用、高效二进制序列化
聊完了JSON和CBOR,咱们今天来聊聊另一个在嵌入式圈子里很常见的二进制序列化方案——MessagePack。说实话,我第一次接触MsgPack是在一个物联网网关项目里。当时设备上报的数据量特别大,用JSON传,带宽吃紧,延迟也高。后来一查,发现MsgPack能把同样的数据压缩到原来的一半甚至更少。嗯,从那天起,我就开始认真研究它了。
7.1 MsgPack是什么?
MessagePack,简称MsgPack,是一种高效的二进制序列化格式。它和JSON很像——支持相同的数据类型,比如整数、浮点数、字符串、数组、字典等。但区别在于,JSON是文本格式,而MsgPack是二进制格式。
说白了,MsgPack就是JSON的二进制版本。它用更少的字节来表达同样的数据。你想想看,JSON里一个数字"12345"要占5个字节,但在MsgPack里,一个整数可能只占1个字节(如果值很小的话)。这就是它高效的原因。
核心特点:
- 紧凑:数据体积小,节省存储和带宽
- 快速:编码解码速度比JSON快很多
- 跨语言:几乎所有主流语言都有实现
- 自描述:不需要外部Schema,数据本身包含类型信息
7.2 msgpack-c库的使用
在C语言里,最常用的MsgPack库就是msgpack-c。这个库由MsgPack官方维护,稳定性和性能都很好。我在项目中用过好几个版本,从早期的0.5.x到现在的3.x,接口变化不小,但核心思想没变。
7.2.1 安装与引入
安装很简单。如果你用的是Linux,直接通过包管理器安装就行:
# Ubuntu/Debian
sudo apt-get install libmsgpack-dev
# macOS
brew install msgpack-c
如果你喜欢从源码编译,也可以去GitHub上拉下来自己编译。我个人习惯用包管理器,省事。
在代码里引入头文件:
#include <msgpack.h>
7.2.2 基本序列化操作
咱们先看一个最简单的例子——序列化一个整数和一个字符串。
#include <stdio.h>
#include <msgpack.h>
int main() {
// 创建一个缓冲区
msgpack_sbuffer buffer;
msgpack_sbuffer_init(&buffer);
// 创建一个打包器
msgpack_packer packer;
msgpack_packer_init(&packer, &buffer, msgpack_sbuffer_write);
// 打包一个整数
msgpack_pack_int(&packer, 42);
// 打包一个字符串
msgpack_pack_str(&packer, 5);
msgpack_pack_str_body(&packer, "hello", 5);
// 现在buffer.data里就是二进制数据了
printf("序列化后的数据长度: %zu 字节\n", buffer.size);
// 清理
msgpack_sbuffer_destroy(&buffer);
return 0;
}
你看,代码很直观。先初始化一个缓冲区,然后创建一个打包器,接着用msgpack_pack_xxx系列函数往里塞数据。最后数据就在buffer.data里了。
小技巧: 如果你要打包多个字段,建议先定义一个结构体,然后用msgpack_pack_array或msgpack_pack_map来组织数据。这样反序列化时也更容易对应上。
7.2.3 反序列化操作
反序列化同样简单。咱们把刚才打包的数据再解析回来:
#include <stdio.h>
#include <msgpack.h>
int main() {
// 假设这是从网络或文件里收到的数据
unsigned char data[] = {0x2a, 0xa5, 0x68, 0x65, 0x6c, 0x6c, 0x6f};
size_t data_len = sizeof(data);
// 创建反序列化对象
msgpack_unpacked unpacked;
msgpack_unpacked_init(&unpacked);
// 开始解析
if (msgpack_unpack_next(&unpacked, data, data_len, NULL) == MSGPACK_UNPACK_SUCCESS) {
msgpack_object obj = unpacked.data;
// 第一个对象是整数
if (obj.type == MSGPACK_OBJECT_POSITIVE_INTEGER) {
printf("整数: %lu\n", obj.via.u64);
}
// 继续解析下一个
if (msgpack_unpack_next(&unpacked, data, data_len, NULL) == MSGPACK_UNPACK_SUCCESS) {
obj = unpacked.data;
if (obj.type == MSGPACK_OBJECT_STR) {
printf("字符串: %.*s\n", (int)obj.via.str.size, obj.via.str.ptr);
}
}
}
msgpack_unpacked_destroy(&unpacked);
return 0;
}
这里要注意一点:msgpack_unpack_next每次只解析一个对象。如果你打包了多个值,就需要多次调用它。我曾经在这个地方踩过坑——以为一次调用就能解析完所有数据,结果只拿到了第一个字段。
7.3 高效二进制序列化的核心原理
MsgPack为什么高效?咱们来拆解一下它的编码规则。
7.3.1 整数编码
MsgPack对整数做了非常精细的优化:
| 值范围 | 编码格式 | 占用字节 |
|---|---|---|
| 0 ~ 127 | 单字节(高位为0) | 1字节 |
| -32 ~ -1 | 单字节(高位为111) | 1字节 |
| 其他8位整数 | 0xcc + 数据 | 2字节 |
| 其他16位整数 | 0xcd + 数据 | 3字节 |
| 其他32位整数 | 0xce + 数据 | 5字节 |
| 其他64位整数 | 0xcf + 数据 | 9字节 |
你看,对于小整数,MsgPack只用1个字节就搞定了。而JSON里,就算你只传一个"0",也得占2个字节(引号+数字)。这就是差距。
7.3.2 字符串编码
字符串的编码也很聪明。短字符串(长度小于32)只用一个字节表示长度+类型,后面直接跟字符串内容。长一点的字符串用额外字节存长度。
举个例子:字符串"hi"在MsgPack里是0xa2 0x68 0x69,总共3个字节。而在JSON里是"hi",4个字节。虽然只省了1个字节,但数据量大了以后,这个差距就很可观了。
7.4 实战:用MsgPack封装传感器数据
咱们来写一个实际点的例子。假设你有一个温度传感器,需要上报数据:
#include <stdio.h>
#include <string.h>
#include <msgpack.h>
typedef struct {
int sensor_id;
float temperature;
float humidity;
char location[32];
} sensor_data_t;
// 序列化传感器数据
void serialize_sensor_data(sensor_data_t *data, msgpack_sbuffer *buffer) {
msgpack_packer packer;
msgpack_packer_init(&packer, buffer, msgpack_sbuffer_write);
// 打包成map,方便扩展
msgpack_pack_map(&packer, 4);
// sensor_id
msgpack_pack_str(&packer, 9);
msgpack_pack_str_body(&packer, "sensor_id", 9);
msgpack_pack_int(&packer, data->sensor_id);
// temperature
msgpack_pack_str(&packer, 11);
msgpack_pack_str_body(&packer, "temperature", 11);
msgpack_pack_float(&packer, data->temperature);
// humidity
msgpack_pack_str(&packer, 8);
msgpack_pack_str_body(&packer, "humidity", 8);
msgpack_pack_float(&packer, data->humidity);
// location
msgpack_pack_str(&packer, 8);
msgpack_pack_str_body(&packer, "location", 8);
size_t len = strlen(data->location);
msgpack_pack_str(&packer, len);
msgpack_pack_str_body(&packer, data->location, len);
}
int main() {
sensor_data_t data = {
.sensor_id = 1,
.temperature = 25.6f,
.humidity = 68.3f,
.location = "room_a"
};
msgpack_sbuffer buffer;
msgpack_sbuffer_init(&buffer);
serialize_sensor_data(&data, &buffer);
printf("序列化后数据大小: %zu 字节\n", buffer.size);
printf("同等JSON数据大约: 80+ 字节\n");
// 这里可以把buffer.data发送到网络或写入文件
msgpack_sbuffer_destroy(&buffer);
return 0;
}
这个例子跑下来,序列化后的数据大概只有30多字节。同样的数据用JSON,至少80字节起步。在物联网场景下,这个差距意味着每月能省下不少流量费。
注意: 使用msgpack_pack_float时,要确保你的平台支持IEEE 754单精度浮点数。大多数现代处理器都支持,但一些老旧的嵌入式MCU可能有问题。我曾经在STM32F103上遇到过浮点数编码不一致的情况,后来改用整数(放大100倍)来传输温度值才解决。
7.5 知识体系总览
下面这张图帮你梳理了MsgPack的核心知识点:
7.6 避坑指南
最后,分享几个我在实际项目中踩过的坑:
- 字节序问题:MsgPack默认使用大端序(网络字节序)。如果你的MCU是小端序(比如ARM Cortex-M系列),编码解码时库会自动处理,但如果你自己手动解析二进制数据,一定要注意字节序转换。
- 浮点数精度:MsgPack支持32位和64位浮点数。如果你用
msgpack_pack_float打包,但接收端用msgpack_unpack_double解析,精度会丢失。我曾经因为这个原因,导致温度数据在服务器端显示总是差0.1度。 - 内存管理:
msgpack_sbuffer内部会动态分配内存。如果你在中断服务函数里使用它,要小心内存分配失败的问题。我建议在初始化时预分配足够大的缓冲区,避免运行时动态扩展。
好了,关于MessagePack的内容就聊到这里。它确实是一个很实用的工具,尤其是在带宽和存储受限的嵌入式场景里。希望这些经验能帮你少走一些弯路。