本文大部分内容来自 Language Guide (proto3)

本指南主要介绍如何使用 Protocol Buffers 语法结构化 Protocol Buffers 数据,包括 .proto 文件的语法结构以及如何生成数据访问类。涵盖 Protocol Buffers 的 proto3 版本

定义message

首先看一个简单的示例,假设定义一个搜索请求的 message 格式,每个搜索请求包含一个关键字,当前页,以及每页显示多少条(经典的分页请求输入参数)。下面是搜索请求的 .proto 示例

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 第一行声明使用的语法版本是 proto3 ,如果不增加这一行, Protocol Buffers 编译器 ( protoc ) 会以 proto2 的标准解析。 snytax 声明必须在文件的第一个非空,非注释行。
  • SearchRequest 包含了三个字段 ( 键值对 ) 。每个字段有字段类型和字段名称。

定义字段类型

上面的示例中,字段都是标量类型(scalar types),两个整型变量(page_number,result_per_page),一个字符型变量(query)。 Protocol Buffers 支持复合类型。包括枚举和已经定义的其它 Message 类型。

字段标识编号(标签)

message 定义中的每个字段都有一个唯一的编号,这些字段编号用于在 message 的二进制格式中标识字段。一旦定义的 Protocol Buffers 数据结构被使用,就不建议再修改这个编号。请注意, 1-15 范围内的字段编号占用一个字节进行编码,包括字段编号和字段类型,( 你可以在 Protocol Buffer Encoding 找到更多信息 ), 16 到 2047 范围内的字段编号占用两个字节,因此建议将最频繁使用的字段的编号设置为1-15,不过也要注意,要为以后可能添加的频繁使用的字段保留一些空间。

可以指定的最小字段编号为 1,最大为 2^29-1 ,即 536,870,911 , 字段编号不能使用 19000~19999 ( FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber )。它们是为 Protocol Buffers 的实现保留的,如果在 .proto 中使用保留的字段编号, Protocol Buffers 编译器会给出警告,同样,不能使用任何以前保留的字段编号。

定义字段规则

message 的字段,可以使用以下修饰符。

  • singular 良好格式的 message 中,只能包含 0 到 1 个此字段。这是 proto3 语法的默认字段规则。

  • repeated 良好格式的 message 中,这种字段可以重复任意多次(包括 0 次)。重复值的顺序会被保留。

在 proto3 中,标量数字类型的重复字段默认使用压缩编码。

更多 message 类型

可以在一个 .proto 文件中定义多个message,多个相关的 message 放一个.proto文件中,会非常有用。例如:搜索请求 message 与搜索结果 message 放在同一个文件中。

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}

message SearchResponse {
 ...
}

注释

Protocol Buffers 支持 C/C++ 风格的注释 : // 或者 /*....*/

/* 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.
}

保留字段

假设在日常开发过程中更新message结构,删除了一个字段,或者注释掉一个字段。在更新之后,该字段的字段标识号可以被新字段使用。此时,使用旧版本数据结构的应用会出现非常严重的后果,包括数据损坏和隐私错误等。

为了防止这种情况,在删除字段的同时,可以将删除的字段的标识号或者字段名 ( 这也可能导致 JSON 序列化问题 ) 声明为 reserved , 如果有修改尝试使用这些字段标识符,Protocol Buffers 编译器会给出警告。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

注意 不可同时将字段的字段名字段编号声明在同一个reserved语句中。即 reserved 2, 15,"foo";

.proto 文件能生成什么 ?

当使用 Protocol Buffers 编译器编译 .proto 文件时,编译器会跟据文件中定义的 message ,生成你选择的语言的代码,包括获取和设置字段值,序列化 message 输出到输出流,从输入流解析 message 。

  • C++      编译器为每个 .proto 文件生成一个 .h.cc 文件,每个 message 类型一个类。

  • Java      编译器生成一个 .java 文件,为每个 message 类型生成一个类, 还会生成用于创建 message 类实例的 Builder 类。

  • Kotlin     除了 Java 生成的代码之外,编译器还会为每种 message 类型生成一个 .kt 文件,其中包含可用于简化创建 message 实例的 DSL。

  • Python     python 有一点不一样, 编译器生成一个模块,其中包含 .proto 中每种 message 类型的静态描述符,在运行时与 metaclass 一起使用创建必要的 Python 数据访问类。

  • Go       编译器生成一个 .pb.go 文件,其中包含 .proto 文件中定义的 message 类型。

  • ruby      编译器生成一个 .rb 文件,包含一个 Ruby 模块,包含 .proto 文件中定义的 message 类型。

  • Objective-C   编译器生成一个 .rb 文件,包含一个 Ruby 模块,包含 .proto 文件中定义的 message 类型。

  • C#       编译器生成一个 .cs 文件,其中包含 .proto 文件中每种 message 类型的类。

  • Dart      编译器生成一个 .pb.dart 文件,其中包含 .proto 文件中每种 message 类型的类。

标量类型

Protocol Buffers支持以下标题类型

.proto C++ Java Python Go Ruby C# PHP
double double double float float64 Float double float
float float float float float32 Float float float
int32 int32 int int int32 Fixnum or Bignum (as required) int integer
int64 int64 long int/long int64 Bignum long integer/string
uint32 uint32 int int/long uint32 Fixnum or Bignum (as required) uint integer
uint64 uint64 long int/long uint64 Bignum ulong integer/string
sint32 int32 int int int32 Fixnum or Bignum (as required) int integer
sint64 int64 long int/long int64 Bignum long integer/string
fixed32 uint32 int int int32 Fixnum or Bignum (as required) uint integer
fixed64 uint64 long int/long uint64 Bignum ulong integer/string
sfixed32 int32 int int int32 Fixnum or Bignum (as required) int integer
sfixed64 int64 long int/long int64 Bignum long integer/string
bool bool boolean bool bool TrueClass/FalseClass bool boolean
string string String str/unicode string String (UTF-8) string string
bytes string ByteString str []byte String (ASCII-8BIT) ByteString string

默认值

在解析 message 时,当编码后的 message 不包含特定字段时,解析对象中的对应字段会被设置为默认值。默认值是针对特定类型的:

  • string      默认是空的字符串

  • byte       默认是空的bytes

  • bool       默认为false

  • numeric     默认为 0

  • enums      定义在第一位的枚举值,也就是 0

  • message     对于其它的message类型,字段不会被设置值。根据生成的不同语言有不同的表现。

对于可重复字段,会被设置为空(通常是语言中的空List)。

注意:对于标量字段,解析后无法判断 message 的标量字段是包含了默认值,还是没包含字段(解析器设置了默认值)。例如,某条 message 没包含某一个bool字段,解析器默认填充了false。另外,如果标量字段设置为其默认值,则该值将不会在线路上序列化。

同时要注意如果一个标题字段被设置了默认值,序列化时,这个字段是不会被序列化的(即二进制数据中不包含这个字段)。

枚举

在定义message时,某些字段可能只包含一些预先指定的值,比如SearchRequest中,可能只希望搜索结果类型,可能的列表为 UNIVERSAL , WEB , IMAGES , LOCAL , NEWS , VIDEO , PRODUCTS 。此时,可以在 SearchRequest 中定义一个 enum 类型的 message 包含这些类型。

在下面的例子中,添加了一个带有所有可能的值的 Cropus 的枚举,和一个类型为 Cropus 的字段。

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 值,以便可以使用 0 作为枚举默认值。

  • 0 值必须是第一个元素,以便与 proto2 语义兼容,其中第一个枚举值始终是默认值。

可以为不同的枚举常量分配相同的值,即枚举别名。要使用别名,需要将 allow_alias 选项设置为 true,否则编译器将在找到别名时报错

message MyMessage1 {
  enum EnumAllowingAlias {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
  }
}
message MyMessage2 {
  enum EnumNotAllowingAlias {
    UNKNOWN = 0;
    STARTED = 1;
    // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
  }
}

枚举常量必须在 32 位整数范围内,因为枚举值在编码时使用 varint 编码,负值效率很低,因此不推荐使用负值。可以如上所示,在 message 中定义枚举,也可以在外部定义。在外部定义的枚举可以在 .proto 文件中的任何 message 定义中重用。还可以在 message 中使用定义在另一个 message 中的枚举, 使用格式是 message 类型.枚举类型

当编译器编译使用枚举的 .proto 文件时,对于 Java , Katlin ,C++ 生成的代码将包含对应的枚举类型,对于 python 将生成特殊的 EnumDescriptor 类 ,它用于在运行时生成的类中创建一组具有整数值的符号常量。

在反序列化时,无法识别的枚举将被保留,尽管在反序列化 message 时如何表示取决于语言。

保留值

如果你删除枚举中的条目或者注释掉某一个条目,在更新之后,用户可以在修改枚举定义时重复使用这个枚举值,如果用户使用了一个旧版本的 proto 文件 ,这会造成严重的问题。确保这种情况不会发生的方法是指定保留已删除的枚举值或者枚举常量。如果用户在以后使用了这些枚举标识,编译器会给出警告。可以使用 max 关键字指定保留的枚举值范围到最大值。

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

请注意,您不能在同一保留语句中混合使用 枚举值 和 枚举常量。

使用其它 message 类型

定义 message 时,可以使用其它的 message 类型做为字段类型。例如:假设想在每一个 SearchResponse 中包含一个 Result 类型的 message,要实现这一点,可以定义一个 Result message ,并在 SearchResponse 中定义一个 Result 类型的字段即可

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}

导入定义

在上面的示例中,ResultSearchResponse 定义在同一个文件中,如果想引用的类型定义在其它的 .proto 文件中呢?

可以 导入 机制来引用其它 .proto 文件中的定义。要引用其它 .proto 文件中的定义,需要在文件的头部添加 import 描述。

import "myproject/other_protos.proto";

默认情况下,只能直接引用导入的 .proto 文件中的定义,但是,有时 .proto 文件可能被移动到其它位置,此时可以在旧的位置创建一个占位的 .proto文件,使用 import public 机制将引用转发到新位置。

public import 在 Java 中不生效

任何导入包含 import public 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";
// You use definitions from old.proto and new.proto, but not other.proto

编译器在一组由编译器命令行参数 -I/--proto_path 指定的目录中搜索导入文件,如果此参数没有指定,则在编译器命令被调用的目录下搜索,通常,--proto_path 参数应该指定为项目的根目录,所有的导入应使用相对完全限定名。

使用 proto2 消息类型

可以在 proto3 message 中引用 proto2 message ,反之亦然。但是 proto2 枚举不能直接在 proto3 中使用。

嵌入类型

message 中可以定义 message,在其它消息中,下面的示例中,Result message 定义在 SearchResponse message 中。

message SearchResponse {
  message Result {
    string url = 1;
    string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

如果想在其它的消息中引用这个内部的消息,可以使用 父类型.子类型 引用

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;
    }
  }
}

更新消息

如果一个 message 不满足当前的需要 - 例如,需要增加一个字段 但是又想使用之前格式的代码。更新 message 而不破坏现有的代码非常简单,只需要遵循以下几点:

  • 不要更改任何现有字段的字段编号。

  • 如果添加了字段,使用旧格式序列化的消息可以被新生成的代码解析。需要注意的是新增字段的默认值,以便新代码可以与旧代码生成的消息正确交互。同样的,新代码创建的消息可以被旧代码解析,旧代码解析时忽略新的字段。

  • 字段可以被删除,只要在更新 message 定义时,不使用被删除的字段编号就可以了。可以重命名字段,或者将字段编号设置为 reserved,以防止修改 .proto 文件时使用这个编号。

  • int32 , uint32 , int64 , uint64bool 是兼容的,这意味着可以向后兼容的把一种类型更新成另一种类型,如果使用不同的类型解析数字,将像 C++ 类型转换一样处理(例如,如果将 64 位数字作为 int32 读取,它将被截断为 32 位)。

  • sint32sint64 相互兼容,但与其他整数类型不兼容。

  • 只要是有效的 UTF-8 字节,则stringbytes 兼容。

  • 如果 bytes 包含 message 的编码版本,则内嵌的 message 与 bytes 兼容。

  • fixed32sfixed32 兼容, fixed64sfixed64 兼容。

  • 对于 string bytes 和 message 字段, optionalrepeated 是兼容的,把 repeated 字段序列化的数据做为 input 输入给期望一个 optional 的客户端,如果是原始类型字段,则将使用最后一个值,如果是 message 类型的字段,则合并所有元素。注意:对于数字类型,包括 bool 和枚举,这通常是不安全的。数字类型的 repeated 字段以 packed 格式进行序列化,当需要 optional 字段时将无法正确解析。

  • enumint32uint32int64uint64 兼容(注意,如果它们不匹配,值将被截断)。但是,在反序列化消息时,客户端代码可能会以不同方式处理它们:例如,无法识别的 proto3 枚举类型将保留在消息中,但在反序列化消息时如何表示它,这是与语言相关的。 int 字段总是保留它们的值。

  • 将单个值更新为 oneof 的成员是安全的,并且二进制兼容的。如果能保证没有代码一次设置多个字段值,则把多个字段移动到新的 oneof 是安全的。但是将任何字段移动到现有的 oneof 是非安全的

未知字段

未知字段是指解析器无法识别的格式良好的 Protocol Buffer 序列化数据。例如,旧的程序解析带有新字段的新格式的数据时,这些新字段对旧程序而言就是未知字段。

最初,proto3 在解析过程中采用丢弃未知字段的策略,但在 3.5 版本中为了匹配 proto2 的行为,未知字段被保留了下来,在3.5及以后的版本中,未知字段在解析过程中被保留并饮食在序列化输出中。

Any

Any 消息类型允许将消息做为嵌入类型而无需 proto定义。Anybytes 封装任意序列化 message 和一个URL,它作为全局唯一标识符来解析消息的类型。为了使用 Any 类型的 message ,需要 import google/protobuf/any.proto

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

Any 消息类型的默认 URL 是 type.googleapis.com/_packagename_._messagename_

不同的语言实现都会支持运行库以类型安全的方式打包和解包任何值。例如在 Java 语言中,Any 类型有专门的 pack()unpack() 访问函数,在 C++ 中对应的是 PackFrom()PackTo() 方法。

// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);

// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
  if (detail.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

目前,用于处理 Any 类型的运行时库正在开发中。

如果熟悉 proto2 语法 ,Any 类型就是替代了 proto2 中的 extensions

Oneof

如果一个消息有多个字段,但是同一时间只有一个字段会被设置,例如聊天的内容包含 消息类型,图片消息的消息体,文本消息的消息体,语音消息的消息体。为了节约存储,可以使用Oneof特征来约束字段。

Oneof 字段类似于普通字段,除了oneof共享内存中的所有字段,并且最多可以同时设置一个字段。设置一个 Oneof 字段成员将清除其它的字段成员。可以使用 case() 或者 WhichOneof() 方法检查哪个字段被设置了值。使用哪个方法取决于语言实现。

使用 Oneof

使用oneof关键字来在.proto中定义Oneof字段, 后面跟 oneof 名字, 在下面的示例中是 test_oneof

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

Oneof 字段中可以添加任意类型的字段, 但是不能使用 repeated 字段和 map.

在生成的代码中,Oneof 字段和普通字段有 getter 和 setter 方法。同时还有一个特征的方法用于检查 Oneof 中哪个值设置了值。详细的检查方法 请参阅你选择的语言的API。

Oneof 的特征

  • 设置一个 Oneof 字段,会清除其它已设置的字段。如果设置了多个 oneof 字段,则只有最后一个设置的字段有值
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());
  • 解析消息时,如果解析到同一个Oneof字段的多个成员。只会保留最后一个。

  • oneof 不能是被修饰为重复字段(repeated)

  • Reflection APIs work for oneof fields.(反射API可以做用于oneof字段)

  • 如果将一个 oneof 字段设置为默认值(例如给 int32 的 oneof 字段设置为 0 ),则此字段将被设置为 case,并被序列化。

  • 如果使用c++,请确保代码不会引起内存 crash ,下面的代码会crash,因为设置 sub_messager 的时候,set_name 被删除

SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // Will delete sub_message
sub_message->set_...            // Crashes here
  • 同样是C++。如果通过调用 Swap() 来交换两个带有oneof的消息,每个消息将会有另外一个消息的oneof。

在下面这个案例中, msg1将会有sub_message和msg2会有name.

SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());

向后兼容问题

添加或者删除 Oneof 字段时要慎重。如果检查 Oneof 的值返回 None/NOT_SET ,这表示 oneof 字段没有被设置,或者设置了一个被删除的字段,无法区分是哪种情况。

标签重用问题

  • 将字段移入或移出 oneof : 消息序列化和反序列化后可能丢失某些字段。但是可以安全的将单个字段移动至新的 Oneof 字段,并且 如果知道只设置了一个字段,则可以移动多个字段。
  • 删除 oneof 字段并将其添加回来 : 可能会在消息序列化和解析后清除当前设置的字段
  • 拆分或合并 Oneof : 与移动常规字段有类似的问题。

Maps

如果消息中要包含一个Map, Protocol Buffers 提供了快捷语法:

map<key_type, value_type> map_field = N;

其中key_type可以是任何整数或字符串类型( 也就是除了 浮点数 及 bytes 之外的任意标量 ),枚举不可以做为key_type。value_type可以是除了另一个map之外的任意类型。

所以,例如如果想定义一个 project 的 map ,每个 project 都关联一个 string 类型的 key , 可以如下定义:

map<string, Project> projects = 3;
  • map类型字段不能重复

  • map中的数据是无序的

  • 生成文本格式时,Map按照key排序,数字类型的 key 按照数字顺序排序

  • 在解析时,重复的key只有最后一个生效。从文本格式解析时,存在重复的key会解析失败。

  • 当value为空时,序列化行为跟据语言有所不同。在C++,java,Python中,值为Value类型的默认值,其它语言则不被序列化。

目前Proto3支持的语言都可以生成 map API

向后兼容

map 语法等效于以下内容,因此不支持 map 的 Protocol Buffers 实现仍然可以处理您的数据:

message MapFieldEntry {
  key_type key = 1;
  value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支持 map 的 Protocol Buffers 实现都必须可以生成和接受上述定义的数据。

Packages

proto 文件中可以添加可选的 package 说明,以防止防止多个模块的 message 产生命名冲突。

package foo.bar;
message Open { ... }

其它包的 message 引用此类型时,需要指定 package

message Foo {
  ...
  foo.bar.Open open = 1;
  ...
}

package 定义影响生成代码

  • C++      生成的类被被封装进命名空间,上面的示例中,Open 会被封装进 foo::bar

  • Java/kotlin   除非显示的添加了option java_package,否则 package 被用于生成 Java 的 package

  • Python     package 指令被忽略,

  • Go      除非显示的添加了option go_package,否则package被用于生成Java的package。

  • ruby     生成的类被包装在嵌套的Ruby 命名空间中,转换为 Ruby capitalization 风格(第一个字符大写;如果第一个字符不是字母则加一个PB_前缀)

  • C#      除非显示的添加了option csharp_namespace,否则 package 在转换为 PascalCase 后用作命名空间。

包和命名解析

Protocol Buffers 的类型命名解析类似 C++: 首先搜索最内层的范围, 然后最内层的外一层, 依此类推。每个包都被认为是父包的“内部”包,.前缀( 例如 .foo.bar.Baz )表示从最外层开始。

Protocol Buffers 编译器通过解析导入的 .proto 文件来解析所有的类型命名。特定语言的代码生成器语言中如何引用这些类型,即使语言有不同的作用范围。

定义服务

如果定义的消息用于RPC(远程过程调用)系统,可以在 .proto 文件中定义 RPC service。编译器会为指定语言生成服务接口代码和 stubs ,例如,定义一个 RPC 服务,接收 SearchRequest,返回 SearchResponse ,可以如下定义:

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

最直接使用 Protocol Buffer 的RPC系统是gRPC:google开源的与平台无关,与语言无关的RPC系统。gRPC允许使用编译器插件通过 .proto 文件直接生成RPC相关代码。

即使不使用gRPC,也可以在自己实现的RPC系统中使用 Protocol Buffer

还有许多正在进行的第三方项目为 Protocol Buffer 开发 RPC 实现。请参阅 third-party add-ons wiki page.

JSON映射

Proto3支持JSON格式的标准编码,这使系统之间分享数据变得容易。

如果在JSON编码的字段存在缺失或者值为null。Protocol Buffer 在解析时会将其设置为对应的默认值。如果一个字段的值是protocol buffer的默认值,在默认情况下,这个字段不会出现在json编码的数据。

JSON的类型映射,参见 Language Guide (proto3)

JSON选项

这里貌似是说一个 proto3 的JSON实现应该具有哪些选项。

proto3 的JSON实现应该提供以下选项:

  • Emit fields with default values

proto3 的JSON输出默认值字段被省略。proto3 的JSON实现应该提供一个选项,用于覆盖此行为,输出默认值的字段。

  • Ignore unknown fields

解析器默认拒绝未知字段,但是,应该提供一个选项,用于忽略未知字段。

  • Use proto field name instead of lowerCamelCase name

默认情况下, proto3 JSON打印工具应该将字段名转换成小写作为JSON字段名称。 proto3 的JSON实现应该提供一个选项,用于使用 Proto 字段名做为 JSON 字段名。解析器应该可以接受转换后的小写字符的字段名和proto的字段名。

  • Emit enum values as integers instead of strings

JSON输出中默认使用枚举的值, proto3 的JSON实现应该提供一个选项,用于把枚举的输出替换成枚举的数字形式。

选项

.proto文件中可以添加选项注释。选项不会改变声明的含义,但会影响上下文的处理方式。可用选项的完整列表在/protobuf/description.proto中定义。

选项分为文件级选项,应该写在文件的最顶级域,而不是在message、enum或service定义中,消息级选项,写在消息定义中,字段级选项(写在字段定义中)。选项还可以写在枚举类型、枚举值、服务类型和服务方法上。但是目前还没有任何有用的这类选项。

在此列举一些常用的选项:

java_package

文件级选项,用于指定生成的 Java / Kotlin 类的包名。如果不指定 `` ,则使用 proto 的 package。当然,proto 的 package 不是一个好的 Java package 。因为 proto 的 package 不会以反向域名开头。如果不生成 Java 或 Kotlin 代码,则此选项无效。

option java_package = "com.example.foo";

java_outer_classname

文件级选项,指定要生成的最外层Java类的类名,如果在.proto文件中没有显式的指定java_outer_classname,那么编译器把.proto文件名转换成驼峰式的名称做为类名( foo_bar.proto 生成 FooBar.java )。如果 java_multiple_files 选项被禁用,则所有其它的 class/enum 等做为 .proto 文件生成的 Java 类的内部类/枚举等。 如果不生成 Java 或 Kotlin 代码,则此选项无效。

option java_outer_classname = "Ponycopter";

java_multiple_files

文件级选项,如果为 false,则只会为此 .proto 文件生成一个 .java 文件,所有 message / enum / service 都做为这个类的内部类生成。如果为 true ,将为每个 message / enum / service 生成单独的 .java 文件。并为此 .proto 文件生成的包装器 Java 类。这是一个布尔选项,默认为 false 。 如果不生成 Java 代码,则此选项无效。

option java_multiple_files = true;

optimize_for

文件级选项,可以设置为SPEEDCODE_SIZELITE_RUNTIME。影响Java与C++代码生成。

SPEED 默认值,速度优先。编译器将生成高度优化的消息序列化、解析和执行其他常见操作的代码。

CODE_SIZE 代码大小优先。编译器会生成最少的类, 会依赖共享的, 基于反射的代码来实现序列化, 解析和其他操作。

LITE_RUNTIME 编译器会生成依赖"lite"运行时类库(用libprotobuf-lite 替代 libprotobuf)的类。lite运行时比完整的库小得多(小一个数量级)但是省略了某些特性,比如描述符和反射。这对于在手机等受限平台上运行的应用程序尤其有用。在此模式下,编译器依然会尽量像SPEED模式那样生成尽量优化的代码。

option optimize_for = CODE_SIZE;

cc_enable_arenas

文件级选项,为 C++ 代码开户 arena allocation

objc_class_prefix

文件级选项,设置 Objective-C 类前缀, 该前缀附加到此 .proto 中所有 Objective-C 生成的类和枚举。没有默认值。

deprecated

字段级选项,如果设置为 true,则表示该字段已废弃,不应该由新代码生成,在大多数语言中,并没有什么实际的效果。在 Java 中,会生成 @Deprecated 注解,在其它语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来会导致编译器在编译时对使用该字段的代码发出警告。如果该字段未被任何人使用并且您希望阻止新用户使用它,请考虑使用 reserved 替换该字段声明。

int32 old_field = 6 [deprecated = true];

自定义选项

Protocol Buffers 还允许定义和使用自定义选项。这是一个高级功能,大多数人不需要的。如果确实认为需要创建自定义,请参考 Proto2 Language Guide。请注意,创建自定义选项使用了 extensions,仅允许用于 proto3 中的自定义选项。

生成代码

.proto 文件中定义的 message 生成 Java , Kotlin , Python , C++ , Go , Ruby , Objective-C , 或者 C# 代码,需要使用 Protocol Buffer 编译器 protoc,如果没有安装,请 下载 该软件,并按照README中的说明进行操作。 对于 Go 语言,还需要为编译器安装一个特殊的代码生成器插件,可以在 GitHub 的 golang/protobuf 中找到它的安装手册。

protoc 的调用方式如下:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

IMPORT_PATH 指定在解析 import 指令时查找 .proto 文件的目录。如果省略,则使用当前目录。可以通过多次传递 --proto_path 选项来指定多个导入目录;编译器将按顺序搜索它们。-I=_IMPORT_PATH_ 可以做为 --proto_path 的简写。

为了方便起见,如果 DST_DIR 以 .zip 或者 .jar 结尾,编译器会把输出写入至单个 zip 格式的文件。 .jar 的输出则会跟据 Java JAR 规范的要求提供清单文件。请注意,如果输出的文件已存在,则会被覆盖。编译器不够聪明,无法将文件添加到现有文件。

  • 必须提供一个或者多个 .proto 文件做为输入,可以一次指定多个 .proto 文件。尽量文件是相对当前目录命令的,但是每个文件都必须驻留在 IMPORT_PATH 之一中,以便编译器可以确定其规范名称。