Protocol Buffer
protocol buffer是谷歌公司开发的一个对结构化数据进行序列化和反序列化的协议.该协议采用了高效的编码方式,降低了序列化后的字节流数据的大小.同时谷歌也提供了多种语言的实现接口,例如C++,C#,Dart,Go,Java,Python … 等. 一些开发者也提供了自己的实现,补充了protobuf在其它语言中的使用,比如C等.总之,ProtocolBuffer已经能够很容易在多种语言之间作为数据交互的载体使用.
本文主要引用:https://developers.google.com/protocol-buffers/docs/proto
定义一个结构类型
首先看一个最简单的例子,例子中定义了一个"查询"消息的格式,在消息里有一个字符串类型的"query",表示页码整型"page_number"和表示每一页个数的整型"result_pre_page".将这个消息保存到以"proto"作为扩展名的文件里;多个message可以放到同一个文件里,message的注释是类C/java风格的.
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest{
required string query = 1;
optional int32 page_number = 2;// Which page number do we want?
optional int32 result_per_page = 3;
}
message SearchRequest_2{
...
}
定义了一个消息类 SearchRequest,其中包含了三个声明域,每一个声明域都是按照 规则 |类型 | 名称 |序号[|可选项]组织的.
指定声明域的规则/类型/序号和可选的限定选项
规则
required : 表明是必须要存在的字段.
optional : 表明是可存在的,optional如果不存在,不会出现在编码后的字符流里,而在解码时会使用默认值填充该字段一般是0,""等.开发者也可以自己提供这个默认值,
repeated: 表明该字段是重复出现的,也可以不出现.
类型
在上一个例子中消息SearchRequest里,所有的声明类型都是ProtocolBuffer内嵌的标准类型,两个整型"int32"和一个字符串类型"string".事实上你可以指定更加复杂的类型包括枚举类型,其它的自定义消息类型等.
序号
每一个消息体里的声明域都有一个唯一的序号,这个序号会用于在编/解码中的标识该域在字节流中的位置.所以该序号一定要保持在编码/解码代码中是一致的.
- 序号段 1~15 在编码时,只占用一个字节.序号段16~2047在编码时需要占用两个字节.我们一般在设计时声明域不会超过15,采取这样的设计方式可以节省空间.
- 可使用1~2^29 -1,其中不包括保留字段19000 - 19999.如果你误用时,在编译时候会出错.
一些Tips: 对于required的使用要很小心,因为该字段要求必须存在,意味着即使将来对于message体有所升级,这些required也不能被弃用.
保留名称和序号
处于升级考虑,你可能会删除一些不再需要的声明域.那么未来的维护者,就可能会在无意中复用你删除字段中的名称和序号.这会导致和旧代码冲突.复用的序号,会导致旧代码在反序列化时不正确的解析数据,而复用的名可能在json报文中产生冲突.为了避免类似情况,需要限定将来的使用者的使用.
message Foo{
reserved 2 , 15, 9 to 11;
reserved "foo" , "bar";
}
这样,将来的维护者就无法再次复用 序号和 名称. 这里注意,序号和名称要分开在两个reserved语句中.
.proto文件的生成语言
编写好.proto文件后,需要使用"protoco buffer compiler"来处理该proto文件,通过指定生成选项生成所需要的编程语言的代码.生成的代码中,包含有对应的api来设置变量和获取变量以及进行序列化和反序列化.
protocolbuffer类型与语言类型的定义关系
| prototype | Notes | C++ | JAVA |
|---|---|---|---|
| double | double | double | |
| float | float | float | |
| int32 | 使用变长整型的编码方式,对负数的编码效率差,如果需要传递负数变量,建议使用sint32 | int32 | int |
| int64 | 使用变长整型的编码方式,对负数的编码效率差,如果需要传递负数变量,建议使用sint64 | int64 | long |
| uint32 | 使用变长整数编码 | uint32 | int |
| uint64 | 使用变长整数编码 | uint64 | long |
| sint32 | 使用变长整数编码,对负数的编码效率比int32高 | int32 | int |
| sint64 | 使用变长整数编码,对负数的编码率比int64高 | int64 | long |
| fixed32 | 4B定长编码,当编码数据>2^28次方的时候,比uint32有效率 | uint32 | int |
| fixed64 | 8B定长编码,当编码数据>2^56次方的时候,比uint64有效率 | uint64 | long |
| sfixed32 | 4B定长编码 | int32 | int |
| sfixed64 | 8B定长编码 | int64 | long |
| bool | bool | boolean | |
| string | utf-8编码 | string | String |
| bytes | 字符流 | string | ByteString |
可选项(Optional) 和 默认值
在message消息体里的声明域的最后一个字段是可选字段,这些字段用作对协议的进一步限定.例如:
optional int32 result_per_page = 3 [default = 10];
这这一条声明语句里,“reulst_pre_page"被定义为int32类型和可选变量,当该变量不存在时,在进行编码的时候不会把这条信息编码到字符流里,在解码程序中如果发现该字段并没在字符流里出现,则使用默认值10.如果此时没有”[default=10]",解码程序会用0来填充该字段.相应的如果对于string类型,其默认值是"",对于bytes其默认值是空,即size = 0. 对于bool类型其默认值是false.
枚举类型
枚举类型在编程语言中也是比较常用的类型,在Protocolbuffer中也有对应的形式enum.枚举类型可以定义在message类型的外部或者内部.例:
message SearchRequest{
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3[default = 10];
enum Corpus{
UNITVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
/**
或者
enum Corpus{
UNITVERSAL = 0;
WEB = 1;
IMAGE = 2;
LOCAL =3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
message SearchRequest{
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3[default=10];
optional Corpus corpus = 4;
}
*/
枚举类型采用变长整型(varint)来编码的,所以不能大于一个整型所能表示的范围.
同样考虑到升级问题,可以采用对序号和名称保留的功能,防止将来的维护者误用.
enum Foo{
reserved 2,4,10,200 to max;
reserved "FOO","BAR";
}
组合类型的使用
类似编程中复合类型,protocolbuffer也允许进行复合的定义.在一个message消息中可以用另一个message消息作为一个声明域.
message SearchResponse{
repeated Result result = 1;
}
message Result{
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
导入定义
使用复合消息类型,意味着使用一个已定义的消息作为另一个消息内部的声明域;如果两个消息都在同一个.proto文件可以直接引用,否则需要在引用的.proto文件里导入被引用的消息所在的proto文件
import “myproject/other_protos.proto”
嵌套定义
message类型的定义是可以嵌套的例如
message SearchResponse{
message Result{
required string url = 1;
optional string title = 2;
optional string snippets = 3;
}
repeated Result result = 1;
}
如果你想在其他的消息中使用该消息内部的消息:
message SomeOtherMessage{
optional SearchResponse.Result result = 1;
}
扩展
扩展允许你在消息定义里声明一个范围的序号,范围内的序号将会被保留由第三方使用,进而扩展该消息的声明域.例子
message Foo{
//...
extensions 100 to 199;
}
在消息类型Fool中,保留了[100,199]范围的序号用于第三方的扩展,此时其他的使用者可以使用这些保留的序号增添自己的声明域
extend Fool{
optional int32 bar = 126;
}
升级一个消息类型
ProtocolBuffer具有一个很好的扩展性,正确的使用时及时升级的协议也不影响老代码的运行.在升级协议的时候你需要注意几点:
- 不要修改已经在使用的序号,这会导致旧代码解析错误,产生难以跟踪的问题
- 增加的新的声明域,必须是"optional"或者"repeated",如果使用了"required"意味着新代码要求字符流里必须有该字段,这样旧代码序列化的数据,新的代码将无法再使用了.
- 不需要的非"required"声明域可以去掉,只要这个序号不要被重复使用就行.使用OBSOLETE_或者使用前面介绍的"reserved"可以防止误用.
- 一个非"required"声明域,可以被转换成扩展类型反过来也行.只要他们的序号和类型都保持一致就行.
- int32,uint32,int64,uint64,以及bool都是兼容的,这意味着你可以修改一个其中一个类型到另一个类型,而不会发生兼容性问题.但是在*64->*32的时候会向编程语言一样进行数据的截断
- sint32 和 sint64是兼容的
- string 和 bytes 是兼容的
- fixed32与fixed64兼容,sfixed32与sfixed64兼容
- 对于string 和 bytes 其规则"repeated"和"optional"是兼容的.不过建议慎用
- 对于可选字段的"default"的值可以改变的.
- 枚举"enum"与int32,uint32,int64,uint64就序列化格式而言是兼容的,但是注意到enum对值是有限制的,如果编码端对eunm编码,解码端使用int32/uint32/int64/uint64是可以正常获取到值的.但是反过来在编码段对int32/uint32/int64/uint64编码,解码段使用enum解码,就有可能会产生冲突,具体的现象在不同的实现的语言是不完全一样的.
总结
介绍了基本的prototobuf格式的语法,类型的兼容等.