当前位置: 首页 > news >正文

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; // 固定电话
}

要注意枚举类型的定义有以下⼏种规则:

  1. 0值常量必须存在,且要作为第⼀个元素。这是为了与proto2的语义兼容:第⼀个元素作为默认值,且值为0。
  2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
  3. 枚举的常量值在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 语法详解 的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉
在这里插入图片描述

相关文章:

ProtoBuf:proto3 语法详解

&#x1f308; 个人主页&#xff1a;Zfox_ &#x1f525; 系列专栏&#xff1a;ProtoBuf 在语法详解部分&#xff0c;依旧使⽤项⽬推进的⽅式完成讲解。这个部分会对通讯录进⾏多次升级&#xff0c;使⽤2.x表⽰升级的版本&#xff0c;最终将会升级如下内容&#xff1a; 不再打…...

博图SCL语言GOTO语句深度解析:精准跳转

在SCL编程中&#xff0c;**GOTO语句**是控制流程的底层工具&#xff0c;它允许程序无条件跳转到指定的**标签位置**。虽然现代编程中较少使用&#xff0c;但在特定工业场景下仍能发挥独特价值。 GOTO语句核心机制 基本语法结构 // 定义标签 <标签名>: // 跳转指令 GOTO…...

面试题-在ts中有两个类型,一个是a,一个是b,这两个联合起来就是c,如何实现联合

在 TypeScript 中&#xff0c;若要将两个类型 a 和 b 联合成一个新类型 c&#xff0c;可以使用 联合类型&#xff08;Union Type&#xff09; 或 交叉类型&#xff08;Intersection Type&#xff09;&#xff0c;具体取决于你的需求&#xff1a; 一、联合类型&#xff08;Unio…...

Mac电脑-触摸板增强工具-BetterTouchTool

BetterTouchTool mac 触摸板增强工具&#xff0c;允许用户使用各种手势来控制其计算机。 Bettertouchtool mac是一个小而高效的macOS应用程序&#xff0c;旨在帮助您为手势定义快捷方式。 此外&#xff0c;Bettertouchtool可用于使用常规鼠标和键盘快捷键&#xff0c;并提供伴…...

MySQL误删数据急救指南:基于Binlog日志的实战恢复详解

背景 数据误删是一个比较严重的场景 1.典型误操作场景 场景1&#xff1a;DELETE FROM orders WHERE status0 → 漏写AND create_time>‘2025-06-20’ 场景2&#xff1a;DROP TABLE customer → 误执行于生产环境 认识 binlog 1.binlog 的核心作用 记录所有 DDL/DML 操…...

API网关Apisix管理接口速查

&#x1f9ed; 管理接口总体分类&#xff08;基于 REST API&#xff09; 资源类别接口路径前缀功能说明路由&#xff08;Routes&#xff09;/apisix/admin/routes/{id}定义 HTTP 请求的匹配规则及转发目标服务&#xff08;Services&#xff09;/apisix/admin/services/{id}封装…...

React 组件通信

父传子 函数式组件 function Footer(props){const [count,setCount] useState(0)const {name,age} propsconst onClick ()>{setCount(count1)}return (<div><button onClick{()>{onClick()}}>点此1</button><div>{count}</div><di…...

Zephyr 电源管理机制深度解析:从 Tickless Idle 到平台 Suspend 实践

本文系统解析 Zephyr 的电源管理机制&#xff0c;包括 Tickless Idle 模式、系统 suspend/resume 生命周期管理、平台级功耗优化 Hook、自定义设备电源域&#xff0c;以及如何结合低功耗 SoC 实现最小化功耗设计。全文超过 5000 字&#xff0c;适合构建对功耗敏感的 IoT、BLE、…...

clickhouse-server连不上clickhouse-keeper的问题记录

背景 想简单部署一个1 shard 2 replica&#xff0c;1keeper的集群。 有两个虚拟机&#xff1a;192.168.1.3&#xff0c;192.168.1.6。 192.168.1.3&#xff1a;部署1个ck&#xff0c;1个keeper 192.168.1.6&#xff1a;部署1个ck 192.168.1.3和192.168.1.6的ck组成1个shar…...

Python 数据分析与可视化 Day 3 - Pandas 数据筛选与排序操作

&#x1f3af; 今日目标 掌握 DataFrame 的条件筛选&#xff08;布尔索引&#xff09;学会多条件筛选、逻辑运算熟练使用排序&#xff08;sort_values&#xff09;提升数据组织力结合列选择进行数据提取分析 &#x1f9ea; 一、列选择与基本筛选 ✅ 选择单列 / 多列 df[&quo…...

Android NDK下载链接及配置版本

Android NDK下载链接及配置版本 https://github.com/android/ndk/releases 在build.gralde里面这样配置ndk具体版本号&#xff1a; android {ndkVersion "27.0.12077973" } Android Studio报错&#xff1a;Could not move temporary workspace () to immutable locat…...

Mac Parallels Desktop Kali 2025 代理设置

Mac Parallels Desktop Kali 2025 代理设置 核心步骤&#xff1a; kali设置桥接wifi 查看kali和主机ip 运行命令ifconfig查看kali ip&#xff1a; mac主机ip&#xff1a; kali设置proxy ip填写主机ip&#xff0c;port为主机proxy端口 enjoy...

Python 的内置函数 hash

Python 内建函数列表 > Python 的内置函数 hash Python 的内置函数 hash() 是一个非常有用的工具函数&#xff0c;主要用于获取对象的哈希值。哈希值是一个固定长度的整数&#xff0c;代表该对象的唯一标识。在 Python 中&#xff0c;hash() 函数常用于字典键值、集合元素等…...

文生视频(Text-to-Video)

&#x1f552; 生成时间&#xff1a;每张图大概 10–60 秒&#xff08;取决于设备&#xff09; ✅ 二、文生视频&#xff08;Text-to-Video&#xff09; 以下项目中&#xff0c;很多都基于 SD 模型扩展&#xff0c;但视频生成复杂度高&#xff0c;生成时间一般 超过 30 秒&am…...

(LeetCode 面试经典 150 题) 80. 删除有序数组中的重复项 II (双指针、栈)

题目&#xff1a;80. 删除有序数组中的重复项 II 思路&#xff1a;左指针 left 类似于指向栈顶的下一个待填的元素&#xff0c;每次遍历只需看当前元素nums[i]和栈顶的下一个元素nums[left-2]是否相等&#xff0c;不等就可以插入栈当中。时间复杂度0(n)。 C版本&#xff1a; …...

【舞蹈】编排:如何对齐拍子并让小节倍数随BPM递减

音的强弱关系 当前划分编排最小单位的代码的分析 📊 代码逻辑分析 ✅ 完整性方面 代码逻辑相对完整,包含了: 结构段落分析(intro, verse, chorus等)强拍时间点提取歌词时间轴处理AI增强的编舞建议生成⚠️ 主要问题 1. 强拍对齐逻辑不够精确 # 当前代码只是简单提取…...

LangGraph--基础学习(工具调用)

本节将详细学习大模型是怎么调用工具的&#xff0c;为什么可以调用工具等等&#xff0c;手写一个工具调用&#xff0c;后续可以通过mcp自己调用即可&#xff0c;没必要自己写&#xff0c;但是学习过程中需要手写&#xff0c;通常怎么使用第三方工具调用呢&#xff1f; import o…...

华为云 Flexus+DeepSeek 实战:华为云单机部署 Dify-LLM 开发平台全流程指南【服务部署、模型配置、知识库构建全流程】

华为云 FlexusDeepSeek 实战&#xff1a;华为云单机部署 Dify-LLM 开发平台全流程指南【服务部署、模型配置、知识库构建全流程】 文章目录 华为云 FlexusDeepSeek 实战&#xff1a;华为云单机部署 Dify-LLM 开发平台全流程指南【服务部署、模型配置、知识库构建全流程】前言1、…...

【appium】2.初始连接脚本配置

连接配置 from appium import webdriver desired_caps {platformName: Android,automationName: UIAutomator2,deviceName: ZTEB880,appPackage: com.taobao.taobao,appActivity: com.taobao.tao.welcome.Welcome,noReset: True }driver webdriver.Remote(http://localhost:…...

磁性传感器在电机控制闭环系统中的反馈作用

磁性传感器的基本原理和类型 基本原理 &#xff1a;磁性传感器是基于磁学原理来检测磁场强度、方向或其他与磁场相关的物理量。常见的磁性传感器有霍尔传感器、磁阻传感器等。霍尔传感器是利用霍尔效应工作的&#xff0c;当电流通过置于磁场中的半导体材料时&#xff0c;在垂直…...

Python:.py文件如何变成双击可执行的windows程序?(版本1)

1、如下.py文件&#xff0c;右键重命名文件后缀名&#xff1a;py改为&#xff1a;pyw 2、修改时&#xff0c;提示如下&#xff1a;不用管点击&#xff1a;是即可 3、之后双击&#xff0c;即可执行python代码文件。 好的&#xff0c;我们来详细介绍一下 Python 的 .pyw 文件。 简…...

Spring Boot + MyBatis + Vue:全栈开发的深度剖析与实践指南

一、技术栈深度剖析 &#xff08;一&#xff09;Spring Boot&#xff1a;后端开发的加速器 Spring Boot 是基于 Spring 框架的一个开源 Java 项目&#xff0c;旨在简化基于 Spring 的应用开发。它通过自动配置机制&#xff0c;能够根据项目中添加的依赖自动配置 Spring 和相关…...

学习C++、QT---03(C++的输入输出、C++的基本数据类型介绍)

每日一言 你比想象中更强大&#xff0c;那些咬牙坚持的瞬间&#xff0c;都在雕刻更好的你。 案例&#xff1a;C的输入输出 但是我也会用c语言的方式来回顾c语言的写法&#xff0c;因为两种语言都是密不可分的&#xff0c;所以不能忘记&#xff0c;所以两个一起写 注意点&#…...

八、Redis的主从原理、哨兵

简介&#xff1a; 想要了解Redis的主从原理&#xff0c;首先得认识一个基本的分布式理论-CAP理论。要理解这个理论&#xff0c;其实也非常简单。 CAP理论 C&#xff1a;Consistency、A&#xff1a;Available、P&#xff1a;Partition tolerance 。这是CAP三个字母的全称。C&…...

springboot通过独立事务管理器实现资源隔离与精准控制​

安心流转站核心业务模块&#xff0c;为什么&#xff01;我们考虑这样设计&#xff0c;下面讲讲专用事务管理器的设计与必要性​&#xff01; 一、为什么需要专属事务管理器&#xff1f;​​ 在安心流转站模块中&#xff0c;存在 ​​「多资源混合操作」​​ 和 ​​「业务高敏…...

59-Oracle 10046事件-知识准备

上一篇说到了autotrace&#xff0c;SQL调试时候的获取性能和参数数据&#xff0c;直接用上trace&#xff0c;还有个更全能的工具10046。是不是很多小伙伴会对这么个数字&#xff0c;觉得起名很奇怪&#xff0c;数字起名任性。“10046”本质是Oracle内核事件的随机性技术编号&am…...

2025年渗透测试面试题总结-2025年HW(护网面试) 03(题目+回答)

安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 2025年HW(护网面试) 03 1. 同源策略&#xff08;Same-Origin Policy&#xff09; 2. XSS攻击用途 3. XSS类…...

嵌入式开发之嵌入式系统硬件架构设计时,如何选择合适的微处理器/微控制器?

在嵌入式系统硬件架构设计中&#xff0c;选择合适的微处理器 / 微控制器&#xff08;MCU/MPU&#xff09;是关键环节&#xff0c;需从多维度综合评估。以下是系统化的选择策略及核心考量因素&#xff1a; 一、明确应用需求与核心指标 1. 性能需求 处理能力&#xff1a;根据任…...

C++(面向对象编程——继承)

继承基础概念 1.什么是继承&#xff1f; 继承是C三大特性之一&#xff1b;继承是一个已经存在的类的基础上新建一个类&#xff0c;新建的类拥有已经存在的类的特性。主要提现的是代码复用的思想。新的类继承了基类的所有成员变量和成员函数&#xff0c;包括不显示的函数&…...

Unity Shader开发-着色器变体(2)-定义着色器变体

一.定义着色器变体 定义一个着色器变体&#xff08;Shader Variant&#xff09;从概念和实现上讲&#xff0c;主要包括以下几个核心部分 1.使用预编译指令来声明变体关键字 关键字是驱动变体生成的“开关”。它们是简单的字符串标识符&#xff0c;用于在 Shader 代码中标记不…...

Cookie和Session的作用和区别

Cookie 客户端持久化保存服务器数据的一种机制&#xff08;持久化存储就是存硬盘里&#xff09;。Cookie文件数据为键值对形式&#xff0c;客户端根据服务器域名的不同分别存储Cookie&#xff0c;不同域名的Cookie不同&#xff0c;不会产生冲突。 典型应用场景&#xff1a; 保…...

Redis集群部署终极指南:架构选型、生产部署与深度优化

第一部分&#xff1a;Redis集群技术全景解析 1.1 Redis集群演进史 单机时代&#xff08;2009-2012&#xff09;&#xff1a;Redis 2.8之前&#xff0c;纯单机模式复制时代&#xff08;2012-2015&#xff09;&#xff1a;Redis 2.8引入PSYNC改进复制哨兵时代&#xff08;2015-…...

腾讯云IM即时通讯:开启实时通信新时代

一、引言 在当今数字化浪潮席卷全球的时代&#xff0c;即时通讯已然成为互联网世界中不可或缺的关键元素。无论是个人日常生活中的社交互动&#xff0c;还是企业运营里的高效协作&#xff0c;即时通讯都发挥着举足轻重的作用&#xff0c;已然渗透到人们生活与工作的每一个角落…...

基于Qt的UDP主从服务器设计与实现

概述 一个基于Qt框架实现的UDP主从服务器系统&#xff0c;该系统具备自动主机选举、故障转移和状态同步等关键功能&#xff0c;适用于分布式能源管理系统中的设备通信与协调。 系统核心功能 1. 自动主机选举与故障转移 系统通过优先级机制实现自动主机选举&#xff0c;当主机…...

JVM(8)——详解分代收集算法

JVM 的分代收集算法不是一种具体的垃圾收集算法实现&#xff0c;而是一种指导思想和设计原则&#xff0c;是现代 JVM 垃圾收集器的基石。其核心思想源于对程序运行过程中对象生命周期分布的观察&#xff08;即弱分代假说&#xff09;。 核心思想与理论基础&#xff1a;分代假说…...

深入Java面试:从Spring Boot到微服务

深入Java面试&#xff1a;从Spring Boot到微服务 在准备互联网大厂的Java岗位面试时&#xff0c;掌握核心技术栈是关键。本文将从技术栈中选取几个重要的技术点进行探讨&#xff0c;帮助你在面试中脱颖而出。 问题一&#xff1a;Spring Boot的核心特性是什么&#xff1f; 面…...

【软考高级系统架构论文】论无服务器架构及其应用

论文真题 近年来,随着信息技术的迅猛发展和应用需求的快速更迭,传统的多层企业应用系统架构面临越来越多的挑战,已经难以适应这种变化。在这一背景下,无服务器架构(Serverless Architecture) 逐渐流行,它强调业务逻辑由事件触发,具有短暂的生命周期,运行于无状态的轻量…...

Snapchat矩阵运营新策略:亚矩阵云手机打造高效社交网络

1. Snapchat平台特性与风控挑战​​ Snapchat作为全球领先的即时社交平台&#xff0c;其独特的阅后即焚功能和强社交属性使其风控系统极为严格&#xff1a; ​​核心风控机制​​ ​​设备指纹检测​​&#xff1a;记录设备ID、系统版本、IP地址等硬件信息​​行为模式分析​…...

BGP路由反射器(RR)实验详解,结尾有详细脚本

目录 路由反射器基础概念 实验拓扑与设计 实验配置步骤 配置验证与排错 实验总结 完整配置命令集 路由反射器基础概念 在传统的IBGP网络中&#xff0c;为了防止路由环路&#xff0c;BGP规定通过IBGP学到的路由不能再传递给其他IBGP对等体&#xff0c;这导致所有IBGP路由…...

【JAVA】数组的使用

文章目录 前言一、数组的基本概念1.1 数组的创建和初始化1.2 数组的基本使用 二、数组是引用类型2.1 初始JVM的内存分布JVM内存划分&#xff08;按功能分区&#xff09; 2.2 基本类型变量与引用类型变量的区别2.3 再谈引用变量2.4 认识null 三、数组作为函数的参数和返回值四、…...

Python的6万张图像数据集CIFAR-10和CIFAR-100说明

CIFAR-10和CIFAR-100数据集是8000万张微小图像数据集的标记子集。CIFAR-10和CIFAR-100都是由AlexKrizhevsky、VinodNair和GeoffreyHinton创建。数据集说明的网页&#xff1a;https://www.cs.toronto.edu/~kriz/cifar.html 一、CIFAR-10数据集 &#xff08;一&#xff09;CIFA…...

CTF--PhP Web解题(走入CTF)

前情提要 分享有趣CTF题目&#xff0c;记录学习过程 题目&#xff08;带注释,方便理解&#xff09; <?php // 开启PHP源代码高亮显示&#xff0c;输出当前文件内容&#xff08;用于调试/展示&#xff09; highlight_file(__FILE__);// 关闭所有错误报告&#xff0c;防止敏感…...

【Linux仓库】进程概念与基本操作【进程·贰】

&#x1f31f; 各位看官好&#xff0c;我是&#xff01; &#x1f30d; Linux Linux is not Unix &#xff01; &#x1f680; 今天来学习Linux中进程概念与基本操作。 &#x1f44d; 如果觉得这篇文章有帮助&#xff0c;欢迎您一键三连&#xff0c;分享给更多人哦&#xff01…...

Z-Ant开源程序是简化了微处理器上神经网络的部署和优化

​一、软件介绍 文末提供程序和源码下载 Z-Ant &#xff08;Zig-Ant&#xff09; 是一个全面的开源神经网络框架&#xff0c;专门用于在微控制器和边缘设备上部署优化的 AI 模型。Z-Ant 使用 Zig 构建&#xff0c;为资源受限的硬件上的模型优化、代码生成和实时推理提供端到端…...

面试题-在ts中类型转换的方法

在 TypeScript 中&#xff0c;类型转换主要分为 类型断言&#xff08;Type Assertion&#xff09;、类型守卫&#xff08;Type Guard&#xff09; 和 类型兼容转换 三种方式。以下是详细分类和示例&#xff1a; 一、类型断言&#xff08;Type Assertion&#xff09; 强制编译…...

【论文笔记】【强化微调】T-GRPO:对视频数据进行强化微调

tulerfeng/Video-R1: Video-R1: Reinforcing Video Reasoning in MLLMs [&#x1f525;the first paper to explore R1 for video] 1. 引述 在强化微调中&#xff0c;像 GRPO、DAPO 这样的方法都是对文本或者图片进行微调思考&#xff0c;所以这类微调方法不对时序信息做处理&…...

`shallowReactive` 与 `shallowRef`:浅层响应式 API

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》、《前端求职突破计划》 &#x1f35a; 蓝桥云课签约作者、…...

使用Node.js开发服务端接口

Node.js是一个基于JavaScript的运行时环境&#xff0c;非常适合开发高性能的服务端接口。以下是开发服务端接口的主要方法和步骤。 选择合适的框架 Express、Koa或Fastify是Node.js中常用的框架。Express是最流行的选择&#xff0c;适合快速开发。Koa更轻量&#xff0c;适合需…...

`teleport` 传送 API 的使用:在 Vue 3 中的最佳实践

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》、《前端求职突破计划》 &#x1f35a; 蓝桥云课签约作者、…...

Linux 多种方式实现行转列

目录 一. 前提二. xargs 实现行转列三. paste 实现行转列四. sed 实现行转列 一. 前提 ⏹之前在这下面篇文章中使用sed命令实现了行专列&#xff0c;本篇文章再介绍几种更加简单的方式。 Linux sed案例 &#x1f449; 20231126-2.log 110120 SPLREQUEST 内容1 AAA memberID1…...