GRPC-语法定义
从官方示例入手
gitlub地址
git地址:https://github.com/grpc/grpc-java.git
分析工程中 examples/protos/route_guide.proto
文件
//
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";
option objc_class_prefix = "RTG";
package routeguide;
service RouteGuide {
rpc GetFeature(Point) returns (Feature) {}
rpc ListFeatures(Rectangle) returns (stream Feature) {}
rpc RecordRoute(stream Point) returns (RouteSummary) {}
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
message Rectangle {
Point lo = 1;
Point hi = 2;
}
message Feature {
string name = 1;
Point location = 2;
}
message FeatureDatabase {
repeated Feature feature = 1;
}
message RouteNote {
Point location = 1;
string message = 2;
}
message RouteSummary {
int32 point_count = 1;
int32 feature_count = 2;
int32 distance = 3;
int32 elapsed_time = 4;
}
上述是一个完整的示例代码,接下来咱们逐个分析。
eg:这里使用PB代指 protocol buffers
文件定义
gRPC的文件都是以.proto
结尾
消息类型定义(Message)
一个message类型看上去很像一个Java class,由多个字段组成。每一个字段都由类型、名称组成,位于等号右边的值不是字段默认值,而是数字标签,可以理解为字段身份的标识符,类似于数据库中的主键,不可重复,标识符用于在编译后的二进制消息格式中对字段进行识别,一旦你的pb消息投入使用,字段的标识就不应该再改变。数字标签的范围是[1, 536870911],其中19000~19999是保留数字。
类型
修饰符
如果一个字段被repeated
修饰,则表示它是一个列表类型的字段,如下所示:
message FeatureDatabase {
repeated Feature features = 1; // 等价于java中的List<Feature> features
repeated string args = 2 // 等价于java中的List<string> args
}
如果你希望可以预留一些数字标签或者字段可以使用reserved
修饰符:
如果你通过删除或者注释所有域,以后的用户可以重用标识号当你重新更新类型的时候。如果你使用旧版本加载相同的.proto文件这会导致严重的问题,包括数据损坏、隐私错误等等。现在有一种确保不会发生这种情况的方法就是指定保留标识符(and/or names, which can also cause issues for JSON serialization不明白什么意思),protocol buffer的编译器会警告未来尝试使用这些域标识符的用户。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
string foo = 3 // 编译报错,因为‘foo’已经被标为保留字段
}
默认值
-
string类型的默认值是空字符串
-
bytes类型的默认值是空字节
-
bool类型的默认值是false
-
数字类型的默认值是0
-
enum类型的默认值是第一个定义的枚举值
-
message类型(对象,如上文的SearchRequest就是message类型)的默认值与 语言 相关
-
repeated修饰的字段默认值是空列表
如果一个字段的值等于默认值(如bool类型的字段设为false),那么它将不会被序列化,这样的设计是为了节省流量。
枚举
每个枚举值有对应的数值,数值不一定是连续的。第一个枚举值的数值必须是0且至少有一个枚举值,否则编译报错。编译后编译器会为你生成对应语言的枚举类。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
一个数值可以对应多个枚举值,必须标明option allow_alias = true;
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
其他类型的Message定义
message Point {
int32 latitude = 1; //对应Java中的 int latiudu;
int32 longitude = 2; //对应Java中的 int longitude;
}
message Feature {
string name = 1; //对应Java中的 String name; 这里理解成基本数据类型
Point location = 2; //对应Java中的 Point location; 这里理解成引用数据类型;
}
message FeatureDatabase {
repeated Feature feature = 1; // 对应Java中的 List<Featrue> featrue; 引用数据类型
}
一个.proto
文件可以定义多个message
类型,并且可以按照逻辑进行引用和定义。除此之外,还可以引用其他文件中定义的message
;
Import
import "proto/other.proto"; //这样就可以使用other.proto文件中定义的message类型了,这里需要注意的是不能导入不使用的.proto文件
import
本身是不具有传递性的,如果希望引入的messgae
具有传递效果的话需要在import
后使用public
进行修饰,见代码:
// 文件a.proto
·····
// 文件b.proto
·····
// 文件c.proto
import "a.proto"; //没有使用 public修饰
import public "b.proto"; //使用 public修饰,会出现继承传递的效果
// 文件文件c_extend.proto
import "c.proto" // 引入c.proto
此时文件c_extend.proto
中可以使用c.proto
中的message
和b.proto
中的message
,但是不能访问a.proto
中的message
。
嵌套类型
可以在一个message
类型中定义另一个message
类型,支持无限嵌套,这里可以理解成Java中的内部类。代码示例如下:
message A {
string name = 1;
int32 age = 2;
message B {
float weight = 1;
bool sex = 2
}
B b_entity = 3;
}
还可以使用ParentType.ChildType
的方式调用,类似Java中的静态内部类比如:
// 使用上述定义好的类型
message C {
A.B a_b_entity = 1;
}
Any类型
Any类型消息允许你在没有指定他们的.proto
定义的情况下使用消息作为一个嵌套类型。一个Any类型包括一个可以被序列化bytes类型的任意消息,以及一个URL作为一个全局标识符和解析消息类型。为了使用Any类型,你需要导入import google/protobuf/any.proto
import "google/protobuf/any.proto";
message Response {
string msg = 1;
int32 code = 2;
google.protobuf.Any data = 3; //可以理解成Object
repeated google.protobuf.Any datas = 4; //可以理解成泛型 List<T> datas;
}
可以通过pack()
和unpack()
(方法名在不同的语言中可能不同)方法装箱/拆箱,以下是Java的例子:
People people = People.newBuilder().setName("proto").setAge(1).build();
// protoc编译后生成的message类
Response r = Response.newBuilder().setData(Any.pack(people)).build();
// 使用Response包装people
System.out.println(r.getData().getTypeUrl());
// type.googleapis.com/example.protobuf.people.People
System.out.println(r.getData().unpack(People.class).getName());
// proto
Any
对包装的类型会生成一个URL,默认是type.googleapis.com/packagename.messagename
(在Java中可以通过这个特性进行***反射***操作)。
Oneof类型
如果你的消息中有很多可选字段, 并且同时至多一个字段会被设置, 你可以加强这个行为,使用oneof
特性节省内存.
Oneof字段就像可选字段, 除了它们会共享内存, 至多一个字段会被设置。 设置其中一个字段会清除其它字段。 你可以使用case()
或者WhichOneof()
方法检查哪个oneof字段被设置, 看你使用什么语言了.
// 你可以增加任意类型的字段, 但是不能使用repeated 关键字.
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
设置oneof会自动清楚其它oneof字段的值. 所以设置多次后,只有最后一次设置的字段有值.
Map(映射)类型
PB
中也可以使用map类型(官方并不认为是一种类型,此处称之为类型仅便于理解),绝大多数scalar类型都可以作为key,除了浮点型和bytes,枚举型也不能作为key,value可以是除了map以外的任意类型:
// map<key_type, value_type> map_field = N;
map<string, Project> projects = 3;
map
实际是一种语法糖,可以理解成下面:
message MapEntity {
key_type key = 1;
value_type value = 2;
}
repeated MapEntity maps = N;
包定义
.proto
文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。
package routeguide;
service RouteGuide {
······
}
可以使用全限定名来引用
// 指定包名后,会对生成的代码产生影响,以Java为例,生成的类会以你指定的package作为包名。
message {
routeguide.RouteGuide rg = 1
······
}
服务定义
要定义一个服务,你必须在你的 .proto 文件中指定 service
:
service RouteGuide {
//A simple RPC. (一个简单的RPC)
rpc GetFeature(Point) returns (Feature) {}
//一个 服务器端流式 RPC , 客户端发送请求到服务器,拿到一个流去读取返回的消息序列。
//客户端读取返回的流,直到里面没有任何消息。从例子中可以看出,通过在 响应 类型前插入
//stream 关键字,可以指定一个服务器端的流方法。
rpc ListFeatures(Rectangle) returns (stream Feature) {}
//一个 客户端流式 RPC , 客户端写入一个消息序列并将其发送到服务器,同样也是使用流。
//一旦客户端完成写入消息,它等待服务器完成读取返回它的响应。通过在 请求 类型前指定
//stream 关键字来指定一个客户端的流方法。
rpc RecordRoute(stream Point) returns (RouteSummary) {}
//一个 双向流式 RPC 是双方使用读写流去发送一个消息序列。两个流独立操作,因此客户端
//和服务器可以以任意喜欢的顺序读写:比如, 服务器可以在写入响应前等待接收所有的客户
//端消息,或者可以交替的读取和写入消息,或者其他读写的组合。 每个流中的消息顺序被预留。
//你可以通过在请求和响应前加 stream 关键字去制定方法的类型。
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
选项(Option)
在定义.proto文件时能够标注一系列的options。Options并不改变整个文件声明的含义,但却能够影响特定环境下处理方式。完整的可用选项可以在google/protobuf/descriptor.proto找到。
一些选项是文件级别的,意味着它可以作用于最外范围,不包含在任何消息内部、enum或服务定义中。一些选项是消息级别的,意味着它可以用在消息定义的内部。当然有些选项可以作用在域、enum类型、enum值、服务类型及服务方法中。到目前为止,并没有一种有效的选项能作用于所有的类型。
如下就是一些常用的选择:
option java_package = "com.example.foo";
编译器为以此作为生成的Java类的包名,如果没有该选项,则会以pb的package
作为包名。
option java_multiple_files = true;
该选项为true
时,生成的Java类将是包级别的,否则会在一个包装类中。
option optimize_for = CODE_SIZE;
该选项会对生成的类产生影响,作用是根据指定的选项对代码进行不同方面的优化。
JSON映射
PB
支持和JSON互相转换。如果一个字段不存在JSON数据中或者为null
,那么pb中会被赋为该字段的默认值,反之,如果一个字段在PB
中是默认值,那么不会写到JSON数据中以节省空间。
gRPC 官方文档中文版V1.0
Protobuf3语法详解
protobuf3 any用法
Protobuf3学习笔记