ProtoBuf:proto3 语法详解

🌈 个人主页:Zfox_
🔥 系列专栏:ProtoBuf

在语法详解部分,依旧使⽤项⽬推进的⽅式完成讲解。这个部分会对通讯录进⾏多次升级,使⽤2.x表⽰升级的版本,最终将会升级如下内容:
- 不再打印联系⼈的序列化结果,⽽是将通讯录序列化后并写⼊⽂件中。
- 从⽂件中将通讯录解析出来,并进⾏打印。
- 新增联系⼈属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注
🔥 字段规则
消息的字段可以⽤下⾯⼏种规则来修饰:
- singular:消息中可以包含该字段零次或⼀次(不超过⼀次)。proto3语法中,字段默认使⽤该规则。
- repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了⼀个数组。
更新contacts.proto, PeopleInfo 消息中新增 phone_numbers 字段,表⽰⼀个联系⼈有多个号码,可将其设置为repeated,写法如下:
yntax = "proto3";
package contacts;message PeopleInfo {string name = 1;int32 age = 2;repeated string phone_numbers = 3;
}
🔥 消息类型的定义与使⽤
🦋 定义
在单个.proto⽂件中可以定义多个消息体,且⽀持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复。
更新contacts.proto,我们可以将phone_number提取出来,单独成为⼀个消息:
// -------------------------- 嵌套写法 -------------------------
syntax = "proto3";
package contacts;message PeopleInfo {string name = 1;int32 age = 2;message Phone {string number = 1;}
}// -------------------------- ⾮嵌套写法 -------------------------
syntax = "proto3";
package contacts;message Phone {string number = 1;
}message PeopleInfo {string name = 1;int32 age = 2;
}
🦋 使⽤
- 消息类型可作为字段类型使⽤
contacts.proto
syntax = "proto3";
package contacts2;// 定义联系人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;
}
- 可导⼊其他.proto⽂件的消息并使⽤
例如Phone消息定义在phone.proto⽂件中:
syntax = "proto3";
package phone;message Phone {string number = 1;
}
contacts.proto中的 PeopleInfo 使⽤ Phone 消息
syntax = "proto3";
package contacts;import "phone.proto"; // 使⽤ import 将 phone.proto ⽂件导⼊进来 !!!message PeopleInfo {string name = 1;int32 age = 2;// 引⼊的⽂件声明了package,使⽤消息时,需要⽤ ‘命名空间.消息类型’ 格式repeated phone.Phone phone = 3;
}
注:在proto3⽂件中可以导⼊proto2消息类型并使⽤它们,反之亦然。
🦋 创建通讯录2.0版本
通讯录2.x的需求是向⽂件中写⼊通讯录列表,以上我们只是定义了⼀个联系⼈的消息,并不能存放通讯录列表,所以还需要在完善⼀下contacts.proto(终版通讯录2.0):
syntax = "proto3";
package contacts2;// 定义联系人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;
}
接着进⾏⼀次编译:
protoc --cpp_out=. contacts.proto
编译后⽣成的 contacts.pb.h contacts.pb.cc 会将在快速上⼿的⽣成⽂件覆盖掉。
contacts.pb.h更新的部分代码展⽰
// 新增了 PeopleInfo_Phone 类
class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message
{
public:using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;void CopyFrom(const PeopleInfo_Phone &from);using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;void MergeFrom(const PeopleInfo_Phone &from){PeopleInfo_Phone::MergeImpl(*this, from);}static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName(){return "PeopleInfo.Phone";}// string number = 1;void clear_number();const std::string &number() const;template <typename ArgT0 = const std::string &, typename... ArgT>void set_number(ArgT0 &&arg0, ArgT... args);std::string *mutable_number();PROTOBUF_NODISCARD std::string *release_number();void set_allocated_number(std::string *number);
};
// 更新了 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message
{
public:using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;void CopyFrom(const PeopleInfo &from);using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;void MergeFrom(const PeopleInfo &from){PeopleInfo::MergeImpl(*this, from);}static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName(){return "PeopleInfo";}typedef PeopleInfo_Phone Phone;// repeated .PeopleInfo.Phone phone = 3;int phone_size() const;void clear_phone();::PeopleInfo_Phone *mutable_phone(int index);::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<::PeopleInfo_Phone> *mutable_phone();const ::PeopleInfo_Phone &phone(int index) const;::PeopleInfo_Phone *add_phone();const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<::PeopleInfo_Phone> &phone() const;
};
// 新增了 Contacts 类
class Contacts final : public ::PROTOBUF_NAMESPACE_ID::Message
{
public:using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;void CopyFrom(const Contacts &from);using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;void MergeFrom(const Contacts &from){Contacts::MergeImpl(*this, from);}static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName(){return "Contacts";}// repeated .PeopleInfo contacts = 1;int contacts_size() const;void clear_contacts();::PeopleInfo *mutable_contacts(int index);::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<::PeopleInfo> *mutable_contacts();const ::PeopleInfo &contacts(int index) const;::PeopleInfo *add_contacts();const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField<::PeopleInfo> &contacts() const;
};
上述的例⼦中:
- 每个字段都有⼀个clear_⽅法,可以将字段重新设置回empty状态。
- 每个字段都有设置和获取的⽅法,获取⽅法的⽅法名称与⼩写字段名称完全相同。但如果是消息类型的字段,其设置⽅法为mutable_⽅法,返回值为消息类型的指针,这类⽅法会为我们开辟好空间,可以直接对这块空间的内容进⾏修改。
- 对于使⽤repeated修饰的字段,也就是数组类型,pb为我们提了add_⽅法来新增⼀个值,并且提供了_size⽅法来判断数组存放元素的个数。
🦋 通讯录2.0的写⼊实现
write.cc
#include <iostream>
#include <fstream>
#include <string>
#include "contacts.pb.h"void AddPeopleInfo(contacts2::PeopleInfo * people)
{std::cout << "--------------------新增联系人--------------------" << std::endl; std::cout << "请输入联系人的姓名: ";std::string name;std::getline(std::cin, name);people->set_name(name);std::cout << "请输入联系人的年龄: ";int age;std::cin >> age;people->set_age(age); std::cin.ignore(256, '\n');for(int i = 0; ; i++) {std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";std::string number;std::getline(std::cin, number);if(number.empty()) break;contacts2::PeopleInfo_Phone *phone = people->add_phone();phone->set_number(number);}std::cout << "-------------------添加联系人成功------------------" << std::endl;
}int main()
{contacts2::Contacts contacts;// 读取本地已存在的通讯录文件std::fstream input("contacts.bin", std::ios::in | std::ios::binary);if (!input.is_open()){std::cout << "contacts.bin not exists, create new file" << std::endl;}else{if (!contacts.ParseFromIstream(&input)){std::cerr << "ParseFromIstream Failed!" << std::endl;input.close();return -1;}}// 向通讯录中添加一个联系人 返回开辟好空间的 peopleinfo对象AddPeopleInfo(contacts.add_contacts());// 将通讯录写入本地文件中std::fstream output("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);if(!contacts.SerializeToOstream(&output)) {std::cerr << "SerializeToOstream Failed!" << std::endl;input.close();output.close();return -1;}std::cout << "SerializeToOstream Success" << std::endl;input.close();output.close();return 0;
}
🦋 通讯录2.0的读取实现
read.cc
#include <iostream>
#include <string>
#include <fstream>
#include "contacts.pb.h"void PrintContacts(contacts2::Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); i++){std::cout << "------------------联系人" << i + 1 << "------------------" << std::endl;const 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++){const contacts2::PeopleInfo_Phone &phone = people.phone(j);std::cout << "联系人电话: " << j + 1 << " " << phone.number() << std::endl;}}
}int main()
{contacts2::Contacts contacts;// 读取本地已存在的通讯录文件std::fstream intput("contacts.bin", std::ios::in | std::ios::binary);if (!contacts.ParseFromIstream(&intput)){std::cout << "ParseFromIstream Failed!" << std::endl;intput.close();return -1;}// 打印通讯录列表PrintContacts(contacts);return 0;
}
另⼀种验证⽅法–decode
我们可以⽤ protoc -h 命令来查看ProtoBuf为我们提供的所有命令option。其中ProtoBuf提供⼀个命令选项 --decode ,表⽰从标准输⼊中读取给定类型的⼆进制消息,并将其以⽂本格式写⼊标准输出。消息类型必须在.proto⽂件或导⼊的⽂件中定义。
# protoc --decode=contacts2.Contacts contacts.proto < contacts.bin
contacts {name: "khj"age: 19phone {number: "12312412"}phone {number: "1231231234"}
}
🔥 enum类型
🦋 定义规则
语法⽀持我们定义枚举类型并使⽤。在 .proto ⽂件中枚举类型的书写规范为:
- 枚举类型名称:
- 使⽤驼峰命名法,⾸字⺟⼤写。例如: MyEnum
- 常量值名称:
- 全⼤写字⺟,多个字⺟之间⽤ _ 连接。例如: ENUM_CONST = 0;
我们可以定义⼀个名为PhoneType的枚举类型,定义如下:
enum PhoneType {MP = 0; // 移动电话TEL = 1; // 固定电话
}
要注意枚举类型的定义有以下⼏种规则:
- 0值常量必须存在,且要作为第⼀个元素。这是为了与proto2的语义兼容:第⼀个元素作为默认值,且值为0。
- 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
- 枚举的常量值在32位整数的范围内。但因负值⽆效因⽽不建议使⽤(与编码规则有关)。
🦋 定义时注意
将两个‘具有相同枚举值名称’的枚举类型放在单个.proto⽂件下测试时,编译后会报错:某某某常量已经被定义!所以这⾥要注意:
- 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
- 单个.proto⽂件下,最外层枚举类型和嵌套枚举类型,不算同级。
- 多个.proto⽂件下,若⼀个⽂件引⼊了其他⽂件,且每个⽂件都未声明package,每个proto⽂件中的枚举类型都在最外层,算同级。
- 多个.proto⽂件下,若⼀个⽂件引⼊了其他⽂件,且每个⽂件都声明了package,不算同级。
🦋 升级通讯录⾄2.1版本
更新contacts.proto(通讯录2.1),新增枚举字段并使⽤,更新内容如下:
syntax = "proto3";
package contacts2;// 定义联系人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; // 电话信息
}// 通讯录message
message Contacts {repeated PeopleInfo contacts = 1;
}
编译
protoc–cpp_out=.contacts.proto
contacts.pb.h更新的部分代码展⽰:
// 新⽣成的 PeopleInfo_Phone_PhoneType 枚举类
enum PeopleInfo_Phone_PhoneType : int
{PeopleInfo_Phone_PhoneType_MP = 0,PeopleInfo_Phone_PhoneType_TEL = 1,PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MIN_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::min(),PeopleInfo_Phone_PhoneType_PeopleInfo_Phone_PhoneType_INT_MAX_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::max()
};
// 更新的 PeopleInfo_Phone 类
class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message
{
public:typedef PeopleInfo_Phone_PhoneType PhoneType;static inline bool PhoneType_IsValid(int value){return PeopleInfo_Phone_PhoneType_IsValid(value);}template <typename T>static inline const std::string &PhoneType_Name(T enum_t_value) { ... }static inline bool PhoneType_Parse(::PROTOBUF_NAMESPACE_ID::ConstStringParam name, PhoneType *value) { ... }// .contacts.PeopleInfo.Phone.PhoneType type = 2;void clear_type();::contacts::PeopleInfo_Phone_PhoneType type() const;void set_type(::contacts::PeopleInfo_Phone_PhoneType value);
};
上述的代码中:
- 对于在.proto⽂件中定义的枚举类型,编译⽣成的代码中会含有与之对应的枚举类型、校验枚举值是否有效的⽅法_IsValid、以及获取枚举值名称的⽅法_Name。
- 对于使⽤了枚举类型的字段,包含设置和获取字段的⽅法,已经清空字段的⽅法clear_。
更新write.cc(通讯录2.1)
#include <iostream>
#include <fstream>
#include <string>
#include "contacts.pb.h"void AddPeopleInfo(contacts2::PeopleInfo *people)
{std::cout << "--------------------新增联系人--------------------" << std::endl;std::cout << "请输入联系人的姓名: ";std::string name;std::getline(std::cin, name);people->set_name(name);std::cout << "请输入联系人的年龄: ";int age;std::cin >> age;people->set_age(age);std::cin.ignore(256, '\n');for (int i = 0;; i++){std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";std::string number;std::getline(std::cin, number);if (number.empty())break;contacts2::PeopleInfo_Phone *phone = people->add_phone();phone->set_number(number);std::cout << "请输入该电话的类型 (1. 移动电话 2. 固定电话): ";int type;std::cin >> type;std::cin.ignore(256, '\n');switch (type){case 1:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);break;case 2:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);break;default:std::cout << "选择有误! " << std::endl;break;}}std::cout << "-------------------添加联系人成功------------------" << std::endl;
}int main()
{contacts2::Contacts contacts;// 读取本地已存在的通讯录文件std::fstream input("contacts.bin", std::ios::in | std::ios::binary);if (!input.is_open()){std::cout << "contacts.bin not exists, create new file" << std::endl;}else{if (!contacts.ParseFromIstream(&input)){std::cerr << "ParseFromIstream Failed!" << std::endl;input.close();return -1;}}// 向通讯录中添加一个联系人 返回开辟好空间的 peopleinfo对象AddPeopleInfo(contacts.add_contacts());// 将通讯录写入本地文件中std::fstream output("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);if (!contacts.SerializeToOstream(&output)){std::cerr << "SerializeToOstream Failed!" << std::endl;input.close();output.close();return -1;}std::cout << "SerializeToOstream Success" << std::endl;input.close();output.close();return 0;
}
更新read.cc(通讯录2.1)
#include <iostream>
#include <string>
#include <fstream>
#include "contacts.pb.h"void PrintContacts(contacts2::Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); i++){std::cout << "------------------联系人" << i + 1 << "------------------" << std::endl;const 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++){const contacts2::PeopleInfo_Phone &phone = people.phone(j);std::cout << "联系人电话: " << j + 1 << " " << phone.number();std::cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << std::endl;}}
}int main()
{contacts2::Contacts contacts;// 读取本地已存在的通讯录文件std::fstream intput("contacts.bin", std::ios::in | std::ios::binary);if (!contacts.ParseFromIstream(&intput)){std::cout << "ParseFromIstream Failed!" << std::endl;intput.close();return -1;}// 打印通讯录列表PrintContacts(contacts);return 0;
}
代码完成后,编译后进⾏读写验证:
@139-159-150-152:~/project/protobuf/contacts$ ./write contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 李四
请输⼊联系⼈年龄: 25
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 12333
选择此电话类型 (1、移动电话 2、固定电话) : 2
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------@139-159-150-152:~/project/protobuf/contacts$ ./read contacts.bin
------------联系⼈1------------
姓名:张三
年龄:20
电话1: 13111111111 (MP) // 这⾥打印出 MP 是因为未设置该字段,导致⽤了枚举的第⼀个
元素作为默认值
电话2: 15111111111 (MP)
------------联系⼈2------------
姓名:李四
年龄:25
电话1: 12333 (TEL)
🔥 Any类型
🐳 字段还可以声明为Any类型,可以理解为泛型类型。使⽤时可以在Any中存储任意消息类型。Any类型的字段也⽤repeated来修饰。
Any类型是google已经帮我们定义好的类型,在安装ProtoBuf时,其中的include⽬录下查找所有 google已经定义好的.proto⽂件。
🦋 升级通讯录⾄2.2版本
通讯录2.2版本会新增联系⼈的地址信息,我们可以使⽤any类型的字段来存储地址信息。
更新contacts.proto(通讯录2.2),更新内容如下:
syntax = "proto3";
package contacts2;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–cpp_out=.contacts.proto
contacts.pb.h更新的部分代码展⽰:
// 新⽣成的 Address 类
class Address final : public ::PROTOBUF_NAMESPACE_ID::Message
{
public:using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;void CopyFrom(const Address &from);using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;void MergeFrom(const Address &from){Address::MergeImpl(*this, from);}// string home_address = 1;void clear_home_address();const std::string &home_address() const;template <typename ArgT0 = const std::string &, typename... ArgT>void set_home_address(ArgT0 &&arg0, ArgT... args);std::string *mutable_home_address();PROTOBUF_NODISCARD std::string *release_home_address();void set_allocated_home_address(std::string *home_address);// string unit_address = 2;void clear_unit_address();const std::string &unit_address() const;template <typename ArgT0 = const std::string &, typename... ArgT>void set_unit_address(ArgT0 &&arg0, ArgT... args);std::string *mutable_unit_address();PROTOBUF_NODISCARD std::string *release_unit_address();void set_allocated_unit_address(std::string *unit_address);
};
// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message
{
public:// .google.protobuf.Any data = 4;bool has_data() const;void clear_data();const ::PROTOBUF_NAMESPACE_ID::Any &data() const;PROTOBUF_NODISCARD ::PROTOBUF_NAMESPACE_ID::Any *release_data();::PROTOBUF_NAMESPACE_ID::Any *mutable_data();void set_allocated_data(::PROTOBUF_NAMESPACE_ID::Any *data);
};
上述的代码中,对于Any类型字段:
- 设置和获取:获取⽅法的⽅法名称与⼩写字段名称完全相同。设置⽅法可以使⽤mutable_⽅法,返回值为 Any 类型的指针,这类⽅法会为我们开辟好空间,可以直接对这块空间的内容进⾏修改。
之前讲过,我们可以在Any字段中存储任意消息类型,这就要涉及到任意消息类型和Any类型的互转。这部分代码就在Google 为我们写好的头⽂件 any.pb.h 中。对 any.pb.h 部分代码展⽰:
class PROTOBUF_EXPORT Any final : public ::PROTOBUF_NAMESPACE_ID::Message
{bool PackFrom(const ::PROTOBUF_NAMESPACE_ID::Message &message){...}bool UnpackTo(::PROTOBUF_NAMESPACE_ID::Message *message) const{...}template <typename T>bool Is() const{return _impl_._any_metadata_.Is<T>();}
};
解释:使⽤ PackFrom() ⽅法可以将任意消息类型转为 Any 类型。使⽤ UnpackTo() ⽅法可以将 Any 类型转回之前设置的任意消息类型。使⽤ Is() ⽅法可以⽤来判断存放的消息类型是否为 typename T。
更新write.cc(通讯录2.2)
#include <iostream>
#include <fstream>
#include <string>
#include "contacts.pb.h"void AddPeopleInfo(contacts2::PeopleInfo *people)
{std::cout << "--------------------新增联系人--------------------" << std::endl;std::cout << "请输入联系人的姓名: ";std::string name;std::getline(std::cin, name);people->set_name(name);std::cout << "请输入联系人的年龄: ";int age;std::cin >> age;people->set_age(age);std::cin.ignore(256, '\n');for (int i = 0;; i++){std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";std::string number;std::getline(std::cin, number);if (number.empty())break;contacts2::PeopleInfo_Phone *phone = people->add_phone();phone->set_number(number);std::cout << "请输入该电话的类型 (1. 移动电话 2. 固定电话): ";int type;std::cin >> type;std::cin.ignore(256, '\n');switch (type){case 1:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);break;case 2:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);break;default:std::cout << "选择有误! " << std::endl;break;}}contacts2::Address address;std::cout << "请输入联系人家庭地址: " << std::endl;std::string home_address;std::getline(std::cin, home_address);address.set_home_address(home_address);std::cout << "请输入联系人的单位地址: " << std::endl;std::string unit_address;std::getline(std::cin, unit_address);address.set_unit_address(unit_address);// Address -> Anypeople->mutable_data()->PackFrom(address);std::cout << "-------------------添加联系人成功------------------" << std::endl;
}int main()
{contacts2::Contacts contacts;// 读取本地已存在的通讯录文件std::fstream input("contacts.bin", std::ios::in | std::ios::binary);if (!input.is_open()){std::cout << "contacts.bin not exists, create new file" << std::endl;}else{if (!contacts.ParseFromIstream(&input)){std::cerr << "ParseFromIstream Failed!" << std::endl;input.close();return -1;}}// 向通讯录中添加一个联系人 返回开辟好空间的 peopleinfo对象AddPeopleInfo(contacts.add_contacts());// 将通讯录写入本地文件中std::fstream output("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);if (!contacts.SerializeToOstream(&output)){std::cerr << "SerializeToOstream Failed!" << std::endl;input.close();output.close();return -1;}std::cout << "SerializeToOstream Success" << std::endl;input.close();output.close();return 0;
}
更新read.cc(通讯录2.2)
#include <iostream>
#include <string>
#include <fstream>
#include "contacts.pb.h"void PrintContacts(contacts2::Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); i++){std::cout << "------------------联系人" << i + 1 << "------------------" << std::endl;const 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++){const contacts2::PeopleInfo_Phone &phone = people.phone(j);std::cout << "联系人电话: " << j + 1 << " " << phone.number();std::cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << std::endl;}if(people.has_data() && people.data().Is<contacts2::Address>()) {contacts2::Address address;people.data().UnpackTo(&address);if(!address.home_address().empty()) {std::cout << "联系人的家庭住址为:" << address.home_address() << std::endl;}if(!address.unit_address().empty()) { std::cout << "联系人的家庭住址为:" << address.unit_address() << std::endl;}}}
}int main()
{contacts2::Contacts contacts;// 读取本地已存在的通讯录文件std::fstream intput("contacts.bin", std::ios::in | std::ios::binary);if (!contacts.ParseFromIstream(&intput)){std::cout << "ParseFromIstream Failed!" << std::endl;intput.close();return -1;}// 打印通讯录列表PrintContacts(contacts);return 0;
}
代码编写完成后,编译后进⾏读写:
139-159-150-152:~/project/protobuf/contacts$ ./write contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 王五
请输⼊联系⼈年龄: 49
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 642
选择此电话类型 (1、移动电话 2、固定电话) : 2
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
请输⼊联系⼈家庭地址: 陕西省西安市
请输⼊联系⼈单位地址: 陕西省西安市
-----------添加联系⼈成功-----------139-159-150-152:~/project/protobuf/contacts$ ./read contacts.bin
# 此处省略前两个添加的联系⼈
------------联系⼈3------------
姓名:王五
年龄:49
电话1: 642 (TEL)
家庭地址:陕西省西安市
单位地址:陕西省西安市
🔥 oneof类型
如果消息中有很多可选字段,并且将来同时只有⼀个字段会被设置,那么就可以使⽤ oneof 加强这个⾏为,也能有节约内存的效果。
🦋 升级通讯录⾄2.3版本
通讯录2.3版本想新增联系⼈的其他联系⽅式,⽐如qq或者微信号⼆选⼀,我们就可以使⽤oneof字段来加强多选⼀这个⾏为。oneof字段定义的格式为: oneof 字段名 { 字段1; 字段2; … } 更新contacts.proto(通讯录2.3),更新内容如下:
syntax = "proto3";
package contacts2;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;oneof other_contact {// 不能使用repeatedstring qq = 5;string wechat = 6;}
}// 通讯录message
message Contacts {repeated PeopleInfo contacts = 1;
}
注意:
- 可选字段中的字段编号,不能与⾮可选字段的编号冲突。
- 不能在oneof中使⽤repeated字段。
- 将来在设置oneof字段中值时,如果将oneof中的字段设置多个,那么只会保留最后⼀次设置的成员,之前设置的oneof成员会⾃动清除。
编译
protoc–cpp_out=.contacts.proto
contacts.pb.h更新的部分代码展⽰:
// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message
{enum OtherContactCase{kQq = 5,kWeixin = 6,OTHER_CONTACT_NOT_SET = 0,};// string qq = 5;bool has_qq() const;void clear_qq();const std::string &qq() const;template <typename ArgT0 = const std::string &, typename... ArgT>void set_qq(ArgT0 &&arg0, ArgT... args);std::string *mutable_qq();PROTOBUF_NODISCARD std::string *release_qq();void set_allocated_qq(std::string *qq);// string weixin = 6;bool has_weixin() const;void clear_weixin();const std::string &weixin() const;template <typename ArgT0 = const std::string &, typename... ArgT>void set_weixin(ArgT0 &&arg0, ArgT... args);std::string *mutable_weixin();PROTOBUF_NODISCARD std::string *release_weixin();void set_allocated_weixin(std::string *weixin);void clear_other_contact();OtherContactCase other_contact_case() const;
};
上述的代码中,对于oneof字段:
- 会将oneof中的多个字段定义为⼀个枚举类型。
- 设置和获取:对oneof内的字段进⾏常规的设置和获取即可,但要注意只能设置⼀个。如果设置多个,那么只会保留最后⼀次设置的成员。
- 清空 oneof 字段:clear_⽅法
- 获取当前设置了哪个字段:_case⽅法
更新write.cc(通讯录2.3),更新内容如下:
#include <iostream>
#include <fstream>
#include <string>
#include "contacts.pb.h"void AddPeopleInfo(contacts2::PeopleInfo *people)
{std::cout << "--------------------新增联系人--------------------" << std::endl;std::cout << "请输入联系人的姓名: ";std::string name;std::getline(std::cin, name);people->set_name(name);std::cout << "请输入联系人的年龄: ";int age;std::cin >> age;people->set_age(age);std::cin.ignore(256, '\n');for (int i = 0;; i++){std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";std::string number;std::getline(std::cin, number);if (number.empty())break;contacts2::PeopleInfo_Phone *phone = people->add_phone();phone->set_number(number);std::cout << "请输入该电话的类型 (1. 移动电话 2. 固定电话): ";int type;std::cin >> type;std::cin.ignore(256, '\n');switch (type){case 1:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);break;case 2:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);break;default:std::cout << "选择有误! " << std::endl;break;}}contacts2::Address address;std::cout << "请输入联系人家庭地址: " << std::endl;std::string home_address;std::getline(std::cin, home_address);address.set_home_address(home_address);std::cout << "请输入联系人的单位地址: " << std::endl;std::string unit_address;std::getline(std::cin, unit_address);address.set_unit_address(unit_address);// Address -> Anypeople->mutable_data()->PackFrom(address);std::cout << "请选择要添加的其他联系方式: (1. qq 2. wechat)";int other_contact;std::cin >> other_contact;std::cin.ignore(256, '\n');if(1 == other_contact) {std::cout << "请输入联系人的QQ号:";std::string qq;std::getline(std::cin, qq);people->set_qq(qq);} else if(2 == other_contact) {std::cout << "请输入联系的人 wechat:";std::string wechat;std::getline(std::cin, wechat);people->set_wechat(wechat);} else {std::cout << "选择有误" << std::endl;}std::cout << "-------------------添加联系人成功------------------" << std::endl;
}int main()
{contacts2::Contacts contacts;// 读取本地已存在的通讯录文件std::fstream input("contacts.bin", std::ios::in | std::ios::binary);if (!input.is_open()){std::cout << "contacts.bin not exists, create new file" << std::endl;}else{if (!contacts.ParseFromIstream(&input)){std::cerr << "ParseFromIstream Failed!" << std::endl;input.close();return -1;}}// 向通讯录中添加一个联系人 返回开辟好空间的 peopleinfo对象AddPeopleInfo(contacts.add_contacts());// 将通讯录写入本地文件中std::fstream output("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);if (!contacts.SerializeToOstream(&output)){std::cerr << "SerializeToOstream Failed!" << std::endl;input.close();output.close();return -1;}std::cout << "SerializeToOstream Success" << std::endl;input.close();output.close();return 0;
}
更新read.cc(通讯录2.3),更新内容如下:
#include <iostream>
#include <string>
#include <fstream>
#include "contacts.pb.h"void PrintContacts(contacts2::Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); i++){std::cout << "------------------联系人" << i + 1 << "------------------" << std::endl;const 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++){const contacts2::PeopleInfo_Phone &phone = people.phone(j);std::cout << "联系人电话: " << j + 1 << " " << phone.number();std::cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << std::endl;}if (people.has_data() && people.data().Is<contacts2::Address>()){contacts2::Address address;people.data().UnpackTo(&address);if (!address.home_address().empty()){std::cout << "联系人的家庭住址为:" << address.home_address() << std::endl;}if (!address.unit_address().empty()){std::cout << "联系人的家庭住址为:" << address.unit_address() << std::endl;}}switch (people.other_contact_case()){case contacts2::PeopleInfo::kQq:std::cout << "联系人 qq: " << people.qq() << std::endl;break;case contacts2::PeopleInfo::kWechat:std::cout << "联系人 wechat: " << people.wechat() << std::endl;break;default:break;}}
}int main()
{contacts2::Contacts contacts;// 读取本地已存在的通讯录文件std::fstream intput("contacts.bin", std::ios::in | std::ios::binary);if (!contacts.ParseFromIstream(&intput)){std::cout << "ParseFromIstream Failed!" << std::endl;intput.close();return -1;}// 打印通讯录列表PrintContacts(contacts);return 0;
}
🔥 map类型
语法⽀持创建⼀个关联映射字段,也就是可以使⽤map类型去声明字段类型,格式为: map<key_type, value_type> map_field = N;
要注意的是:
- key_type 是除了float和bytes类型以外的任意标量类型。 value_type 可以是任意类型。
- map字段不可以⽤repeated修饰
- map中存⼊的元素是⽆序的
🦋 升级通讯录⾄2.4版本
最后,通讯录2.4版本想新增联系⼈的备注信息,我们可以使⽤map类型的字段来存储备注信息。
更新contacts.proto(通讯录2.4),更新内容如下:
syntax = "proto3";
package contacts2;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;oneof other_contact {// 不能使用repeatedstring qq = 5;string wechat = 6;}map<string, string> remark = 7; // 备注
}// 通讯录message
message Contacts {repeated PeopleInfo contacts = 1;
}
编译
protoc–cpp_out=.contacts.proto
contacts.pb.h更新的部分代码展⽰:
// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message
{// map<string, string> remark = 7;int remark_size() const;void clear_remark();const ::PROTOBUF_NAMESPACE_ID::Map<std::string, std::string> &remark() const;::PROTOBUF_NAMESPACE_ID::Map<std::string, std::string> *mutable_remark();
};
上述的代码中,对于Map类型的字段:
- 清空map:clear_⽅法
- 设置和获取:获取⽅法的⽅法名称与⼩写字段名称完全相同。设置⽅法为mutable_⽅法,返回值为 Map 类型的指针,这类⽅法会为我们开辟好空间,可以直接对这块空间的内容进⾏修改。
更新write.cc(通讯录2.4),更新内容如下:
#include <iostream>
#include <fstream>
#include <string>
#include "contacts.pb.h"void AddPeopleInfo(contacts2::PeopleInfo *people)
{std::cout << "--------------------新增联系人--------------------" << std::endl;std::cout << "请输入联系人的姓名: ";std::string name;std::getline(std::cin, name);people->set_name(name);std::cout << "请输入联系人的年龄: ";int age;std::cin >> age;people->set_age(age);std::cin.ignore(256, '\n');for (int i = 0;; i++){std::cout << "请输入联系人的电话 " << i + 1 << " " << "(只输⼊回⻋完成电话新增): ";std::string number;std::getline(std::cin, number);if (number.empty())break;contacts2::PeopleInfo_Phone *phone = people->add_phone();phone->set_number(number);std::cout << "请输入该电话的类型 (1. 移动电话 2. 固定电话): ";int type;std::cin >> type;std::cin.ignore(256, '\n');switch (type){case 1:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);break;case 2:phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);break;default:std::cout << "选择有误! " << std::endl;break;}}contacts2::Address address;std::cout << "请输入联系人家庭地址: " << std::endl;std::string home_address;std::getline(std::cin, home_address);address.set_home_address(home_address);std::cout << "请输入联系人的单位地址: " << std::endl;std::string unit_address;std::getline(std::cin, unit_address);address.set_unit_address(unit_address);// Address -> Anypeople->mutable_data()->PackFrom(address);std::cout << "请选择要添加的其他联系方式: (1. qq 2. wechat)";int other_contact;std::cin >> other_contact;std::cin.ignore(256, '\n');if(1 == other_contact) {std::cout << "请输入联系人的QQ号:";std::string qq;std::getline(std::cin, qq);people->set_qq(qq);} else if(2 == other_contact) {std::cout << "请输入联系的人 wechat:";std::string wechat;std::getline(std::cin, wechat);people->set_wechat(wechat);} else {std::cout << "选择有误" << std::endl;}for(int i = 0; ; i++) {std::cout << "请输入备注" << i + 1 << "标题:(只输入会车完成备注新增)";std::string remark_key;std::getline(std::cin, remark_key);if(remark_key.empty()) {break;}std::cout << "请输入备注内容: ";std::string remark_value;std::getline(std::cin, remark_value);people->mutable_remark()->insert({remark_key, remark_value});}std::cout << "-------------------添加联系人成功------------------" << std::endl;
}int main()
{contacts2::Contacts contacts;// 读取本地已存在的通讯录文件std::fstream input("contacts.bin", std::ios::in | std::ios::binary);if (!input.is_open()){std::cout << "contacts.bin not exists, create new file" << std::endl;}else{if (!contacts.ParseFromIstream(&input)){std::cerr << "ParseFromIstream Failed!" << std::endl;input.close();return -1;}}// 向通讯录中添加一个联系人 返回开辟好空间的 peopleinfo对象AddPeopleInfo(contacts.add_contacts());// 将通讯录写入本地文件中std::fstream output("contacts.bin", std::ios::out | std::ios::trunc | std::ios::binary);if (!contacts.SerializeToOstream(&output)){std::cerr << "SerializeToOstream Failed!" << std::endl;input.close();output.close();return -1;}std::cout << "SerializeToOstream Success" << std::endl;input.close();output.close();return 0;
}
更新read.cc(通讯录2.4),更新内容如下:
#include <iostream>
#include <string>
#include <fstream>
#include "contacts.pb.h"void PrintContacts(contacts2::Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); i++){std::cout << "------------------联系人" << i + 1 << "------------------" << std::endl;const 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++){const contacts2::PeopleInfo_Phone &phone = people.phone(j);std::cout << "联系人电话: " << j + 1 << " " << phone.number();std::cout << " (" << phone.PhoneType_Name(phone.type()) << ")" << std::endl;}if (people.has_data() && people.data().Is<contacts2::Address>()){contacts2::Address address;people.data().UnpackTo(&address);if (!address.home_address().empty()){std::cout << "联系人的家庭住址为:" << address.home_address() << std::endl;}if (!address.unit_address().empty()){std::cout << "联系人的家庭住址为:" << address.unit_address() << std::endl;}}switch (people.other_contact_case()){case contacts2::PeopleInfo::kQq:std::cout << "联系人 qq: " << people.qq() << std::endl;break;case contacts2::PeopleInfo::kWechat:std::cout << "联系人 wechat: " << people.wechat() << std::endl;break;default:break;}if (people.remark_size()){std::cout << "备注信息: " << std::endl;for (auto it = people.remark().cbegin(); it != people.remark().cend(); it++){std::cout << " " << it->first << ": " << it->second << std::endl;}}}
}int main()
{contacts2::Contacts contacts;// 读取本地已存在的通讯录文件std::fstream intput("contacts.bin", std::ios::in | std::ios::binary);if (!contacts.ParseFromIstream(&intput)){std::cout << "ParseFromIstream Failed!" << std::endl;intput.close();return -1;}// 打印通讯录列表PrintContacts(contacts);return 0;
}
到此,我们对通讯录2.x要求的任务全部完成。在这个过程中我们将通讯录升级到了2.4版本,同时对 ProtoBuf的使⽤也进⼀步熟练了,并且也掌握了ProtoBuf的proto3语法⽀持的⼤部分类型及其使⽤,但只是正常使⽤还是完全不够的。通过接下来的学习,我们就能更进⼀步了解到ProtoBuf深⼊的内容。
🔥 默认值
反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段 (比如我要反序列化年龄,但是没有这个字段) ,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为false。
- 对于数值类型,默认值为0。
- 对于枚举,默认值是第⼀个定义的枚举值,必须为0。
- 对于消息字段,未设置该字段。它的取值是依赖于语⾔。
- 对于设置了repeated的字段的默认值是空的(通常是相应语⾔的⼀个空列表)。
- 对于消息字段 、 oneof 字段 和 any 字段 ,C++和Java语⾔中都有has_⽅法来检测当前字段是否被设置。
🔥 更新消息
🦋 更新规则
如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型⾮常简单。遵循如下规则即可:
- 禁⽌修改任何已有字段的字段编号。
- 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号(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 将指定字段的编号或名称设置为保留项。当我们再使⽤这些编号或名称时,protocolbuffer的编译器将会警告这些编号或名称不可⽤。举个例⼦:
message Message {// 设置保留项reserved 100, 101, 200 to 299;reserved "field3", "field4";// 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称。// reserved 102, "field5";// 设置保留项之后,下⾯代码会告警int32 field1 = 100; //告警:Field 'field1' uses reserved number 100int32 field2 = 101; //告警:Field 'field2' uses reserved number 101int32 field3 = 102; //告警:Field name 'field3' is reservedint32 field4 = 103; //告警:Field name 'field4' is reserved
}
🦋 创建通讯录3.0版本—验证错误删除字段造成的数据损坏
现模拟有两个服务,他们各⾃使⽤⼀份通讯录.proto⽂件,内容约定好了是⼀模⼀样的。
- 服务1(service):负责序列化通讯录对象,并写⼊⽂件中。
- 服务2(client):负责读取⽂件中的数据,解析并打印出来。
⼀段时间后,service更新了⾃⼰的.proto⽂件,更新内容为:删除了某个字段,并新增了⼀个字段,新增的字段使⽤了被删除字段的字段编号。并将新的序列化对象写进了⽂件。
但client并没有更新⾃⼰的.proto⽂件。根据结论,可能会出现数据损坏的现象,接下来就让我们来验证下这个结论。
新建两个⽬录:service、client。分别存放两个服务的代码。
service⽬录下新增contacts.proto(通讯录3.0)
syntax = "proto3";
package s_contacts;// 联系⼈
message PeopleInfo {string name = 1; // 姓名int32 age = 2; // 年龄message Phone {string number = 1; // 电话号码}repeated Phone phone = 3; // 电话
}// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
client⽬录下新增contacts.proto(通讯录3.0)
syntax = "proto3";
package c_contacts;// 联系⼈
message PeopleInfo {string name = 1; // 姓名int32 age = 2; // 年龄message Phone {string number = 1; // 电话号码}repeated Phone phone = 3; // 电话
}// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
分别对两个⽂件进⾏编译,可⾃⾏操作。
继续对service⽬录下新增service.cc(通讯录3.0),负责向⽂件中写通讯录消息,内容如下:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"using namespace std;
using namespace s_contacts;/*** 新增联系⼈*/
void AddPeopleInfo(PeopleInfo *people_info_ptr)
{cout << "-------------新增联系⼈-------------" << endl;cout << "请输⼊联系⼈姓名: ";string name;getline(cin, name);people_info_ptr->set_name(name);cout << "请输⼊联系⼈年龄: ";int age;cin >> age;people_info_ptr->set_age(age);cin.ignore(256, '\n');for (int i = 1;; i++){cout << "请输⼊联系⼈电话" << i << "(只输⼊回⻋完成电话新增): ";string number;getline(cin, number);if (number.empty()){break;}PeopleInfo_Phone *phone = people_info_ptr->add_phone();phone->set_number(number);}cout << "-----------添加联系⼈成功-----------" << endl;
}
int main(int argc, char *argv[])
{GOOGLE_PROTOBUF_VERIFY_VERSION;if (argc != 2){cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;return -1;}Contacts contacts;// 先读取已存在的 contactsfstream input(argv[1], ios::in | ios::binary);if (!input){cout << argv[1] << ": File not found. Creating a new file." << endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "Failed to parse contacts." << endl;input.close();return -1;}// 新增⼀个联系⼈AddPeopleInfo(contacts.add_contacts());// 向磁盘⽂件写⼊新的 contactsfstream output(argv[1], ios::out | ios::trunc | ios::binary);if (!contacts.SerializeToOstream(&output)){cerr << "Failed to write contacts." << endl;input.close();output.close();return -1;}input.close();output.close();google::protobuf::ShutdownProtobufLibrary();return 0;
}
client⽬录下新增client.cc(通讯录3.0),负责向读出⽂件中的通讯录消息,内容如下:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"using namespace std;
using namespace c_contacts;/*** 打印联系⼈列表*/
void PrintfContacts(const Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); ++i){const PeopleInfo &people = contacts.contacts(i);cout << "------------联系⼈" << i + 1 << "------------" << endl;cout << "姓名:" << people.name() << endl;cout << "年龄:" << people.age() << endl;int j = 1;for (const PeopleInfo_Phone &phone : people.phone()){cout << "电话" << j++ << ": " << phone.number() << endl;}}
}
int main(int argc, char *argv[])
{GOOGLE_PROTOBUF_VERIFY_VERSION;if (argc != 2){cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;return -1;}// 以⼆进制⽅式读取 contactsContacts contacts;fstream input(argv[1], ios::in | ios::binary);if (!contacts.ParseFromIstream(&input)){cerr << "Failed to parse contacts." << endl;input.close();return -1;}// 打印 contactsPrintfContacts(contacts);input.close();google::protobuf::ShutdownProtobufLibrary();return 0;
}
代码编写完成后,进⾏⼀次读写(读写前的编译过程省略,⾃⾏操作)
~/project/protobuf/update/service$ ./service
../contacts.bin
../contacts.bin: File not found. Creating a new file.
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 张珊
请输⼊联系⼈年龄: 34
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 131
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------~/project/protobuf/update/client$ ./client ../contacts.bin
------------联系⼈1------------
姓名:张珊
年龄:34
电话1: 131
确认⽆误后,对service⽬录下的contacts.proto⽂件进⾏更新:除age字段,新增birthday字段,新增的字段使⽤被删除字段的字段编号。
更新后的contacts.proto(通讯录3.0)内容如下:
syntax = "proto3";
package s_contacts;// 联系⼈
message PeopleInfo {string name = 1; // 姓名// int32 age = 2; // 年龄int32 birthday = 2;message Phone {string number = 1; // 电话号码}repeated Phone phone = 3; // 电话
}// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
编译⽂件.proto后,还需要更新⼀下对应的service.cc(通讯录3.0):
cout << "请输⼊联系⼈生日: ";int birthday;cin >> birthday;people_info_ptr->set_birthday(birthday);cin.ignore(256, '\n');
我们对client相关的代码保持原样,不进⾏更新。
再进⾏⼀次读写(对service.cc编译过程省略,⾃⾏操作)。
root@VM-12-8-ubuntu:~/job_skill/protobuf/update/service# ./service ../contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 李四
请输⼊联系⼈生日: 24
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 123
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------
root@VM-12-8-ubuntu:~/job_skill/protobuf/update/client# ./client ../contacts.bin
------------联系⼈1------------
姓名:张三
年龄:20
电话1: 123123
------------联系⼈2------------
姓名:李四
年龄:24
电话1: 123
这时问题便出现了,我们发现输⼊的⽣⽇,在反序列化时,被设置到了使⽤了相同字段编号的年龄上!!所以得出结论:若是移除⽼字段,要保证不再使⽤移除字段的字段编号
,不建议直接删除或注释掉字段。
那么正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤。
正确service⽬录下的contacts.proto写法如下(终版通讯录3.0)。
syntax = "proto3";
package s_contacts;// 联系⼈
message PeopleInfo {reserved 2, 10, 11, 100 to 200;reserved "age";string name = 1; // 姓名// int32 age = 2; // 年龄int32 birthday = 4;message Phone {string number = 1; // 电话号码}repeated Phone phone = 3; // 电话
}// 通讯录
message Contacts {repeated PeopleInfo contacts = 1;
}
编译.proto⽂件后,还需要重新编译下service.cc,让service程序保持使⽤新⽣成的pbC++⽂件。
root@VM-12-8-ubuntu:~/job_skill/protobuf/update/service# ./service ../contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 王五
请输⼊联系⼈生日: 1020
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 123123
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------
root@VM-12-8-ubuntu:~/job_skill/protobuf/update/client# ./client ../contacts.bin
------------联系⼈1------------
姓名:张三
年龄:20
电话1: 123123
------------联系⼈2------------
姓名:李四
年龄:24
电话1: 123
------------联系⼈3------------
姓名:王五
年龄:0
电话1: 123123
根据实验结果,发现‘王五’的年龄为0,这是由于新增时未设置年龄,通过client程序反序列化时,给年龄字段设置了默认值0。这个结果显然是我们想看到的。
还要解释⼀下‘李四’的年龄依旧使⽤了之前设置的⽣⽇字段‘1221’,这是因为在新增‘李四’的时候,⽣⽇字段的字段编号依旧为2,并且已经被序列化到⽂件中了。最后再读取的时候,字段编号依旧为2。
还要再说⼀下的是:因为使⽤了reserved关键字,ProtoBuf 在编译阶段就拒绝了我们使⽤已经保留的字段编号。到此实验结束,也印证了我们的结论。
根据以上的例⼦,有的同学可能还有⼀个疑问:如果使⽤了 reserved 2 了,那么service给‘王五’设置的⽣⽇‘1112’,client就没法读到了吗?答案是可以的。继续学习下⾯的未知字段即可揭晓答案。
🔥 未知字段
在通讯录3.0版本中,我们向service⽬录下的contacts.proto新增了‘⽣⽇’字段,但对于client相关的代码并没有任何改动。验证后发现新代码序列化的消息(service)也可以被旧代码(client)解析。并且这⾥要说的是,新增的‘⽣⽇’字段在旧程序(client)中其实并没有丢失,⽽是会作为旧程序的未知字段。
- 未知字段:解析结构良好的protocolbuffer已序列化数据中的未识别字段的表⽰⽅式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
- 本来,proto3在解析消息时总是会丢弃未知字段,但在3.5版本中重新引⼊了对未知字段的保留机制。所以在3.5或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。
🦋 未知字段从哪获取
了解相关类关系图
MessageLite类介绍(了解)
- MessageLite从名字看是轻量级的message,
仅仅提供序列化、反序列化功能
- 类定义在google提供的message_lite.h中。
Message类介绍(了解)
- 我们⾃定义的 message类,都是继承⾃ Message。
- Message最重要的两个接⼝
GetDescriptor
/GetReflection
,可以获取该类型对应的 Descriptor 对象指针和Reflection对象指针。 - 类定义在google提供的message.h中。
//google::protobuf::Message 部分代码展⽰
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;
Descriptor类介绍(了解)
- Descriptor:是对 message 类型定义的描述,包括 message 的名字、所有字段的描述、原始的 proto⽂件内容等。
- 类定义在google提供的descriptor.h中。
class PROTOBUF_EXPORT Descriptor : private internal::SymbolBase
{string& name () constint field_count() const;const FieldDescriptor* field(int index) const;const FieldDescriptor* FindFieldByNumber(int number) const;const FieldDescriptor* FindFieldByName(const std::string& name) const;const FieldDescriptor* FindFieldByLowercaseName(const std::string& lowercase_name) const;const FieldDescriptor* FindFieldByCamelcaseName(const std::string& camelcase_name) const;int enum_type_count() const;const EnumDescriptor* enum_type(int index) const;const EnumDescriptor* FindEnumTypeByName(const std::string& name) const;const EnumValueDescriptor* FindEnumValueByName(const std::string& name)const;
}
Reflection类介绍(了解)
- Reflection 接⼝类,主要提供了动态读写消息字段的接⼝,对消息对象的⾃动读写主要通过该类完成。
- 提供⽅法来动态访问/修改message中的字段,对每种类型,Reflection都提供了⼀个单独的接⼝⽤于读写字段对应的值。
- 针对所有不同的 field 类型 FieldDescriptor::TYPE_* ,需要使⽤不同的 Get*() / Set*() / Add*() 接⼝;
- repeated 类型需要使⽤ GetRepeated*() / SetRepeated*() 接⼝,不可以和⾮ repeated 类型接⼝混⽤;
- message 对象只可以被由它⾃⾝的 reflection(message.GetReflection()) 来操作;
- 类中还包含了访问 / 修改未知字段的⽅法。
- 类定义在google提供的message.h中。
// 部分代码展⽰
class PROTOBUF_EXPORT Reflection final
{const UnknownFieldSet &GetUnknownFields(const Message &message) const;UnknownFieldSet *MutableUnknownFields(Message *message) const;bool HasField(const Message &message, const FieldDescriptor *field) const;int FieldSize(const Message &message, const FieldDescriptor *field) const;void ClearField(Message *message, const FieldDescriptor *field) const;bool HasOneof(const Message &message,const OneofDescriptor *oneof_descriptor) const;void ClearOneof(Message *message,const OneofDescriptor *oneof_descriptor) const;const FieldDescriptor *GetOneofFieldDescriptor(const Message &message, const OneofDescriptor *oneof_descriptor) const;// Singular field getters ------------------------------------------// These get the value of a non-repeated field. They return the default// value for fields that aren't set.int32_t GetInt32(const Message &message, const FieldDescriptor *field) const;int64_t GetInt64(const Message &message, const FieldDescriptor *field) const;uint32_t GetUInt32(const Message &message,const FieldDescriptor *field) const;uint64_t GetUInt64(const Message &message,const FieldDescriptor *field) const;float GetFloat(const Message &message, const FieldDescriptor *field) const;double GetDouble(const Message &message, const FieldDescriptor *field) const;bool GetBool(const Message &message, const FieldDescriptor *field) const;std::string GetString(const Message &message,const FieldDescriptor *field) const;const EnumValueDescriptor *GetEnum(const Message &message,const FieldDescriptor *field) const;int GetEnumValue(const Message &message, const FieldDescriptor *field) const;const Message &GetMessage(const Message &message,const FieldDescriptor *field,MessageFactory *factory = nullptr) const;// Singular field mutators -----------------------------------------// These mutate the value of a non-repeated field.void SetInt32(Message *message, const FieldDescriptor *field,int32_t value) const;void SetInt64(Message *message, const FieldDescriptor *field,int64_t value) const;void SetUInt32(Message *message, const FieldDescriptor *field,uint32_t value) const;void SetUInt64(Message *message, const FieldDescriptor *field,uint64_t value) const;void SetFloat(Message *message, const FieldDescriptor *field,float value) const;void SetDouble(Message *message, const FieldDescriptor *field,double value) const;void SetBool(Message *message, const FieldDescriptor *field,bool value) const;void SetString(Message *message, const FieldDescriptor *field,std::string value) const;void SetEnum(Message *message, const FieldDescriptor *field,const EnumValueDescriptor *value) const;void SetEnumValue(Message *message, const FieldDescriptor *field,int value) const;Message *MutableMessage(Message *message, const FieldDescriptor *field,MessageFactory *factory = nullptr) const;PROTOBUF_NODISCARD Message *ReleaseMessage(Message *message, const FieldDescriptor *field,MessageFactory *factory = nullptr) const;// Repeated field getters ------------------------------------------// These get the value of one element of a repeated field.int32_t GetRepeatedInt32(const Message &message, const FieldDescriptor *field,int index) const;int64_t GetRepeatedInt64(const Message &message, const FieldDescriptor *field,int index) const;uint32_t GetRepeatedUInt32(const Message &message,const FieldDescriptor *field, int index) const;uint64_t GetRepeatedUInt64(const Message &message,const FieldDescriptor *field, int index) const;float GetRepeatedFloat(const Message &message, const FieldDescriptor *field,int index) const;double GetRepeatedDouble(const Message &message, const FieldDescriptor *field,int index) const;bool GetRepeatedBool(const Message &message, const FieldDescriptor *field,int index) const;std::string GetRepeatedString(const Message &message,const FieldDescriptor *field, int index) const;const EnumValueDescriptor *GetRepeatedEnum(const Message &message,const FieldDescriptor *field,int index) const;int GetRepeatedEnumValue(const Message &message, const FieldDescriptor *field,int index) const;const Message &GetRepeatedMessage(const Message &message,const FieldDescriptor *field,int index) const;const std::string &GetRepeatedStringReference(const Message &message,const FieldDescriptor *field,int index,std::string *scratch) const;// Repeated field mutators -----------------------------------------// These mutate the value of one element of a repeated field.void SetRepeatedInt32(Message *message, const FieldDescriptor *field,int index, int32_t value) const;void SetRepeatedInt64(Message *message, const FieldDescriptor *field,int index, int64_t value) const;void SetRepeatedUInt32(Message *message, const FieldDescriptor *field,int index, uint32_t value) const;void SetRepeatedUInt64(Message *message, const FieldDescriptor *field,int index, uint64_t value) const;void SetRepeatedFloat(Message *message, const FieldDescriptor *field,int index, float value) const;void SetRepeatedDouble(Message *message, const FieldDescriptor *field,int index, double value) const;void SetRepeatedBool(Message *message, const FieldDescriptor *field,int index, bool value) const;void SetRepeatedString(Message *message, const FieldDescriptor *field,int index, std::string value) const;void SetRepeatedEnum(Message *message, const FieldDescriptor *field,int index, const EnumValueDescriptor *value) const;void SetRepeatedEnumValue(Message *message, const FieldDescriptor *field,int index, int value) const;Message *MutableRepeatedMessage(Message *message,const FieldDescriptor *field,int index) const;// Repeated field adders -------------------------------------------// These add an element to a repeated field.void AddInt32(Message *message, const FieldDescriptor *field,int32_t value) const;void AddInt64(Message *message, const FieldDescriptor *field,int64_t value) const;void AddUInt32(Message *message, const FieldDescriptor *field,uint32_t value) const;void AddUInt64(Message *message, const FieldDescriptor *field,uint64_t value) const;void AddFloat(Message *message, const FieldDescriptor *field,float value) const;void AddDouble(Message *message, const FieldDescriptor *field,double value) const;void AddBool(Message *message, const FieldDescriptor *field,bool value) const;void AddString(Message *message, const FieldDescriptor *field,std::string value) const;void AddEnum(Message *message, const FieldDescriptor *field,const EnumValueDescriptor *value) const;void AddEnumValue(Message *message, const FieldDescriptor *field,int value) const;Message *AddMessage(Message *message, const FieldDescriptor *field,MessageFactory *factory = nullptr) const;const FieldDescriptor *FindKnownExtensionByName(const std::string &name) const;const FieldDescriptor *FindKnownExtensionByNumber(int number) const;bool SupportsUnknownEnumValues() const;
};
UnknownFieldSet类介绍(重要)
- UnknownFieldSet包含在分析消息时遇到但未由其类型定义的所有字段。
- 若要将UnknownFieldSet附加到任何消息,请调⽤Reflection::GetUnknownFields()。
- 类定义在unknown_field_set.h中
class PROTOBUF_EXPORT UnknownFieldSet
{inline void Clear();void ClearAndFreeMemory();inline bool empty() const;inline int field_count() const;inline const UnknownField &field(int index) const;inline UnknownField *mutable_field(int index);// Adding fields ---------------------------------------------------void AddVarint(int number, uint64_t value);void AddFixed32(int number, uint32_t value);void AddFixed64(int number, uint64_t value);void AddLengthDelimited(int number, const std::string &value);std::string *AddLengthDelimited(int number);UnknownFieldSet *AddGroup(int number);// Parsing helpers -------------------------------------------------// These work exactly like the similarly-named methods of Message.bool MergeFromCodedStream(io::CodedInputStream *input);bool ParseFromCodedStream(io::CodedInputStream *input);bool ParseFromZeroCopyStream(io::ZeroCopyInputStream *input);bool ParseFromArray(const void *data, int size);inline bool ParseFromString(const std::string &data){return ParseFromArray(data.data(), static_cast<int>(data.size()));}// Serialization.bool SerializeToString(std::string *output) const;bool SerializeToCodedStream(io::CodedOutputStream *output) const;static const UnknownFieldSet &default_instance();
};
UnknownField类介绍(重要)
- 表⽰未知字段集中的⼀个字段。
- 类定义在unknown_field_set.h中
class PROTOBUF_EXPORT UnknownField
{
public:enum Type{TYPE_VARINT,TYPE_FIXED32,TYPE_FIXED64,TYPE_LENGTH_DELIMITED,TYPE_GROUP};inline int number() const;inline Type type() const;// Accessors -------------------------------------------------------// Each method works only for UnknownFields of the corresponding type.inline uint64_t varint() const;inline uint32_t fixed32() const;inline uint64_t fixed64() const;inline const std::string &length_delimited() const;inline const UnknownFieldSet &group() const;inline void set_varint(uint64_t value);inline void set_fixed32(uint32_t value);inline void set_fixed64(uint64_t value);inline void set_length_delimited(const std::string &value);inline std::string *mutable_length_delimited();inline UnknownFieldSet *mutable_group();
};
🦋 8.3.2 升级通讯录3.1版本—验证未知字段
更新client.cc(通讯录3.1),在这个版本中,需要打印出未知字段的内容。更新的代码如下:
#include <iostream>
#include <fstream>
#include "contacts.pb.h"using namespace std;
using namespace c_contacts;
using namespace google::protobuf;/*** 打印联系⼈列表*/
void PrintfContacts(const Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); ++i){const PeopleInfo &people = contacts.contacts(i);cout << "------------联系⼈" << i + 1 << "------------" << endl;cout << "姓名:" << people.name() << endl;cout << "年龄:" << people.age() << endl;int j = 1;for (const PeopleInfo_Phone &phone : people.phone()){cout << "电话" << j++ << ": " << phone.number() << endl;}// 获取到 reflection 对象const Reflection* reflection = PeopleInfo::GetReflection();// 通过 reflection 获取到未知字段的集合const UnknownFieldSet& set = reflection->GetUnknownFields(people);// 遍历集合for(int j = 0; j < set.field_count(); j++) {// 获取到单个未知字段const UnknownField& unknown_field = set.field(j);std::cout << "未知字段" << j + 1 << ": " << " 编号:" << unknown_field.number();switch (unknown_field.type()){case UnknownField::Type::TYPE_VARINT:std::cout << " 值: " << unknown_field.varint() << std::endl;break;case UnknownField::Type::TYPE_LENGTH_DELIMITED:std::cout << " 值: " << unknown_field.length_delimited() << std::endl;break;default:break;}}}
}
int main(int argc, char *argv[])
{GOOGLE_PROTOBUF_VERIFY_VERSION;if (argc != 2){cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;return -1;}// 以⼆进制⽅式读取 contactsContacts contacts;fstream input(argv[1], ios::in | ios::binary);if (!contacts.ParseFromIstream(&input)){cerr << "Failed to parse contacts." << endl;input.close();return -1;}// 打印 contactsPrintfContacts(contacts);input.close();google::protobuf::ShutdownProtobufLibrary();return 0;
}
其他⽂件均不⽤做任何修改,重新编译client.cc,进⾏⼀次读操作可得如下结果:
root@VM-12-8-ubuntu:~/job_skill/protobuf/update/client# ./client ../contacts.bin
------------联系⼈1------------
姓名:张三
年龄:20
电话1: 123123
------------联系⼈2------------
姓名:李四
年龄:24
电话1: 123
------------联系⼈3------------
姓名:王五
年龄:0
电话1: 123123
未知字段1: 编号:4 值: 1020
🔥 前后兼容性
根据上述的例⼦可以得出,pb是具有向前兼容的。为了叙述⽅便,把增加了“⽣⽇”属性的service称为“新模块”;未做变动的client称为“⽼模块”。
- 向前兼容:⽼模块能够正确识别新模块⽣成或发出的协议。这时新增加的“⽣⽇”属性会被当作未知字段(pb3.5版本及之后)。
- 向后兼容:新模块也能够正确识别⽼模块⽣成或发出的协议。
前后兼容的作⽤:当我们维护⼀个很庞⼤的分布式系统时,由于你⽆法同时升级所有模块,为了保证在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。
🔥 选项option
.proto⽂件中可以声明许多选项,使⽤ option 标注。选项能影响proto编译器的某些处理⽅式。
🦋 选项分类
选项的完整列表在 google/protobuf/descriptor.proto 中定义。部分代码:
syntax = "proto2"; // descriptor.proto 使⽤ proto2 语法版本message FileOptions { ... } // ⽂件选项 定义在 FileOptions 消息中message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中message MethodOptions { ... } // 服务⽅法选项 定义在 MethodOptions 消息中
...
由此可⻅,选项分为 ⽂件级、消息级、字段级 等等,但并没有⼀种选项能作⽤于所有的类型。
🦋 常⽤选项列举
optimize_for:该选项为⽂件选项,可以设置protoc编译器的优化级别,分别为 SPEED 、CODE_SIZE 、 LITE_RUNTIME 。受该选项影响,设置不同的优化级别,编译.proto⽂件后⽣成的代码内容不同。
- SPEED :protoc编译器将⽣成的代码是⾼度优化的,代码运⾏效率⾼,但是由此⽣成的代码编译后会占⽤更多的空间。 SPEED 是默认选项。
- CODE_SIZE :proto编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运⾏效率较低。这种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中。
- LITE_RUNTIME :⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。这是以牺牲ProtocolBuffer提供的反射功能为代价的,仅仅提供encoding+序列化功能,所以我们在链接BP库时仅需链接libprotobuf-lite,⽽⾮libprotobuf。这种模式通常⽤于资源有限的平台,例如移动⼿机平台中。
syntax = "proto3";
option optimize_for = LITE_RUNTIME;message PeopleInfo {enum PhoneType {option allow_alias = true;MP = 0;TEL = 1;LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错}string name = 1;
}
allow_alias:允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。
🦋 设置⾃定义选项
ProtoBuf允许⾃定义选项并使⽤。该功能⼤部分场景⽤不到,在这⾥不拓展讲解。
有兴趣可以参考:https://developers.google.cn/protocol-buffers/docs/proto?hl=zhcn#customoptions
🔥 共勉
😋 以上就是我对 ProtoBuf:proto3 语法详解
的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉