Proto3入门
本文基于Google提供的ProtolBuffer LanguageGuide英文文档: ProtolBuffer3 Language Guide
ProtoBuf的API文档
定义一个message
首先以一个简单的例子开头:比如查百度,那么需要一个查询语句:query,还有查询的页面号:page_number,然后就是查询的每一页的结果数:result_per_page。 这样就有三个字段:query,page_number和result_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文件生成一对pbobjc和pbobjc.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中又有消息(message)URL,在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消息调用details的get方法时,返回的实例是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_package、java_multiple_files、java_outer_classname等,分别是:编译后在哪个java包下,是否将.proto文件中不同消息分成多个文件,定义编译后的java类名。
之前定义的计算服务就是如上,生成的package在tech.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文件路径。
完
作者:LudwigWuuu
链接:https://juejin.im/post/5c0a82ed6fb9a049ad76dd56
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
本文暂时没有评论,来添加一个吧(●'◡'●)