Protobuf简介
Protobuf是谷歌开源的一个序列化框架,支持跨语言,高性能等特性,在用于网络传输进行序列化的场景下使用广泛。
它具有以下优点:
- 性能好,效率高
- 代码生成机制,数据解析类自动生成
- 支持向后兼容和向前兼容
- 支持多种编程语言(java,c++,python)
这里学习的基于proto3
版本,与proto2
有稍许区别。
Protobuf编译
学习Protobuf需要下载两个文件:
- protoc:用于将.proto反序列成对应的代码文件,这个可以直接从GitHub上下载。
- 类库:这里使用java类库,也可以从Github上下载,但是下载的是源码,需要编译。
下载地址
- protoc下载
- 类库下载
类库编译
本次编译是在Mac OS上进行的,在Windows上类似。
- 安装Maven,在Mac OS上直接使用
HomeBrew
进行安装,安装命令如下:
brew install maven
- 解压
protoc-3.5.1-osx-x86_64.zip
和protobuf-java-3.5.1.zip
压缩包,得到对应的文件目录; - 重要:将protoc文件拷贝到protobuf目录的src目录下。假设现在在
protoc-3.5.1-osx-x86_64
目录下,执行如下命令:
cp ./bin/protoc ../protobuf-3.5.1/src
- 进入
protobuf-3.5.1/java
目录下,执行命令
mvn install
mvn package
- 编译打包完成后,分别在
core/target
和util/target
目录下得到两个jar包。
开始使用
定义一个消息类型
先定义一个简单的消息类型,假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 第一行通过定义
syntax
表明是使用的proto3语法,如果想使用proto2语法,就改成proto2
; - SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。
指定字段类型
在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。
分配标识号
正如上述文件格式,在消息定义中,每个字段都有唯一的一个数字标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。
注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。
字段修饰符类型
所指定的消息字段修饰符必须是如下之一:
- singular:一个格式良好的消息可以有0个或1个值(不超过1个)。
- repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。
在proto3
中,repeated
修饰的数字类型的字段默认使用packed
编码。
在同一个文件中添加其他消息类型
在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用——例如,如果想定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中,如:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
message SearchResponse {
...
}
添加注释
向.proto文件添加注释,可以使用C/C++/java风格的双斜杠//
语法格式或者/*....*/
,如:
/* 这是一个注释 */
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;// Which page number do we want?
optional int32 result_per_page = 3;// Number of results to return per page.
}
从.proto文件生成了什么?
当用protocolbuffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。
- 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
- 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的
Builder
类(该类是用来创建消息类接口的)。 - 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
数值类型对应关系
.proto类型 | Java类型 | C++类型 |
---|---|---|
double | double | double |
float | float | float |
int32 | int | int32 |
int64 | long | int64 |
uint32 | int | uint32 |
uint64 | long | uint64 |
sint32 | int | int32 |
bool | boolean | bool |
string | String | string |
bytes | ByteString | string |
默认值
当解析消息时,如果它不包含singular的元素值,那么解析出来的对象中的对应字段就被置为默认值。默认值可以在消息描述文件中指定。
各个数据类型的默认值如下:
- string类型,默认是空字符串;
- bytes类型,默认是空bytes
- bool类型,默认是false;
- 数字类型,默认是0;
- enum类型,默认是第一个枚举元素的值,必须是0;
repeated修饰的元素默认值是空。
枚举
当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)就可以了。一个enum类型的字段只能用指定的常量集中的一个值作为其值(如果尝 试指定不同的值,解析器就会把它当作一个未知的字段来对待)。在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
注意:任何一个枚举类型,第一个元素必须是一个值为0的枚举元素
枚举常量必须在32位整型值的范围内。因为enum值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。如上例所示,可以在 一个消息定义的内部或外部定义枚举——这些枚举可以在.proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同 的消息中使用它——采用MessageType.EnumType的语法格式。
当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。
使用其他消息类型
你可以将其他消息类型用作字段类型。例如,假设在每一个SearchResponse消息中包含Result消息,此时可以在相同的.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如:
message SearchResponse {
repeated Result result = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
导入定义
在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?
你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:
import "myproject/other_protos.proto";
嵌套类型
你可以在其他消息类型中定义、使用消息类型,在下面的例子中,Result消息就定义在SearchResponse消息内,如:
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
如果你想在它的父消息类型的外部重用这个消息类型,你需要以Parent.Type
的形式使用它,如:
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
当然,你也可以将消息嵌套任意多层,如:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
required int32 ival = 1;
optional bool booly = 2;
}
}
}
Map类型
如果想创建一个Map类型的数据,可以使用如下语法:
map<key_type,value_type> map_field = N;
key_type
可以是int32类型或者string类型。value_type
可以是任何除了map类型之外的其他类型。map
类型不可以用repeated修饰。
包
当然可以为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。如:
package foo.bar;
message Open { ... }
在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如:
message Foo {
...
foo.bar.Open open = 1;
...
}
包的声明符会根据使用语言的不同影响生成的代码。
- 对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中;
- 对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有
java_package
;
package foo.bar
option java_package = "com.foo.bar"
- 对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。
选项(Options)
在定义.proto文件时能够标注一系列的options。Options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。
- 一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。
- 一些选项是消息级别的,意味着它可以用在消息定 义的内部。
- 当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中。
- 到目前为止,并没有一种有效的选项能作用于所有的类型。
如下就是一些常用的选择:
java_package (file option)
这个选项表明生成java类所在的包。如果在.proto文件中没有明确的声明java_package,就采用默认的包名。当然了,默认方式产生的 java包名并不是最好的方式,按照应用名称倒序方式进行排序的。如果不需要产生java代码,则该选项将不起任何作用。如:
option java_package = "com.example.foo";
java_outer_classname (file option)
: 该选项表明想要生成Java类的名称。如果在.proto文件中没有明确的java_outer_classname定义,生成的class名称将会根据.proto文件的名称采用驼峰式的命名方式进行生成。如(foo_bar.proto生成的java类名为FooBar.java),如果不生成java代码,则该选项不起任何作用。如:
option java_outer_classname = "Ponycopter";
optimize_for (fileoption)
可以被设置为SPEED
,CODE_SIZE
,orLITE_RUNTIME
。这些值将通过如下的方式影响C++及java代码的生成:
SPEED
默认选项,protocol buffer编译器将通过在消息类型上执行序列化、语法分析及其他通用的操作。这种代码是最优的。CODE_SIZE
protocol buffer编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。采用该方式产生的代码将比SPEED要少得多, 但是操作要相对慢些。当然实现的类及其对外的API与SPEED模式都是一样的。这种方式经常用在一些包含大量的.proto文件而且并不盲目追求速度的 应用中。LITE_RUNTIME
protocol buffer编译器依赖于运行时核心类库来生成代码(即采用libprotobuf-lite 替代libprotobuf)。这种核心类库由于忽略了一 些描述符及反射,要比全类库小得多。这种模式经常在移动手机平台应用多一些。编译器采用该模式产生的方法实现与SPEED模式不相上下,产生的类通过实现 MessageLite接口,但它仅仅是Messager接口的一个子集。
option optimize_for = CODE_SIZE;
packed (field option)
如果该选项在一个整型基本类型上被设置为真,则采用更紧凑的编码方式。当然使用该值并不会对数值造成任何损失。在2.3.0版本之前,解析器将会忽略那些 非期望的包装值。因此,它不可能在不破坏现有框架的兼容性上而改变压缩格式。在2.3.0之后,这种改变将是安全的,解析器能够接受上述两种格式,但是在 处理protobuf老版本程序时,还是要多留意一下。
使用protoc生成访问类
可以通过定义好的.proto文件来生成Java、Python、C++代码,需要基于.proto文件运行protocol buffer编译器protoc。运行的命令如下所示:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
IMPORT_PATH声明了一个.proto文件所在的具体目录。如果忽略该值,则使用当前目录。如果有多个目录则可以 对–proto_path 写多次,它们将会顺序的被访问并执行导入。-I=IMPORT_PATH是它的简化形式。
注意:--proto_path
仅仅表示.proto文件在哪个目录,具体文件名需要在最后申明。