目录
一、需求
- 了解.proto文件的配置语法规则
- 目前平台上关于protocol buffer的使用例子较少
二、环境
- 版本:Android 12
- 平台:展锐 SPRD8541E
三、相关概念
3.1 protocol buffer介绍
protocol buffer是一种google开发的,语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于通信协议、数据存储等。google在2008年7月7号将其作为开源项目对外公布。值得注意的是,proto buffer是以二进制来存储数据的。相对于JSON和XML具有以下优点:
- 简洁
- 体积小:消息大小只需要XML的1/10 ~ 1/3;
- 速度快:解析速度比XML快20 ~ 100倍;
- json\xml都是基于文本格式,protobuf是二进制格式;
protobuf是PB协议使用较广的一个框架,支持C++,JAVA,Python,Ruby,Go,PHP等多种语言。
3.2 nanopb(支持C语言)
protobuf支持多种语言,但是却不支持纯C语言,而且protobuf的使用笨重,在一些内存紧张的嵌入式设备上不能使用,nanopb是谷歌协议缓冲数据格式的一个纯C实现。它的目标是32位微控制器,但也适用于其他嵌入式系统的严格(< 10kB ROM,< 1kB RAM)内存限制。
3.3 proto文件
.proto文件是Google Protocol Buffers的核心组成部分,定义了数据的结构和格式。它支持多种基本数据类型和自定义数据类型的定义,可以嵌套定义。每个字段有类型、名称和字段序号三个特性,字段规则定义了字段是单值、重复值还是可选值。在.proto文件定义完成后,需要使用protobuf编译器将其编译成对应语言的代码,然后在代码中使用这些生成的代码文件定义数据类型、序列化和反序列化数据。
四、proto基本语法
4.1 proto文件的定义
如下为一个*.proto文件的基本定义:
4.2 字段规则
字段 | 介绍 |
---|---|
required | 格式良好的 message 必须包含该字段一次(在proto3中已经为兼容性彻底抛弃 required。) |
optional | 格式良好的 message 可以包含该字段零次或一次(不超过一次)。 |
repeated | 该字段可以在格式良好的消息中重复任意多次(包括零)。其中重复值的顺序会被保留。 |
4.3 字段类型
proto类型 | 介绍 |
---|---|
double | 64位浮点数 |
float | 32位浮点数 |
int32 | 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint32。 |
int64 | 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint64。 |
uint32 | 使用可变长度编码。 |
uint64 | 使用可变长度编码。 |
sint32 | 使用可变长度编码。符号整型值。这些比常规int32s编码负数更高效。 |
sint64 | 使用可变长度编码。符号整型值。这些比常规int64s编码负数更高效。 |
fixed32 | 总是四字节。如果值通常大于228,则比uint 32更高效 |
fixed64 | 总是八字节。如果值通常大于256,则比uint64更高效 |
sfixed32 | 总是四字节。 |
sfixed64 | 总是八字节。 |
bool | 布尔类型 |
string | 字符串必须始终包含UTF - 8编码或7位ASCII文本 |
bytes | 可以包含任意字节序列 |
4.4 字段编号
message 定义中的每个字段都有唯一编号。这些数字以message二进制格式标识你的字段,并且一旦你的message被使用,这些编号就无法再更改。请注意,1到15范围内的字段编号需要一个字节进行编码,编码结果将同时包含编号和类型。16到2047范围内的字段编号占用两个字节。因此,你应该为非常频繁出现的message元素保留字段编号1到15。
4.5 proto语法
目前proto语法,可以分为proto2版本和proto3版本,proto3在proto2的基础上做了升级与改动,其区别如下:
https://blog.csdn.net/ymzhu385/article/details/122307593
Android12上发现采用proto2语法场景较多,本文的话我也将继续沿用proto2语法进行分析。
4.6 进阶语法
4.6.1 message嵌套
messsage除了能放简单数据类型外,还能存放另外的message类型:
message CarMessage {
required string name = 1;
required int32 price = 2;
}
message UserMessage {
enum Sex {
WOMAN = 0;
MAN = 1;
}
required string username = 1;
optional int32 age = 2;
required Sex sex = 3;
repeated CarMessage cars = 4;
}
4.6.2 enum关键字
在定义消息类型时,可能会希望其中一个字段有一个预定义的值列表,我们可以通过enum在消息定义中添加每个可能值的常量来非常简单的执行此操作:
message UserMessage {
enum Sex {
WOMAN = 0;
MAN = 1;
}
required string username = 1;
optional int32 age = 2;
required Sex sex = 3;
}
4.6.3 oneof关键字
如果有一个包含许多字段的消息,并且最多只能同时设置其中的一个字段,则可以使用oneof功能,示例如下:
message OneOfMessage {
oneof IdData {
int32 id = 1;
int32 passport = 2;
};
}
五、nanopb分析
5.1 nanopb版本下载
nanopb各个版本: https://jpa.kapsi.fi/nanopb/download/
5.2 nanopb相关Api
Protocol指导文档: https://jpa.kapsi.fi/nanopb/docs/index.html
5.2.1 编码相关API
API | 说明 |
---|---|
pb_ostream_t pb_ostream_from_buffer(pb_byte_t *buf, size_t bufsize); | 构造用于写入内存缓冲区的输出流。 |
bool pb_encode(pb_ostream_t *stream, const pb_msgdesc_t *fields, const void *src_struct); | 将结构的内容编码为协议缓冲区消息,并将其写入输出流 |
bool pb_encode_ex(pb_ostream_t *stream, const pb_msgdesc_t *fields, const void *src_struct, unsigned int flags); | 使用由标志设置的扩展行为对消息进行编码: |
bool pb_get_encoded_size(size_t *size, const pb_msgdesc_t *fields, const void *src_struct); | 计算已编码消息的长度。 |
bool pb_encode_tag(pb_ostream_t *stream, pb_wire_type_t wiretype, uint32_t field_number); | 以Protocol Buffers二进制格式开始一个字段:编码字段号和数据的类型。 |
bool pb_encode_tag_for_field(pb_ostream_t *stream, const pb_field_iter_t *field); | 与pb_encode_tag相同,只是从pb_field_iter_t结构体获取参数。 |
bool pb_encode_varint(pb_ostream_t *stream, uint64_t value); | 以可变格式编码有符号或无符号整数。适用于bool、enum、int32、int64、uint32和uint64类型的字段: |
bool pb_encode_string(pb_ostream_t *stream, const pb_byte_t *buffer, size_t size); | 将字符串的长度写入变量,然后写入字符串的内容。适用于bytes和string类型的字段: |
bool pb_encode_submessage(pb_ostream_t *stream, const pb_msgdesc_t *fields, const void *src_struct); | 对子消息字段进行编码,包括它的大小报头。适用于任何消息类型的字段。 |
... | ... |
5.2.2 解码相关API
API | 说明 |
---|---|
pb_istream_t pb_istream_from_buffer(const pb_byte_t *buf, size_t bufsize); | 用于创建从内存缓冲区读取数据的输入流的辅助函数。 |
bool pb_decode(pb_istream_t *stream, const pb_msgdesc_t *fields, void *dest_struct); | 读取和解码结构的所有字段。读取输入流直到EOF。 |
bool pb_decode_ex(pb_istream_t *stream, const pb_msgdesc_t *fields, void *dest_struct, unsigned int flags); | 与pb_decode相同,但允许扩展选项。 |
bool pb_decode_varint(pb_istream_t *stream, uint64_t *dest); | 读取和解码一个变量编码的整数。 |
bool pb_decode_svarint(pb_istream_t *stream, int64_t *dest); | 类似于pb_decode_varint,不同之处在于它对值执行zigzag解码。这对应于协议缓冲区sint32和sint64数据类型。 |
... | ... |
5.3 总体结构
5.3.1 结构图
Step 1. 第一阶段: MyMessage.proto文件经过编译,会生成MyMessage.pb.c和MyMessage.pb.h临时文件;
Step 2. 第二阶段: 通过Nanopb提供的相关库文件,以及第一个阶段生成的MyMessage.pb.c和MyMessage.pb.h临时文件,可以编写我们的应用程序User application;
Step 3. 第三阶段: 我们的业务数据Data structures和Protocol Buffers messages的数据,通过Nanopb library提供的编解码方法pb_encode()和pb_decode(), 实现序列化和反序列化的操作。
5.3.2 相关文件
一个标准的nanopb项目,会包含如下文件:
类型 | 文件 | 备注 |
Nanopb runtime library | pb.h | 必须有 |
pb_common.h pb_common.c |
必须有 | |
pb_decode.h pb_decode.c |
编码相关 | |
pb_encode.h pb_encode.c |
解码相关 | |
Protocol description | MyMessage.proto | 必须有 |
MyMessage.pb.c MyMessage.pb.h |
编译后自动生成 |
六、nanopb应用
6.1 基于Android平台nanopb应用
基于Android平台,创建一个c程序,用于测试nanopb的使用规则。文末附上相关demo仓库地址。
6.1.2 定义.proto文件
定义.proto文件,定义数据结构与格式。
syntax = "proto2";
...
message CarMessage {
required string name = 1;
required int32 price = 2;
}
message PetMessage {
required string name = 1;
}
message UserMessage {
enum Sex {
WOMAN = 0;
MAN = 1;
}
required string username = 1;
optional int32 age = 2;
required Sex sex = 3;
repeated CarMessage cars = 4;
optional PetMessage pets = 5;
}
6.1.3 生成动态库
将proto相关文件打包成libprototest动态库,以便于需要使用的模块去引用。(之前想将proto直接编译到对应的测试程序,但是export_proto_headers等相关编译标识未找到,导致无法正常编译,故将其先编译成一个动态库)
cc_library {
name: "libprototest",
srcs: [
"proto/simple.proto",
],
...
proto: {
type: "nanopb-c-enable_malloc-32bit",
export_proto_headers: true,
},
vendor: true,
}
6.1.4 临时文件
libprototest模块编译后,会根据.proto文件的数据结构,生成一个临时文件simple.pb.c和simple.pb.h(临时文件路径:out\soong\ .intermediates\vendor\sprd\proprietories-source\rild\protocol\libprototest\android_vendor.31_arm_armv8-a_cortex-a53_static\gen\proto\vendor\sprd\proprietories-source\rild\protocol\proto\),相关文件也有备份到Demo代码仓库。
@simple.pb.h
...
typedef enum _UserMessage_Sex {
UserMessage_Sex_WOMAN = 0,
UserMessage_Sex_MAN = 1
} UserMessage_Sex;
#define _UserMessage_Sex_MIN UserMessage_Sex_WOMAN
#define _UserMessage_Sex_MAX UserMessage_Sex_MAN
#define _UserMessage_Sex_ARRAYSIZE ((UserMessage_Sex)(UserMessage_Sex_MAN+1))
/* Struct definitions */
typedef struct _CarMessage {
char name[100];
int32_t price;
/* @@protoc_insertion_point(struct:CarMessage) */
} CarMessage;
typedef struct _PetMessage {
char name[100];
/* @@protoc_insertion_point(struct:PetMessage) */
} PetMessage;
...
typedef struct _UserMessage {
char username[200];
bool has_age;
int32_t age;
UserMessage_Sex sex;
pb_callback_t cars;
bool has_pets;
PetMessage pets;
/* @@protoc_insertion_point(struct:UserMessage) */
} UserMessage;
...
/* Struct field encoding specification for nanopb */
extern const pb_field_t SimpleMessage_fields[5];
extern const pb_field_t CarMessage_fields[3];
extern const pb_field_t PetMessage_fields[2];
extern const pb_field_t UserMessage_fields[6];
...
6.1.5 测试用例
message嵌套使用测试,其相关流程如下:
void test_nest(void){
RLOGD("lzq add for test_nest START >>>>>>>>\n");
/*************************写入数据***************************/
//Step 1.创建写入数据
UserInfo userinfo;
strcpy(userinfo.username,"linzhiqin");
userinfo.age = 18;
userinfo.has_age = true;
strcpy(userinfo.cars[0].name,"BMW");
userinfo.cars[0].price = 380000;
strcpy(userinfo.cars[1].name,"Benz");
userinfo.cars[1].price = 450000;
strcpy(userinfo.cars[2].name,"Audi");
userinfo.cars[2].price = 280000;
userinfo.car_num = 3;
//Step 2.写入数据赋值给编码相关对象
uint8_t encodeBuffer[1024] = {0};
int encodeBufferLen = 0;
UserMessage pack_user = UserMessage_init_zero;
strcpy(pack_user.username,userinfo.username);
pack_user.age = userinfo.age;
pack_user.has_age = userinfo.has_age;
pack_user.sex = UserMessage_Sex_MAN;
//strcpy(pack_user.pets.name,"Ragdoll");
pack_user.cars.funcs.encode = carEncode;//编码回调函数
pack_user.cars.arg = &userinfo;
//Step 3.数据编码
pb_ostream_t o_stream = {0};
o_stream = pb_ostream_from_buffer(encodeBuffer, 1024);
if(pb_encode(&o_stream, UserMessage_fields, &pack_user) == false){
printf("encode failed\n");
return;
}
encodeBufferLen = o_stream.bytes_written;
/**************************读取数据**************************/
//Step 4.创建解码相关对象
UserInfo userinfo2;
memset(&userinfo2,0,sizeof(UserInfo));
UserMessage unpack_user = UserMessage_init_zero;
unpack_user.cars.funcs.decode = carDecode;//解码回调
unpack_user.cars.arg = &userinfo2;
//Step 5.数据解码&打印
pb_istream_t i_stream = {0};
i_stream = pb_istream_from_buffer(encodeBuffer, encodeBufferLen);
if(pb_decode(&i_stream, UserMessage_fields, &unpack_user) == true){
strcpy(userinfo2.username,unpack_user.username);
//strcpy(userinfo2.pets.name,unpack_user.pets.name);
if(unpack_user.has_age) {
userinfo2.age = unpack_user.age;
}
printf("\n");
printf("UserInfo.pets.name = %s\n", userinfo2.pets.name);
printf("UserInfo.username = %s\n", userinfo2.username);
printf("UserInfo.age = %d\n", userinfo2.age);
printf("UserInfo.sex = %d\n", unpack_user.sex);
for(int i=0;i<userinfo2.car_num;i++)
printf("CarInfo name:%s score:%d\n",userinfo2.cars[i].name,userinfo2.cars[i].price);
}
RLOGD("lzq add for test_nest END >>>>>>>>\n");
}
打印结果:
6.2 基于Windows平台的nanopb应用
6.2.1 环境配置
(1)Windows版本: Windows 10 专业版
(2)gcc版本: gcc version 8.1.0(https://sourceforge.net/projects/mingw-w64/)
(3)C程序IDE: January 2024 (version 1.86)(插件: C/C++、Code Runner)
(4)nanopb版本: nanopb-0.4.8-windows-x86.zip(https://jpa.kapsi.fi/nanopb/download/)
6.2.2 临时文件生成
Step 1. 解压nanopb文件夹 解压nanopb-0.4.8-windows-x86.zip文件,其相关内容如下:
Step 2. 新增.proto文件 在nanopb-0.4.8-windows-x86.zip文件文件夹下,新增simple.proto文件,相关内容如下:
syntax = "proto2";
message SimpleMessage {
enum Sex {
WOMAN = 0;
MAN = 1;
}
required int32 code = 1;
optional string msg = 2;
repeated int32 data = 3;
required Sex sex = 4;
}
Step 3. 设置系统环境变量 将nanopb-0.4.8-windows-x86\generator-bin设置为系统全局变量,方便引用;
Step 4. 生成临时文件 进入当前文件夹下,通过如下指令生成simple.pb.c和simple.pb.h:
protoc --nanopb_out=. simple.proto
Step 5. nanopb程序关键文件 nanopb 将需要保存的参数写在.proto文件里面,然后生成对应的.pb.h和.pb.c 文件。nanopb程序需要将如下几个关键文件拷贝到对应工程:
![](/i/l/?n=24&i=blog/2832116/202402/2832116-20240227090842106-230003921.jpg)6.2.3 测试用例
#include <stdio.h>
#include <stdlib.h>
#include "pb_decode.h"
#include "pb_encode.h"
#include "simple.pb.h"
/******************** test_simple *************************/
/*
* 简单测试
*/
void test_simple(){
SimpleMessage req;
memset(&req, 0, sizeof(SimpleMessage));
req.code = 1109;
req.sex = SimpleMessage_Sex_MAN;
size_t encodedSize = 0;
if (!pb_get_encoded_size(&encodedSize, SimpleMessage_fields, &req)) {
exit(0);
}
uint8_t *buffer = (uint8_t *)calloc(1, encodedSize);
if (buffer == NULL) {
exit(0);
}
pb_ostream_t stream = pb_ostream_from_buffer(buffer, encodedSize);
if (!pb_encode(&stream, SimpleMessage_fields, &req)) {
exit(0);
}
/**************************读取数据**************************/
SimpleMessage message = SimpleMessage_init_zero;
pb_istream_t stream2 = pb_istream_from_buffer(buffer, encodedSize);
if (!pb_decode(&stream2, SimpleMessage_fields, &message))
{
exit(0);
}
printf("code = %d | sex = %d \n",message.code,message.sex);
}
int main(int argc, char **argv) {
//简单测试
test_simple();
...
}
打印结果:
七、遗留问题
- const pb_field_t UserMessage_fields[6] 数组的数据打印异常(nanopb例子using_union_messages)
- message多级嵌套除了使用repeated关键字,采用回调函数处理外,不知道是否有其他处理方式?
八、代码仓库
Demo地址: https://gitee.com/linzhiqin/protocol
九、参考资料
https://zhuanlan.zhihu.com/p/494788890#nanopb的使用
https://www.jianshu.com/p/6f68fb2c7d19
https://blog.csdn.net/hsy12342611/article/details/129517588
https://www.jianshu.com/p/bdd94a32fbd1
https://blog.csdn.net/Gefangenes/article/details/131319610
参考例子:
https://blog.csdn.net/du2005023029/article/details/130861308
VsCode调试C程序
https://blog.csdn.net/ABYSS_CL/article/details/119961975
nanopb在window平台的使用
https://www.cnblogs.com/ymchen/p/16861605.html