Protocol Buffer-nanopb介绍

一、需求

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

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

posted @ 2024-02-27 09:14  林奋斗同学  阅读(776)  评论(0编辑  收藏  举报