什么是 Protobuf?
Protobuf(Protocol Buffers)是谷歌开源的一种序列化数据结构格式,不依赖特定平台和语言,具备可扩展、轻量且高效的特点。它能够把自定义数据结构序列化成字节流,也能反向将字节流还原成数据结构,所以很适合不同应用之间的数据交换。使用时需要先编写 .proto 文件定义数据格式,编译后即可解析。Protobuf 支持 Java、Python、C/C++ 等多种语言。
Protobuf 的一个重要应用场景,是作为 RPC(远程过程调用)协议中的序列化/反序列化工具。谷歌推出的 gRPC 框架,底层就采用 Protobuf 负责序列化工作。
Protobuf 语法
使用 protobuf 之前,需要先编写一个 .proto 文件来描述消息格式。通过一个简单的 demo 来看:
// syntax关键词定义使用的是proto3语法版本
syntax = "proto3";
// message关键词,标记开始定义一个消息
message Student{
// 字段类型 名字 = 唯一标识号
string name = 1;
int32 age = 2;
}
第一行 syntax 声明了使用的 proto 语法版本,proto3 对应的就是 proto3 语法。
message 用来定义消息类型,可以有多个。它的结构与 C 语言结构体非常相似,Student 是消息名称,类比 C 语言结构体的名称。
字段类型表示字段的数据类型,包括 string、int32、uint32、float、double、bool、bytes 等,含义和 C 语言中的类型很接近。唯一标识号:消息定义中每个字段后面都有一个唯一编号,用于在二进制格式中识别各个字段。
以上是 protobuf 最简单的语法。
protobuf-c 的使用
默认安装的 protobuf 支持 C++、Java、Python、Rust 等多种语言,唯独没有原生支持 C 语言。想在 C 语言中使用 protobuf,需要单独编译安装 protobuf-c。
# protobuf-c 需要 protobuf
sudo apt install protobuf-compiler
# 一键三连安装 protobuf-c
git clone https://github.com/protobuf-c/protobuf-c.git && cd protobuf-c
./autogen.sh && ./configure
make -j && sudo make install
安装完成后,protobuf-c 的编译方式与其它语言一致,指定需要编译的文件和输出文件类型即可。
protoc-c demo.proto --c_out=.
protoc-c 是 .proto 文件的 C 语言编译器,--c_out 用于指定输出文件类型和路径。
编译完成后,会在指定路径生成两个文件:demo.pb-c.c 和 demo.pb-c.h,分别负责消息的打包与解析。生成的头文件提供了以下 API:
// demo.pb-c.h
/* Student methods */
void student__init(Student *message);
size_t student__get_packed_size(const Student *message);
size_t student__pack(const Student *message, uint8_t *out);
size_t student__pack_to_buffer(const Student *message, ProtobufCBuffer *buffer);
Student *student__unpack(ProtobufCAllocator *allocator, size_t len,
const uint8_t *data);
void student__free_unpacked(Student *message, ProtobufCAllocator *allocator);
下面基于这些 API 写一个完整的 demo。
#include "demo.pb-c.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main() {
Student student = STUDENT__INIT;
void *buffer = NULL;
int32_t len;
Student *msg = NULL;
// 初始化数据
student.name = "student";
student.age = 28;
// 打包数据
len = student__get_packed_size(&student);
buffer = malloc(len);
student__pack(&student, buffer);
// TODO: 发送数据到远端设备
// TODO: 从远端设备接受数据
// 解包数据
msg = student__unpack(NULL, len, buffer);
printf("student name : %s, age : %d\n", msg->name, msg->age);
// 释放资源
student__free_unpacked(msg, NULL);
free(buffer);
return 0;
}
性能对比
有人对多种通用序列化协议做过横向对比。从结果来看,protobuf 在序列化和反序列化环节的性能都相当出色,同时产出的数据体积也非常小。


数据来源:https://www.iteye.com/blog/agapple-859052
在 GitHub 上有更详细、更完整的性能对比数据:https://github.com/eishay/jvm-serializers/wiki ,可进一步查阅。
嵌入式使用
了解完 protobuf 的基础用法后,再考虑它在嵌入式设备上的落地,就会发现标准 protobuf 很难直接跑在资源受限的小型嵌入式环境中。
它几百 KB 的代码体积,已经超过不少芯片的 Flash 容量了。不过查找资料时,有一个成熟的替代方案:nanopb。
nanopb 的使用方式和 .proto 语法与 protobuf 完全一致,核心只有三个文件,编译后代码不到 10K,非常适合嵌入式环境。当然它也有代价:用时间去换空间,序列化和反序列化耗时相对长一些,不过相比 JSON 等文本格式仍然快上不少。