定义消息类型
首先让我们看一个非常简单的例子. 假设要定义搜索请求消息格式, 其中每个搜索请求都有一个查询字符串, 您感兴趣的特定结果页面以及每页的一些结果. 这是用于定义消息类型的 .proto
文件.
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
注意以下几点:
- 该文件的第一行指定使用
proto3
语法; 如果不写则使用proto2
. - 一个消息对应一个字段, 一个字段对应一个数据类型.
值得注意的是: 字段类型可以是枚举或其他数据类型.
字段编号
比如 string query = 1;
字段, 1
就是字段编号(unique number).
字段编号的范围为 1 到 536,870,911. 不能使用数字 19000 到 19999, 因为它们是为 Google Protobuf 保留的.
在同一个 .proto 文件中添加多个消息类型
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
保留字段
当定义好字段后, 在后续开发中发现某个字段根本没用.
例如 string userName = 2;
字段, 这个时候最好不要进行注释或删除.
有可能以后加载相同的旧版本, 这可能会导致数据损坏, 隐私错误等. 确保不会发生这种情况的一种方法是指定要删除的字段为保留字段.
message SubscribeReq {
reserved 2;
int32 subReqID = 1;
string userName = 2;
string productName = 3;
string address = 4;
}
顾名思义, 就是此字段会被保留可能在以后会使用此字段. 使用关键字 reserved
表示要保留字段编号为 2
.
上面代码我们在生成 Java 文件的时候会出现 ubscribeReqPeoro.proto: Field "userName" uses reserved number 2
错误信息, 所以需要将 string userName = 2;
注释, 或者删除.
值得注意的是: 您不能在同一
reserved
语句中混合字段名称和字段编号. 下面是表准格式:message Foo { reserved 2, 15, 9 to 11; reserved "foo", "bar"; }
指定字段规则
所指定的消息字段修饰符必须是如下之一:
-
singular
: 一个格式良好的消息应该有0个或者1个这种字段 (但是不能超过1个). -
repeated
: 一个格式良好的消息中, 这种字段可以重复任意多次 (包括 0 次). 重复值的顺序会被保留.
枚举
当需要定义一个消息类型的时候, 可能想为一个字段指定某 “预定义值序列” 中的一个值.
例如, 假设要为每一个 SearchRequest 消息添加一个 corpus 字段, 而 corpus 的值可能是 UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS 或 VIDEO 中的一个.
其实可以很容易地实现这一点: 通过向消息定义中添加一个枚举 (enum) 并且为每个可能的值定义一个常量就可以了.
在下面的例子中, 在消息格式中添加了一个叫做 Corpus 的枚举类型——它含有所有可能的值——以及一个类型为 Corpus 的字段:
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;
}
值得注意的是:
每个枚举类型必须将其第一个类型映射为 0, 这是因为:
- 必须有有一个 0 值, 我们可以用这个 0 值作为默认值.
- 这个零值必须为第一个元素, 为了兼容 proto2 语义, 枚举类的第一个值总是默认值.
另外, 枚举常量必须在 32 位整数范围内, 并且尽量不要使用负数.
你可以通过将不同的枚举常量指定为相同的值. 如果这样做你需要将 allow_alias
设置为 true
.
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
其它消息类型
你可以将其他消息类型用作字段类型. 例如, 假设在每一个 SearchResponse
消息中包含 Result
消息, 此时可以在相同的 .proto
文件中定义一个 Result
消息类型, 然后在 SearchResponse
消息中指定一个 Result
类型的字段, 如:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
嵌套类型
你可以在其他消息类型中定义、使用消息类型, 在下面的例子中, Result
消息就定义在 SearchResponse
消息内, 如:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果你想在它的父消息类型的外部重用这个消息类型, 你需要以 Parent.Type
的形式使用它, 如:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
当然, 你也可以将消息嵌套任意多层, 如:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
导入定义
在上面的示例中, Result
和 SearchResponse
消息类型在同一文件中的, 如果要用作字段类型的消息类型在另一个 .proto
文件中定义, 该怎么办?
可以 .proto
通过导入来使用其他文件中的定义. 需要在文件顶部添加 import
语句:
import "myproject/other_protos.proto";
默认情况下, 使用直接导入的 .proto
文件中的定义.
但是, 有时可能需要将 .proto
文件移动到新位置. 如果直接移动文件位置可能要修改许多代码位置.
我们可以使用 import public
来解决这个问题.
实例代码:
// new.proto
// All definitions are moved here
// old.proto
//This is the proto that all clients are importing.
import public“new.proto”;
import“other.proto”;
// client.proto
import "old.proto";
//您使用old.proto和new.proto中的定义,但不使用other.proto
Maps
如果要在数据定义中创建关联映射, 协议缓冲区提供了一种方便的快捷方式语法:
map < key_type ,value_type > map_field = N ;
key_type
可以是任何整数或字符串类型.value_type
可以是除了 map
的其他类型.
例如, 如果要创建项目映射, 其中每条 Project
消息都与字符串键相关联, 则可以像下面这样定义它:
map < string,Project > projects = 3;
值得注意的是:map
不能使用repeated
.
包
您可以向 .proto
文件添加 package
可选说明符, 以防止协议消息类型之间的名称冲突.
package foo.bar;
message Open { ... }
然后, 您可以在定义消息类型的字段时使用包说明符:
message Foo {
...
foo.bar.Open open = 1;
...
}
Any (任意类型)
// new.proto
syntax = "proto3";
import "google/protobuf/any.proto";
message SearchRequest {
string query = 1;
int32 page_number = 2;
repeated google.protobuf.Any result_per_page = 3;
}
编译的时候需要将 google/protobuf/any.proto
和 new.proto
文件放到一起. 要注意目录结构.
然后执行下面命令进行编译:
sudo ./protoc -I=/home/sc-ik/桌面/ --java_out=./java /home/sc-ik/桌面/*.proto
在对 result_per_page
进行赋值时, 需要用到 Any
类中的 public static
方法.
在 java 代码中, 先创建 SearchRequest
对象, 然后对其他两个属性进行赋值.
New.SearchRequest.Builder searchRequest = New.SearchRequest.newBuilder();
searchRequest.setQuery("test");
searchRequest.setPageNumber(10086);
在对 result_per_page
进行赋值时, 需要注意: pack
方法的参数类型为 com.google.protobuf.Message
接口.
生成的 java 代码中, 类是继承自 GeneratedMessageV3
.
public static final class SearchRequest extends
com.google.protobuf.GeneratedMessageV3 implements
// @@protoc_insertion_point(message_implements:SearchRequest)
SearchRequestOrBuilder {
private static final long serialVersionUID = 0L;
但是如果将result_per_page
赋值为 SearchRequest
, 应该怎么操作呢?
重点就是继承的 GeneratedMessageV3
抽象类, 这个抽象类又继承了 AbstractMessage
抽象类, 而 AbstractMessage
抽象类就是 com.google.protobuf.Message
接口的实现.
public abstract class AbstractMessage
// TODO(dweis): Update GeneratedMessage to parameterize with MessageType and BuilderType.
extends AbstractMessageLite implements Message {
所以我们可以使用以下方法进行赋值.
searchRequest.addResultPerPage(
com.google.protobuf.Any.pack(
searchRequest.build()
)
);
pack()
方法我个人理解为序列化, 那么和他对应的是反序列化 unpack()
.
// 将接收到的字节数组饭序列化.
New.SearchRequest.Builder builder2 = New.SearchRequest.newBuilder().mergeFrom(bytes1);
// 获取到 result_per_page 字段的值.
Any.Builder resultPerPageBuilder = builder2.getResultPerPageBuilder(0);
// 然后使用 unpack 将任意类型 进行反序列化, 得到想要的数据.
New.SearchRequest unpack1 = resultPerPageBuilder.build().unpack(New.SearchRequest.class);
编译
编译命令:
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/*.proto
如下:
sudo ./protoc -I=/home/sc-ik/桌面/ --java_out=./java /home/sc-ik/桌面/*.proto
各种语言的文件参数以及 API
https://developers.google.com...