6. Protocol Buffers:从零开始掌握 protobuf-c
说到序列化,大家可能都用过 JSON、XML。但在嵌入式领域,这些文本格式有个致命问题——太胖了。我早年做物联网项目时,一个传感器数据包用 JSON 要 200 字节,换成 Protobuf 后直接压缩到 30 字节。嗯,这就是我们今天要聊的主角。
6.1 Protobuf 是什么?
Protocol Buffers,简称 Protobuf,是 Google 搞出来的一种高效、跨语言的序列化方案。说白了,它就是把结构化的数据变成二进制流,反过来也能从二进制流还原出原始数据。
跟 JSON 比,Protobuf 有几个硬核优势:
- 体积小:二进制编码,没有冗余的字段名和括号
- 速度快:解析时不需要做字符串匹配
- 强类型:字段类型在 .proto 文件中就定死了
- 向后兼容:老程序可以读新数据,反之亦然
我在项目中遇到过最典型的场景:一个温湿度采集器,每 5 秒上报一次数据。用 JSON 时,一个月流量费多花了 300 块。换成 Protobuf 后,流量直接砍半。你想想看,对于 NB-IoT 这种按流量计费的场景,这省下来的可都是真金白银。
核心概念:Protobuf 不是一种编程语言,而是一种接口定义语言(IDL)。你写 .proto 文件定义数据结构,然后用编译器生成对应语言的代码。
6.2 protobuf-c 库的安装
在 C 语言生态里,最常用的 Protobuf 实现就是 protobuf-c。它轻量、无依赖,特别适合嵌入式环境。
安装方式很简单,我一般用源码编译:
# 1. 下载源码
git clone https://github.com/protobuf-c/protobuf-c.git
cd protobuf-c
# 2. 安装依赖(Ubuntu/Debian)
sudo apt-get install autoconf automake libtool
# 3. 编译安装
./autogen.sh
./configure --prefix=/usr/local
make -j4
sudo make install
# 4. 验证安装
protoc-c --version
# 输出类似:libprotoc 3.21.12
小技巧:如果你在交叉编译环境中使用,记得在 ./configure 时指定 --host=arm-linux-gnueabihf 这类参数。我曾经因为忘记指定交叉编译链,在开发板上跑了一下午才发现库链接错了。
安装完成后,你会得到两个关键工具:
protoc-c:编译器,把 .proto 文件转成 .h 和 .clibprotobuf-c:运行时库,提供序列化和反序列化函数
6.3 定义 .proto 文件
.proto 文件是 Protobuf 的灵魂。我们来定义一个传感器数据包:
syntax = "proto3";
package sensor;
message SensorData {
uint32 device_id = 1; // 设备ID
float temperature = 2; // 温度,单位℃
float humidity = 3; // 湿度,单位%
uint32 timestamp = 4; // 时间戳,Unix时间
enum Status {
NORMAL = 0;
ALARM = 1;
ERROR = 2;
}
Status status = 5; // 设备状态
}
这里有几个要点:
- syntax:指定 proto3 语法,proto2 和 proto3 不兼容
- package:命名空间,避免不同模块的 message 冲突
- message:定义数据结构,类似 C 语言的结构体
- 字段编号:等号后面的数字是字段的唯一标识,1-15 用 1 字节编码,16-2047 用 2 字节
注意:字段编号一旦确定,就不要改了。否则旧数据反序列化时会解析错误。我见过有人把字段 1 和字段 2 互换,结果线上服务全崩了——因为旧数据里的字段 1 被当成了字段 2 来解析。
6.4 编译 .proto 文件
写好 .proto 文件后,用 protoc-c 编译:
protoc-c --c_out=. sensor.proto
这条命令会生成两个文件:
sensor.pb-c.h:头文件,包含结构体定义和函数声明sensor.pb-c.c:源文件,包含序列化/反序列化实现
打开头文件,你会看到类似这样的结构体:
typedef struct _SensorData {
ProtobufCMessage base;
uint32_t device_id;
float temperature;
float humidity;
uint32_t timestamp;
SensorData__Status status;
} SensorData;
注意,每个 message 都内嵌了一个 ProtobufCMessage 基类。这是 protobuf-c 实现反射和内存管理的基础。
6.5 在 C 代码中使用
编译出来的代码怎么用?来看一个完整的例子:
#include <stdio.h>
#include <string.h>
#include "sensor.pb-c.h"
int main() {
// 1. 初始化消息
SensorData data = SENSOR_DATA__INIT;
data.device_id = 1001;
data.temperature = 25.6f;
data.humidity = 60.2f;
data.timestamp = 1700000000;
data.status = SENSOR_DATA__STATUS__NORMAL;
// 2. 序列化:计算需要的缓冲区大小
size_t size = sensor_data__get_packed_size(&data);
uint8_t *buffer = malloc(size);
// 3. 序列化:打包成二进制
sensor_data__pack(&data, buffer);
printf("序列化后大小: %zu 字节\n", size);
// 4. 反序列化:从二进制还原
SensorData *decoded = sensor_data__unpack(NULL, size, buffer);
if (decoded == NULL) {
fprintf(stderr, "反序列化失败\n");
free(buffer);
return -1;
}
// 5. 验证结果
printf("设备ID: %u\n", decoded->device_id);
printf("温度: %.1f℃\n", decoded->temperature);
printf("湿度: %.1f%%\n", decoded->humidity);
// 6. 清理
sensor_data__free_unpacked(decoded, NULL);
free(buffer);
return 0;
}
编译时记得链接 protobuf-c 库:
gcc -o test test.c sensor.pb-c.c -lprotobuf-c -I.
关键函数说明:
sensor_data__get_packed_size():计算序列化后的字节数sensor_data__pack():把结构体打包成二进制流sensor_data__unpack():从二进制流还原结构体sensor_data__free_unpacked():释放反序列化时分配的内存
6.6 核心流程一览
为了让你更直观地理解整个流程,我画了一张图:
6.7 避坑指南
最后,分享几个我踩过的坑:
- 内存泄漏:每次调用
unpack()都会分配内存,记得用free_unpacked()释放。我曾经在一个循环里忘了释放,跑了 3 天内存就爆了。 - 字段默认值:proto3 中,所有字段都有默认值(数值型为 0,字符串为空)。如果你需要区分「没设置」和「设置为 0」,要用
optional关键字。 - 大端小端:protobuf-c 默认使用小端编码。如果你的嵌入式平台是大端(比如某些 PowerPC 芯片),记得在编译时加上
-DPROTOBUF_C_LITTLE_ENDIAN=0。 - 嵌套消息:如果 message 里嵌套了其他 message,记得先初始化子消息。否则序列化时会崩溃。
我的习惯:在嵌入式项目中,我会把 .proto 文件和生成的 .pb-c.c/.pb-c.h 一起提交到 Git 仓库。这样其他同事拉下来就能直接编译,不用每个人都装 protoc-c。而且,如果哪天 .proto 文件改了,对比生成的代码也能快速定位问题。
好了,Protobuf 的基础用法就讲到这里。记住,它的核心价值在于「定义一次,到处使用」。你只需要维护 .proto 文件,编译器帮你搞定所有语言的代码生成。这在多平台、多语言协作的项目中,能省下大量时间。
公众号:蓝海资料掘金营,微信deep3321