目录
- 1.默认值
- 2.更新消息
- 1.更新规则
- 2.保留字段reserved
- 3.未知字段
- 1.是什么?
- 2.未知字段从哪获取
- 4.前后兼容性
- 5.选项option
- 1.选项分类
- 2.常用选项列举
- 3.设置自定义选项
1.默认值
- 反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值
- 不同的类型对应的默认值不同
- 字符串:默认值为空字符串
- 字节:默认为空字节
- 布尔值:默认值为
false
- 数值类型:
- 整数默认为0
- 浮点数默认为0.0
- 枚举:默认值是第一个定义的枚举值,必须为0
- 消息字段:未设置该字段,它的取值依赖于语言
- 对于设置了
repeated
的字段的默认值是空的- 通常是相应语言的一个空列表
- 对于
message
、oneof
、any
字段,C++和Java中都有has_
方法来检测当前字段是否被设置 - 对于标量数据类型,在proto3语法下,没有生成
has_
语法
2.更新消息
1.更新规则
- 如果现有的消息类型已经不再满⾜需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型⾮常简单。遵循如下规则即可
- 新增:不要和老字段冲突即可
- 修改:
- 禁⽌修改任何已有字段的字段编号
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),以确保该编号将不能被重复使⽤
- 不建议直接删除或注释掉字段
2.保留字段reserved
- 如果通过删除或注释掉字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号
- 将来使⽤该
.proto
的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等
- 将来使⽤该
- 确保不会发⽣这种情况的⼀种⽅法是:使⽤
reserved
将指定字段的编号或名称设置为保留项- 当再使⽤这些编号或名称时,Protocol Buffer的编译器将会警告这些编号或名称不可⽤
- 示例:
message Message {// 设置保留项reserved 100, 101, 200 to 299;reserved "field3", "field4";// 注意:不要在⼀⾏ reserved 声明中同时声明字段编号和名称// reserved 102, "field5"; // ERROR// 设置保留项之后,下⾯代码会告警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 }
- 总结:
- 若是移除⽼字段,要保证不再使⽤移除字段的字段编号,不建议直接删除或注释掉字段
- 正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤
3.未知字段
1.是什么?
- 未知字段:解析结构良好的Protocol Buffer已序列化数据中的未识别字段的表⽰⽅式
- 例如:当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段
- 本来proto3在解析消息时总是会丢弃未知字段,但在3.5版本中重新引⼊了对未知字段的保留制
- 在3.5或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中
2.未知字段从哪获取
-
类图
-
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; }
- Descriptor:是对message类型定义的描述,包括message的名字、所有字段的描述、原始的
-
Reflection类介绍
- Reflection接⼝类,主要提供了动态读写消息字段的接⼝,对消息对象的⾃动读写主要通过该类完成
- 提供⽅法来动态访问/修改message中的字段,对每种类型,Reflection都提供了⼀个单独的接⼝⽤于读写字段对应的值
- 针对所有不同的
field
类型FieldDescriptor::TYPE_*
,需要使⽤不同的Get*()/Set* ()/Add*()
接⼝ repeated
类型需要使⽤GetRepeated*()/SetRepeated*()
接⼝,不可以和⾮repeated
类型接⼝混⽤message
对象只可以被由它⾃⾝的Reflection(message.GetReflection()
)来操作
- 针对所有不同的
- 类中还包含了访问/修改未知字段的⽅法
- 类定义在Google提供的
message.h
中
-
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(); };
4.前后兼容性
- Protobuf是具有前后兼容性的,为了叙述⽅便,把增加了新属性的
Service
称为“新模块”,未做变动的Client
称为“⽼模块”- 向前兼容:⽼模块能够正确识别新模块⽣成或发出的协议
- 这时新增加的属性会被当作未知字段(3.5版本及之后)
- 向后兼容:新模块也能够正确识别⽼模块⽣成或发出的协议
- 向前兼容:⽼模块能够正确识别新模块⽣成或发出的协议
- 前后兼容的作⽤:当维护⼀个很庞⼤的分布式系统时,由于你⽆法同时升级所有模块,为了保证 在升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”
5.选项option
.proto
文件中可以声明许多选项,使⽤option
标注,选项能影响proto编译器的某些处理⽅式
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 消息中 ...
- 选项分为⽂件级、消息级、字段级等等, 但并没有⼀种选项能作⽤于所有的类型
2.常用选项列举
optimize_for
:该选项为⽂件选项,可以设置protoc编译器的优化级别,设置不同的优化级别,编译.proto
⽂件后⽣成的代码内容不同SPEED
:- protoc编译器将⽣成的代码是⾼度优化的,代码运⾏效率⾼,但是由此⽣成的代码编译后会占⽤更多的空间
SPEED
是默认选项
CODE_SIZE
:- proto编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来 实现序列化、反序列化和各种其他操作
- 和
SPEED
恰恰相反,它的代码运⾏效率较低 - 这种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中
LITE_RUNTIME
:- ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常 少
- 这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供encoding+序列化功能
- 所以在链接PB库时仅需链接
libprotobuf-lite
,⽽⾮libprotobuf
- 这种模式通常⽤于资源有限的平台,例如移动⼿机平台中
allow_alias
:- 允许将相同的常量值分配给不同的枚举常量,⽤来定义别名
- 该选项为枚举选项,例⼦
enum PhoneType {option allow_alias = true;MP = 0;TEL = 1;LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错 }
3.设置自定义选项
- ProtoBuf允许⾃定义选项并使⽤,该功能⼤部分场景⽤不到,在这⾥不拓展讲解
- 参考资料