Protobuf学习笔记

Protobuf简介

Protobuf是谷歌开源的一个序列化框架,支持跨语言,高性能等特性,在用于网络传输进行序列化的场景下使用广泛。

它具有以下优点:

  • 性能好,效率高
  • 代码生成机制,数据解析类自动生成
  • 支持向后兼容和向前兼容
  • 支持多种编程语言(java,c++,python)

这里学习的基于proto3版本,与proto2有稍许区别。

Protobuf编译

学习Protobuf需要下载两个文件:

  1. protoc:用于将.proto反序列成对应的代码文件,这个可以直接从GitHub上下载。
  2. 类库:这里使用java类库,也可以从Github上下载,但是下载的是源码,需要编译。

下载地址

Github地址

类库编译

本次编译是在Mac OS上进行的,在Windows上类似。

  • 安装Maven,在Mac OS上直接使用HomeBrew进行安装,安装命令如下:
brew install maven
  • 解压protoc-3.5.1-osx-x86_64.zipprotobuf-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/targetutil/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文件中使用这些预留标识号,编译时就会报警。

扫描二维码关注公众号,回复: 2512699 查看本文章

字段修饰符类型

所指定的消息字段修饰符必须是如下之一:

  • 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,or LITE_RUNTIME。这些值将通过如下的方式影响C++及java代码的生成:
    • SPEED默认选项,protocol buffer编译器将通过在消息类型上执行序列化、语法分析及其他通用的操作。这种代码是最优的。
    • CODE_SIZEprotocol buffer编译器将会产生最少量的类,通过共享或基于反射的代码来实现序列化、语法分析及各种其它操作。采用该方式产生的代码将比SPEED要少得多, 但是操作要相对慢些。当然实现的类及其对外的API与SPEED模式都是一样的。这种方式经常用在一些包含大量的.proto文件而且并不盲目追求速度的 应用中。
    • LITE_RUNTIMEprotocol 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文件在哪个目录,具体文件名需要在最后申明。

猜你喜欢

转载自blog.csdn.net/twypx/article/details/80290790