proto 3 语法详解 ----(2)

1. 更新规则

(1)如果现有的消息类型已经不再满足我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型非常简单。遵循如下规则即可:

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

2. 保留字段 reserved

(1)如果通过 删除 或 注释掉 字段来更新消息类型,未来的用户在添加新字段时,有可能会使用以前已经存在,但已经被删除或注释掉的字段编号。将来使用该 .proto 的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。

  • 确保不会发生这种情况的⼀种方法是:使用 reserved 将指定字段的编号或名称设置为保留项 。当我们再使用这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可用。举个例子:
message Message {
	// 设置保留项
	reserved 100, 101, 200 to 299;
	reserved "field3", "field4";
	// 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称。
	// reserved 102, "field5";
	// 设置保留项之后,下⾯代码会告警
	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
}
  • 下面提供通讯录来验证。

3. 创建通讯录 3.0 版本—验证 错误删除字段 造成的数据损坏

(1)现模拟有两个服务,他们各自使用一份通讯录 .proto 文件,内容约定好了是一模一样的。

  • 服务1(service):负责序列化通讯录对象,并写入文件中。
  • 服务2(client):负责读取文件中的数据,解析并打印出来。
  • 一段时间后,service 更新了自己的 .proto 文件,更新内容为:删除了某个字段,并新增了⼀个字段,新增的字段使用了被删除字段的字段编号。并将新的序列化对象写进了文件。
  • 但 client 并没有更新自己的 .proto 文件。根据结论,可能会出现数据损坏的现象,接下来就让我们来验证下这个结论。

(2)新建两个目录: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;
	// 先读取已存在的 contacts
	fstream 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());
	// 向磁盘⽂件写⼊新的 contacts
	fstream 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;
}
  • service 目录下新增 Makefile:
service:service.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
	rm -f service
  • 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;
	}
	
	// 以⼆进制⽅式读取 contacts
	Contacts contacts;
	fstream input(argv[1], ios::in | ios::binary);
	if (!contacts.ParseFromIstream(&input)) 
	{
		cerr << "Failed to parse contacts." << endl;
		input.close();
		return -1;
	}
	
	// 打印 contacts
	PrintfContacts(contacts);
	input.close();
	google::protobuf::ShutdownProtobufLibrary();
	return 0;
}
  • client 目录下新增 Makefile:
client:client.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
	rm -f client

(3)代码编写完成后,进行一次读写(读写前的编译过程省略,自行操作)。

[xiaomaker@xiaomaker-virtual-machine:service]$ ./service ../contacts.bin
../contacts.bin: File not found. Creating a new file.
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 张珊
请输⼊联系⼈年龄: 34
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 131
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------

[xiaomaker@xiaomaker-virtual-machine:client]$ ./client ../contacts.bin
------------联系⼈1------------
姓名:张珊
年龄:34
电话1: 131
  • 确认无误后,对 service ⽬录下的 contacts.proto 文件进行更新:删除 age 字段,新增 birthday 字段,新增的字段使用被删除字段的字段编号。

(4)更新后的 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):
#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'); */
	cout << "请输⼊联系⼈⽣⽇: ";
	int birthday;
	cin >> birthday;
	people_info_ptr->set_birthday(birthday);
	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[]) 
{
	...
}
  • 我们对 client 相关的代码保持原样,不进行更新。
  • 再进行一次读写(对 service.cc 编译过程省略,自行操作)。
[xiaomaker@xiaomaker-virtual-machine:service]$ ./service ../contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 李四
请输⼊联系⼈⽣⽇: 1221
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 151
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------
[xiaomaker@xiaomaker-virtual-machine:client]$ ./client ../contacts.bin
------------联系⼈1------------
姓名:张珊
年龄:34
电话1: 131
------------联系⼈2------------
姓名:李四
年龄:1221
电话1: 151
  • 这时问题便出现了,我们发现输入的生日,在反序列化时,被设置到了使用了相同字段编号的年龄上!!所以得出结论:若是移除老字段,要保证不再使用移除字段的字段编号,不建议直接删除或注释掉字段。
  • 那么正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使用。

(5)正确 service 目录下的 contacts.proto 写法如下(终版通讯录 3.0)。

syntax = "proto3";
package s_contacts;
// 联系⼈
message PeopleInfo {
	reserved 2;
	string name = 1; // 姓名
	int32 birthday = 4; // ⽣⽇
	
	message Phone {
		string number = 1; // 电话号码
	}
	
	repeated Phone phone = 3; // 电话
}

// 通讯录
message Contacts {
	repeated PeopleInfo contacts = 1;
}
  • 编译 .proto 文件后,还需要重新编译下 service.cc,让 service 程序保持使用新生成的 pb C++文件。
[xiaomaker@xiaomaker-virtual-machine:service]$ ./service ../contacts.bin
-------------新增联系⼈-------------
请输⼊联系⼈姓名: 王五
请输⼊联系⼈⽣⽇: 1112
请输⼊联系⼈电话1(只输⼊回⻋完成电话新增): 110
请输⼊联系⼈电话2(只输⼊回⻋完成电话新增):
-----------添加联系⼈成功-----------
[xiaomaker@xiaomaker-virtual-machine:client]$ ./client ../contacts.bin
------------联系⼈1------------
姓名:张珊
年龄:34
电话1: 131
------------联系⼈2------------
姓名:李四
年龄:1221
电话1: 151
------------联系⼈3------------
姓名:王五
年龄:0
电话1: 110
  • 根据实验结果,发现 ‘王五’ 的年龄为 0,这是由于新增时未设置年龄,通过 client 程序反序列化时,给年龄字段设置了默认值 0。这个结果显然是我们想看到的。
  • 还要解释⼀下 ‘李四’ 的年龄依旧使用了之前设置的生日字段 ‘1221’,这是因为在新增 ‘李四’ 的时候,生日字段的字段编号依旧为 2,并且已经被序列化到文件中了。最后再读取的时候,字段编号依旧为 2。
  • 还要再说⼀下的是:因为使用了 reserved 关键字,ProtoBuf在编译阶段就拒绝了我们使用已经保留的字段编号。到此实验结束,也印证了我们的结论。
  • 根据以上的例自,还有⼀个疑问:如果使用了 reserved 2 了,那么 service 给 ‘王五’ 设置的生日 ‘1112’,client 就没法读到了吗? 答案是可以的。后面的未知字段即可揭晓答案。

4. 未知字段

(1)在通讯录 3.0 版本中,我们向 service 目录下的 contacts.proto 新增了‘生日’字段,但对于 client 相关的代码并没有任何改动。验证后发现 新代码序列化的消息(service)也可以被旧代码(client)解析。并且这里要说的是,新增的 ‘生日’字段在旧程序(client)中其实并没有丢失,而是会作为旧程序的未知字段。

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

4.1 未知字段从哪获取

(1)了解相关类关系图:

(2)MessageLite 类介绍(了解):

  • MessageLite 从名字看是轻量级的 message,仅仅提供序列化、反序列化功能。
  • 类定义在 google 提供的 message_lite.h 中。

(3)Message 类介绍(了解) :

  • 我们自定义的message类,都是继承自Message。
  • Message 最重要的两个接口 GetDescriptor/GetReflection,可以获取该类型对应的Descriptor对象指针 和 Reflection 对象指针。
  • 类定义在 google 提供的 message.h 中。
//google::protobuf::Message 部分代码展⽰
const Descriptor* GetDescriptor() const;
const Reflection* GetReflection() const;

(4)Descriptor 类介绍(了解) :

  • Descriptor:是对message类型定义的描述,包括message的名字、所有字段的描述、原始的proto文件内容等。
  • 类定义在 google 提供的 descriptor.h 中。
// 部分代码展示
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;
}

(5)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;
}

(6)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();
}

(7)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();
};

4.2 升级通讯录 3.1 版本—验证未知字段

(1)更新 client.cc (通讯录 3.1),在这个版本中,需要打印出未知字段的内容。更新的代码如下:

#include <iostream>
#include <fstream>
#include <google/protobuf/unknown_field_set.h>
#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;
		}
		
		// 打印未知字段
		const Reflection* reflection = PeopleInfo::GetReflection();
		const UnknownFieldSet& unknowSet = reflection->GetUnknownFields(people);
		for (int j = 0; j < unknowSet.field_count(); j++) 
		{
			const UnknownField& unknow_field = unknowSet.field(j);
			cout << "未知字段" << j+1 << ":"
			<< " 字段编号: " << unknow_field.number()
			<< " 类型: "<< unknow_field.type();
			switch (unknow_field.type()) 
			{
				case UnknownField::Type::TYPE_VARINT:
					cout << " 值: " << unknow_field.varint() << endl;
					break;
				case UnknownField::Type::TYPE_LENGTH_DELIMITED:
					cout << " 值: " << unknow_field.length_delimited() << endl;
					break;
			}
		}
	}
}

int main(int argc, char* argv[]) 
{
	GOOGLE_PROTOBUF_VERIFY_VERSION;
	if (argc != 2) 
	{
		cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;
		return -1;
	}
	
	// 以⼆进制⽅式读取 contacts
	Contacts contacts;
	fstream input(argv[1], ios::in | ios::binary);
	if (!contacts.ParseFromIstream(&input)) 
	{
		cerr << "Failed to parse contacts." << endl;
		input.close();
		return -1;
	}
	
	// 打印 contacts
	PrintfContacts(contacts);
	input.close();
	google::protobuf::ShutdownProtobufLibrary();
	return 0;
}

(2)其他文件均不用做任何修改,重新编译 client.cc,进行一次读操作可得如下结果:

[xiaomaker@xiaomaker-virtual-machine:client]$ ./client ../contacts.bin
------------联系⼈1------------
姓名:张珊
年龄:34
电话1: 131
------------联系⼈2------------
姓名:李四
年龄:1221
电话1: 151
------------联系⼈3------------
姓名:王五
年龄:0
电话1: 110
未知字段1: 字段编号: 4 类型: 0 值: 1112
// 类型为何为 0 ?在介绍 UnknownField 类中讲到了类中包含了未知字段的⼏种类型:
enum Type {
	TYPE_VARINT,
	TYPE_FIXED32,
	TYPE_FIXED64,
	TYPE_LENGTH_DELIMITED,
	TYPE_GROUP
};
// 类型为 0,即为 TYPE_VARINT。

5. 前后兼容性

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

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

(2)前后兼容的作用:

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

6. 选项 option

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

6.1 选项分类

(1)选项的完整列表在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 消息中

...
  • 由此可见,选项分为 文件级、消息级、字段级 等等, 但并没有⼀种选项能作用于所有的类型。

6.2 常用选项列举

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

  • SPEED:protoc 编译器将生成的代码是高度优化的,代码运行效率高,但是由此生成的代码编译后会占用更多的空间。 SPEED 是默认选项。
  • CODE_SIZE:proto 编译器将生成最少的类,会占用更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运行效率较低。这种方式适合用在包含大量的.proto文件,但并不盲目追求速度的应用中。
  • LITE_RUNTIME:生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供 encoding+序列化 功能,所以我们在链接 BP 库时仅需链接libprotobuf-lite,而非libprotobuf。这种模式通常用于资源有限的平台,例如移动手机平台中。
option optimize_for = LITE_RUNTIME;
  • allow_alias:允许将相同的常量值分配给不同的枚举常量,用来定义别名。该选项为枚举选项。举个例子:
enum PhoneType {
	option allow_alias = true;
	MP = 0;
	TEL = 1;
	LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}

6.3 设置自定义选项

(1)ProtoBuf 允许自定义选项并使用。该功能大部分场景用不到,在这里不拓展讲解。

7. proto 3 语法总结

(1)基础语法结构:

syntax = "proto3"; // 必须声明使用 proto3 语法

package mypackage; // 包名(防止命名冲突)

option java_package = "com.example"; // 语言特定选项
option java_multiple_files = true;

// 导入其他 proto 文件
import "google/protobuf/timestamp.proto";

(2)消息类型 (Message)。消息是 Protobuf 的核心数据结构:

message Person {
  // 字段规则:singular (默认), repeated, oneof
  string name = 1;               // 标量类型
  int32 id = 2;                  // 字段编号 (1-15 占 1 字节,16-2047 占 2 字节)
  string email = 3;
  
  // 枚举类型
  enum PhoneType {
    MOBILE = 0;  // 枚举值必须从 0 开始
    HOME = 1;
    WORK = 2;
  }
  
  // 嵌套消息
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }
  
  repeated PhoneNumber phones = 4; // 重复字段(数组)
  
  // 时间戳(使用导入的类型)
  google.protobuf.Timestamp last_updated = 5;
  
  // Oneof 字段(互斥字段)
  oneof contact_preference {
    string email_preference = 6;
    bool sms_preference = 7;
  }
  
  // Map 类型
  map<string, string> attributes = 8;
  
  // 保留字段(防止重用)
  reserved 9, 15 to 20;
  reserved "old_field", "deprecated_field";
}

(3)服务定义:

service UserService {
  // 简单 RPC
  rpc GetUser (GetUserRequest) returns (User) {}
  
  // 服务端流式 RPC
  rpc ListUsers (ListUsersRequest) returns (stream User) {}
  
  // 客户端流式 RPC
  rpc CreateUsers (stream User) returns (CreateUsersResponse) {}
  
  // 双向流式 RPC
  rpc Chat (stream ChatMessage) returns (stream ChatMessage) {}
}

message GetUserRequest {
  string user_id = 1;
}

message User {
  string id = 1;
  string name = 2;
  string email = 3;
}

(4)重要特性:

  1. 默认值:
    • 数值类型:0
    • 字符串:空字符串
    • 布尔值:false
    • 字节:空字节
    • 枚举:第一个定义的值(必须为0)
    • 消息字段:未设置(与空消息不同)
  2. 字段规则:
    • singular:0或1个值(默认)
    • repeated:0或多个值(有序列表)
    • oneof:多个字段中同时只能设置一个
  3. JSON 映射:
    • Protobuf 与 JSON 可相互转换
    • 特殊类型有特定映射规则(如 Timestamp → ISO 8601)
  4. 更新规则:
    • 可修改:字段名、可选字段添加、新 oneof 字段
    • 不可修改:字段编号、字段类型(兼容类型除外)
    • 删除字段:使用 reserved 标记

(5)编译命令:

# 基本编译
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto

# 包含 gRPC 插件
protoc -I=$SRC_DIR --grpc_out=$DST_DIR --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` $SRC_DIR/route_guide.proto

(6)protobuf 主要流程:

(7)官方资源:

  1. Protocol Buffers 官方文档。
  2. Proto3 语言指南。
  3. Protobuf GitHub 仓库。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Smile丶凉轩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值