protobuf数据类型

protobuf特殊数据类型

  • 一,特殊数据类型
    • 1,enum
      • 使用规则
      • 注意事项
    • 2,Any
    • 3,oneof
    • 4,map
    • 通讯录demo升级
  • 二,默认值
  • 三,更新消息
    • 更新规则
    • 保留字段reserved
    • 未知字段
      • 获取未知字段
  • 四,选项option
    • 常见选项

一,特殊数据类型

借助这些新的数据类型,来继续完善前面使用的通讯录demo。

1,enum

使用规则

首先见一下枚举类型如何使用,以及编译后生成的.h文件中为我们提供了那些方法:

syntax = "proto3";

enum PhoneType{
    MP = 0;    //移动电话
    TEL = 1;   //固定电话
}

message Phone {
    string number = 1;
    PhoneType type = 2;
}

Phone这个消息体内包括电话号码,以及电话的类型(座机还是手机)。
看看编译后的文件中为我们提供了那些关于枚举类型的方法:

protoc --cpp_out=./ test_enum.proto //使用该指令编译.proto文件

protobuf数据类型_第1张图片
PhoneType_Name();函数作用是将枚举常量转化为,对应的枚举值的名称。
(例如:0 -> MP)
protobuf数据类型_第2张图片

使用枚举时的规则:

0值常量必须存在且必须为第一个。
枚举定义可以定义在消息体外侧,也可以定义在消息体内。(嵌套定义)
枚举常量取值范围在32位整数范围内,不建议使用负数。(负值无效,与编码规则有关)

注意事项

同级的枚举常量中,各个枚举类型中常量的名称不能重名。

//在同一proto文件下,会编译报错
enum PhoneType{
    MP = 0;    //移动电话
    TEL = 1;   //固定电话
}
enum Test {
    TEL = 0;
}

在这里插入图片描述
在同一proto文件中,外层的枚举类型和嵌套定义在消息体内的枚举类型不算同级。

enum PhoneType{
    MP = 0;    //移动电话
    TEL = 1;   //固定电话
}

message Phone {
    string number = 1;
    PhoneType type = 2;
    enum Test {
        TEL = 0;
    }
}

引入其他proto文件时,若两个proto文件都没有声明package,且两个枚举类型都在最外侧,算同级处理。

//test_enum2.proto
syntax = "proto3";
enum Test {
    TEL = 0;
}

//test_enum.proto
syntax = "proto3";
import "test_enum2.proto";
enum PhoneType{
    MP = 0;    //移动电话
    TEL = 1;   //固定电话
}

message Phone {
    string number = 1;
    PhoneType type = 2;
}

同样会编译报错。
在这里插入图片描述
若引入其他proto文件时,枚举类型定义在package内部,那么两个枚举类型不算同级。

//test_enum2.proto
syntax = "proto3";
package test2;
enum Test {
    TEL = 0;
}

加上package后就不会编译报错。

2,Any

字段声明为Any类型,可以理解为泛型类型。在使用时。Any中可以存储任意类型的消息类型。同时Any类型可以用repeated修饰。
Any类型是google已经帮我们定义好的类型,可以在protobuf的安装路径下的include路ing下查看。
protobuf数据类型_第3张图片
在使用时引入 “google/protobuf/any.proto”文件即可。并且使用时要指明其命名空间。

syntax = "proto3";
import "google/protobuf/any.proto";

message Test {
    string name = 1;
    google.protobuf.Any msg = 2;
}

看下编译后生成了哪些它用于操作Any类型的方法。
protobuf数据类型_第4张图片
mutable函数返回的是指向该类型的指针变量,这类方法以为我们开辟好空间,我们可以通过指针来对这块空间操作。

Any类型可以存储任意类型的消息,这就要设计任意类型和Any类型之间的转换,any.pb.h已经为我们实现了这些方法。
在这里插入图片描述
在这里插入图片描述
PackFrom方法是将别的消息类型转化为Any类型,UnpackTo方法是将Any类型转化为特定的消息类型。

在这里插入图片描述
any类型中还带了一个Is函数,其作用是判断any中存储的消息类型是否与模板类型一致。

3,oneof

syntax = "proto3";
message PeopleInfo {
    string name  = 1;
    string phone_number = 2;
    oneof other_contact {
        string qq = 3;
        string wechat = 4;
    }
}

oneof 顾名思义:多选一,如果消息中声明了很多字段,但最终只会有一个字段会被设置,那么就可以用oneof来完成这个效果。上面的other_contact只能被设置为qq 和 微信中的一个。

protobuf数据类型_第5张图片
对于qq,wechat除了提供get/set方法外,还提供了一个_case();的接口,
首先,编译器会将oneof中的多个字段名定义为枚举类型,_case函数返回的就是一个枚举类型表示设置了哪一个字段。
protobuf数据类型_第6张图片
注意:如果对oneof字段设置了多次,那么只会保留最后一次设置的字段。

4,map

protobuf中也支持map这种类型,注意的是map中key-type可以是除了float和bytes类型之外的所有标量类型。value-type可以是任意类型。
注意: map类型不能被repeated修饰。并且插入到map中的数据是无序的。

syntax = "proto3";

message PeopleInfo {
    string name = 1;
    map<string,string> remark = 2;
}

protobuf数据类型_第7张图片

map类型的set放法也是返回一块已开辟的空间的地址,用户通过指针去操控这块空间。

通讯录demo升级

contacts.proto

syntax = "proto3";
package contacts;
import "google/protobuf/any.proto";
message Address {
    string home_address = 1;
    string uint_address = 2;
}
message PeopleInfo {
    string name = 1;       //姓名
    int32 age = 2;         //年龄
    enum PhoneType {
        MP = 0;
        TEL = 1;
    }
    message Phone {
        string number = 1;
        PhoneType type = 2;
    }
    repeated Phone phone = 3; //多个电话 [电话类型]
    google.protobuf.Any data = 4;  //地址信息
    oneof other_contact {
        string qq = 5;
        string wechat = 6;       //其他联系方式
    }
    map<string,string> remark = 7; //备注信息
}
message Contact {
    repeated PeopleInfo people = 1;
}

write.cc

#include 
#include 
#include 
#include "contacts.pb.h"

using namespace std;

void AddPerson(contacts::PeopleInfo* p) {
    cout << "------------添加联系人------------" << endl;
    cout << "------联系人姓名: ";
    string name;
    getline(cin,name);
    p->set_name(name);
    cout << "------联系人年龄: ";
    int age;
    cin >> age;
    cin.ignore(256,'\n'); //清除缓冲区内的'\n'
    p->set_age(age);
    for (int i = 0; ; i++) {
        cout << "联系人第" << i + 1 << "个电话(只输入空格结束): ";
        string number;
        getline(cin,number);
        if (number.empty()) {
            break;
        }
        contacts::PeopleInfo_Phone* phone = p->add_phone();
        phone->set_number(number);
        cout << "请输入电话类型(1.移动电话 2.固定电话): ";
        int type;
        cin >> type;
        cin.ignore(256,'\n');
        switch (type) {
            case 1 :
                phone->set_type(contacts::PeopleInfo_PhoneType::PeopleInfo_PhoneType_MP);
                break;
            case 2 :
                phone->set_type(contacts::PeopleInfo_PhoneType::PeopleInfo_PhoneType_TEL);
                break;
            default:
                cout << "输入有误!!!" << endl;
                break;
        }
    }
    contacts::Address address;
    cout << "------请输入家庭地址: ";
    string home_address;
    getline(cin,home_address);
    address.set_home_address(home_address);
    cout << "------请输入工作地址: ";
    string unit_address;
    getline(cin,unit_address);
    address.set_uint_address(unit_address);
    google::protobuf::Any* data = p->mutable_data();
    data->PackFrom(address);

    cout << "请输入其他联系方式(1.qq 2.微信): ";
    int other;
    cin >> other;
    cin.ignore(256,'\n');
    switch (other) {
        case 1 :
        {
            string qq;
            cout << "------用户qq号码: ";
            getline(cin,qq);
            p->set_qq(qq);
            break;
        }
        case 2 :
        {
            string wechat;
            cout << "------用户微信号码: ";
            getline(cin,wechat);
            p->set_wechat(wechat);
            break;
        }
        default:
            cout << "输入有误!!!" << endl;
            break;
    }
    google::protobuf::Map< std::string, std::string >* map = p->mutable_remark();
    for (int i = 0; ; i++) {
        cout << "请输入第" << i + 1 << "个备注标题(输入空格结束): ";
        string key;
        getline(cin,key);
        if (key.empty()) {
            break;
        }
        string value;
        cout << "请输入第" << i + 1 << "个备注内容: ";
        getline(cin,key);
        map->insert({key,value});
    }

    cout << "-----------成功添加联系人-----------" << endl;
}
int main() {
    contacts::Contact contact;
    //1.打开通讯录文件将文件的内容反序列化到内存中
    fstream input("./contacts.bin",ios::in | ios::binary);
    if (!input) {
        cout << "contacts.bin not exist,create it!!!" << endl;
    } else if (!contact.ParseFromIstream(&input)) {
        cout << "prase error" << endl;
        input.close();
        return -1;
    }

    fstream output("./contacts.bin",ios::out | ios::binary | ios::trunc);
    AddPerson(contact.add_people());
    //序列化写入文件
    if (!contact.SerializeToOstream(&output)) {
        cout << "Serialize error" << endl;
        input.close();
        output.close();
        return -1;
    }
    cout << "write sucess!!!" << endl;
    input.close();
    output.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

read.cc

#include 
#include 
#include 
#include "contacts.pb.h"

using namespace std;

void ReadContact(const contacts::Contact& contact) {
    for (int i = 0; i < contact.people_size(); i++) {
        const ::contacts::PeopleInfo& person = contact.people(i);
        cout << "------联系人" << i + 1 << "------" << endl;
        cout << "姓名: " << person.name() << endl;
        cout << "年龄: " << person.age() << endl;
        //电话信息
        for (int j = 0; j < person.phone_size(); j++) {
            const ::contacts::PeopleInfo_Phone& phone = person.phone(j);
            cout << "第" << j + 1 << "电话号码: " << phone.number()
                 << "     [" << contacts::PeopleInfo::PhoneType_Name(phone.type()) << "]" << endl; 
        }
        if (person.has_data() && person.data().Is<contacts::Address>()) {
            contacts::Address address;
            person.data().UnpackTo(&address);
            cout << "家庭住址: " << address.home_address() << endl;
            cout << "工作地址: " << address.uint_address() << endl;
        }
        switch (person.other_contact_case()) {
            case contacts::PeopleInfo::kQq :
                cout << "qq号码: " << person.qq() << endl;
                break;
            case contacts::PeopleInfo::kWechat :
                cout << "微信号码: " << person.wechat() << endl;
            default:
                break;
        }
        if (person.remark().size()) {
            auto it = person.remark().begin();
            while (it != person.remark().end()) {
                cout << "备注信息: " << endl
                    << "          " << it->first << ":" << it->second << endl;
                ++it;
            }
        }
    }
}
int main (){
    fstream input("contacts.bin",ios::in | ios::binary);
    //1.从文件中反序列化读取通讯录信息
    contacts::Contact contact;
    if (!contact.ParseFromIstream(&input)) {
        cout << "prase from file error" << endl;
    }
    ReadContact(contact);
    return 0;
}

二,默认值

反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中有相应字段,该字段就会被设置成该字段对应的默认值。不同类型对应的默认值是不同的:

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于布尔值,默认值为false。
  • 对于数值类型,默认值为0。(浮点型就是0.0)
  • 对于枚举,默认值是第一个枚举常量,必须为0。
  • 对于消息字段,未设置该字段。它的取值依赖于语言。
  • 对于设置了repeated的字段,默认值是空的。
  • 对于消息类型,oneof字段,和any字段,C++和Java语言中都有has_方法来检测当前字段是否被设置。

三,更新消息

假设通讯录中只有三个字段,姓名,年龄和电话。在写端将年龄字段修改为了生日,但是在读端并没有做出相应的修改,在添加新的联系人时会发生什么现象呢?

//修改之前的proto文件,读端和写段是一致的
message PeopleInfo{
    string name = 1;
    int32 age = 2;
    string Phone = 3;
}
message Contact{
    repeated PeopleInfo people = 1;
}
//修改之后,写端的proto文件做了以下修改
message PeopleInfo{
    string name = 1;
    int32 birthday = 2;  //修改项
    string Phone = 3;
}
message Contact{
    repeated PeopleInfo people = 1;
}

在这里插入图片描述
很显然出了一个问题,写端的生日字段在反序列化的时候被解释成了年龄字段,这是因为我们并没有遵循更新消息的规则,而导致的问题。

更新规则

更新消息时注意以下几点:

  • 禁⽌修改任何已有字段的字段编号。
  • 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤。不建议直接删除或注释掉字段。
  • int32, uint32, int64, uint64 和 bool 是完全兼容的。可以从这些类型中的⼀个改为另⼀个,⽽不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采⽤与 C++ ⼀致的处理⽅案(例如,若将 64 位整数当做 32 位进⾏读取,它将被截断为 32 位)。
  • sint32 和 sint64 相互兼容但不与其他的整型兼容。
  • string 和 bytes 在合法 UTF-8 字节前提下也是兼容的。
  • bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
  • fixed32 与 sfixed32 兼容, fixed64 与 sfixed64兼容。
  • enum 与 int32,uint32, int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语⾔采⽤不同的处理⽅案:例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表⽰是依赖于编程语⾔的。整型字段总是会保持其的值。
  • oneof:
    ◦ 将⼀个单独的值更改为 新 oneof 类型成员之⼀是安全和⼆进制兼容的。
    ◦ 若确定没有代码⼀次性设置多个值那么将多个字段移⼊⼀个新 oneof 类型也是可⾏的。
    ◦ 将任何字段移⼊已存在的 oneof 类型是不安全的。

保留字段reserved

如果通过 删除 或 注释掉 字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号。将来使⽤该 .proto 的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。
为了防止这一现象发生,可以使用reserved关键字,对之前的字段编号或者名称进行保留,当我们再使用这些编号或者名称的时候,编译器会报错。

message PeopleInfo{
    reserved 2 , 7, 100 to 200; //可以一次保留多个字段编号用逗号隔开
    reserved "age";             //也可以像 100 to 200这样使用,表示保留的100到200的字段编号
    string name = 1;            //也可以一次保留多个字段名称用逗号隔开即可。
    //int32 age = 2;
    int32 birthday = 4; 
    string Phone = 3;
}

未知字段

在上面我们更新了写段的proto文件,对于读端的proto文件没有修改,并且我们对旧字段的名称和编号做了reserved处理,这样就意味着序列化的二进制文件中存在生日这个字段,但是在读端反序列化后的对象中不存在生日的字段,这时候这个生日字段就会被当作未知字段来处理。

  • 未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表⽰⽅式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
  • 本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引⼊了对未知字段的保留机制。所以在 3.5 或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。

获取未知字段

protobuf数据类型_第8张图片

获取未知字段的流程:GetReflection();—>GetUnknownfields();---->field();

protobuf数据类型_第9张图片
UnknowField类中,提供了获取未知字段的字段编号,字段的值,字段类型等相关方法。并且对字段类型进行了划分也就是上图中的枚举类型。

对于上面使用reserved保留字段的名称和字段编号后,再次添加联系人的时候,在由于读端仍旧是老版本的proto文件,那么对于生日字段对于其就是未知字段。

const google::protobuf::Reflection *reflection = person.GetReflection();
const google::protobuf::UnknownFieldSet &set = reflection->GetUnknownFields(person);
for (int j = 0; j < set.field_count(); j++)
{
    const google::protobuf::UnknownField &f = set.field(j);
    cout << "未知字段: " << j + 1 << " :" << endl;
    cout << "字段编号: " << f.number()
         << "类型: " << f.type();

    switch (f.type())
    {
    case google::protobuf::UnknownField::Type::TYPE_VARINT:
        cout << " 值: " << f.varint() << endl;
        break;
    case google::protobuf::UnknownField::Type::TYPE_LENGTH_DELIMITED:
        cout << " 值: " << f.length_delimited() << endl;
        break;

        // case ...
    }
}

protobuf数据类型_第10张图片

四,选项option

.proto ⽂件中可以声明许多选项,使⽤ option 标注。选项能影响 proto 编译器的某些处理⽅式。选项的完整列表在google/protobuf/descriptor.proto中定义。

常见选项

option optimize_for = LITE_RUNTIME;

• optimize_for : 该选项为⽂件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED 、CODE_SIZE 、 LITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译 .proto ⽂件后生成的代码内容不同。

◦ SPEED : protoc 编译器将⽣成的代码是⾼度优化的,代码运⾏效率⾼,但是由此⽣成的代码编译后会占⽤更多的空间。 SPEED 是默认选项。

◦ CODE_SIZE : proto 编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运⾏效率较低。这种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中。

◦ LITE_RUNTIME : ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供 encoding+序列化 功能,所以我们在链接 BP 库时仅需链接libprotobuf-lite,⽽⾮libprotobuf。这种模式通常⽤于资源有限的平台,例如移动⼿机平台中。

enum PhoneType {
 option allow_alias = true;
 MP = 0;
 TEL = 1;
 LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}

allow_alias : 允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。

你可能感兴趣的:(Linux,c++,linux,protobuf)