Protocol Buffer-1

简介

Protocol Buffer是Google提供的一种数据序列化协议,官方对protobuf的定义如下

Protocol Buffer是一种轻便高效的结构化数据存储格式,可以洪湖结构化数据序列化,很适合做数据存储或RPC数据交换格式,它可用于通讯协议,数据存储等领域的语言无关,平台无关,扩展的序列化结构数据格式。

看到数据序列化,我们脑海中首先想到的肯定是JSON和XML。那么他们的区别在哪呢?

  1. Protocol Buffer序列化之后得到的数据不是可读的字符串,而是二进制流
  2. JSON和XML的数据信息都包含在序列化后的数据中,不需要任何信息就能还原序列化后的数据。但是使用Protocol Buffer之后,需要事先定义一个.proto的文件,之后还原需要用到这个.proto文件定义好的数据格式
  3. 在传输数据量较大的场景下,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"

参考文献


Reprint please specify: wbl Protocol Buffer-1

Previous
Spring Boot JPA--DataSource Spring Boot JPA--DataSource
周所周知,SpringBoot可以非常方便的实现数据库的连接访问。只要配置数据库的相关属性,定义对应的Entity以及Repository就可以实现对数据库的增删改查。 properties spring.datasource.url=jd
2020-03-31
Next
从EnableScheduling说起 从EnableScheduling说起
在之前一篇文章Spring Boot定时任务,我们已经了解了如何在Spring Boot中创建并管理定时任务,今天我们通过源码来看下Spring Boot中的具体实现。 EnableScheduling注解要想在Spring Boot中创建
2020-03-12