hfrommane 2019-08-16
首先让我们看一个非常简单的例子. 假设要定义搜索请求消息格式, 其中每个搜索请求都有一个查询字符串, 您感兴趣的特定结果页面以及每页的一些结果. 这是用于定义消息类型的 .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 保留的.
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
如果要在数据定义中创建关联映射, 协议缓冲区提供了一种方便的快捷方式语法:
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; ... }
// 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 <T extends com.google.protobuf.Message> Any pack(T message)
方法.
在 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...