在序列化存储时,read的message文件可以参照write的message编写。
在语法详解部分,依旧使⽤项⽬推进的⽅式完成教学。这个部分会对通讯录进⾏多次升级,使⽤2.x
表⽰升级的版本,最终将会升级如下内容:
消息的字段可以⽤下⾯⼏种规则来修饰:
更新contacts.proto, PeopleInfo 消息中新增 phone_numbers 字段,表⽰⼀个联系⼈有多个号码,可将其设置为repeated,写法如下:
syntax = "proto3";
package contacts;
message PeopleInfo {
string name = 1;
int32 age = 2;
repeated string phone_numbers = 3;// 一个人有1个及以上的手机号码
}
在单个.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 contacts;
// 联系⼈
message PeopleInfo {
string name = 1;
int32 age = 2;
message Phone {
string number = 1;
}
repeated Phone phone = 3;
}
例如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.x的需求是向⽂件中写⼊通讯录列表,以上我们只是定义了⼀个联系⼈的消息,并不能存放通
讯录列表,所以还需要在完善⼀下contacts.proto(终版通讯录2.0):
syntax = "proto3";
package contacts2;
//import "phone.proto"
message PeopleInfo{
string name = 1 ; //姓名
int32 age =2 ; // 年龄
message Phone{
string number = 1;
}
repeated Phone phone = 3;//电话号码
}
// 通讯录
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);//返回array[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_数组名
⽅法来新增⼀个值,add_返回的类型可能是新开辟的数组元素地址todo。
并且提供了数组名_size
⽅法来判断数组存放元素的个数。
数组名(index):这样的函数用来获取数组指定元素.
write.cc(通讯录2.0)
#include
#include
#include
#include
using namespace std;
void addPersonInfo(contacts2::Contacts& contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input ("contacts.bin", std::fstream::in | std::fstream::out);
if(!input){
cerr<<"contacts.bin 不存在该文件\n";
return -1;
}else if(!contacts.ParseFromIstream(&input)){
cerr<<"contacts.bin 文件序列化失败\n";
input.close();
return -1;
}
// 添加一个联系人信息
addPersonInfo(contacts);
// 序列化通讯录写会到contacts.bin文件里
ofstream out("contacts.bin",std::ios::out|std::ios::binary);
if(!out){
cerr<<"contacts.bin文件打开失败\n";
return-1;
}else if(!contacts.SerializePartialToOstream(&out)){
cerr << "contacts对象序列化失败\n";
out.close();
return -1;
}
input.close();
out.close();
return 0;
}
void addPersonInfo(contacts2::Contacts& contacts)
{
contacts2::PeopleInfo* p = contacts.add_contacts();
if(!p){
cerr << "添加联系人失败\n";
return ;
}
cout<< "------------添加联系人-----------\n";
cout<< "------------请输出姓名-----------\n";
string name ;
getline(cin,name);
p->set_name(name);
cout<< "------------请输出年龄-----------\n";
int age;
cin>>age;
p->set_age(age);
cin.ignore(256,'\n');// 在256个字符前,如果遇到'\n'就跳过'\n'并停下来,忽略256个字符后还没遇到'\n'就停下来.
int i=1;
while(true)
{
string number;
cout<< "------------请输出手机号码"<<i<<"---------\n";
getline(cin,number);
if(number.empty()){
cout<<"手机号码读取成功\n";
break;
}
contacts2::PeopleInfo_Phone* phone = p->add_phone();
phone->set_number(number);
i++;
}
}
makefile
all:write read
write:write.cpp contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
read:read.cpp contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
rm -rf write read
make之后,运⾏write
如下,添加了两个联系人信息.
[YYK@VM-8-7-centos proto3]$ ./write
------------添加联系人-----------
------------请输出姓名-----------
yyk
------------请输出年龄-----------
10
------------请输出手机号码1---------
123
------------请输出手机号码2---------
手机号码读取成功
[YYK@VM-8-7-centos proto3]$ ./write
------------添加联系人-----------
------------请输出姓名-----------
bbq
------------请输出年龄-----------
12
------------请输出手机号码1---------
8989
------------请输出手机号码2---------
9999
------------请输出手机号码3---------
手机号码读取成功
查看二进制文件
用来验证写入的数据是否正确, 还可以使用 指令:protoc --decode=contacts2.Contacts contacts.proto < contacts.bin
[YYK@VM-8-7-centos proto3]$ hexdump -C contacts.bin
00000000 0a 0e 0a 03 79 79 6b 10 0a 1a 05 0a 03 31 32 33 |....yyk......123|
00000010 0a 17 0a 03 62 62 71 10 0c 1a 06 0a 04 38 39 38 |....bbq......898|
00000020 39 1a 06 0a 04 39 39 39 39 |9....9999|
00000029
解释:
`hexdump`:是Linux下的⼀个⼆进制⽂件查看⼯具,它可以将⼆进制⽂件转换为ASCII、⼋进制、
⼗进制、⼗六进制格式进⾏查看。
`-C`: 表⽰每个字节显⽰为16进制和相应的ASCII字符
read.cc(通讯录2.0)
#include
#include
#include
#include
using namespace std;
void printContacts(const contacts2::Contacts& contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
if (!input)
{
cerr << "contacts.bin 不存在该文件\n";
return -1;
}
else if (!contacts.ParseFromIstream(&input))
{
cerr << "contacts.bin 文件序列化失败\n";
input.close();
return -1;
}
// 打印contacts
printContacts(contacts);
return 0;
}
void printContacts(const contacts2::Contacts& contacts)
{
for(int i=0;i<contacts.contacts_size();i++)
{
const contacts2::PeopleInfo& people = contacts.contacts(i);
cout<<"----------联系人"<<i<<"----------"<<endl;
cout<<"联系人姓名:"<<people.name()<<endl;
cout<<"联系人年龄:"<<people.age()<<endl;
for(int j=0;j<people.phone_size();j++)
{
cout<<"联系手机"<<j+1<<":"<<people.phone(j).number()<<endl;
}
}
}
make后运⾏read
[YYK@VM-8-7-centos proto3]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:10
联系手机1:123
----------联系人1----------
联系人姓名:bbq
联系人年龄:12
联系手机1:8989
联系手机2:9999
方法:
我们可以⽤ protoc -h
命令来查看ProtoBuf为我们提供的所有命令option。其中ProtoBuf提供
⼀个命令选项 --decode
,表⽰从标准输⼊中读取给定类型的⼆进制消息,并将其以⽂本格式写⼊标准输出。消息类型必须在.proto⽂件或导⼊的⽂件中定义。
如何使用:
[YYK@VM-8-7-centos proto3]$ protoc --decode=contacts2.Contacts contacts.proto < contacts.bin
contacts {
name: "yyk"
age: 10
phone {
number: "123"
}
}
contacts {
name: "bbq"
age: 12
phone {
number: "8989"
}
phone {
number: "9999"
}
}
指令解释:
语法⽀持我们定义枚举类型并使⽤。在.proto⽂件中枚举类型的书写规范为:
命名规则:
我们可以定义⼀个名为PhoneType的枚举类型,定义如下:
enum PhoneType{
MP = 0 ; //移动电话
TEL = 1; // 固定电话
}
要注意枚举类型的定义有以下⼏种规则:
如下代码:
// ---------------------- 情况1:同级枚举类型包含相同枚举值名称--------------------
package=Phone
enum PhoneType {
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
enum PhoneTypeCopy {
MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}
// ------------------------解决方法 -----------------------------------
// phone1.proto
==============================================
syntax = "proto3";
package phone1;
import "phone2.proto";
enum PhoneType{
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
==============================================
// phone2.proto
=================================
syntax = "proto3";
package phone;
enum PhoneTypeCopy {
MP = 0; // 移动电话 // 编译后报错:MP 已经定义
}
==============================================
write.cc(通讯录2.1)
#include
#include
#include
#include
using namespace std;
void addPersonInfo(contacts2::Contacts &contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
if (!input)
{
cerr << "contacts.bin 不存在该文件\n";
return -1;
}
// else if (!contacts.ParseFromIstream(&input))
// {
// cerr << "contacts.bin 文件序列化失败\n";
// input.close();
// return -1;
// }
// 添加一个联系人信息
addPersonInfo(contacts);
// 序列化通讯录写会到contacts.bin文件里
ofstream out("contacts.bin", std::ios::out | std::ios::binary|std::ios::app);
if (!out)
{
cerr << "contacts.bin文件打开失败\n";
return -1;
}
else if (!contacts.SerializePartialToOstream(&out))
{
cerr << "contacts对象序列化失败\n";
out.close();
return -1;
}
input.close();
out.close();
return 0;
}
void addPersonInfo(contacts2::Contacts &contacts)
{
contacts2::PeopleInfo *p = contacts.add_contacts();
if (!p)
{
cerr << "添加联系人失败\n";
return;
}
cout << "------------添加联系人-----------\n";
cout << "------------请输出姓名-----------\n";
string name;
getline(cin, name);
p->set_name(name);
cout << "------------请输出年龄-----------\n";
int age;
cin >> age;
p->set_age(age);
cin.ignore(256, '\n'); // 在256个字符前,如果遇到'\n'就跳过'\n'并停下来,忽略256个字符后还没遇到'\n'就停下来.
int i = 1;
while (true)
{
string number;
cout << "------------请输出手机号码" << i << "---------\n";
getline(cin, number);
if (number.empty())
{
cout << "手机号码读取成功\n";
break;
}
contacts2::PeopleInfo_Phone *phone = p->add_phone();
phone->set_number(number);
i++;
cout << "------------请输入手机号码类型(MP:1 or TEL:2)"
<< "---------\n";
int type = 0;
cin >> type;
cin.ignore(256,'\n');
switch (type)
{
case 1:
cout<<"type:"<<type<<endl;
phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
break;
case 2:
cout<<"type:"<<type<<endl;
phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
break;
default:
cout << "输入手机号码类型有误" << endl;
break;
}
}
}
read.cc(通讯录2.1)
#include
#include
#include
#include
using namespace std;
void printContacts(const contacts2::Contacts& contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
if (!input)
{
cerr << "contacts.bin 不存在该文件\n";
return -1;
}
else if (!contacts.ParseFromIstream(&input))
{
cerr << "contacts.bin 文件序列化失败\n";
input.close();
return -1;
}
// 打印contacts
printContacts(contacts);
return 0;
}
void printContacts(const contacts2::Contacts& contacts)
{
for(int i=0;i<contacts.contacts_size();i++)
{
const contacts2::PeopleInfo& people = contacts.contacts(i);
cout<<"----------联系人"<<i<<"----------"<<endl;
cout<<"联系人姓名:"<<people.name()<<endl;
cout<<"联系人年龄:"<<people.age()<<endl;
for(int j=0;j<people.phone_size();j++)
{
cout<<"联系手机"<<j+1<<":"<<people.phone(j).number();
cout<< " type:("<<people.phone(j).PhoneType_Name(people.phone(j).type())<<")"<<endl;
}
}
}
contacts.proto (通讯录2.1)
syntax = "proto3";
package contacts2;
//import "phone.proto"
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 Contacts{
repeated PeopleInfo contacts = 1 ;
}
运行结果
[YYK@VM-8-7-centos proto3_2.1_]$ make
g++ -o write write.cpp contacts.pb.cc -std=c++11 -lprotobuf
[YYK@VM-8-7-centos proto3_2.1_]$ ./write
------------添加联系人-----------
------------请输出姓名-----------
uu
------------请输出年龄-----------
20
------------请输出手机号码1---------
87654
------------请输入手机号码类型(MP:1 or TEL:2)---------
2
type:2
------------请输出手机号码2---------
手机号码读取成功
[YYK@VM-8-7-centos proto3_2.1_]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:29
联系手机1:890 type:(MP)
----------联系人1----------
联系人姓名:hh
联系人年龄:19
联系手机1:88899903 type:(TEL)
----------联系人2----------
联系人姓名:uu
联系人年龄:20
联系手机1:87654 type:(TEL)
字段还可以声明为Any类型,可以理解为泛型类型。使⽤时可以在Any中存储任意消息类型。Any类
型的字段也⽤repeated来修饰。
Any类型是google已经帮我们定义好的类型,在安装ProtoBuf时,其中的include⽬录下查找所有
google已经定义好的.proto⽂件。
/usr/local/protobuf/include/google/protobuf/any.proto 文件
message Any {
string type_url = 1;
bytes value = 2;
}
这是Protocol Buffers中的一个消息类型定义。该Any消息类型用于表示在编译时未知的消息类型,并允许将其包含在其他消息中。具体来说:
使用Any类型可以使消息更加灵活和可扩展,但需要确保各方都能够正确解析和处理任意类型的消息。
通讯录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 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<T>() ⽅法可以⽤来判断存放的消息类型是否为 typename T.
Any类包含:
class Any
{
string type_url; // 存储的类型名
vector<bits> value; // 使用字节的存储数据
packfrom();
uppackfrom();
Is<T>();
}
如何理解Any:
更新write.cc(通讯录2.2)
#include
#include
#include
#include
using namespace std;
void addPersonInfo(contacts2::Contacts &contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
if (!input)
{
cerr << "contacts.bin 不存在该文件\n";
return -1;
}
// else if (!contacts.ParseFromIstream(&input))
// {
// cerr << "contacts.bin 文件序列化失败\n";
// input.close();
// return -1;
// }
// 添加一个联系人信息
addPersonInfo(contacts);
// 序列化通讯录写会到contacts.bin文件里
ofstream out("contacts.bin", std::ios::out | std::ios::binary|std::ios::app);
if (!out)
{
cerr << "contacts.bin文件打开失败\n";
return -1;
}
else if (!contacts.SerializePartialToOstream(&out))
{
cerr << "contacts对象序列化失败\n";
out.close();
return -1;
}
input.close();
out.close();
return 0;
}
void addPersonInfo(contacts2::Contacts &contacts)
{
contacts2::PeopleInfo *p = contacts.add_contacts();
if (!p)
{
cerr << "添加联系人失败\n";
return;
}
cout << "------------添加联系人-----------\n";
cout << "------------请输出姓名-----------\n";
string name;
getline(cin, name);
p->set_name(name);
cout << "------------请输出年龄-----------\n";
int age;
cin >> age;
p->set_age(age);
cin.ignore(256, '\n'); // 在256个字符前,如果遇到'\n'就跳过'\n'并停下来,忽略256个字符后还没遇到'\n'就停下来.
int i = 1;
while (true)
{
string number;
cout << "------------请输出手机号码" << i << "---------\n";
getline(cin, number);
if (number.empty())
{
cout << "手机号码读取成功\n";
break;
}
contacts2::PeopleInfo_Phone *phone = p->add_phone();
phone->set_number(number);
i++;
cout << "------------请输入手机号码类型(MP:1 or TEL:2)"
<< "---------\n";
int type = 0;
cin >> type;
cin.ignore(256,'\n');
switch (type)
{
case 1:
cout<<"type:"<<type<<endl;
phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
break;
case 2:
cout<<"type:"<<type<<endl;
phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
break;
default:
cout << "输入手机号码类型有误" << endl;
break;
}
}
contacts2::Address address;
cout<<"请输入联系人家庭地址"<<endl;
string home_address;
getline(cin,home_address);
address.set_home_address(home_address);
cout<<"请输入联系人单位地址"<<endl;
string unit_address;
getline(cin,unit_address);
address.set_unit_address(unit_address);
google::protobuf::Any* data = p->mutable_data();
data->PackFrom(address);
cout<<"添加联系人成功"<<endl;
}
更新read.cc(通讯录2.2)
#include
#include
#include
#include
using namespace std;
void printContacts(const contacts2::Contacts &contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
if (!input)
{
cerr << "contacts.bin 不存在该文件\n";
return -1;
}
else if (!contacts.ParseFromIstream(&input))
{
cerr << "contacts.bin 文件序列化失败\n";
input.close();
return -1;
}
// 打印contacts
printContacts(contacts);
return 0;
}
void printContacts(const contacts2::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); i++)
{
const contacts2::PeopleInfo &people = contacts.contacts(i);
cout << "----------联系人" << i << "----------" << endl;
cout << "联系人姓名:" << people.name() << endl;
cout << "联系人年龄:" << people.age() << endl;
for (int j = 0; j < people.phone_size(); j++)
{
cout << "联系手机" << j + 1 << ":" << people.phone(j).number();
cout << " type:(" << people.phone(j).PhoneType_Name(people.phone(j).type()) << ")" << endl;
}
if (people.has_data() && people.data().Is<contacts2::Address>())
{
contacts2::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;
}
}
}
}
运行 write read
[YYK@VM-8-7-centos proto3_2.2_]$ ./write
------------添加联系人-----------
------------请输出姓名-----------
Ben
------------请输出年龄-----------
30
------------请输出手机号码1---------
19999999999
------------请输入手机号码类型(MP:1 or TEL:2)---------
2
type:2
------------请输出手机号码2---------
手机号码读取成功
请输入联系人家庭地址
china
请输入联系人单位地址
china
添加联系人成功
bash: __vsc_prompt_cmd_original: command not found
[YYK@VM-8-7-centos proto3_2.2_]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:29
联系手机1:890 type:(MP)
----------联系人1----------
如果消息中有很多可选字段,并且将来同时只有⼀个字段会被设置,那么就可以使⽤ oneof 加强这个⾏为,也能有节约内存的效果。
通讯录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 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{
string qq=5;
string wechat=6;
//uint32 work_number = 7;
};
}
// 通讯录
message Contacts{
repeated PeopleInfo contacts = 1 ;
}
注意:
编译 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字段:
更新write.cc(通讯录2.3),更新内容如下:
#include
#include
#include
#include
using namespace std;
void addPersonInfo(contacts2::Contacts &contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
if (!input)
{
cerr << "contacts.bin 不存在该文件\n";
return -1;
}
// else if (!contacts.ParseFromIstream(&input))
// {
// cerr << "contacts.bin 文件序列化失败\n";
// input.close();
// return -1;
// }
// 添加一个联系人信息
addPersonInfo(contacts);
// 序列化通讯录写会到contacts.bin文件里
ofstream out("contacts.bin", std::ios::out | std::ios::binary|std::ios::app);
if (!out)
{
cerr << "contacts.bin文件打开失败\n";
return -1;
}
else if (!contacts.SerializePartialToOstream(&out))
{
cerr << "contacts对象序列化失败\n";
out.close();
return -1;
}
input.close();
out.close();
return 0;
}
void addPersonInfo(contacts2::Contacts &contacts)
{
contacts2::PeopleInfo *p = contacts.add_contacts();
if (!p)
{
cerr << "添加联系人失败\n";
return;
}
cout << "------------添加联系人-----------\n";
cout << "------------请输出姓名: ";
string name;
getline(cin, name);
p->set_name(name);
cout << "------------请输出年龄: ";
int age;
cin >> age;
p->set_age(age);
cin.ignore(256, '\n'); // 在256个字符前,如果遇到'\n'就跳过'\n'并停下来,忽略256个字符后还没遇到'\n'就停下来.
int i = 1;
while (true)
{
string number;
cout << "------------请输出手机号码" << i << ": ";
getline(cin, number);
if (number.empty())
{
cout << "手机号码读取成功\n";
break;
}
contacts2::PeopleInfo_Phone *phone = p->add_phone();
phone->set_number(number);
i++;
cout << "------------请输入手机号码类型(MP:1 or TEL:2)"
<< ": ";
int type = 0;
cin >> type;
cin.ignore(256,'\n');
switch (type)
{
case 1:
cout<<"type:"<<type<<endl;
phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
break;
case 2:
cout<<"type:"<<type<<endl;
phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
break;
default:
cout << "输入手机号码类型有误" << endl;
break;
}
}
contacts2::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);
google::protobuf::Any* data = p->mutable_data();
data->PackFrom(address);
cout<<"请输入其他联系方式 (1:qq 2.wechat): ";
int select =1;
cin>>select;
cin.ignore(256,'\n');
if(select == 1){
cout<<"请输入qq号:";
string qq_str;
getline(cin,qq_str);
p->set_qq(qq_str);
}else if(select == 2){
cout<<"请输入wechat:";
string wechat;
getline(cin,wechat);
p->set_wechat(wechat);
}else{
cout<<"输入有误"<<endl;
}
cout<<"添加联系人成功"<<endl;
}
更新read.cc(通讯录2.3),更新内容如下:
#include
#include
#include
#include
using namespace std;
void printContacts(const contacts2::Contacts &contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
if (!input)
{
cerr << "contacts.bin 不存在该文件\n";
return -1;
}
else if (!contacts.ParseFromIstream(&input))
{
cerr << "contacts.bin 文件序列化失败\n";
input.close();
return -1;
}
// 打印contacts
printContacts(contacts);
return 0;
}
void printContacts(const contacts2::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); i++)
{
const contacts2::PeopleInfo &people = contacts.contacts(i);
cout << "----------联系人" << i << "----------" << endl;
cout << "联系人姓名:" << people.name() << endl;
cout << "联系人年龄:" << people.age() << endl;
for (int j = 0; j < people.phone_size(); j++)
{
cout << "联系手机" << j + 1 << ":" << people.phone(j).number();
cout << " type:(" << people.phone(j).PhoneType_Name(people.phone(j).type()) << ")" << endl;
}
if (people.has_data() && people.data().Is<contacts2::Address>())
{
contacts2::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 contacts2::PeopleInfo::OtherContactCase::kQq :
cout<<"其他联系方式qq号:"<<people.qq()<<endl;
break;
case contacts2::PeopleInfo::OtherContactCase::kWechat :
cout<<"其他联系方式wechat号:"<<people.wechat()<<endl;
break;
case contacts2::PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:
cout<<"其他联系方式:无"<<endl;
break;
}
}
}
代码编写完成后,编译后进⾏读写:
[YYK@VM-8-7-centos proto3_2.3_]$ ./write
------------添加联系人-----------
------------请输出姓名: bbq
------------请输出年龄: 21
------------请输出手机号码1: 123456
------------请输入手机号码类型(MP:1 or TEL:2): 1
------------请输出手机号码2:
手机号码读取成功
请输入联系人家庭地址:guangz
请输入联系人单位地址:guangz
请输入其他联系方式 (1:qq 2.wechat): 2
请输入wechat:YYKGentleman
添加联系人成功
[YYK@VM-8-7-centos proto3_2.3_]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:20
联系手机1:289984 type:(MP)
家庭地址:guangd
单位地址:guand
其他联系方式qq号:2658875847
----------联系人1----------
联系人姓名:bbq
联系人年龄:21
联系手机1:123456 type:(MP)
家庭地址:guangz
单位地址:guangz
其他联系方式wechat号:YYKGentleman
在Protocol Buffers中,oneof实现的基本原理是使用联合体(union)来表示不同字段的内存布局。生成的C++代码中,对于每个oneof定义的字段,都会生成一个相应的枚举类型和一个联合体。
具体来说,假设我们有如下的message定义:
message MyMessage {
oneof my_field {
int32 foo = 1;
string bar = 2;
}
}
生成的C++代码将包含如下的定义:
class MyMessage {
public:
enum MyFieldCase {
kFoo = 1,
kBar = 2,
MY_FIELD_NOT_SET = 0
};
// Getter and setter for the 'foo' field.
int32 foo() const;
void set_foo(int32 value);
// Getter and setter for the 'bar' field.
const std::string& bar() const;
std::string* mutable_bar();
MyFieldCase my_field_case() const;
private:
union MyField {
MyField() {}
~MyField() {}
int32 foo_;
std::string bar_;
} my_field_;
MyFieldCase my_field_case_;
};
其中,MyMessage类中声明了一个枚举类型MyFieldCase用于表示当前哪个字段被设置,同时还有与每个字段对应的getter和setter方法。联合体MyField中包含了所有可能的字段类型,并且只有一个字段会被赋值。MyFieldCase字段用于标识当前哪个字段被设置。
在序列化和反序列化时,根据MyFieldCase字段的值来确定哪个字段应该被序列化或反序列化。在设置或获取某个字段的值时,会将对应的MyFieldCase字段设置为相应的值。需要注意的是,联合体和枚举类型都是C++中的底层语言特性,在使用时需要谨慎处理可能出现的内存错误和类型安全问题。
语法⽀持创建⼀个关联映射字段,也就是可以使⽤map类型去声明字段类型,格式为:
map<key_type, value_type> map_field = N;
要注意的是:
key_type
是除了float
和bytes
类型以外的任意标量类型。 value_type
可以是任意类型。map
字段不可以⽤repeated
修饰map
中存⼊的元素是⽆序的最后,通讯录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 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{
string qq=5;
string wechat=6;
//uint32 work_number = 7;
};
map<string,string> remark = 7;
}
// 通讯录
message Contacts{
repeated PeopleInfo contacts = 1 ;
}
编译
protoc--cpp_out=.contacts.proto
contacts.pb.h更新的部分代码展⽰:
// 更新的 PeopleInfo 类
class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
// map 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
#include
#include
#include
using namespace std;
void addPersonInfo(contacts2::Contacts &contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
if (!input)
{
cerr << "contacts.bin 不存在该文件\n";
return -1;
}
// else if (!contacts.ParseFromIstream(&input))
// {
// cerr << "contacts.bin 文件序列化失败\n";
// input.close();
// return -1;
// }
// 添加一个联系人信息
addPersonInfo(contacts);
// 序列化通讯录写会到contacts.bin文件里
ofstream out("contacts.bin", std::ios::out | std::ios::binary | std::ios::app);
if (!out)
{
cerr << "contacts.bin文件打开失败\n";
return -1;
}
else if (!contacts.SerializePartialToOstream(&out))
{
cerr << "contacts对象序列化失败\n";
out.close();
return -1;
}
input.close();
out.close();
return 0;
}
void addPersonInfo(contacts2::Contacts &contacts)
{
contacts2::PeopleInfo *p = contacts.add_contacts();
if (!p)
{
cerr << "添加联系人失败\n";
return;
}
cout << "------------添加联系人-----------\n";
cout << "------------请输出姓名: ";
string name;
getline(cin, name);
p->set_name(name);
cout << "------------请输出年龄: ";
int age;
cin >> age;
p->set_age(age);
cin.ignore(256, '\n'); // 在256个字符前,如果遇到'\n'就跳过'\n'并停下来,忽略256个字符后还没遇到'\n'就停下来.
int i = 1;
while (true)
{
string number;
cout << "------------请输出手机号码" << i << ": ";
getline(cin, number);
if (number.empty())
{
cout << "手机号码读取成功\n";
break;
}
contacts2::PeopleInfo_Phone *phone = p->add_phone();
phone->set_number(number);
i++;
cout << "------------请输入手机号码类型(MP:1 or TEL:2)"
<< ": ";
int type = 0;
cin >> type;
cin.ignore(256, '\n');
switch (type)
{
case 1:
cout << "type:" << type << endl;
phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
break;
case 2:
cout << "type:" << type << endl;
phone->set_type(contacts2::PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_TEL);
break;
default:
cout << "输入手机号码类型有误" << endl;
break;
}
}
contacts2::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);
google::protobuf::Any *data = p->mutable_data();
data->PackFrom(address);
cout << "请输入其他联系方式 (1:qq 2.wechat): ";
int select = 1;
cin >> select;
cin.ignore(256, '\n');
if (select == 1)
{
cout << "请输入qq号:";
string qq_str;
getline(cin, qq_str);
p->set_qq(qq_str);
}
else if (select == 2)
{
cout << "请输入wechat:";
string wechat;
getline(cin, wechat);
p->set_wechat(wechat);
}
else
{
cout << "输入有误" << endl;
}
for (int i = 0;; i++)
{
cout << "请输入备注" << i << "信息(备注标题):";
string title;
getline(cin, title);
if (title.empty())
{
break;
}
cout << "请输入备注" << i << "信息(备注内容):";
string content;
getline(cin, content);
p->mutable_remark()->insert({title, content});
}
cout << "添加联系人成功" << endl;
}
更新read.cc(通讯录2.4),更新内容如下:
#include
#include
#include
#include
using namespace std;
void printContacts(const contacts2::Contacts &contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input("contacts.bin", std::fstream::in | std::fstream::out);
if (!input)
{
cerr << "contacts.bin 不存在该文件\n";
return -1;
}
else if (!contacts.ParseFromIstream(&input))
{
cerr << "contacts.bin 文件序列化失败\n";
input.close();
return -1;
}
// 打印contacts
printContacts(contacts);
return 0;
}
void printContacts(const contacts2::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); i++)
{
const contacts2::PeopleInfo &people = contacts.contacts(i);
cout << "----------联系人" << i << "----------" << endl;
cout << "联系人姓名:" << people.name() << endl;
cout << "联系人年龄:" << people.age() << endl;
for (int j = 0; j < people.phone_size(); j++)
{
cout << "联系手机" << j + 1 << ":" << people.phone(j).number();
cout << " type:(" << people.phone(j).PhoneType_Name(people.phone(j).type()) << ")" << endl;
}
if (people.has_data() && people.data().Is<contacts2::Address>())
{
contacts2::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 contacts2::PeopleInfo::OtherContactCase::kQq:
cout << "其他联系方式qq号:" << people.qq() << endl;
break;
case contacts2::PeopleInfo::OtherContactCase::kWechat:
cout << "其他联系方式wechat号:" << people.wechat() << endl;
break;
case contacts2::PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:
cout << "其他联系方式:无" << endl;
break;
}
if (people.remark().empty() != true)
{
cout << "备注:" << endl;
}
int j = 0;
for (auto &it : people.remark())
{
cout << "\t备注" << j << it.first << " : " << it.second << endl;
j++;
}
}
}
代码编写完成后,编译后进⾏读写:
[YYK@VM-8-7-centos proto3_2.4_]$ ./write
------------添加联系人-----------
------------请输出姓名: rrr
------------请输出年龄: 333
------------请输出手机号码1: 3333333
------------请输入手机号码类型(MP:1 or TEL:2): 1
------------请输出手机号码2:
手机号码读取成功
请输入联系人家庭地址:ggg
请输入联系人单位地址:ggg
请输入其他联系方式 (1:qq 2.wechat): 1
请输入qq号:3333
请输入备注信息0(备注标题):日期
请输入备注信息0(备注内容):2002/12/23
请输入备注信息1(备注标题):
添加联系人成功
[YYK@VM-8-7-centos proto3_2.4_]$ ./read
----------联系人1---------
联系人姓名:rrr
联系人年龄:333
联系手机1:3333333 type:(MP)
家庭地址:ggg
单位地址:ggg
其他联系方式qq号:3333
备注:
备注0日期 : 2002/12/23
到此,我们对通讯录2.x要求的任务全部完成。在这个过程中我们将通讯录升级到了2.4版本,同时对ProtoBuf的使⽤也进⼀步熟练了,并且也掌握了ProtoBuf的proto3语法⽀持的⼤部分类型及其使⽤,但只是正常使⽤还是完全不够的。通过接下来的学习,我们就能更进⼀步了解到ProtoBuf深⼊的内容。
反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
消息字段
、 oneof
字段 和 any
字段 ,C++
和Java
语⾔中都有has_
⽅法来检测当前字段是否被设置。注意事项:
has_
方法的, 影响不大。has_
方法可能带来的问题.如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型⾮常简单。遵循如下规则即可:
如果通过删除或注释掉字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号。将来使⽤该.proto的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。
确保不会发⽣这种情况的⼀种⽅法是:使⽤ reserved
将指定字段的编号或名称设置为保留项。当我们再使⽤这些编号或名称时,protocolbuffer
的编译器将会警告这些编号或名称不可⽤。举个例⼦:
message Message {
// 设置保留项
reserved 100, 101, 200 to 299; // 保留100,101, 200~299的字段
reserved "field3", "field4"; // 保留字段名
// 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称。
// reserved 102, "field5"; //erro
// 设置保留项之后,下⾯代码会告警
int32 field1 = 100; //告警:Field 'field1' uses reserved number 100
int32 field2 = 101; //告警:Field 'field2' uses reserved number 101
int32 field3 = 102; //告警:Field name 'field3' is reserved
int32 field4 = 103; //告警:Field name 'field4' is reserved
}
现模拟有两个服务,他们各⾃使⽤⼀份通讯录.proto⽂件,内容约定好了是⼀模⼀样的。
案例1分析:
第一步: 准备些文件, client和server有相同的contacts.proto .
syntax = "proto3";
package contacts2;
message PeopleInfo{
//int32 age =1 ; // 年龄
int32 birthday = 1;//生日
string name = 2 ; //姓名
}
// 通讯录
message Contacts{
repeated PeopleInfo contacts = 1 ;
}
Client/contacts.proto
syntax = "proto3";
package contacts2;
message PeopleInfo{
int32 age =1 ; // 年龄
string name = 2 ; //姓名
}
// 通讯录
message Contacts{
repeated PeopleInfo contacts = 1 ;
}
第三步: Server编译并写入一个联系人
第四 步: 可以发现client获取年龄时却获取到了生日,因为在两个proto文件里,生日字段和年龄字段编号相同.
案例2分析:
现模拟有两个服务,他们各⾃使⽤⼀份通讯录.proto⽂件,内容约定好了是⼀模⼀样的。
第一步: 准备些文件, client和server有相同的contacts.proto .
Server/contacts.proto
syntax = "proto3";
package contacts2;
message PeopleInfo{
int32 age =1 ; // 年龄
string name = 2 ; //姓名
}
// 通讯录
message Contacts{
repeated PeopleInfo contacts = 1 ;
}
第二步: 编译并写入一个联系人
[YYK@VM-8-7-centos server]$ ./write
------------添加联系人-----------
------------请输出姓名: yyk
------------请输出年龄: 20
添加联系人成功
[YYK@VM-8-7-centos client]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:20
第三步: 更改Server的proto文件
syntax = "proto3";
package contacts2;
message PeopleInfo{
int32 age =1 ; // 年龄
string name = 2 ; //姓名
int32 birthday =3 ; // 添加生日字段
}
// 通讯录
message Contacts{
repeated PeopleInfo contacts = 1 ;
}
第四步: 编译并录入一个联系人
[YYK@VM-8-7-centos server]$ protoc --cpp_out=. contacts.proto
[YYK@VM-8-7-centos server]$ g++ -o write write.cpp contacts.pb.cc -std=c++11 -lprotobuf ;./write
------------添加联系人-----------
------------请输出姓名: bbq
------------请输出年龄: 22
------------请输出生日: 20021223
添加联系人成功
第五步: 执行client
[YYK@VM-8-7-centos client]$ ./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:20
----------联系人1----------
联系人姓名:bbq
联系人年龄:22
第六步: 可以发现 由于client没有改变proto文件,所以读取不了birthday字段的数据.
在通讯录3.0版本中,我们向service⽬录下的contacts.proto新增了‘⽣⽇’字段,但对于client相关的代码并没有任何改动。验证后发现新代码序列化的消息(service)也可以被旧代码(client)解析。并且这⾥要说的是,新增的‘⽣⽇’字段在旧程序(client)中其实并没有丢失,⽽是会作为旧程序的未知字段。
了解相关类关系图
//google::protobuf::Message 部分代码展⽰
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;
Descriptor类介绍(了解)
// 部分代码展⽰
class PROTOBUF_EXPORT Descriptor : private internal::SymbolBase {
string& name () const
int 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类介绍(了解)
//google::protobuf::Message 部分代码展⽰
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;
Descriptor类介绍(了解)
// 部分代码展⽰
class PROTOBUF_EXPORT Descriptor : private internal::SymbolBase {
string& name () const
int 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;
}
FieldDescriptor类的介绍(了解)
在protobuf3中,FieldDescriptor表示了一个字段的描述信息,包括字段的名称、编号、类型、是否为重复字段、默认值等。
一个FieldDescriptor对象可以从MessageDescriptor中获取,表示了一个Message中的一个字段。
FieldDescriptor有如下属性:
`
name:字段的名称
full_name:字段的完整名称,包括Message的名称和字段的名称
index:字段在Message中的编号
number:字段的编号
type:字段的类型,可以是double、float、int32、int64、uint32、uint64、sint32、sint64、fixed32、fixed64、sfixed32、sfixed64、bool、string、bytes、enum、message其中之一
label:字段的标签,可以是optional、required、repeated其中之一
default_value:字段的默认值,根据类型不同而不同
`
例如,对于以下的protobuf定义:
message Person { string name = 1; int32 age = 2; repeated string email = 3; }
我们可以通过以下代码获取对应的FieldDescriptor:
person_desc = Person.DESCRIPTOR name_desc = person_desc.fields_by_name[‘name’] age_desc = person_desc.fields_by_name[‘age’] email_desc = person_desc.fields_by_name[‘email’]
Reflection类介绍(了解)
在 protobuf3 中,Reflection 是一种机制,用于在运行时动态地操作消息类型,包括访问/修改字段值、获取字段名和类型、查询消息是否存在字段等。
Reflection 的主要接口是 Message 类的 GetReflection() 方法,通过该方法可以获取一个 MessageReflection 对象。MessageReflection 对象提供了一组方法,用于访问消息类型的所有字段。
例如,通过 MessageReflection 的 GetField() 方法可以获取消息类型中指定字段的值,如下所示:
google::protobuf::MessageReflection* reflection = message->GetReflection();
const google::protobuf::FieldDescriptor* field = reflection->FindFieldByName("field_name");
if (field != NULL) {
switch (field->type()) {
case google::protobuf::FieldDescriptor::TYPE_INT32:
int32_t value = reflection->GetInt32(*message, field);
break;
case google::protobuf::FieldDescriptor::TYPE_STRING:
std::string value = reflection->GetString(*message, field);
break;
// handle other types
// ...
}
}
通过 MessageReflection 的 MutableField() 方法可以修改消息类型中指定字段的值,如下所示:
google::protobuf::MessageReflection* reflection = message->GetReflection();
const google::protobuf::FieldDescriptor* field = reflection->FindFieldByName("field_name");
if (field != NULL) {
switch (field->type()) {
case google::protobuf::FieldDescriptor::TYPE_INT32:
reflection->SetInt32(message, field, 42);
break;
case google::protobuf::FieldDescriptor::TYPE_STRING:
reflection->SetString(message, field, "hello");
break;
// handle other types
// ...
}
}
除了访问/修改字段值之外,Reflection 还提供了一些其他的方法,例如:
GetDescriptor():获取消息类型的描述符;
GetRepeatedFieldSize():获取重复字段的大小;
AddMessage():向消息类型的重复字段中添加一个新的消息;
ClearField():清空消息类型的指定字段的值。
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;
// 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();
};
需求:
更新client.cc(通讯录3.1),在这个版本中,需要打印出未知字段的内容。更新的代码如下:
#include
#include
#include
#include
using namespace std;
void printContacts(const contacts2::Contacts &contacts);
int main()
{
contacts2::Contacts contacts;
// 将contacts.bin 文件的数据进行反序列化
std::ifstream input("../contacts.bin", std::fstream::in | std::fstream::out);
if (!input)
{
cerr << "contacts.bin 不存在该文件\n";
return -1;
}
else if (!contacts.ParseFromIstream(&input))
{
cerr << "contacts.bin 文件序列化失败\n";
input.close();
return -1;
}
// 打印contacts
printContacts(contacts);
return 0;
}
void printContacts(const contacts2::Contacts &contacts)
{
for (int i = 0; i < contacts.contacts_size(); i++)
{
const contacts2::PeopleInfo &people = contacts.contacts(i);
cout << "----------联系人" << i << "----------" << endl;
cout << "联系人姓名:" << people.name() << endl;
cout << "联系人年龄:" << people.age() << endl;
const google::protobuf::Reflection* reflection = contacts2::PeopleInfo::GetReflection();
const google::protobuf::UnknownFieldSet &fields=reflection->GetUnknownFields(people);
for(int j=0;j<fields.field_count();j++)
{
const google::protobuf::UnknownField & filed = fields.field(j);
cout<<"未知字段"<<j+1<<":";
cout<<"\t字段编号:"<<filed.number();
cout<<"\t类型:" << filed.type();
switch(filed.type())
{
case google::protobuf::UnknownField::Type::TYPE_VARINT :
{
cout<<"\t值:"<<filed.varint()<<endl;
break;
}
case google::protobuf::UnknownField::Type::TYPE_LENGTH_DELIMITED :
{
cout<<"\t值:"<<filed.length_delimited()<<endl;
break;
}
default:
break;
// 等等
}
}
}
}
编译运行 client ,结果
[YYK@VM-8-7-centos client]$ g++ -o read read.cpp contacts.pb.cc -std=c++11 -lprotobuf;./read
----------联系人0----------
联系人姓名:yyk
联系人年龄:20
----------联系人1----------
联系人姓名:bbq
联系人年龄:20021223
----------联系人2----------
联系人姓名:bbq
联系人年龄:22
未知字段1: 字段编号:3 类型:0 值:20021223
根据上述的例⼦可以得出,pb是具有向前兼容的。为了叙述⽅便,把增加了“⽣⽇”属性的service称为“新模块”;未做变动的client称为“⽼模块”。
.proto
⽂件中可以声明许多选项,使⽤ option 标注。选项能影响proto编译器的某些处理⽅式。
proto3 中的可选字段是什么意思
在proto3中,所有的字段都是可选的,即使你不显式地标记它们为可选的。这意味着,如果你没有为该字段提供值,它将被设置为默认值。如果你希望指定某个字段的默认值,可以在消息定义中使用[default = value]语法在prot3中已经不支持显示默认值了, 之前的其中value是你希望该字段默认设置的值。如果你不指定默认值,则proto3将为该字段设置一个合理的默认值,例如0、false、空字符串或空数组,具体取决于字段的类型。
proto3 中 option 和optional的区别
在proto3中,option是一个关键字,用于设置message或field的选项,例如设置默认值、序列化等选项。而optional是在proto2中使用的关键字,用于定义可选的field,被proto3废弃。
在proto3中,默认情况下所有的field都是可选的,不需要使用optional关键字来指定。如果一个field不被设置值,那么在解析时会返回该field对应类型的默认值,例如int32类型的默认值为0,bool类型的默认值为false。
因此,option和optional在proto3中已经没有区别了。
pb已经写好了可选字段,分别在这些message里.,选项的完整列表在google/protobuf/descriptor.proto中定义。部分代码:
syntax = "proto2"; // descriptor.proto 使⽤ proto2 语法版本
message FileOptions {// ⽂件选项 定义在 FileOptions 消息中
optional string name = 1;
optional int32 number = 3;
optional Label label = 4;
optional Type type = 5;
optional string type_name = 6;
...
}
message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中
message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中
message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中
message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中
message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中
message MethodOptions { ... } // 服务⽅法选项 定义在 MethodOptions 消息中
...
由此可⻅,选项分为 ⽂件级、消息级、字段级 等等,但并没有⼀种选项能作⽤于所有的类型。
案例一
syntax = "proto3";
import "google/protobuf/descriptor.proto";
extend google.protobuf.FieldOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "hello shuaige" ;
optional int32 age = 1;
}
如上代码解释:
google.protobuf.FieldOptions
这个message
添加一个 my_option 的可选字段.想要获取my_option,如下c++所示 :
#include"test.pb.h"
#include
#include
using namespace std;
int main()
{
string value = MyMessage::descriptor()->options().GetExtension(my_option);
cout<<value<<endl;
return 0;
}
运行结果
g++ -o test test.cpp test.pb.cc -std=c++11 -lprotobuf;./test
hello shuaige
SPEED:优化为序列化和反序列化速度最快,但可能会使生成的文件大小较大。
CODE_SIZE:优化为生成较小的代码,可能会牺牲一些序列化和反序列化的速度。
LITE_RUNTIME:仅生成最小限度的运行时代码,可能会使序列化和反序列化的速度更快,并且生成的文件更小。
例如,以下是在 .proto 文件中指定 optimize_for 的示例:
syntax = "proto3";
package example;
option optimize_for = LITE_RUNTIME;
message Person {
string name = 1;
int32 age = 2;
}
这个示例将 optimize_for 设置为 LITE_RUNTIME,以生成最小限度的运行时代码。
optimize_for 可选字段定义在descriptor.proto
文件里,如下部分代码
message FileOptions {
enum OptimizeMode {
SPEED = 1; // Generate complete code for parsing, serialization,
// etc.
CODE_SIZE = 2; // Use ReflectionOps to implement these methods.
LITE_RUNTIME = 3; // Generate code using MessageLite and the lite runtime.
}
optional OptimizeMode optimize_for = 9 [default = SPEED];
}
举个例⼦:
enum PhoneType {
option allow_alias = true;
MP = 0;
TEL = 1;
LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}