ProtoBuf:proto3语法、更新消息、option选项

ProtoBuf使用特点

ProtoBuf是需要依赖通过编译生成的头文件和源文件来使用的

class XXX{
    定义一系列属性字段
    处理字段的方法: get set
    处理类的方法:序列化、反序列化(将结构化的数据与二进制字节序列之间进行相互转化)
}

在ProtoBuf中,这不叫做类,叫做message(.proto文件),只需要定义一系列属性字段,经过PB编译器,生成上面的代码

.proto文件结构

  • 首行:语法指定行,没有指定,就默认使用proto2语法
  • package定义在.proto文件内的声明空间
  • 定义联系人message,建议使用大驼峰;字段编号必须存在,因为编译原理。
  • 字段编号不能够使用19000~19999,这个范围的数字是被ProtoBur所保留的。
  • int32和sint32都是变长编码,但是对于负数编码建议采取sint32;fixed32和fixed64是定长编码
  • 对于将会经常被使用的字段,建议将其字段编号设置为1~15,因为这个区间的数字序列化之后只有一个字节。
syntax = "proto3";

package contacts;

message PeopleInfo{
    string name = 1; 
    int32 age = 2; 
}

.proto文件的编译

编译命令:protoc -I path/to/proto --cpp_out=target_dir name.proto其中protoc为基本编译指令,-I用来指定编译.proto文件的搜索路径,-cpp_out表示生成的C++代码的存放位置。

随后,将在我们的指定位置生成分别存放方法声明和实现的头源文件。其中有各种get、set方法,至于序列化和反序列化,是继承自父类MessageLite。

序列化的函数均为const函数,并不会将类中的内容进行修改,只是将序列化之后的内容保存到函数指定的位置

.proto编译出的结果是二进制序列,所以破解成本增大,但是也意味着相对安全

编译使用protobuf的代码:g++ -o TestPb main.cc contacts.pb.cc -std=c++11 -l protobuf

要指定生成PB编译器生成的源文件,同时还要指定c++标准为c++11,因为PB编译器生成的源文件含有C++11语法,同时还需要指定protobuf链接库,否则会有语法错误.

proto3语法

字段规则与消息定义.

写入

在定义message时,可以嵌套定义(在一个message中定义另一个message),也可以在一个.proto文件中定义多个message,同时,我们也可以引入在另一个.proto文件中定义的message(需要注意的是,通过impout引入之后,还需要在定义字段时,加上引入文件的package的名称作为使用前缀)
对于嵌套定义的message、enum等,在访问时采取BaseName_Targetname的方式访问。
例如下面的例子中,就是contacts2.PeopleInfo_Address

//通讯录.proto文件
syntax="proto3";
package contacts2;

import "school.proto";

message Phone{
    string number = 1;
}

message PeopleInfo{
    string name = 1;
    int32 age = 2;

    message Address{
        string number = 1;
    }

    Address address = 3;
    repeated Phone phone = 4;
    school.School school = 5; 
}
message Contacts{
    repeated PeopleInfo contacts = 1;
}
//用来引入的school.proto文件
syntax="proto3";
package school;
message School{
    string name = 1;
}

repeated关键字用来创建一个数组。插入的时候,对于非repeated修饰的字段,直接通过set_typename(val)插入即可,对于repeated的字段,通过set_typename()获得一个指向数组的指针,在通过往这个指针中调用set_typename(val)的方式插入(如果其中依然有数组的话,那么继续通过获取指针的方式插入)

//向一个通讯录中插入姓名、年龄、电话的演示(文件)
#include <iostream>
#include <fstream>
#include "./pb_files/contacts.pb.h"

void AddContacts(contacts2::PeopleInfo* people)
{
    std::cout << "----------新增联系人----------" << std::endl;
    std::string name;
    std::cout << "请输入姓名:";
    std::getline(std::cin, name);
    people->set_name(name);

    int age;
    std::cout << "请输入年龄:";
    std::cin >> age;
    people->set_age(age);
    std::cin.ignore(256, '\n');

    for(int i = 1;; i++){
        std::cout << "请输入电话" << i << ":";
        std::string number;
        std::getline(std::cin, number);
        if(number.empty()){
            break;
        }
        contacts2::Phone* phone = people->add_phone();
        phone->set_number(number);
    }
    std::cout << "----------新增成功----------" << std::endl;
}

int main(){
    contacts2::Contacts contacts;
    std::ifstream input("contacts.bin", std::ios::in | std::ios::binary);
    if(!input){
        std::cout << "----------file not exists create one----------" << std::endl;
    }
    else if(!contacts.ParseFromIstream(&input)){
        std::cout << "Parse from file fail" << std::endl;
        return -1;
    }
    std::cout << "----------Parse success----------" << std::endl;

    AddContacts(contacts.add_contacts());

    std::ofstream write("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);
    if(!contacts.SerializeToOstream(&write)){
        std::cout << "write fail" << std::endl;
        return -1;
    } 
    std::cout << "----------write success----------" << std::endl;
    return 0;
}

读取

对于数组的读取,也是获得相应元素的指针,然后从中使用get方法读取,不过set函数采取的是下划线加成员变量名的方式,但是get方法省略的get,直接使用成员名即可

#include <iostream>
#include <fstream>
#include "./pb_files/contacts.pb.h"

void PrintContacts(contacts2::Contacts& contacts){
    for(int i = 0; i < contacts.contacts_size(); i++){
        std::cout << "----------联系人信息" << i + 1 << "----------" << std::endl;
        contacts2::PeopleInfo people = contacts.contacts(i);
        std::cout << "姓名:" << people.name() << std::endl;;
        std::cout << "年龄" << people.age() << std::endl;
        for(int j = 0; j < people.phone_size(); j++){
            contacts2::Phone phone = people.phone(i);
            std::cout << "联系人电话" << j + 1 << phone.number() << std::endl;
        }
    }
}

int main(){
    contacts2::Contacts contacts;
    std::fstream input("contacts.bin", std::ios::in | std::ios::binary);
    if(!contacts.ParseFromIstream(&input)){
        std::cout << "Parse from contacts.bin fail" << std::endl;
        input.close();
        return -1;
    }
    PrintContacts(contacts);
    return 0;
}

decode指令

可以快速查看二进制文件中的内容,默认是从标准输入中读取文件,但是可以通过输入重定向的方式来获取二进制文件的内容。

wangjiale@ubuntu:~/protobuf/proto3$ protoc --decode=contacts2.Contacts contacts.proto < contacts.bin

其中decode指定要查看那个package中的那个message,再指定要看哪个.proto文件,最后输入重定向

枚举类型

在.proto文件中使用enum关键字,字段编号必须从0开始,且字段编号为0的部分将被作为枚举默认值。在同级枚举中,不可存在相同名称的枚举值(对于引入的外部.proto文件,如果未指定package名称,则也算统计枚举,不可重名)
获取枚举值的名称(不是数字,采用EnumName_Name(target)的方法)

Any类型

是一个泛型,可以转化为任意其他类型。

使用时需要引入google/protobuf/any.proto

需要使用它时,首先需要先实例化一个目标message,然后将目标父指针中的Any类型通过mutable_name()获取(_mutable字段用来获取一个可变引用),获取到之后通过PackFrom()来将目标类型转化为Any类型存储。

读取时,通过UnPackTo来读取到指定message中进行处理

import "google/protobuf/any.proto";
message PeopleInfo{
    string name = 1;
    int32 age = 2;

    repeated Phone phone = 3; 
    google.protobuf.Any data = 4;
}
//写入
    contacts2::Address address;
    people->mutable_data()->PackFrom(address);
//读取
    if(people.has_data() && people.data().Is<contacts2::Address>()){
        contacts2::Address address;
        people.data().UnpackTo(&address);
    }

oneof

  • 设定的值属于同一个message中其他元素的同一级,所以字段编号、名称不能重复。设置时也直接采用set_name的方式即可
  • oneof不能设定repeated修饰,oneof中的内容,都通过枚举类型的方式存储,但是多了一个not known字段。
  • 读取的时候,要获取时,通过oneof_name_case方式获取,如果需要获得int编号,可以通过OneofNameCase()方式获取
  • 枚举值的命名,通过kName的方式生成。

map

map类型通过键值对的方式存储,key值可以是除了float和byte以外的任意标量类型,value可以是任意类型;与Any一样不能用repeated修饰

插入数据时,通过mutable_字段函数即可获取可改变的对象指针,然后通过insert构造pair插入即可;读取时,使用_size字段结合迭代器即可完成遍历,通过first获取key,second获取value

字段的删除

字段是不建议直接删除的,如果删除,要保证该字段编号不再被其他字段使用,因为PB编译器是根据字段编号来进行值的存储分配的,如果继续使用,会导致可能会字段内容的不一致。解决方法是使用reserved关键字,使用reserved修饰的字段,会被pb编译器警告该字段不可用。

未知字段

解析不出的字段在3.5版本恢复之后会被保留在未知字段。

通过MessageList中的GetReflection来获得指向google::protobuf::Reflection的指针,通过其中的GetUnknownFields(const Message& message),获得UnknownFidlsSet,遍历获取所有的未知字段。

未知字段信息的获取,首先通过number获取字段编号,通过type获取类型的枚举值,在通过对应的类型名称的方法就可以获取未知字段的值。
(比如TYPE_TARINT表示未知字段是int32或者int64类型,可以通过varint来获取具体值)

前后兼容性

一般来说,只要不破坏字段规则是不会破坏前后兼容性的;

向前兼容是指老模块能够正确识别新模块生成或发出的协议,在3.5之后可以做到,不可识别的字段会被放在未知字段当中;向后兼容是指新模块能够正确识别老模块发出或生成的协议。

option选项

optimize_for设置pb编译器优化级别

  • SPEED高度优化,默认选项,速度更快,但是大小更大
  • CODE_SIZE,生成最少的类,占用空间更小,但是运行效率更低
  • LITE_RUNTIME,效率高,空间也小,代价是牺牲了反射能力(比如说不再继承Message而是直接继承子MessageLite,会导致不能够使用Message的方法比如reflection,只能够使用序列化、反序列化方法)。

allow_alias=true允许别名。

允许别名之后,同一个字段编号可以设置相同名称

Protobuf3 中,可以通过自定义 option 来扩展 Protobuf3语法,实现一些特定的功能。在 Protobuf3 中,option 是一个关键字,用于在 .proto 文件中定义选项,可以用于定义消息、枚举、服务等。以下是 Protobuf3 自定义 option语法详解: 1. 定义 option 在 .proto 文件中,可以使用以下语法来定义 option: ``` option <option_name> = <option_value>; ``` 其中,`<option_name>` 是自定义的 option 名称,`<option_value>` 是 option 的值,可以是一个字符串、数字、布尔值等。 2. 使用 option 在使用自定义 option 时,可以将其加入到消息、枚举、服务等的定义中。以下是使用自定义 option语法: ``` <message/enum/service> <name> { option <option_name> = <option_value>; ... } ``` 其中,`<message/enum/service>` 是消息、枚举、服务的类型,`<name>` 是其名称,`<option_name>` 是自定义的 option 名称,`<option_value>` 是 option 的值。 3. 解析 option 在使用自定义 option 后,可以通过解析 option 来获取其值。以下是解析 option语法: ``` message <name> { ... optional <option_type> <option_name> = <field_number> [(option) = <option_value>]; ... } ``` 其中,`<name>` 是消息的名称,`<option_type>` 是 option 的类型,`<option_name>` 是自定义的 option 名称,`<field_number>` 是字段号,`(option) = <option_value>` 是 option 的值。 以上是 Protobuf3 自定义 option语法详解,希望能帮助你更好地理解和使用自定义 option
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值