简介
Protocol Buffers简称Protobuf,是google公司推出的一种数据描述语言。Protocol buffers具有平台无关、语言无关、二进制格式编码、编码后体积小,序列化和反序列化快、类型安全、向后兼容等特点。
Protocol buffers有专门的语法结构来定义数据结构。消息和RPC服务接口是Protocol buffers中两大基本组成。消息类似一个Json object,RPC服务接口定义了服务所具有的接口和所依赖的消息类型。
Protocol buffers定义的数据结构应该保存在.proto后缀名的文件中。目前最新版本的语法协议是proto3。
定义消息
message(消息)是protobuf中最基本数据单元。protobuf中使用message关键字来定义消息。假设想要定义一个搜索请求消息格式,其中包含搜索的查询字符串,分页参数。下面是用于定义消息类型的.proto文件内容:
1 | syntax = "proto3"; |
.proto
文件的第一行使用syntax = "proto3"
表明使用proto3
语法。
上面代码中定义了一个名字为SearchRequest
的消息,它包含了四个字段,每个字段都有名字(Field Name),类型(Field Type),唯一编号(Field Number),**字段规则(Field Rule) **。其中字段规则不是必须。
字段编号
消息中定义的每个字段都必须有唯一的编号。字段编号是反序列化时候重要依据。当编号范围在1到15之间时候只需要一个字节进行编码,当范围16到2047的字段时候占用两个字节。所以应该为频繁出现的消息元素保留1到15的字段编号。
字段编号不一定从1开始。最小的字段编号是1,最大可到2^29。其中19000到19999位proto保留编号,不可以使用。
字段规则
proto3
语法与proto2
语法不同之处,其中一项就是去掉了proto2中required
,optional
规则,只保留了repeated
规则,并且对于由于repeated
规则的标量类型的字段默认采用了packed
编码,而proto2中需要额外指定选项才能采用packed
编码。
proto3消息中定义的字段需要满足以下规则之一:
-
singular
proto3的默认规则,字段前面不需要加任何关键字。表明该字段可以出现0次或者1次。相当于proto2中的optional规则
-
repeated
消息中该字段可以重复出现0次或多次
默认值
当反序列化消息时,如果消息中不包含特定的字段时候,则解析对象中的对应字段将被设置为该字段的默认值。默认值规则如下:
- 字符串类型默认值是空字符
- 字节类型默认值是空字节
- 布尔类型默认值是false
- 数值类型默认值是0
- 枚举类型默认值是枚举第一个元素。第一个元素必须是0.
- 消息类型默认值依赖于具体编程语言
repeated
规则的字段默认值是空
字段类型
Protocol Buffer中字段的类型既可以是标量类型( scalar type),也可以是复合类型(composite type)。
标量类型
枚举类型
我们可以通过enum
关键字定义枚举类型。
1 | message SearchRequest { |
上面结构体中Corpus是一个枚举类型,它的值可以是UNIVERSAL
,WEB
,IMAGES
,LOCAL
,NEWS
,PRODUCTS
,VIDEO
。
注意:
- 枚举常量必须是32位整数范围
- 由于枚举值采用varint编码,负值编码效率不高,不推荐使用负值作为枚举值
- 每一个枚举定义必须要包含映射到0的元素(比如Corpus中UNIVERSAL)。一方面0值用来作为默认值。二来为了兼容proto2语法,在proto2中第一个元素总是作为默认值
其他消息类型
我们可以使用其他消息类型作为某个字段的类型:
1 | message SearchResponse { |
上面proto定义中可以看出来,在SearchResponse
消息中,我们使用Result
类型来定义字段result的类型。
嵌套类型
我们可以在一个消息类型中,嵌套其他类型的消息。比如下面的SearchResponse
消息中嵌套了Result
类型
1 | message SearchResponse { |
我们一个通过_Parent_._Type_
语法来复用父级消息的类型。SomeOtherMessage
消息中的result字段的类型SearchResponse
中的Result
类型
1 | message SomeOtherMessage { |
任意类型
通过任意类型,可以将消息作为嵌入类型使用,任意类型的字段以字节的形式进行序列化。使用任意类型,需要导入google/protobuf/any.proto
类型
1 | import "google/protobuf/any.proto"; |
Oneof类型
当一个消息中包含多个字段,并且最多同时设置一个字段。我们就可以使用Oneof类型节省内存。
1 | message SampleMessage { |
Oneof字段特性:
- 除了
map
类型字段和repeated
规则字段外,Oneof字段支持其他任意类型 - 当给Oneof字段设置值时,会自动清除该字段已有值。这就是说Oneof字段的值只有最后一次设置才有效
Map类型
我们可以通过下面语法声明map类型字段:
1 | map<key_type, value_type> map_field = N; |
其中key_type可以除了float
和bytes
之外的任意标量类型。value_type可以是除了map类型的任意类型。map类型字段不能是repeated
规则。
未知字段
未知字段指的是反序列化时候,无法识别的字段。当旧代码解析带有新字段的消息生成的序列化数据时候,该字段对就代码来说就是未知字段。对于未知字段处理规则:
- proto2默认保留未知字段
- proto3总是丢弃该未知字段。但3.5版本及更高版本会保留未知字段
更新时注意事项
当需要更新消息格式时候,比如增加一个额外的字段,为了不影响已有功能。需要注意以下几个规则:
- 不要更改字段编号
- 新增字段时候,旧消息格式序列化的数据依然能被新生成的代码解析,此时新字段值为默认值,我们要注意到这一点。而旧代码处理新格式序列化的数据会丢弃新增的字段信息
- 不再使用的字段可以删除,或者保留以防止该字段的字段编号被其他字段使用
int32
,uint32
,int64
,uint64
以及bool
类型都是兼容的。从其中一种类型更改为另一种类型,不会破坏向前或向后兼容性,但要注意截断问题(比如:int64向int32转换时候,会被截断)sint32
和sint64
彼此兼容,但与其他整数类型不兼容- 只要字节是有效的UTF-8编码,字符串和字节是兼容的
- 当
bytes
类型包含一个消息体,嵌套类型的消息类型是与其兼容的 fixed32
和sfixed32
,fixed64
,sfixed64
是兼容的- 对于
string
,bytes
以及消息类型字段,optional
和repeated
规则是兼容的 enum
类型与int32
,uint32
,int64
,uint64
是兼容的。同规则4一样,需注意截断问题- 将一个值更改为新
oneof
成员是安全的和二进制兼容的。如果确信没有代码会一次设置多个字段,那么将多个字段移到一个新的字段中可能是安全的。将任何字段移动到现有字段中是不安全的
定义服务
通过在.proto
文件中定义RPC服务接口,接着我们就可以使用protocol buffer编译器生成特定语言的服务接口代码和stub。
比如定义一个RPC服务,该服务具有Search接口,该接口接收SearchRequest参数并返回一个SearchResponse,你可以在你的.proto文件中定义它如下:
1 | service SearchService { |