proto3语法详解

目录

字段规则和消息类型的定义与使用

编译.proto文件

实现通讯录2.0版本

enum类型

实现通讯录2.1版本

 Any类型

实现通讯录2.2版本

oneof类型

实现通讯录2.3版本

map类型

实现通讯录2.4版本

默认值

更新消息

reserved

前后兼容性

未知字段

option选项


为了具体讲解 proto3语法,将通讯录的需求变化为:

  • 不再打印联系人的序列化结果,而是将通讯录序列化后并写入文件

  • 从文件中将通讯录解析出来,并进行打印

  • 新增联系人属性,共包括:姓名、年龄、电话信息、地址、其他联系方式、备注

下面将使用不同的 proto3 语法,将上述需求依次实现,帮助我们更好的理解 proto语法


字段规则和消息类型的定义与使用

消息的字段可以用下面几种规则来修饰:

  • singular:消息中可以包含该字段零次或一次(不超过一次),proto3 语法中,字段默认使用该规则

  • repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组

现在新增电话号码的联系人属性,因为一个人可能有多个电话号码,所以需要加 repeated,表示可以由多个该字段

方式一

方式二

另一种方式是,将电话信息单独定义一个类,也就是新增一个 message:
不同的 message,字段编号可以从1开始

方式三

方式二是在 PeopleInfo外面定义 Phone,也可以嵌套定义,也就是在 PeopleInfo 里面定义:
同样嵌套定义的 message,字段编号可以从1开始

方式四

上面的几种方式都是在同一个 .proto文件 中定义的message,也可以在不同的文件,例如创建一个 phone.proto,里面创建 message Phone:

此时在 contacts.proto文件 中,就需要 import 引入 phone.proto,并在使用 Phone 时,加上 package 指定的 phone 命名空间,如果 phone.proto 中没有写 package,这里就不需要加了:


编译.proto文件

在通讯录2.0版本,只新增联系人属性,共包括:姓名、年龄、电话信息

首先采用嵌套定义的方式,新增 电话信息:

又因为需求是将一个 通讯录 序列化后写入文件中,而此时只定义了 联系人message,并没有定义 通讯录message,所以新增 通讯录message:

此时编译 proto文件:

会生成对应的头文件和源文件:

又因为这个 proto文件 中,定义了三个 message,所以在生成的头文件中会有相应的类:

因为在 PeopleInfo 中有类似于数组的成员,所以在编译后,针对这个成员肯定有类似的插入数组的方法:

同样通讯录类中也有类似的方法:

此时的 contacts.proto文件:

syntax = "proto3";
package contact2;

// 定义联系人 message
message PeopleInfo
{
    string name = 1;  // 姓名
    int32 age = 2;    // 年龄
    message Phone
    {
        string number = 1;
    }
    repeated Phone phone = 3; // 电话信息
}

// 通讯录 message
message Contacts
{
    repeated PeopleInfo contacts = 1;
}


实现通讯录2.0版本

将通讯录序列化后写入文件中

下面实现将通讯录序列化后写入文件中,分为下面三步:

  • 读取本地已存在的通讯录文件

  • 向通讯录中只添加一个联系人属性(其余属性后续添加)

  • 将通讯录序列化后并写入文件中的功能

首先创建 makefile 和 write.cc,方便编译:

makefile:

write:write.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
	rm -f write

write.cc:

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

using namespace std;

void AddPeopleInfo(contact2::PeopleInfo* people)
{
    cout << "-------------新增联系人-------------" << endl;

    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people->set_name(name);

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

    for(int i = 1; ; i++)
    {
        cout << "请输入联系人电话" << i << "(只输入回车表示输入完毕): ";
        string number;
        getline(cin, number);
        if(number.empty())
        {
            break;
        }

        contact2::PeopleInfo_Phone* phone = people->add_phone();
        phone->set_number(number);
    }

    cout << "-----------添加联系人成功-----------" << endl;
}

int main()
{
    contact2::Contacts contacts;
    // 1. 读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if(!input)
    {
        cout << "contacts.bin不存在, 创建了一个新的该文件" << endl;
    }
    else if(!contacts.ParseFromIstream(&input))
    {
        cerr << "反序列化失败" << endl;
        input.close();
        return -1;
    }

    // 2. 向通讯录中添加一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 3. 将通讯录序列化后并写入文件中
    fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
    if(!contacts.SerializeToOstream(&output))
    {
        cerr << "序列化失败" << endl;
        input.close();
        output.close();
        return -1;
    }
    cout << "序列化成功" << endl;

    return 0;
}

write.cc 代码中的注意事项

  • 因为 ProtoBuf 序列化的结果是二进制字节序列,所以 input 需要传入ios::binary

  • 程序中任意地方如果出现失败的情况,都需要将 input/output close掉

  • output 之所以传入 ios::trunc 覆盖写入,是因为在最开始就已经读取本地文件了

  • ParseFromIstream方法实现了先读取本地文件,再将数据进行反序列化操作

  • SerializeToOstream方法实现了将通讯录对象序列化,并将序列化对象写入文件中

  • AddPeopleInfo函数传入自动生成的 add_contacts函数,是利用该函数的返回值添加内容

  • cin.ignore(256, '\n') 表示清除输入缓冲区中的 \n 字符就结束,或是清除了 256个字符就结束,这句代码是为了消除输入完年龄后的回车键,避免影响接下来的号码的输入,

  • AddPeopleInfo函数中插入电话号码时,同样利用 add_phone函数 的返回值插入


此时执行 make,再运行生成的 write,输入后就添加成功:

就会生成一个 contacts.bin 的二进制文件:

因为是二进制文件,所以会出现一些乱码的情况,到此完成联系人的添加,并序列化后写入文件中了

可以使用 hexdump工具 查看二进制文件,将其转化为 16进制 和 对应的ASCII码,执行:

hexdump -C contacts.bin


将文件中通讯录对象反序列化并打印

将文件中通讯录对象反序列化并打印共以下两步:

  • 读取本地已存在的通讯录文件

  • 打印通讯录列表

此时新增 read.cc 和 改变Makefile:

makefile:

all:write read

write:write.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

read:read.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
	rm -f write read

read.cc:

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

using namespace std;

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

int main()
{
    contact2::Contacts contacts;
    // 1. 读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if(!contacts.ParseFromIstream(&input))
    {
        cerr << "反序列化失败" << endl;
        input.close();
        return -1;
    }
    // 2. 打印通讯录列表
    PrintContacts(contacts);
    return 0;
}

read.cc 代码中的注意事项

  • 读取本地已存在的通讯录文件与 write.cc 中的实现方式是一样的

  • 传入 PrintContacts函数 中的通讯录对象是反序列化好的数组,只需要遍历该数组即可

  • contacts_size() 和 phone_size() 是自动生成的返回数组中元素个数的函数

  • contacts.contacts(i) 代码的含义是:返回 contacts数组 中下标为 i 的对象
    该函数的返回值是:const contact2::PeopleInfo&,用于接收该下标对应的对象
    打印联系人电话时的 people.phone(j) 同理


此时执行 make,再运行生成的 read,输入后就成功打印刚刚添加的联系人信息:

成功完成将文件中的通讯录对象反序列化并打印


--decode命令查看二进制文件

--decode 是 ProtoBuf 中的一种命令选项,使用 protoc -h 可以查看所有的命令选项:

其中就有--decode,--decode 是从 标准输入 中读取二进制消息,但是我们希望是从文件中读取的,所以使用输入重定向到 contacts.bin 文件中:

此时就能看到二进制消息的内容了
其中 name 后面的就是将 utf8 汉字,转化为 8进制 的格式了

所以如果想查看二进制消息,直接执行 --decode 的命令就能够查看二进制的消息,比上面代码的方式更简便


enum类型

proto3语法 还支持我们定义一个枚举类型的字段:

注意事项:

  • 0值常量必须存在,并且必须要作为枚举类型的第一个元素的枚举值

  • 枚举常量值必须在 32位 整数范围内,不能设置为负值

  • 枚举类型能够定义在 message 内部

  • 同级的枚举类型下,各个枚举的枚举类型中,不能有相同的常量名称

  • 如果依赖了其他的 .proto文件,这两个文件中如果都没有声明不同的 package 的话,两个文件中也不能有相同的常量名称 如果两个文件中想使用相同的常量名称,解决方式就是加上 package 即可


实现通讯录2.1版本

学习了 enum类型 后,就可以对 通讯录2.0版本 做以更新,可以将电话信息分为 固定电话(TEL)和移动电话(MP),所以此时的 proto文件 更新为:

在执行完 protoc --cpp_out=. contacts.proto 后,生成的 contacts.pb.h 中就有相应的方法了,例如与 type 相对的,就有 clear、get、set 相关方法:


下面实现在 write.cc 中,添加一个联系人时,设置联系人的类型

也就是在 write.cc 中的 AddPeopleInfo函数 中,输入联系人电话后的部分,加上电话类型:

write.cc 改为:

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

using namespace std;

void AddPeopleInfo(contact2::PeopleInfo* people)
{
    cout << "-------------新增联系人-------------" << endl;

    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people->set_name(name);

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

    for(int i = 1; ; i++)
    {
        cout << "请输入联系人电话" << i << "(只输入回车表示输入完毕): ";
        string number;
        getline(cin, number);
        if(number.empty())
        {
            break;
        }
        contact2::PeopleInfo_Phone* phone = people->add_phone();
        phone->set_number(number);

        cout << "请输入该电话类型(1.移动电话 2.固定电话)";
        int type;
        cin >> type;
        cin.ignore(256, '\n');
        switch(type)
        {
        case 1:
            phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
            break;
        case 2:
            phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
            break;
        default:
            cout << "输入有误!" << endl;
            break;
        }
    }

    cout << "-----------添加联系人成功-----------" << endl;
}

int main()
{
    contact2::Contacts contacts;
    // 1. 读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if(!input)
    {
        cout << "contacts.bin不存在, 创建了一个新的该文件" << endl;
    }
    else if(!contacts.ParseFromIstream(&input))
    {
        cerr << "反序列化失败" << endl;
        input.close();
        return -1;
    }

    // 2. 向通讯录中添加一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 3. 将通讯录序列化后并写入文件中
    fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
    if(!contacts.SerializeToOstream(&output))
    {
        cerr << "序列化失败" << endl;
        input.close();
        output.close();
        return -1;
    }
    cout << "序列化成功" << endl;

    return 0;
}

同时 read.cc 也需要更新,在读取时将电话类型也读取出来:
格式为:联系人电话1:123456   (MP)

注意:

  • phone.type() 可以得到电话的类型,但是得到的是 int 类型的值

  • phone.PhoneType_Name() 可以根据得到的 int 类型的值打印出 对应的 MP 或 TEL

此时 read.cc 代码变为了:

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

using namespace std;

void PrintContacts(contact2::Contacts& contacts)
{
    for(int i = 0; i < contacts.contacts_size(); i++)
    {
        cout << "-----------联系人" << i+1 << "-----------" << endl;
        const contact2::PeopleInfo& people = contacts.contacts(i);
        cout << "联系人姓名: " << people.name() << endl;
        cout << "联系人年龄: " << people.age() << endl;
        for(int j = 0; j < people.phone_size(); j++)
        {
            const contact2::PeopleInfo_Phone& phone = people.phone(j);
            cout << "联系人电话" << j+1 << ":" << phone.number();
            // 格式为: 联系人电话1:123456   (MP)
            cout << "   (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
        }
    }
}

int main()
{
    contact2::Contacts contacts;
    // 1. 读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if(!contacts.ParseFromIstream(&input))
    {
        cerr << "反序列化失败" << endl;
        input.close();
        return -1;
    }
    // 打印通讯录列表
    PrintContacts(contacts);
    return 0;
}

此时改动完 write.cc 和 read.cc 后,make 编译这两个文件,先新增一个联系人李四,此时就会多出一步,询问电话类型是哪一种:

下面读取联系人:

可以看到刚刚新增的李四设为了 固定电话TEL,而之前增加的 张三 的两个号码都默认为 移动电话MP,这就是枚举类型的默认值

在读取文件中联系人时,ProtoBuf 会对没有指定类型的电话,设置一个默认值,默认值就是常量值为 0 的枚举常量,也就是 MP

通讯录2.1版本实现完毕


 Any类型

any 是存在于 /usr/local/protobuf/ 下的 include/google/protobuf/any.proto 文件中的
下面使用 cat 查看这个文件:


可以得知:

  • Any 其实就是一个 message

  • Any 有两个属性,分别是 type_url 和 value

  • Any 是在 google.protobuf 的 package 中,所以使用 Any 时格式为:google.protobuf.Any

  • Any 类型字段中可以存储任意的消息类型

所以想使用Any时,就可以直接将字段设置为 Any类型了

接下来就可以实现 实现通讯录2.2版本了,新增一个地址属性,类型就是 Any


实现通讯录2.2版本

先在 contacts.proto 中添加地址属性:

改变后的 contacts.proto文件为:

syntax = "proto3";
package contact2;

import "google/protobuf/any.proto";

message Address
{
    string home_address = 1; // 家庭地址
    string unit_address = 2; // 单位地址
}

// 定义联系人 message
message PeopleInfo
{
    string name = 1;  // 姓名
    int32 age = 2;    // 年龄
    message Phone
    {
        string number = 1; // 电话号码
        enum PhoneType
        {
            MP = 0;  // 移动电话
            TEL = 1; // 固定电话
        }
        PhoneType type = 2; // 电话类型
    }
    repeated Phone phone = 3; // 电话信息
    google.protobuf.Any data = 4;
}

// 通讯录 message
message Contacts
{
    repeated PeopleInfo contacts = 1;
}


了解生成的方法

此时 protoc 编译 contacts.proto文件,查看 contacts.pb.h 中新增的信息,可以看到新增的 Address类中的 home_address 和 unit_address 有各自的 clear、get、set方法: 


在 PeopleInfo类 中,Any字段 data 相关的方法有:

  • has_data()方法 用于判断当前的消息是否设置过data,返回值是 bool

  • data()方法 也就是 get方法,返回值是 ProtoBuf 定义好的类型: PROTOBUF_NAMESPACE_ID::Any&

  • mutable_data()方法也就是 set方法,返回值是一个空间的地址 PROTOBUF_NAMESPACE_ID::Any* 也就是 ProtoBuf 已经帮我们开辟好了一块空间,我们只需要在这块空间上进行一定的操作即可


想要在 Any类型 中存放我们的地址信息,而地址信息在 C++文件中是一个类,所以下面需要明白:如何让 Any类型 存放 地址信息,也就是 Class Address

所以打开 Any类型,查看一些方法:

  • PackFrom 和 UnpackTo 函数,它们的方法都是 Message类型的,Message是我们自定义的 Message 的父类

  • PackFrom方法,可以将之前设置的任意消息类型,转换为 Any类型

  • UnpackTo方法,可以将 Any类型,转换为之前设置的任意消息类型,结果放在参数的地址中

  • Is方法, 用于判断 Any 字段中存放的消息类型,是否为 T类型,返回值是 bool


正式实现通讯录2.2版本

在上面定义了 Any类型的 data,也定义了 message Address

在 write.cc 中,输入完地址后,调用 PackFrom 先将 Address类型 转换为 Any类型

此时完整的 write.cc文件 为:

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

using namespace std;

void AddPeopleInfo(contact2::PeopleInfo* people)
{
    cout << "-------------新增联系人-------------" << endl;

    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people->set_name(name);

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

    for(int i = 1; ; i++)
    {
        cout << "请输入联系人电话" << i << "(只输入回车表示输入完毕): ";
        string number;
        getline(cin, number);
        if(number.empty())
        {
            break;
        }
        contact2::PeopleInfo_Phone* phone = people->add_phone();
        phone->set_number(number);

        cout << "请输入该电话类型(1.移动电话 2.固定电话)";
        int type;
        cin >> type;
        cin.ignore(256, '\n');
        switch(type)
        {
        case 1:
            phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
            break;
        case 2:
            phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
            break;
        default:
            cout << "输入有误!" << endl;
            break;
        }
    }
    // 输入地址信息
    contact2::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_unit_address(unit_address);
    // Address -> Any
    people->mutable_data()->PackFrom(address);

    cout << "-----------添加联系人成功-----------" << endl;
}

int main()
{
    contact2::Contacts contacts;
    // 1. 读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if(!input)
    {
        cout << "contacts.bin不存在, 创建了一个新的该文件" << endl;
    }
    else if(!contacts.ParseFromIstream(&input))
    {
        cerr << "反序列化失败" << endl;
        input.close();
        return -1;
    }

    // 2. 向通讯录中添加一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 3. 将通讯录序列化后并写入文件中
    fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
    if(!contacts.SerializeToOstream(&output))
    {
        cerr << "序列化失败" << endl;
        input.close();
        output.close();
        return -1;
    }
    cout << "序列化成功" << endl;

    return 0;
}

所以接着在 read.cc 中,打印完电话信息后,就可以对地址信息进行一些打印了:

先调用 has_data() 和 Is() 方法,如果都满足,再 调用 UnoackTo 方法,将 Any类型 转化为 Address类型 再打印

此时完整的 read.cc文件 为:

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

using namespace std;

void PrintContacts(contact2::Contacts& contacts)
{
    for(int i = 0; i < contacts.contacts_size(); i++)
    {
        cout << "-----------联系人" << i+1 << "-----------" << endl;
        const contact2::PeopleInfo& people = contacts.contacts(i);
        cout << "联系人姓名: " << people.name() << endl;
        cout << "联系人年龄: " << people.age() << endl;
        for(int j = 0; j < people.phone_size(); j++)
        {
            const contact2::PeopleInfo_Phone& phone = people.phone(j);
            cout << "联系人电话" << j+1 << ":" << phone.number();
            // 格式为: 联系人电话1:123456   (MP)
            cout << "   (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
        }
        // 判断people中data是否设置过类型,如果设置过,设置的类型是否是 Address
        if(people.has_data() && people.data().Is<contact2::Address>())
        {
            contact2::Address address;
            // 将Any类型转化为Address,并将结果存放在 address 中
            people.data().UnpackTo(&address);
            if(!address.home_address().empty())
            {
                cout << "联系人家庭地址: " << address.home_address() << endl;
            }
            if(!address.unit_address().empty())
            {
                cout << "联系人单位地址: " << address.unit_address() << endl;
            }
        }
    }
}

int main()
{
    contact2::Contacts contacts;
    // 1. 读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if(!contacts.ParseFromIstream(&input))
    {
        cerr << "反序列化失败" << endl;
        input.close();
        return -1;
    }
    // 打印通讯录列表
    PrintContacts(contacts);
    return 0;
}

下面 make 编译后,新增联系人 王五,并设置他的地址信息:

下面读取文件:

至此使用 Any类型 新增 联系人地址 的版本就实现完成了


oneof类型

接下来使用 oneof类型,新增联系人的 qq 和 微信号
这里使用 oneof类型,就是为了做到 要么使用 qq,要么使用微信号

  • oneof 中的字段编号不能和外面的字段编号重复

  • oneof 中不能使用 repeated 修饰

  • oneof 中徐国设置了多个字段,只会保留最后一次设置的字段

contacts.proto文件 修改如下:

编译后,观察 contacts.pb.h 中新增的方法,都有has、clear、get、set方法:

  • qq和wechat的 has方法,都是判断是否设置了该字段

  • clear_other_contact方法 是清除 oneof 字段

  • other_contact_case方法 通过返回值就知道设置的哪个字段,
    返回值是枚举类型,新增一个常量0,表示没有设置:


实现通讯录2.3版本

下面在 write.cc 中新增联系人的其他联系方式:

read.cc 也添加读取其他联系方式的逻辑:


下面编译 write.cc 和 read.cc,并新增联系人赵六:

读取联系人信息:

新增 联系人其他联系方式 的版本就实现完成了


map类型

下面使用 oneof类型,新增联系人的 备注信息

有三个注意点:

  • map 的Key支持除了 float和bytes 之外的类型,Value支持所有类型

  • map 不能使用 repeated字段修饰

  • map 中设置多多个键值对,设置的元素是无序的

对 contacts.proto文件 编译后,观察对 map字段 生成的方法:


实现通讯录2.4版本

首先在 write.cc 中新增备注信息:

write.cc 完成代码为:

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

using namespace std;

void AddPeopleInfo(contact2::PeopleInfo* people)
{
    cout << "-------------新增联系人-------------" << endl;

    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people->set_name(name);

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

    for(int i = 1; ; i++)
    {
        cout << "请输入联系人电话" << i << "(只输入回车表示输入完毕): ";
        string number;
        getline(cin, number);
        if(number.empty())
        {
            break;
        }
        contact2::PeopleInfo_Phone* phone = people->add_phone();
        phone->set_number(number);

        cout << "请输入该电话类型(1.移动电话 2.固定电话): ";
        int type;
        cin >> type;
        cin.ignore(256, '\n');
        switch(type)
        {
        case 1:
            phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
            break;
        case 2:
            phone->set_type(contact2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
            break;
        default:
            cout << "输入有误!" << endl;
            break;
        }
    }
    // 输入地址信息
    contact2::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_unit_address(unit_address);
    // Address -> Any
    people->mutable_data()->PackFrom(address);

    cout << "请选择要添加的其他联系方式(1.qq  2.微信): ";
    int other_contact;
    cin >> other_contact;
    cin.ignore(256, '\n');
    if(1 == other_contact)
    {
        cout << "请输入联系人qq号: ";
        string qq;
        getline(cin, qq);
        people->set_qq(qq);
    }
    else if(2 == other_contact)
    {
        cout << "请输入联系人微信号: ";
        string wechat;
        getline(cin, wechat);
        people->set_wechat(wechat);
    }
    else
    {
        cout << "选择有误, 未成功设置其他联系方式!" << endl;
    }

    for(int i = 0; ; i++)
    {
        cout << "请输入备注" << i+1 << "标题(只输入回车结束备注新增): ";
        string remark_key;
        getline(cin, remark_key);
        if(remark_key.empty())
        {
            break;
        }

        cout << "请输入备注" << i+1 << "内容: ";
        string remark_value;
        getline(cin, remark_value);        
        people->mutable_remark()->insert({remark_key, remark_value});
    }

    cout << "-----------添加联系人成功-----------" << endl;
}

int main()
{
    contact2::Contacts contacts;
    // 1. 读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if(!input)
    {
        cout << "contacts.bin不存在, 创建了一个新的该文件" << endl;
    }
    else if(!contacts.ParseFromIstream(&input))
    {
        cerr << "反序列化失败" << endl;
        input.close();
        return -1;
    }

    // 2. 向通讯录中添加一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 3. 将通讯录序列化后并写入文件中
    fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
    if(!contacts.SerializeToOstream(&output))
    {
        cerr << "序列化失败" << endl;
        input.close();
        output.close();
        return -1;
    }
    cout << "序列化成功" << endl;

    return 0;
}

read.cc 也新增读取备注信息的逻辑:

read.cc 完成代码为:

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

using namespace std;

void PrintContacts(contact2::Contacts& contacts)
{
    for(int i = 0; i < contacts.contacts_size(); i++)
    {
        cout << "-----------联系人" << i+1 << "-----------" << endl;
        const contact2::PeopleInfo& people = contacts.contacts(i);
        cout << "联系人姓名: " << people.name() << endl;
        cout << "联系人年龄: " << people.age() << endl;
        for(int j = 0; j < people.phone_size(); j++)
        {
            const contact2::PeopleInfo_Phone& phone = people.phone(j);
            cout << "联系人电话" << j+1 << ":" << phone.number();
            // 格式为: 联系人电话1:123456   (MP)
            cout << "   (" << phone.PhoneType_Name(phone.type()) << ")" << endl;
        }
        // 判断people中data是否设置过类型,如果设置过,设置的类型是否是 Address
        if(people.has_data() && people.data().Is<contact2::Address>())
        {
            contact2::Address address;
            // 将Any类型转化为Address,并将结果存放在 address 中
            people.data().UnpackTo(&address);
            if(!address.home_address().empty())
            {
                cout << "联系人家庭地址: " << address.home_address() << endl;
            }
            if(!address.unit_address().empty())
            {
                cout << "联系人单位地址: " << address.unit_address() << endl;
            }
        }

        switch(people.other_contact_case())
        {
        case contact2::PeopleInfo::OtherContactCase::kQq:
            cout << "联系人qq: " << people.qq() << endl;
            break;
        case contact2::PeopleInfo::OtherContactCase::kWechat:
            cout << "联系人微信: " << people.wechat() << endl;
            break;
        default:
            break;
        }

        if(people.remark_size())
        {
            cout << "备注信息: " << endl;
        }
        for(auto it = people.remark().cbegin(); it != people.remark().cend(); it++)
        {
            cout << "    " << it->first << ": " << it->second << endl;
        }
    }
}

int main()
{
    contact2::Contacts contacts;
    // 1. 读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if(!contacts.ParseFromIstream(&input))
    {
        cerr << "反序列化失败" << endl;
        input.close();
        return -1;
    }
    // 打印通讯录列表
    PrintContacts(contacts);
    return 0;
}

编译并运行 write,新增联系人田七:

接下来读取联系人:

通讯录2.4版本实现完毕


默认值

反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值

不同的类型对应的默认值不同:

  • 对于字符串,默认值为空字符串

  • 对于字节,默认值为空字节

  • 对于布尔值,默认值为 false

  • 对于数值类型,默认值为 0

  • 对于枚举,默认值是第一个定义的枚举值,必须为 0

  • 对于消息字段,未设置该字段。它的取值是依赖于语言

  • 对于设置了 repeated 的字段的默认值是空的 (通常是相应语言的一个空列表)

  • 对于 消息字段、oneof字段 和 any字段 ,C++ 和 Java 中都有 has 方法来检测当前字段是否被设置


对于标量数据类型,在proto3语法下,没有生成 has方法

例如:一个 message 中有三个字段 a、b、c,都是 int32类型的,这时有一个已经被序列化过的二进制序列消息,只有a、b两个字段被设置了,a = 1,b=2,c字段并没有被设置

下面对这一份消息进行反序列化操作,会在内存中生成反序列化内存对象,这个对象中包含了三个字段:
a = 1、b=2、c=0(0是默认值)

这里我们反序列化后,得到 c 的值是0,我们不确定这里的0,是我们自己设置的,还是反序列化时给的默认值
所以如果有 has方法,就可以判断这里的 c=0 到底是自己设置的还是默认值,但是标量数据类型是没有 has方法的

其实这种情况也不是大问题,在大部分的业务场景下,是可以兼容默认值的,比如说这里的c表示银行卡余额,那么我们设置为0和默认值为0,其实意义都是一样的


更新消息

更新消息包括:新增、修改、删除

新增:
注意不要和老字段冲突即可,包括名称和字段编号

修改:

  • 禁止修改任何已有字段的字段编号

  • int32, uint32,int64, uint64和 bool是完全兼容的,可以从这些类型中的一个改为另一个而不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采用与 C++一致的处理方案(例如,若将 64 位整数当做 32 位进行读取,它将被截断为 32 位)

  • sint32 和 sint64 相互兼容但不与其他的整型兼容

  • string 和 bytes 在合法 UTF-8 字节前提下也是兼容的

  • bytes 包含消息编码版本的情况不,嵌套消息与 bytes 也是兼容的

  • fixed64 与 sfixed64兼容,fixed32 与 sfixed32 兼容

  • enum 与 int32,uint32,int64和 uint64兼容(注意若值不匹配会被截断),但要注意当反序列化消息时会根据语言采用不同的处理方案
    例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值

  • oneof:
    将一个单独的值更改为 新 oneof 类型成员之一是安全和二进制兼容的
    若确定没有代码一次性设置多个值那么将多个字段移入一个新 oneof 类型也是可行的
    将任何字段移入已存在的 oneof 类型是不安全的。


删除:

如果字段编号2原本表示的是生日,我们将这个字段删除,新的字段编号2表示年龄,此时就会混淆,会出问题

如果要删除老字段,要保证不使用已经被删除的或者已经被注释掉的字段编号

reserved

为了保证不使用已经被删除的或者已经被注释掉的字段编号,ProtoBuf 引入了 reserved 关键字,能够指定字段编号为保留项,如果以后使用被保留的字段编号,ProtoBuf 就会产生告警

如果代码如下所示:

此时使用 ProtoBuf 编译,就会告警 字段编号2 是被保留的,建议使用 字段编号4:

  • 可以 reserved 2 这样单独保留字段编号

  • 可以 reserved 2,3 这样列举保留字段编号

  • 可以 reserved 2 to 10 这样保留一批字段编号

  • 想保留名称也是可以的,例如 reserved "age",如果有多个就用逗号分隔


前后兼容性

根据上述的例子可以得出,pb是具有向前兼容的。为了叙述方便,把增加了“生日”属性的 service称为“新模块”,未做变动的 client 称为“老模块”

  • 向前兼容:老模块能够正确识别新模块生成或发出的协议,这时新增加的“生日”属性会被当作未知字段(pb 3.5版本及之后)

  • 向后兼容:新模块也能够正确识别老模块生成或发出的协议

前后兼容的作用:
当我们维护一个很庞大的分布式系统时,由于你无法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”


未知字段

未知字段:解析结构良好的 protocolbuffer 已序列化数据中的未识别字段的表示方式

例如:
刚开始的字段名称是age,字段编号是2,新增几个联系人后,将这个字段名称和字段编号删除了,使用 reserved 保留
此时新的联系人信息就没有 age 了,但是序列化后的二进制文件中还会存在原来的 age 信息,此时 age 字段就会被存放在 未知字段中

未知字段可以打印出来:


option选项

.proto 文件中可以声明许多选项,使用 option 标注,选项能影响 proto 编译器的某些处理方式

例如:option.proto文件 初始代码为:

编译后,观察生成的 option.pb.h,PeopleInfo类的父类是 Message:

如果在 option.proto文件 新增这一行代码:

此时编译该文件,观察 PeopleInfo类 的父类,变为 MessageLite类了:


通过上述的例子,就可以明白 option选项 能影响 proto 编译器的某些处理方式

选项的完整列表在google/protobuf/descriptor.proto中定义:

message FileOptions {...}       // 文件选项 定义在 File0ptions 消息中
message Message0ptions { ... }  // 消息类型选项 定义在 Message0ptions 消息中
message FieldOptions { ... }    // 消息字段选项 定义在 Fieldoptions 消息中
message Oneofoptions { ... }    // oneof字段选项 定义在 0neofoptions 消息中
message EnumOptions{...}        // 枚举类型选项 定义在 Enumoptions 消息中
message EnumValue0ptions {...}  // 枚举值选项 定义在 EnumValueOptions 消息中

 常用选项

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

  • SPEED:protoc编译器将生成的代码是高度优化的,代码运行效率高,但是由此生成的代码编译后会占用更多的空间,SPEED 是默认选项

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

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

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

表示给 字段编号 为1的 TEL,起一个 LANDLINE 的别名


以上就是 proto3 的语法部分

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值