默认情况下,gRPC 使用 Protocol Buffers 服务定义语言(IDL)来描述服务接口和请求响应消息的结构。
前言
Google 在其开发者网站提供了关于 Protocol Buffers 的详细介绍,当然由于国内的原因我们往往无法访问。下面是根据官方文档整理的 proto3 版本的 Protocol Buffers 使用指南。本指南主要介绍如何通过 Protocol Buffers 语法去描述我们的服务接口和请求响应对象,以及 .proto 文件的语法和如何通过 .proto 文件生成对应语言的代码。
这里主要以 java 语言作为开发语言作为示例语言,带来不便,还请见谅。
其他教程参见:
正文
我们先来看一个简单的 Message 定义。这里,我们假设要定义一个用于搜索的请求报文 SearchRequest
,该报文包含一个查询字符串 query
,指定的查询页 page_number
,和每一页的条目数 result_per_page
。下面是对应的 .proto
文件:
1
2
3
4
5
6
7
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 第一行我们声明了使用哪个版本的 Protocol Buffers 语法,默认的情况下是使用
proto2
的语法。该声明必须位于文件非空行和非注释行的第一行。 SearchRequest
消息定义三个字段(键值对)。每个字段对应要包含在此类消息中的数据; 每个字段都有一个名称,类型和唯一的序号。
指定消息字段类型
在上面的例子中我们指定了两个 Integer 字段 page_number
和 result_per_page
和一个 String 类型的字段 query
。Protocol Buffers 允许消息为任意的标准字段类型,或者是枚举类型或其他自定义的数据类型。
分配消息字段编号
从例子中可以看到,每一个字段都指定了一个唯一的编号。这个序号用于指定在二进制编码后消息的类型(可以理解为将 5 个字节的 query
替换成了一个字节的 1
),并且在使用了消息类型后不应该更改该编号。注意:编号 [1, 15] 在编码过程中仅使用 1 个字节编码,编号 [16, 2047] 会占用两个字节。所以,我们应该将频繁使用的字段的编号放到 [1, 15] 区间内,并且注意为以后会新增的频繁使用的字段预留空间。
编号的范围在 [1, 536870911] 区间,其中 [19000, 19999] 区间的编号作为协议的保留编号不建议使用。如果我们使用了该部分的编号,在编译 proto 文件的时候编译器会发出警告。
指定消息字段规则
消息字段有下面两种类型:
- 单数形式:proto3 默认的字段规则为单数形式,格式正确的消息中可以包含一个或领个该字段。
- 复数形式:通过
repeated
关键字可以将自定为复数形式的字段,格式正确的消息中可以包含零个或无数个该字段,并且这些会保留这些字段的添加顺序。
在 proto3 中,标准类型的复数形式字段默认使用压缩编码。
添加更多的消息
我们可以将多个 Message 的定义写到同一个 proto 文件中,比如说我们需要定义一些互相关联的消息的时候。下面的例子中我们在文件中添加了一个关于 SearchRequest
的响应消息 SearchResponse
:
1
2
3
4
5
6
7
8
9
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
添加注释
Protocol Buffers 使用 C/C++ 风格的注释://
或 /* .... */
。
1
2
3
4
5
6
7
8
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}
保留字段
如果我们在更新 proto 文件的时候通过删除不要的字段或注释掉不要的字段来更新,那么未来的用户可能会在同样的编号或字段名称下定义新的内容。这可能导致不同版本的 proto 文件中具有相同编号或字段名但是意义不一样的字段,这可能会带来数据损坏或安全性方面的严重问题。避免这种情况的方法是指定已删除的字段的编号为保留字段编号(也可以指定字段名称为保留名称,但可能引起 JSON 序列化问题),指定之后,未来的用户在这使用这些字段的时候,编译器会进行告警。
1
2
3
4
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意:我们可以在同一个 reserved
语句中既指定编号,又指定字段名称。
.proto 文件可以生成什么代码?
当我们编译 .proto 文件的时候,编译器会根据我们选择的语言生成对应的源代码。当我们需要使用我们定义的消息的时候,我们可以通过调用源代码中的 getter/setter 方法进行取值和赋值。并且编译出的代码会负责将消息对象序列化为 OutputStream
或从 InputStream
中解析出对应的消息对象。
- 对于 C++ 语言,编译器将
.proto
文件编译成一个.cc
文件和一个.h
文件,.proto
文件中的每一个消息都使用一个 class 进行描述。 - 对于 Java 语言,编译器将每一个消息编译成一个
.java
文件,并且通过构建器模式Builder
对消息进行赋值和构造。 - Python 有点不同 - Python 编译器生成一个模块,其中包含
.proto
中每种消息类型的静态描述符,然后与元类一起使用,以在运行时创建必要的 Python 数据访问类。 - 对于 Go ,编译器会生成一个
.pb.go
文件,其中包含文件中每种消息类型的类型。 - 对于 Ruby,编译器生成一个带有包含消息类型的 Ruby 模块的 .rb 文件。
- 对于 Objective-C,编译器从每个
.proto
生成一个pbobjc.h
和pbobjc.m
文件,其中包含文件中描述的每种消息类型的类。 - 对于 C#,编译器从每个
.proto
生成一个.cs
文件,其中包含文件中描述的每种消息类型的类。 - 对于 Dart,编译器会生成一个
.pb.dart
文件,其中包含文件中每种消息类型的类。
官方的 API reference 包含了各种语言 API 的更详细的介绍。