ProtoBuffer-nanopb介绍

目录

  • 一、需求
  • 二、环境
  • 三、相关概念
    • [3.1 protocol buffer介绍](#3.1 protocol buffer介绍)
    • [3.2 nanopb(支持C语言)](#3.2 nanopb(支持C语言))
    • [3.3 proto文件](#3.3 proto文件)
  • 四、proto基本语法
    • [4.1 proto文件的定义](#4.1 proto文件的定义)
    • [4.2 字段规则](#4.2 字段规则)
    • [4.3 字段类型](#4.3 字段类型)
    • [4.4 字段编号](#4.4 字段编号)
    • [4.5 proto语法](#4.5 proto语法)
    • [4.6 进阶语法](#4.6 进阶语法)
      • [4.6.1 message嵌套](#4.6.1 message嵌套)
      • [4.6.2 enum关键字](#4.6.2 enum关键字)
      • [4.6.3 oneof关键字](#4.6.3 oneof关键字)
  • 五、nanopb分析
    • [5.1 nanopb版本下载](#5.1 nanopb版本下载)
    • [5.2 nanopb相关Api](#5.2 nanopb相关Api)
      • [5.2.1 编码相关API](#5.2.1 编码相关API)
      • [5.2.2 解码相关API](#5.2.2 解码相关API)
    • [5.3 总体结构](#5.3 总体结构)
      • [5.3.1 结构图](#5.3.1 结构图)
      • [5.3.2 相关文件](#5.3.2 相关文件)
  • 六、nanopb应用
    • [6.1 基于Android平台nanopb应用](#6.1 基于Android平台nanopb应用)
      • [6.1.2 定义.proto文件](#6.1.2 定义.proto文件)
      • [6.1.3 生成动态库](#6.1.3 生成动态库)
      • [6.1.4 临时文件](#6.1.4 临时文件)
      • [6.1.5 测试用例](#6.1.5 测试用例)
    • [6.2 基于Windows平台的nanopb应用](#6.2 基于Windows平台的nanopb应用)
      • [6.2.1 环境配置](#6.2.1 环境配置)
      • [6.2.2 临时文件生成](#6.2.2 临时文件生成)
      • [6.2.3 测试用例](#6.2.3 测试用例)
  • 七、遗留问题
  • 八、代码仓库
  • 九、参考资料

一、需求

  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类型:

c 复制代码
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在消息定义中添加每个可能值的常量来非常简单的执行此操作:

c 复制代码
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 | 必须有 |
| Nanopb runtime library | pb_common.h pb_common.c | 必须有 |
| Nanopb runtime library | pb_decode.h pb_decode.c | 编码相关 |
| Nanopb runtime library | pb_encode.h pb_encode.c | 解码相关 |
| Protocol description | MyMessage.proto | 必须有 |
| Protocol description | MyMessage.pb.c MyMessage.pb.h | 编译后自动生成 |

六、nanopb应用

6.1 基于Android平台nanopb应用

基于Android平台,创建一个c程序,用于测试nanopb的使用规则。文末附上相关demo仓库地址。

6.1.2 定义.proto文件

定义.proto文件,定义数据结构与格式。

c 复制代码
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等相关编译标识未找到,导致无法正常编译,故将其先编译成一个动态库)

c 复制代码
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代码仓库。

c 复制代码
@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嵌套使用测试,其相关流程如下:

c 复制代码
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 测试用例

c 复制代码
#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