反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
- 对于字符串,默认值为空字符串
- 对于字节,默认值为空字节
- 对于布尔值,默认值为
false - 对于数值类型,默认值为
0 - 对于枚举,默认值是第一个定义的枚举值, 必须为
0 - 对于消息字段,未设置该字段。它的取值是依赖于语言
- 对于设置了
repeated的字段的默认值是空的( 通常是相应语言的一个空列表 ) - 对于消息字段 、
oneof字段 和any字段 ,C++和Java语言中都有has_方法来检测当前字段是否被设置;而对于标量数据类型,在proto3语法下是不会生成has_方法的!
Ⅱ. 消息的更新规则
一、更新规则
如果现有的消息类型已经不再满足我们的需求,例如需要扩展一个字段,在不破坏任何现有代码的情况下更新消息类型非常简单。遵循如下规则即可:
- 新增内容:
- 禁⽌新增任何已有字段的字段编号。
- 修改内容:
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),以确保该编号将不能被重复使用。不建议直接删除或注释掉字段。
- 若是移除老字段,要保证不再使用移除字段的字段编号。正确的做法是保留字段编号(
二、保留字段 reserved
如果通过【删除】或【注释掉】字段来更新消息类型,未来的用户在添加新字段时,有可能会使用以前已经存在,但已经被删除或注释掉的字段编号,将来使用该 .proto 的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。
确保不会发生这种情况的一种方法是:使用 reserved 将指定字段的编号或名称设置为保留项 。当我们再使用这些编号或名称时,protobuf 的编译器将会警告这些编号或名称不可用。举个例子:
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
} 还要再说一下的是:因为使用了 reserved 关键字,ProtoBuf 在编译阶段就拒绝了我们使用已经保留的字段编号!
三、未知字段
在 protobuf 中,【未知字段】是指接收到的数据中包含的字段,这些字段未在接收端的 .proto 文件定义中描述。比如我们在服务端的 .proto 文件中添加了新字段,这些字段可能没来得及在客户端那边更新,所以客户端那边的 .proto 文件并没有该新字段,那么在接收数据的时候,这个新字段就会被客户端处理为未知字段!
protobuf 的设计允许在不同版本之间进行扩展,而不破坏现有功能,这也是未知字段的一个重要应用场景。
- 产生原因:
- 当一个消息被序列化后发送到接收端,如果接收端的
.proto定义缺少某些字段(例如因为版本较旧),这些字段就被视为未知字段。
- 当一个消息被序列化后发送到接收端,如果接收端的
- 行为特点:
- 未知字段不会导致解析失败,
protobuf的解析器会跳过这些字段。 - 在接收端,如果消息重新序列化,未知字段将被保留。
- 如果消息被修改并重新序列化,修改的内容不会包含未知字段。
- 未知字段不会导致解析失败,
- 作用:
- 向后兼容性:允许老版本的程序正常接收新版本的数据,而不需要理解新增字段。
- 向前兼容性:新版本的程序可以处理旧版本的数据,不受未知字段的影响。
- 管理未知字段:
- 在
protobuf的C++或Java实现中,支持以某种方式读取、操作或清除未知字段。 - 例如:
- 在
C++中,UnknownFieldSet类可以用于访问和操作未知字段。 - 在新版的
proto3中,默认情况下未知字段不会被保留。
- 在
- 在
1. 未知字段的获取
下面是消息相关类的关系图:
通常我们通过 google::protobuf::Message 来获取 Reflection 对象和 Description 对象,然后通过它们提供的接口比如 Reflection.GetUnknownFields() 来获取未知字段的集合,每个集合又是一个 UnknownFieldSet 对象,通过它的内部接口即可实现一些操作!
2. MessageLite 类介绍(了解)
MessageLite 从名字看是轻量级的 Message,仅仅提供序列化、反序列化功能。
类定义在 google 提供的 message_lite.h 中。
3. 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,
iuint64_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();
};8. 打印未知字段样例
下面展示之前 2.x 的简化版本,主要看一下打印未知字段的操作:
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(); // 获取reflection对象
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;
}
}
}
}
// 运行效果如下所示:
姓名:王五
年龄:0
电话1: 110
未知字段1: 字段编号: 4 类型: 0 值: 1112四、前后兼容性
根据上述的例子可以得出,protbuf 是具有向前兼容的。为了叙述方便,把增加了“生日”属性的 service 称为“新模块”;未做变动的 client 称为 “老模块”。
- 向前兼容:⽼模块能够正确识别新模块⽣成或发出的协议。这时新增加的“⽣⽇”属性会被当作未知字段(
pb 3.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:protoc编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和SPEED恰恰相反,它的代码运⾏效率较低。这种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中。 -
LITE_RUNTIME:⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供encoding+序列化功能,所以我们在链接pb库时仅需链接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; 这⼀⾏会编译报错 }