Proto3入门

Proto3入门

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

ProtoBuf的API文档

定义一个message

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

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

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

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

分配字段号

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

指定字段规则

消息(message)的字段可以使用两种规则描述(proto2与proto3不同):

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

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

      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++风格,用//注释单方,或/**/注释多行。

预留(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习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值