一、需求

  1. 了解.proto文件的配置语法规则
  2. 目前平台上关于protocol buffer的使用例子较少

二、环境

  1. 版本:Android 12
  2. 平台:展锐 SPRD8541E

三、相关概念

3.1 protocol buffer介绍

protocol buffer是一种google开发的,语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于通信协议、数据存储等。google在2008年7月7号将其作为开源项目对外公布。值得注意的是,proto buffer是以二进制来存储数据的。相对于JSON和XML具有以下优点:

  1. 简洁
  2. 体积小:消息大小只需要XML的1/10 ~ 1/3;
  3. 速度快:解析速度比XML快20 ~ 100倍;
  4. 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.csimple.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程序需要将如下几个关键文件拷贝到对应工程:

![](https://img2024.cnblogs.com/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();
...
}

打印结果:

七、遗留问题

  1. const pb_field_t UserMessage_fields[6] 数组的数据打印异常(nanopb例子using_union_messages)
  2. 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

ProtoBuffer-nanopb介绍的更多相关文章

  1. Erlang使用ProtoBuffer

    最近有工作需要打算为项目服务器做一个机器人,测试测试压力,根据自己的经验,使用Erlang来做是最合适不过的了,但是服务器使用的C++语言,使用了Google的ProtoBuffer作为协议进行数据交 ...

  2. 基于.NET CORE微服务框架 -surging 基于messagepack、protobuffer、json.net 性能对比

    1.前言 surging内部使用的是高性能RPC远程服务调用,如果用json.net序列化肯定性能上达不到最优,所以后面扩展了protobuf,messagepack序列化组件,以支持RPC二进制传输 ...

  3. 基于.NET CORE微服务框架 -谈谈surging 的messagepack、protobuffer、json.net 序列化

    1.前言 surging内部使用的是高性能RPC远程服务调用,如果用json.net序列化肯定性能上达不到最优,所以后面扩展了protobuf,messagepack序列化组件,以支持RPC二进制传输 ...

  4. Protobuffer简介c#

    一.Protobuffer和json深度对比 JSON相信大家都知道是什么东西,如果不知道,那可就真的OUT了,GOOGLE一下去.这里就不介绍啥的了. Protobuffer大家估计就很少听说了,但 ...

  5. 从微信SDK看ProtoBuffer文件的生成

    前言 Protocol Buffers (下面简称PB)是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,很适合做数据存储或 RPC 数据交换格式.它可用于通讯协议.数据存储等领域的语言无 ...

  6. Thrift RPC框架介绍

    u 简介 Thrift是一种开源的跨语言的RPC服务框架.Thrift最初由facebook公司开发的,在2007年facebook将其提交apache基金会开源了.对于当时的facebook来说创造 ...

  7. 实战:一种在http请求中使用protobuffer+nginx+lua收集打点日志的方案

    背景 app打点日志的上报和收集,是互联网公司的基本需求. 一.方案选择 1.1 protobuffer vs json 探究一种以最高效的方式上报和解析打点数据是一个系统性的问题,需要解决的子问题有 ...

  8. CSS3 background-image背景图片相关介绍

    这里将会介绍如何通过background-image设置背景图片,以及背景图片的平铺.拉伸.偏移.设置大小等操作. 1. 背景图片样式分类 CSS中设置元素背景图片及其背景图片样式的属性主要以下几个: ...

  9. MySQL高级知识- MySQL的架构介绍

    [TOC] 1.MySQL 简介 概述 MySQL是一个关系型数据库管理系统,由瑞典MySQL AB公司开发,目前属于Oracle公司. MySQL是一种关联数据库管理系统,将数据保存在不同的表中,而 ...

  10. Windows Server 2012 NIC Teaming介绍及注意事项

    Windows Server 2012 NIC Teaming介绍及注意事项 转载自:http://www.it165.net/os/html/201303/4799.html Windows Ser ...

随机推荐

  1. [转帖]Windows磁盘性能压测(1)-DiskSpd

    http://www.manongjc.com/detail/59-xrydhtisrajqsxn.html 本文章向大家介绍Windows磁盘性能压测(1)-DiskSpd,主要内容包括其使用实例. ...

  2. [转帖]kubelet 原理解析四:probeManager

    https://segmentfault.com/a/1190000022163835 概述 在Kubernetes 中,系统和应用程序的健康检查任务是由 kubelet 来完成的,本文主要讨论kub ...

  3. 部署于K8S集群上面应用性能影响点推测

    前言 本人2017年第一次接触K8S. 中间断断续续学习K8S相关的内容. 但是最近一年,几乎没太有学习. 因为之前学习了四五年, 一直以为产品马上要用 结果一直被浇冷水. 去年开始学乖了. 不这么搞 ...

  4. 查看dmesg 里面部分内容的精确时间

    for i in `dmesg |grep "stuck for" |awk '{print $1}' |awk -F "." '{print $1}' |aw ...

  5. node借助jsonwebtoken生成token以及验证token是否过期

    生成token使用 jsonwebtoken 插件 我当时使用的版本"jsonwebtoken": "^9.0.0", cnpm i jsonwebtoken ...

  6. 【解决了一个小问题】terraform apply 的时候出现访问 localhost 出错

    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢! cnblogs博客 zhihu Github 公众号:一本正经的瞎扯 有这样一段 terraform 的部署脚本: provid ...

  7. vim 从嫌弃到依赖(7)——可视模式

    vim 的可视模式下可以选择一个区域,然后针对区域进行操作.可视模式有点类似于在其他编辑器上使用鼠标选中一块区域然后针对区域进行操作. vim中有3种可视模式,分别用来处理不同范围的文本: 处理字符的 ...

  8. TienChin 活动管理-修改活动接口

    前端 activity.js 直接替换现有的,最求速度了,后面在详细一个个记录,不在过多解释了. import request from '@/utils/request' /** * 查询活动列表 ...

  9. 获取Visual Studio所用MSVC编译器版本:_MSC_VER数值

      本文介绍查看Visual Studio软件_MSC_VER值的方法.   _MSC_VER是微软公司推出的C/C++编译器--MSVC编译器的一个内置宏,其值表示当前Visual Studio软件 ...

  10. 推荐系统[四]:精排-详解排序算法LTR (Learning to Rank)_ poitwise, pairwise, listwise相关评价指标,超详细知识指南。

    0.前言召回排序流程策略算法简介 推荐可分为以下四个流程,分别是召回.粗排.精排以及重排: 召回是源头,在某种意义上决定着整个推荐的天花板: 粗排是初筛,一般不会上复杂模型: 精排是整个推荐环节的重中 ...