一、需求

  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. [转帖]linux--Segfault详解

    linux--Segfault详解 1 简介 1.1 段错误的定义 1.2 痛点 2 知识点 2.1 报错内容 2.2 error number 3 排除步骤(借助汇编) 3.1 日志确定错误类型 3 ...

  2. 01显示转换隐私转换 有8个值转为false 显示转换Number的注意点

    prompt()函数会弹出一个框,接受用户的输入.但是在实际的开发中.这样的操作是很少. 至少在我做开发的过程中没有使用过.我二没有看见人家在过开发的使用使用. console.log(Number( ...

  3. elementui表格内容超出显示省略号

    有些时候表格的内容太长了: 但是elementui中的表格,会进行换行处理: 此时表格的高度就会发生变化 这样就不好看,此时就要进行省略号来出来这个问题: el-table是有这个控制属性的::sho ...

  4. 【云原生】为什么要虚拟化,为什么要容器,为什么要Docker,为什么要K8S?

    前言 如标题中的问题所提到的虚拟化,容器,Docker和K8s那样,我们不妨这样问:这些技术到底适用于哪些场景,有没有别的技术可以替代?这些技术的优劣在哪里? 下面我将针对性地从以上几个问题的出发点, ...

  5. 史上最大电池!小米智能家庭屏Pro 8图赏

    今天小米智能家庭屏 Pro 8正式开售,集智能家居中控,智能网关以及娱乐教育三大功能为一体,首发749元. 它是一款全新的智能生态产品中控屏,配备了7500mAh大容量电池以及通用性更好的USB Ty ...

  6. 1cm+1kg纤薄身材 带来14小时超长续航!华硕灵耀13 2023评测

    一.前言:华硕推出1cm+1kg超轻薄笔记本 还有OLED好屏加持 随着处理器能效的不断进步,轻薄本已经不存在性能羸弱的问题了,也给了各大厂商极大的发挥空间,不过市面上的轻薄本厚度普遍在15~20mm ...

  7. CF351D Jeff and Removing Periods 题解

    题目链接:CF 或者 洛谷 挺有意思的题,一开始看到了 \(start+k\times step\),以为是根号分治方向的题,结果发现这题还给了一个"重排"操作玩玩.所以这题其实算 ...

  8. 错误:基于tensorflow识别mnist数据集出现ResourceExhaustedError (see above for traceback): OOM when allocating tensor with shape[10000,32,28,28] and type float on

    错误:最近,在尝试运行我以前博客代码的时候出现了如下错误 2020-04-03 10:53:22.982491: W tensorflow/core/common_runtime/bfc_alloca ...

  9. 【译】介绍 MSTest Runner – CLI、Visual Studio 等

    原文 | Amaury Levé, Marco Rossignoli, Jakub Jareš 翻译 | 郑子铭 我们很高兴推出 MSTest runner,这是一个用于 MSTest 测试的新型轻量 ...

  10. 《ASP.ENT Core 与 RESTful API 开发实战》-- 读书笔记(第1章)

    第 1 章 REST 简介 1.1 API 与 REST API 是一个系统向外暴露或公开的一套接口,通过这些接口,外部应用程序能够访问该系统 REST 是一种基于资源的架构风格,任何能够命名的对象都 ...