Protobuf 语言指南(proto3)

定义消息类型

首先让我们看一个非常简单的例子. 假设要定义搜索请求消息格式, 其中每个搜索请求都有一个查询字符串, 您感兴趣的特定结果页面以及每页的一些结果. 这是用于定义消息类型的 .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;
    }
  }
}

导入定义

在上面的示例中, ResultSearchResponse 消息类型在同一文件中的, 如果要用作字段类型的消息类型在另一个 .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.protonew.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...

相关推荐