简介
Protocol Buffer是Google提供的一种数据序列化协议,官方对protobuf的定义如下
Protocol Buffer是一种轻便高效的结构化数据存储格式,可以洪湖结构化数据序列化,很适合做数据存储或RPC数据交换格式,它可用于通讯协议,数据存储等领域的语言无关,平台无关,扩展的序列化结构数据格式。
看到数据序列化,我们脑海中首先想到的肯定是JSON和XML。那么他们的区别在哪呢?
- Protocol Buffer序列化之后得到的数据不是可读的字符串,而是二进制流
- JSON和XML的数据信息都包含在序列化后的数据中,不需要任何信息就能还原序列化后的数据。但是使用Protocol Buffer之后,需要事先定义一个.proto的文件,之后还原需要用到这个.proto文件定义好的数据格式
- 在传输数据量较大的场景下,Protocol Buffer比XML,JSON更小,更快,另外Protocol Buffer可以跨平台,跨语言使用
语法
接下来我们来看下Protocol Buffer的相关语法,即在.proto文件汇总如何定义一个结构化的数据
Protocol Buffer存在包(package)的概念,与Java中包的概念类似;可以通过option来控制如何生成对应语言的文件。Protocol Buffer中有两种类型的数据,一种是message,一种是service。前者可以类比为Java中的class,后者可以类比为Java中的interface。
message有多个filed组成,每一个field包含字段类型,字段名称以及字段号。具体示例如下:
syntax = "proto3"; //protobuf目前存在两个版本,proto2以及proto3。不设置syntax则认为是proto2
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
我们定义了一个名为SearchRequest的message,它有三个字段,一个是string类型,还有两个int32类型,字段号从低到高为1,2,3。
字段号的作用
通过刚刚的示例,我们发现每一个字段都有一个独一无二的字段号。那么字段号的作用是干啥的呢?还记得前面提到过,protobuf序列化之后得到的是二进制流。有了字段号,在进行反序列化的时候,我们可以通过字段号来识别字段。
范围从1到15的字段号需要一个字节进行编码,16到2047的字段则需要两个字节,所以对于频繁出现的字段,我们可以用1-15的字段号。
字段号的取值范围为[1,2^29-1],但是19000到19999是保留字段号。如果使用了保留字段号,则在编译文件时会报错。
Field Type
protobuf中的字段类型与各编程语言字段类型的对应关系如下
除了上述这些基本的字段类型,protobuf中还有一些复合形态的类型。
例如,将message作为另外一个message的字段
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
在上述示例中,SearchResponse将Result作为它的字段,其中关键字repeated表示这个字段可以包含多个数值,可以类比为Java中的List
protobuf也存在类似Java中的内部类的概念,我们可以在一个message内部直接定义另一个message
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
protobuf中还有一种非常特殊的类型Any,可以将其类比为Java中的Object,它可以表示任一类型。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
在使用Any类型时,需要引入google/protobuf/any.proto
Enumerations
在protobuf中可以定义枚举类型
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
每一个枚举项都对应了一个常数,常数为0的枚举值表示枚举的默认值,在定义枚举类型时必须设置这个默认值
import
我们就可以从其他文件中引入需要的message。
import "myproject/other_protos.proto";
import不具有传递性,除非使用import public。怎么理解呢,我们通过示例可以更好的说明
假设有一个old.proto文件表示之前定义的message,之后我们新增了一些message,这些message统一放在new.proto文件中。同时有一个other.proto被old.proto引用。此时如果有文件引入了old.proto会出现什么情况呢?
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
在old.proto中通过import public引入了new.proto。当old.proto被其他文件引入时,new.proto文件也会被引入进来,而other.proto则不会
Map
protobuf允许用户定义一个map,格式如下:
map<key_type, value_type> map_field = N;
但是存在几点限制
- key_type可以为任一基础类型,除了浮点类型以及bytes
- 类型为map的字段,不可以使用repeated关键字
- 序列化时,map并不保证顺序
- 当反序列化时,map会按照key进行排序。对于数字,则按照数字顺序排序
- 在进行序列化时,如果存在重复的key,则编译失败。在反序列化时,如果发现重复的key,则取最新的key
option
protobuf存在几类的option,一类是file-level,只对.proto文件生效,需要声明在文件开头;另一类是field-level,只对字段生效,需要在message内部中使用
- java_package (file option):将.proto文件编译为Java Class时的包名
option java_package = "com.example.foo";
- java_multiple_files (file option):将一个.proto文件定义的message,enums,service生成多个class文件,而不是统一放入一个class中
option java_multiple_files = true;
- java_outer_classname (file option):指定编译生成的class文件的名称,如果没有指定,将则.proto文件的文件名作为class的名称。
option java_outer_classname = "Ponycopter";
- optimize_for: 可以设置SPEED, CODE_SIZE,LITE_RUNTIME。表示编译器在编译.proto文件时的侧重点。
编译
将.proto文件编译成Java文件有两种方式,一种是通过maven插件,一种是通过protoc命令
maven
使用maven插件进行编译时,需要把.proto文件放入到/src/main目录
- 引入依赖
<properties>
<grpc.version>1.28.0</grpc.version>
<protobuf.version>3.3.0</protobuf.version>
</properties>
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
</dependencies>
- 引入插件
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.6.2</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
- mvn compile
执行以下命令,即可以在target目录下找到编译后的Java文件
mvn compile
protoc
编译多个.proto文件
protoc -I=./src/main/proto/ --java_out=./src/main/java/ ./src/main/proto/*.proto
protoc-gen-grpc-java
注意上述命令仅仅是生成了相关实体类的信息,也就是我们在.proto定义的message,但是定义的service却没有生成对应的类。
如果期望通过protoc命令生成定义的service,需要用到protoc-gen-grpc-java插件
我们需要自己编译插件,具体可以参考官方文档
编译生成插件之后,通过–plugin指定插件位置
protoc --plugin=protoc-gen-grpc-java \
--grpc-java_out="$OUTPUT_FILE" --proto_path="$DIR_OF_PROTO_FILE" "$PROTO_FILE"