Google在Proto2的基础上发布了Proto3,并且Proto3和Proto2在语法规范上存在一定的差异(据说是更简单了)。本文只针对Proto2的语法规范进行学习和总结,对Proto3有兴趣的同学可参考文末第一个参考链接。
1.message类型
前一篇文章我们已经接触过一个简单的.proto文件了,在文件中我们定义了一个名为Person的message类型,并且在Person的内部,我们还嵌套的定义了其他的message类型。此处,我们再定义一个message类型用来满足某种查询请求的场景,其中每个查询请求都有一个“查询字符串”、“感兴趣的目标页面”以及“每个页面的许多结果”。相应地,我们可以在.proto文件中这样定义一个message类型:
1 2 3 4 5 |
message SearchRequest{ required string query = 1; optional int32 page_number = 2; optional int32 result_per_page = 3; } |
(1)字段类型
如上示例,使用了两个整型int32类型以及一个字符串string类型。除此之外,您还可以根据自己的需要使用复合类型,如枚举类型、message类型等。
(2)字段编号
SearchRequest类型中的三个字段被分别指定了三个不同的编号,这些编号是用来标识二进制格式中的各个字段的,所以不要轻易修改哦。
其次,编号1-15需要一个字节进行编码,编码16-2017则需要两个字节。所以,您应该将1-15用于那些需要频繁使用的字段上。
最后,最小的字段编号为1,最大的字段编号为2^29-1,即536870911。19000-1999为保留字段,不要使用。
(3)字段限定符
有如下三个字段限定符:
1)required:该限定符修饰的字段,表示一个符合语法规则的消息(well-formed message)必须具有此字段;
2)optional:该限定符修饰的字段,表示一个符合语法规则的消息可以有0个或不超过1个字段;
3)repeated:该限定符修饰的字段,表示一个符合语法规则的消息中,此字段可以重复多次(包括零次)。
由于历史原因,repeated修饰的数值类型字段没有得到高效的编码,所以,新代码应该使用一个特殊的选项——[packed=true]来获得高效的编码效果:
1 |
repeated int32 samples = 4 [packed=true]; |
注:更多有关Protocol Buffer编码相关的内容,请参考https://developers.google.com/protocol-buffers/docs/encoding.html#packed
(4)其他
学习了以上三点之后,编写一个message类型对你来说应该就没有问题了。此外,还有几点需要提及的是:
1)在一个.proto文件中可以定义多个message类型;
2)编写.proto文件时,我们可以使用C/C++中的那一套注释风格来帮助我们注释,如//或/*…*/;
3)保留字段。
使用reserved关键字可以保留已删除字段和字段编号,如果将来用户尝试使用这些字段标识符,protobuf编译器将会报错,避免导致其他严重的问题:
1 2 3 4 |
message Foo{ reserved 2, 15, 9 to 11; reserved "foo", "bar"; } |
2.基本类型
protobuf具有以下若干种基本类型,并且这些类型在相应的语言中的一一对应关系如下图2-1所示:
图2-1
3.可选字段和默认值
如上所述,message类型中的字段可以标记为optional。在解析消息时,如果该消息不包含可选的字段内容,则解析对象中的相应字段将被设置为该字段的默认值。比如,我们可以指定result_per_page的默认值为10:
1 |
optional int32 result_per_page = 3 [default = 10]; |
如果没有显式地为一个optional字段指定默认值,那么相应的optional字段将使用该字段类型的默认值。比如,string类型的默认值为空;bool类型的默认值为false;数值类型的默认值为0;枚举类型的默认值是枚举类型定义中列出的第一个值。
4.枚举类型
protobuf复合类型中提供了枚举类型,类似于C/C++中的枚举类型(不同点在于,C/C++中枚举类型中的字段是以逗号分隔的,protobuf则是以分号分隔)。你可以这样使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
message SearchRequest{ required string query = 1; optional int32 page_number = 2; optional int32 result_per_page = 3 [default = 10]; enum Corpus{ UNIVERSIAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; WIDEO = 6; } optional Corps corps = 4 [default = UNIVERSIAL]; } |
此外,在message类型中介绍的reserved规则同样适用于枚举类型。当你更新或删除enum中的条目时,记得将其置为reserved,避免产生严重的灾难后果:
1 2 3 4 |
enum Foo{ reserved 2, 15, 9 to 11, 40 to max; reserved "FOO", "BAR"; } |
5.使用其他message
前面也有说过,我们可以在一个message类型中使用另一个message类型:
1 2 3 4 5 6 7 8 9 |
message SearchResponse{ repeated Result result = 1; } message Result{ required string url = 1; optional string title = 2; repeated string snippers = 3; } |
(1)导入其他.proto文件
上面的示例中,SearchResponse类型中引用了Result类型,并且二者都定义在一个.proto文件中。假设我们需要引入的message类型已经定义在了其他某个.proto文件中,该怎么办呢?
使用import导入相应.proto文件即可:
1 |
import "myproject/other_protos.proto"; |
(2)使用proto3的消息类型
值得一提的是,在proto2的语法规则下,我们可以导入及使用在proto3中定义的message类型,反之亦然。然而有一点例外的是,proto2的枚举类型不能在proto3中使用。
6.嵌套类型
Protobuf还允许我们在message类型中再定义其他的message,比如前面定义的SearchResponse等价定义:
1 2 3 4 5 6 7 8 |
message SearchResponse{ message Result{ required string url = 1; optional string title = 2; repeated string snippers = 3; } repeated Result result = 1; } |
7.更新message
永远不会变化的就是变化了,假如现在的message类型已经满足不了你的需求了,你希望在现有的message中添加一个额外的字段,同时你仍然希望使用旧的message所生成的代码。那么,在不破坏现有代码的情况下更新message类型也是非常简单的,你只需记住以下规则:
(1)不要试图改变已有字段的编号;
(2)新增字段的限定符必须为optional或repeated,而决不能是required;
(3)可以删除非required修饰的字段,前提是该字段的编码不要在你更新的message类型中再次使用;
(4)int32, uint32, int64, uint64以及bool类型都是互相兼容的。这意味着您可以将这些类型更改为其中的另一种类型;
(5)sint32和sint64是互相兼容的,但不兼容与其他的数值类型;
(6)如果字节是utf8编码的,那么string和bytes是互相兼容的;
(7)fixed32和sfixed32兼容,fixed64和sfixed64兼容;
(8)optional与repeated是兼容的。
(9)…
8.Package
为了防止协议消息类型之间的名称冲突,Protobuf引入了package特性,你可以在.proto文件中添加一条package语句(该语句是可选的):
1 2 3 |
package foo.bar; message Open{...} |
如果你生成的是C++源文件,那么这里的package的作用就类似于C++中的命名空间namespace,如果你生成的是Java源文件,那么这里的package的作用就类似于Java中的package。
9.Protobuf编译器
要生成C++,Java或Python等代码,您需要使用Protobuf的编译器protoc去编译.proto文件。一般使用方法如下:
1 |
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto |
其中,
1)IMPORT_PATH指定在解析import指令时查找proto文件的目录,如果省略则使用当前目录,缩写形式为-I=IMPORT_PATH;
2)–cpp_out表示在DST_DIR中生成C++代码,类似的有–java_out, –python_out等;
3)此外,如果DST_DIR是以.zip或.jar结尾的,编译器将把输出写入相应的归档文件中。如果输出的文件已经存在,将会覆盖原有文件。
10.其他
现在我们对Protocol Buffer的主要特性已经有了较清晰的认知了。除此之外,还有部分高级特性未做介绍(如extensions, maps, oneof,options),有兴趣的同学请参考文末参考链接。
参考:
[1]. https://developers.google.com/protocol-buffers/docs/proto3
[2]. https://developers.google.com/protocol-buffers/docs/proto