Proto3入门

Proto3入门

本文基于Google提供的ProtolBuffer LanguageGuide英文文档: ProtolBuffer3 Language Guidephp

ProtoBuf的API文档java

定义一个message

首先以一个简单的例子开头:好比查百度,那么须要一个查询语句:query,还有查询的页面号:page_number,而后就是查询的每一页的结果数:result_per_page。 这样就有三个字段:querypage_numberresult_per_page。 那么这个消息(message)定义以下:python

/*选中语法格式proto3,也就是ProtocolBuffer的版本3*/
syntax = "proto3";
/*定义一个消息,消息名字为SearchRequest*/
message SearchRequest{
	/*键值对,每一个字段则须要字段名和具体的类型*/
    string query = 1;
    int32 page_number = 2;
    int32 result_per_page = 3;
}
复制代码

对于每个字段,必须指定具体的类型

除了一个基础类型(字符串string,整型int32等)还能够定义其余综合类型,如枚举类型和其余的message类型。后面将列出。api

分配字段号

在上面定义的SearchRequest消息中,对于每一个字段,都有惟一标识的编号。这些字段号在消息(message)的二进制格式中惟一识别,在该消息(message)投入到使用后不该该被更改。 在ProtocolBuffer中将消息序列化为二进制后,对于1~15编号的字段,只须要一个字节编码,对于16~2047则须要两个字节。因此,对于把常用的字段元素编号到1~15中,而且预留(reserved)几位以便于之后扩展。 字段号的范围为:1~536870911(2^29-1)。其中19000~19999为ProtocolBuffer本身预留(reserved)的字段号不能使用。其余均可以本身使用。固然,本身预留(reserved)的编号在后续扩展也不能使用。对于预留(reserved)的后面将讲到。ruby

指定字段规则

消息(message)的字段可使用两种规则描述(proto2与proto3不一样):框架

  • 单一的(singular):0个或1个,不用在字段定义中指出。dom

  • 重复的(repeated):0个到多个,须要在字段定义中指出。 看以下例子:一我的,只有一个正式的名字(在刚出生的时候名字还没登记),可是他能够有多个外号,也能够没有。ide

    syntax = "proto3";
      message Person{
          string name = 1;
          repeated string nickname = 2;
      }
    复制代码

添加更多的消息类型

多个消息类型能够在一个.proto文件中定义。 好比在上面SearchRequest中添加一个SearchResponse。函数

message SearchRequest{
    string query = 1;
    int32 page_number = 2;
    int32 result_per_page = 3;
}

message SearchResponse{
    repeated string result = 1;
    int32 page_number = 2;
}
复制代码

注释

.proto文件中注释为C/C++风格,用//注释单方,或/**/注释多行。ui

预留(reserved)字段

前面提到ProtocolBuffer本身预留(reserved)的字段号19000~19999。 能够本身预留字段名或者字段号,这样预留(reserved)的字段将不会被之后的用户修改了。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}
复制代码

编译器编译.proto文件

可使用ProtocolBufer编译器将.proto文件编译成本身选择的语言。在以后可使用编译后的消息(message)进行get/set字段值,序列化消息(message)到输出流,或者从输入流中反序列化获得消息(message)。

  • C++:一个.proto文件编译生成一对.h.cc文件。
  • Java:生成.java文件,使用消息(message)指定的Builder类建立消息(message)对象的实例。
  • Python:Python有点不同:会生成一个module其中包含每一个消息(message)的静态描述符。
  • Go:每个消息(message)生成一个.pb.go文件。
  • Ruby:生成.rb文件,是一个module中包含各个消息(message)。
  • Objective-C:一个.proto文件生成一对pbobjcpbobjc.m文件,每一个消息(message)对应一个class。
  • C#:生成.cs文件,每一个消息(message)对应一个class。
  • Dart:生成.pb.dart文件,每一个消息(message)对应一个class。

基础类型

  • double
  • float
  • int32:使用可变长编码,若是使用的该字段会有负数,效率将变低,这时最好使用sint32。
  • int64:使用可变长编码,若是使用的该字段会有负数,效率将变低,这时最好使用sint64。
  • uint32:使用可变长编码。
  • uint64:使用可变长编码。
  • sin32:使用可变长编码,有符号整形,有负数时使用常规的int32更有效率。
  • sint64:使用可变长编码,有符号整形,有负数时使用常规的int64更有效率。
  • fixed32:固定4个字节,若是数字大于2^28比uint32更有效率。
  • fixed64:固定8个字节,若是数字大于2^56比uint64更有效率。
  • sfixed32:固定4个字节。
  • sfixed64:固定8个字节。
  • bool
  • string:字符串,必须使用UTF-8或者7位ASCII编码格式。
  • bytes:有任意的byte序列。 具体的proto中各个字段类型映射到对应语言中时,见下图:

默认值

若是一个消息(message)被解析了,可是其中的字段并无被赋值,那么将会被设置为默认值。

  • string:空串
  • bytes:空的bytes序列
  • bool:false
  • 数字类型:0
  • 枚举类型:默认值为枚举类型中定义的第一个值,也就是0
  • 消息类型(message:取决于所编译的语言。 对于repeated,为空的list。

枚举

这里仍是以以前的查百度的例子来讲,有了查询关键字query,对于结果,你有可能不仅是想要浏览一下WEB页面,还行看看视频、图片、新闻啥的。那么这样定义:

syntax = "proto3";

message SearchRequest{
    string query = 1;
    int32 page_number = 2;
    int32 result_per_page = 3;

    enum Category{
        option allow_alias = true;
        //第一个值必须为0。
        UNIVERSAL = 0;
        WEB = 1;
        IMAGES = 2;
        NEWS = 3;
        PRODUCT = 4;
        VIDEO = 5;
        //启用了别名,则能够赋同一个值
        GENERAL = 0;
    }
    Category result_type = 4;
}
复制代码

使用enum关键字定义枚举类型。 枚举常量数值必须在32bit的整型中。使用负数赋值枚举常量效率低,不推荐。对于枚举常量,能够定义在消息(message)中,也可定义在消息(message)外。好比上面定义在SearchRequest中的Category,以SearchRequest.Category的方式来复用。

预留(reserved)值

一样的,对于消息(message)中能够预留(reserved)字段号,在枚举中,能够预留(reserved)值。 下面预留(reserved)了,值,名字。(2,15,9到11,40到最大值都不能后续使用)。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}
复制代码

使用其余消息(message)做为字段

以前定义的SearchRespon消息(message):

message SearchResponse{
	repeated string result = 1;
	int32 page_number = 2;
}
复制代码

对于结果,咱们只能获取多个字符串,让他回应的消息功能更强大一点,咱们定义一个Result消息(message):

message SearchResponse{
	repeated Result result = 1;
	int32 page_number = 2;
}
message Result{
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
}
复制代码

SearchRespon中,咱们嵌套了一个Result消息(message),Result中有请求的地址url,标题title还有描述片断snippets

嵌套定义

接下来,咱们再具体化URL:

message SearchResponse{
    repeated Result result = 1;
    int32 page_number = 2;
}
message Result{
    URL url = 1;
    string title = 2;
    repeated string snippets = 3;
}

message URL{
    enum Protocol{
        HTTP = 0;
        HTTPS = 1;
    }
    Protocol protocol = 1;
    string domain = 2;
    int32 port = 3;
    string filepath = 4;
}
复制代码

对于SearchResponse消息(message)中返回的结果result,在Result中又有消息(messageURL,在URL中咱们具体到,使用的协议、域名、端口、请求文件路径。因此,消息之间能够互相嵌套,定义更加复杂的消息。

导入

在Java中,或者其余语言,须要导入其余以及写好的包,在ProtocolBuffer中也是同样,能够导入先前定义好的.proto文件,使用其中定义的消息(message)或者服务(service)。 在同一目录下,我将写好的URL放入URL.proto文件中,在定义SearchResponse消息(message)中导入该文件:

import "URL.proto";

message Result{
    URL url = 1;
    string title = 2;
    repeated string snippets = 3;
}
复制代码

这样就能够复用更多的自定义消息(message)了。 对于import,只能导入其后续指定的.proto文件中定义的消息(message)或服务。好比有3个.proto文件

/*file A.proto*/
syntax = "proto3";

message A{

}
/*file B.proto*/
syntax = "proto3";

import "A.proto";

message B{
    A a = 1;
}
/*file C.proto*/
syntax = "proto3";

import "B.proto";

message C{
    A a = 1;
}
复制代码

在这其中,C是看不到A的,只有在B中import public "A.proto",C才能看见A。

Any字段类型

Any字段类型是Google本身对于Proto中类型的封装,并提供必定特定方法。 以下定义一个Any字段,须要导入Google提供的any.proto

在Java中使用 ErrorStatus消息调用 detailsget方法时,返回的实例是 com.google.protobuf.Any,对于该类型提供了pack和unpack方法,以下:

class Any {
  // 对于给定的消息打包成Any类型,前缀则是默认的:type.googleapis.com
  public static Any pack(Message message);
  // 对于给定的消息打包成Any类型,前缀则是typeUrlPrefix指定的
  public static Any pack(Message message,
                         String typeUrlPrefix);

  // 检查该Any类型是不是给定clazz的消息类型
  public <T extends Message> boolean is(class<T> clazz);
  // 给定clazz消息类型,将Any类型拆包成指定的消息类型,若是不匹配抛出异常
  public <T extends Message> T unpack(class<T> clazz)
      throws InvalidProtocolBufferException;
}
复制代码

Any字段给了必定的灵活性,在传递消息时不用指定特定的类型,能够在传递不一样消息中传递不一样的类型,在接收端进行判断便可。在传输时,底层仍是被转换为bytes类型。

Oneof字段类型

Oneof类型以下定义。

oneof oneof_name {
	int32 foo_int = 4;
	string foo_string = 9;
	...
}
复制代码

对于这个oneof消息类型,咱们能够这样理解,它相似与C语言中的union类型(联合体),最后生成的Java代码是这样的:

public enum OneofNameCase
    implements com.google.protobuf.Internal.EnumLite {
  FOO_INT(4),
  FOO_STRING(9),
  ...
  ONEOFNAME_NOT_SET(0);
  ...
};
复制代码

若是设置了oneof_name消息中的foo_int字段,那foo_string就无效。一样的,若是设置了foo_string字段,那么foo_int字段就无效。在Oneof类型的消息中,只有一片共享内存,每次只有一个字段被设置。 须要注意,Oneof的消息不能使用repeated描述。 在Java中提供了一下方法进行辅助使用: 对于生成类中的枚举类:

  • int getNumber(): 返回在.proto文件中定义的索引值,如foo_int则返回4。
  • static OneofNameCase forNumber(int value): 返回使用索引值相应的对象,若是该对象未设置则返回null,如4则返回foo_int。 生成类中:
  • OneofNameCase getOneofNameCase(): 返回已经设置了的对象,若是都没有被设置返回ONEOFNAME_NOT_SET。 生成类中的Builder:
  • Builder clearOneofName(): 清空全部设置。

Map字段类型

使用这样定义Map类型:

map<key_type, value_type> map_field = N;
复制代码
  • key_type:可使用任何常规类型(int32或者string等),不能使用浮点数bytes类型定义。
  • value_type:能够是任何类型,除了又是一个Map。 和Oneof一样,使用Map定义的字段不能够是**repeated**的。

包:package

对于.proto文件,可使用包组织,package字段就是相似于Java中的Package。 定义的计算CalculateMsg消息,在proto.Calculation文件夹下:

其中的包就是 Calculation
最好使用 package和文件夹想对应,在Java中的习惯哈。

定义服务Service

在这里我使用上面CalculationMsg的消息类,定义了其相应了服务,RPC(Remote Procedure Call)。 package Calculation;

import "Calculation/CalculatMsg.proto";

service Calculator{
    rpc Calc( CalRequest ) returns (CalResponse){}
}
复制代码

使用Proto编译器编译上面的文件,相应于选择的语言将生成服务的接口(interface)和客户端的stub。 可使用Google提供的gRPC,也可使用第三方的RPC框架。 这里我给你们看看模仿grpc.io提供例子写的计算服务: 对于服务端:

复写编译生成的gRPC接口类,实现以前定义的calc函数:获取请求的须要计算方法,数值1和数值2,计算,而后放入输出流中,最后OnComplete。
客户端则先Build一个请求,阻塞调用获取结果。

映射到JSON

Proto3可以转换到JSON数据格式,其相应的数据类型映射以下: 若是Proto中某个字段未设置,在JSON中就是null。

选项Option

.proto文件中可使用option字段声明特定选项。Opion不会影响总体消息的定义,可是在特定的上下文中进行影响。 Option选项也是分级别的,有时候在外定义,则影响的是文件级别,如:java_packagejava_multiple_filesjava_outer_classname等,分别是:编译后在哪一个java包下,是否将.proto文件中不一样消息分红多个文件,定义编译后的java类名。

以前定义的计算服务就是如上,生成的 packagetech.sylardaemon.Calculation中,生成后的类名 CalculatorProt,java_generic_services为true则是生成gRPC相应的服务接口和客户stub。 还能够本身定义option,是ProtoBuf的一种高级应用,这里就略过了,有兴趣的同窗能够本身查查看。

编译器使用

编译器的使用以下:

protoc --proto_path = IMPORT_PATH --Language_out = DST_DIR path/to/*.proto
复制代码
  • --proto_path:该参数输入的IMPORT_PATH是指定你要编译的***.proto文件中import指令中查找的目录。若是省略,则使用当前编译器执行的目录。也能够屡次使用--proto_path指定多个导入目录。可使用-I**缩短。
  • --Language_out:能够提供一个或多个输出目录:
    • --cpp_out:生成C ++代码的目的目录
    • --java_out:生成Java代码的目的目录
    • --python_out:生成Python代码的目的目录
    • --go_out:生成Go代码的目的目录
    • --ruby_out:生成Ruby代码的目的目录
    • --objc_out:生成Objective-C代码的目的目录
    • --csharp_out:生成C#代码的目的目录
    • --php_out:生成PHP代码的目的目录
  • path/to/*.proto:最后的则是将要被编译的proto文件路径。

整篇就差很少完成了,最后还有一点考试,若是有错误或者缺了啥,欢迎提出,大概寒假有时间就来改改,你们一块儿进步xio习。

相关文章
相关标签/搜索