首页 > 其他分享 >ProtoBuffer-nanopb介绍

ProtoBuffer-nanopb介绍

时间:2024-02-27 09:25:00浏览次数:29  
标签:UserMessage nanopb proto ProtoBuffer 介绍 Sex pb message

目录

一、需求

  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程序需要将如下几个关键文件拷贝到对应工程:

![](/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();
    ...
}

打印结果:

七、遗留问题

  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

标签:UserMessage,nanopb,proto,ProtoBuffer,介绍,Sex,pb,message
From: https://www.cnblogs.com/zhiqinlin/p/18036136

相关文章

  • Sass基础功能介绍
    本文将从入门角度介绍Sass的极大特性和基本语法,由于作者初学Sass,若有不当之处还请指正.Sass介绍Sass是一个CSS预处理器。Sass是CSS扩展语言,可以帮助我们减少CSS重复的代码,使css代码更"像代码"。有sass和scss两种写法,有区别符号分别为缩进和大括号,下......
  • 智慧安防视频监控平台EasyCVR通道播放支持添加水印及操作步骤介绍
    智慧安防视频监控平台EasyCVR采用了开放式的网络结构,系统可支持的接入协议包括:国标GB28181、RTSP/Onvif、RTMP,以及厂家的私有协议与SDK,如:海康ehome、海康sdk、大华sdk、宇视sdk、华为sdk、萤石云sdk、乐橙sdk等,兼容各品牌的IPC、NVR、移动手持终端、执法仪、布控球、无人机等设备......
  • 一机在手,畅游全园——麻城孝感乡文化园电子导游介绍
    作为大型景区(或自然园区等),你是否担心游客有以下苦恼“卫生间在哪?””停车场怎么去?“......别担心!看看人家的电子导览怎么做的!下面请和小编一起解锁这个宝藏平台吧。​作者:轻轻的烟雾(z281099678) 搜索并关注园区官方微信公众号“麻城孝感乡文化园”,在底部菜单栏选择【电......
  • 关于 ‘--exec’ 参数( find 命令)及介绍 ‘xargs ’命令区别
    findgoal.log.*.gz-mtime+2-execrm-rf{}\;findgoal.log.*.gz-mtime+3|xargsrm-f前言:find命令一直都是系统管理员的常用命令之一,其参数中“-exec”尤其实用。而“xargs”命令,针对查询也有属于自己的见解。本文着重讲解的是围绕find命令查询为主线,使用-exe......
  • PS基本介绍
    1.主界面构成1.1菜单栏1.2工具栏自定义工具栏切换单列双列显示1.3选项栏1.4工作信息栏左键单击按住不放即可查看详细信息1.5快捷键1.ctrl+鼠标滚轮:水平移动2.shift+鼠标滚轮:垂直移动3.Alt+鼠标滚轮:缩放画面4.G:油漆桶工具1.6修改工作面颜色......
  • 前端介绍
    前端Web前端开发者-学习Web开发|MDN(mozilla.org)【一】前端的概念【1】前端与后端前端是指网站或Web应用程序的用户界面部分,负责展示数据和与用户进行交互。它是用户直接接触和感知到的部分,包括网页的布局、样式和交互功能。后端是指网站或Web应用程序的服务......
  • Ehcache 介绍(1)--Ehcache 功能特性
    Ehcache是一个开源的、基于标准的缓存工具,它能提升性能、减轻数据库负载并简化可扩展性。由于其稳健性、经得起考验的特点以及与其他流行框架的集成,Ehcache成为最广泛使用的基于Java的缓存工具。Ehcache从进程内缓存一直扩展到混合的进程内/进程外部署,可以处理TB的数据。1......
  • Cursor 介绍与基础生成用法
    Cursor介绍与基础生成用法实验介绍Cursor是一款与OpenAI合作并且基于GPT-4的新一代辅助编程神器,国内直接可以访问,它可以根据你的输入和需求自动生成代码片段,还可以帮助你重构、理解和优化代码,提高开发效率。在本节课程中,我们将介绍Cursor的基本使用方法。知识点Curso......
  • 多线程系列(八) -ReentrantLock基本用法介绍
    一、简介在之前的线程系列文章中,我们介绍到了使用synchronized关键字可以实现线程同步安全的效果,以及采用wait()、notify()和notifyAll()方法,可以实现多个线程之间的通信协调,基本可以满足并发编程的需求。但是采用synchronized进行加锁,这种锁一般都比较重,里面的实现机制也非常复......
  • ffmpeg 工具及命令介绍
    ffprobe工具介绍查看帮助信息:ffprobe--help使用方式:ffprobe[OPTIONS][INPUT_FILE]查看多媒体数据包:ffprobe-show_packetsoutput.mp4ffprobe-show_packets-show_dataoutput.mp4查看封装格式:ffprobe-show_formatoutput.mp4查看视频文件的帧信息:ffprobe-sh......