Any类型
字段还可以声明为Any类型,可以理解为泛型类型。使⽤时可以在Any中存储任意消息类型。Any类型的字段也⽤repeated来修饰。
Any类型是google已经帮我们定义好的类型,在安装ProtoBuf时,其中的include⽬录下查找所有google已经定义好的.proto⽂件
![![[Pasted image 20260305234727.png]]](https://i-blog.csdnimg.cn/direct/c3b1132db41142d78dec3618ca15970f.png)
升级通讯录⾄2.2版本
通讯录2.2版本会新增联系⼈的地址信息,可以使⽤any类型的字段来存储地址信息
更新contacts.proto(通讯录2.2)
![![[Pasted image 20260306014719.png]]](https://i-blog.csdnimg.cn/direct/788172f1c51c4c9f961b3dc9cd9eaabd.png)
- 编译
protoc--cpp_out=.contacts.proto
contacts.pb.h更新的部分代码
![![[Pasted image 20260306015806.png]]](https://i-blog.csdnimg.cn/direct/ba57f90abae04604ab5d6179b567e52c.png)
![![[Pasted image 20260306020206.png]]](https://i-blog.csdnimg.cn/direct/d7cdfbf5f54c402ba95f0510288676eb.png)
![![[Pasted image 20260306020622.png]]](https://i-blog.csdnimg.cn/direct/78c6cfd07bf74c52a0af028f72f85746.png)
对于Any类型字段:
- 设置和获取:获取⽅法的⽅法名称与⼩写字段名称完全相同。设置⽅法可以使⽤mutable_⽅法,返回值为Any类型的指针,这类⽅法会为我们开辟好空间,可以直接对这块空间的内容进⾏修改。
可以在Any字段中存储任意消息类型,这就要涉及到任意消息类型和Any类型的互转。这部分代码就在Google为我们写好的头⽂件 any.pb.h 中
any.pb.h 部分代码
![![[Pasted image 20260306021131.png]]](https://i-blog.csdnimg.cn/direct/cf4f1f7f96f1458b82b0f5a38773203b.png)
使⽤ PackFrom() ⽅法可以将任意消息类型转为 Any 类型。
使⽤ UnpackTo() ⽅法可以将 Any 类型转回之前设置的任意消息类型。
使⽤ Is() ⽅法可以⽤来判断存放的消息类型是否为 typename T
- 更新write.cc(通讯录2.2)
![![[Pasted image 20260306022758.png]]](https://i-blog.csdnimg.cn/direct/466149921e814008b274da8878ec00b1.png)
- 更新read.cc(通讯录2.2)
![![[Pasted image 20260306024121.png]]](https://i-blog.csdnimg.cn/direct/845602ccfba5441dbeb1ca79e863759b.png)
- 编译
![![[Pasted image 20260306024728.png]]](https://i-blog.csdnimg.cn/direct/958c4d4c787e40069b7023f0e292548e.png)
![![[Pasted image 20260306025030.png]]](https://i-blog.csdnimg.cn/direct/8e5bbdce9fa94269a9d5f160e8f5a942.png)
oneof类型
如果消息中有很多可选字段,并且将来同时只有⼀个字段会被设置,那么就可以使⽤ oneof 加强这个⾏为,也能有节约内存的效果
升级通讯录⾄2.3版本
通讯录2.3版本想新增联系⼈的其他联系⽅式,⽐如qq或者微信号⼆选⼀,我们就可以使⽤oneof字段来加强多选⼀这个⾏为。oneof字段定义的格式为: oneof 字段名 { 字段1; 字段2; … }
- 更新contacts.proto(通讯录2.3)
![![[Pasted image 20260306025400.png]]](https://i-blog.csdnimg.cn/direct/9b92f8388076447ca248befd9e7706ce.png)
-
可选字段中的字段编号,不能与⾮可选字段的编号冲突。
-
不能在oneof中使⽤repeated字段。
-
将来在设置oneof字段中值时,如果将oneof中的字段设置多个,那么只会保留最后⼀次设置的成员,之前设置的oneof成员会⾃动清除。
-
编译
protoc --cpp_out=.contacts.proto
![![[Pasted image 20260306025433.png]]](https://i-blog.csdnimg.cn/direct/e0edea56ff3e4e8893f408eec7671792.png)
contacts.pb.h更新的部分代码
![![[Pasted image 20260306025513.png]]](https://i-blog.csdnimg.cn/direct/0f9aa7e865c940fdae27124ac4fb4209.png)
对于oneof字段:
-
会将oneof中的多个字段定义为⼀个枚举类型。
-
设置和获取:对oneof内的字段进⾏常规的设置和获取即可,但要注意只能设置⼀个。如果设置多个,那么只会保留最后⼀次设置的成员。
-
清空oneof字段:clear_⽅法
-
获取当前设置了哪个字段:
_case⽅法 -
更新write.cc(通讯录2.3)
![![[Pasted image 20260306025952.png]]](https://i-blog.csdnimg.cn/direct/80f22ead5bac40e9ade29c9371d9f786.png)
- 更新read.cc(通讯录2.3)
![![[Pasted image 20260306032244.png]]](https://i-blog.csdnimg.cn/direct/9e5e1f676dd04ea09ae318d1dab5db0d.png)
- 编译
![![[Pasted image 20260306032725.png]]](https://i-blog.csdnimg.cn/direct/84596f7981fa4d40b5467349ea08d93e.png)
![![[Pasted image 20260306033058.png]]](https://i-blog.csdnimg.cn/direct/57da1ab7522b44dcae7fc4869f357003.png)
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)
![![[Pasted image 20260306033351.png]]](https://i-blog.csdnimg.cn/direct/620953aad72141caa3db16eb1d2adae1.png)
- 编译
protoc --cpp_out=. contacts.proto
![![[Pasted image 20260306033547.png]]](https://i-blog.csdnimg.cn/direct/178f8c1ba66e41ecb1b62cfd6f46dd02.png)
contacts.pb.h更新的部分代码
![![[Pasted image 20260306033831.png]]](https://i-blog.csdnimg.cn/direct/0c7764997a8e4eee876149d449b56ed7.png)
对于Map类型的字段:
-
清空map:clear_⽅法
-
设置和获取:获取⽅法的⽅法名称与⼩写字段名称完全相同。设置⽅法为mutable_⽅法,返回值为Map类型的指针,这类⽅法会为我们开辟好空间,可以直接对这块空间的内容进⾏修改。
-
更新write.cc(通讯录2.4)
![![[Pasted image 20260306034935.png]]](https://i-blog.csdnimg.cn/direct/a73dcd4969814acdb717a93767831567.png)
- 更新read.cc(通讯录2.4)
![![[Pasted image 20260306040252.png]]](https://i-blog.csdnimg.cn/direct/b527b60c6e6a47509ed8438aa92a0d05.png)
- 编译
![![[Pasted image 20260306041053.png]]](https://i-blog.csdnimg.cn/direct/39868039769e4a3eb0d073734eb2f3c7.png)
![![[Pasted image 20260306041359.png]]](https://i-blog.csdnimg.cn/direct/f9825104053a4662ba15be9e4bd8e680.png)
默认值
反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为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类型是不安全的。
创建通讯录3.0版本—验证错误删除字段造成的数据损坏
现模拟有两个服务,他们各⾃使⽤⼀份通讯录.proto⽂件,内容约定好了是⼀模⼀样的。
服务1(service):负责序列化通讯录对象,并写⼊⽂件中。
服务2(client):负责读取⽂件中的数据,解析并打印出来。
⼀段时间后,service更新了⾃⼰的.proto⽂件,更新内容为:删除了某个字段,并新增了⼀个字段,新增的字段使⽤了被删除字段的字段编号。并将新的序列化对象写进了⽂件。
但client并没有更新⾃⼰的.proto⽂件。根据结论,可能会出现数据损坏的现象
- service端
![![[Pasted image 20260306044503.png]]](https://i-blog.csdnimg.cn/direct/de693a5c9a43425c9aa52da5b77a89a3.png)
- client端
![![[Pasted image 20260306044710.png]]](https://i-blog.csdnimg.cn/direct/c07e32f0166646d9af12e8ba143df7d7.png)
- 输入消息
![![[Pasted image 20260306045109.png]]](https://i-blog.csdnimg.cn/direct/0b2dcdd8b709415696c34de573d6132e.png)
- 更新service
确认⽆误后,对service⽬录下的contacts.proto⽂件进⾏更新:删除age字段,新增birthday字段,新增的字段使⽤被删除字段的字段编号
![![[Pasted image 20260306050059.png]]](https://i-blog.csdnimg.cn/direct/388da34f890f4bdcad882037867ddbd6.png)
- 更新service.cc
![![[Pasted image 20260306074939.png]]](https://i-blog.csdnimg.cn/direct/66d525e19005473389a8c72d01d5d415.png)
- 读取数据
![![[Pasted image 20260306075749.png]]](https://i-blog.csdnimg.cn/direct/4ca6a81cc8b44d678435a4c3af27f814.png)
输⼊的⽣⽇,在反序列化时,被设置到了使⽤了相同字段编号的年龄上!!所以得出结论:若是移除⽼字段,要保证不再使⽤移除字段的字段编号,不建议直接删除或注释掉字段。
那么正确的做法是保留字段编号(reserved),以确保该编号将不能被重复使⽤
保留字段reserved
如果通过删除或注释掉字段来更新消息类型,未来的⽤⼾在添加新字段时,有可能会使⽤以前已经存在,但已经被删除或注释掉的字段编号。将来使⽤该.proto的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。
确保不会发⽣这种情况的⼀种⽅法是:使⽤ reserved 将指定字段的编号或名称设置为保留项。当我们再使⽤这些编号或名称时,protocol buffer的编译器将会警告这些编号或名称不可⽤。举个例⼦:
![![[Pasted image 20260306080425.png]]](https://i-blog.csdnimg.cn/direct/218d6db9871944b9bcedc78f66793b65.png)
- 正确service⽬录下的contacts.proto
![![[Pasted image 20260306082159.png]]](https://i-blog.csdnimg.cn/direct/d63266a927a142f7a618bc01169376b1.png)
![![[Pasted image 20260306082728.png]]](https://i-blog.csdnimg.cn/direct/55910e3cfe61481b997d1817ee172d52.png)
王五的年龄为0,这是由于新增时未设置年龄,通过client程序反序列化时,给年龄字段设置了默认值0。
李四的年龄依旧使⽤了之前设置的⽣⽇字段‘1221’,这是因为在新增李四的时候,⽣⽇字段的字段编号依旧为2,并且已经被序列化到⽂件中了。最后再读取的时候,字段编号依旧为2。
因为使⽤了reserved关键字,ProtoBuf在编译阶段就拒绝了我们使⽤已经保留的字段编号。
未知字段
service⽬录下的contacts.proto新增了‘⽣⽇’字段,但对于client相关的代码并没有任何改动。验证后发现新代码序列化的消息(service)也可以被旧代码(client)解析。并且这⾥要说的是,新增的‘⽣⽇’字段在旧程序(client)中其实并没有丢失,⽽是会作为旧程序的未知字段。
- 未知字段:解析结构良好的protocol buffer已序列化数据中的未识别字段的表⽰⽅式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
- 本来,proto3在解析消息时总是会丢弃未知字段,但在3.5版本中重新引⼊了对未知字段的保留机制。所以在3.5或更⾼版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。
未知字段从哪获取
![![[Pasted image 20260322041245.png]]](https://i-blog.csdnimg.cn/direct/4ba38772a0194cc3a8409f4fa62ff62d.png)
MessageLite类
- MessageLite从名字看是轻量级的message,仅仅提供序列化、反序列化功能。
- 类定义在google提供的message_lite.h中。
Message类 - 我们⾃定义的message类,都是继承⾃Message。
- Message最重要的两个接⼝GetDescriptor/GetReflection,可以获取该类型对应的Descriptor对象指针和Reflection对象指针。
- 类定义在google提供的message.h中
Descriptor类 - Descriptor:是对message类型定义的描述,包括message的名字、所有字段的描述、原始的proto⽂件内容等。
- 类定义在google提供的descriptor.h中。
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中
UnknownField类 - 表⽰未知字段集中的⼀个字段。
- 类定义在unknown_field_set.h中
升级通讯录3.1版本—验证未知字段
- 更新client.cc(通讯录3.1)
![![[Pasted image 20260306090151.png]]](https://i-blog.csdnimg.cn/direct/f79346da56c247a793f84229271ce309.png)
- 编译
![![[Pasted image 20260306090237.png]]](https://i-blog.csdnimg.cn/direct/1c746ea2db09413793d134fe37355f9f.png)
前后兼容性
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 :⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供encoding+序列化功能,所以我们在链接BP库时仅需链接libprotobuf-lite,⽽⾮libprotobuf。这种模式通常⽤于资源有限的平台,例如移动⼿机平台中
option optimize_for = LITE_RUNTIME;
![![[Pasted image 20260306090931.png]]](https://i-blog.csdnimg.cn/direct/45cf9badf3924287bd1dfa3e7442a93c.png)
- allow_alias:允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。
举个例⼦:
enum PhoneType {
option allow_alias = true;
MP = 0;
TEL = 1;
LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}
![![[Pasted image 20260306090954.png]]](https://i-blog.csdnimg.cn/direct/314d76f144fd4850b07ae4c2638e8477.png)
3190



被折叠的 条评论
为什么被折叠?



