Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data.
— https://developers.google.com/protocol-buffers

本指南讲解了如何使用 Protocol Buffer 语言来构造你的 Protocol Buffer 数据,包含 .proto 文件语法以及如何从你的 .proto 文件生成数据访问类。

这是一个参考指南 —— 关于一个使用本文档中介绍的许多特性的引导示例,参见你所选语言的对应 教程

定义一个消息类型

首先我们看一个非常简单的示例。 假如说你想定义一个搜索请求格式,每个搜索请求中包含一个 查询字符串 ,搜索结果你感兴趣的 页码 ,以及 每页结果数 。 那么你可以使用下面这个 .proto 文件来定义你的消息类型。

syntax = "proto3"

message SearchRequest {
	string query = 1;
	int32 page_number = 2;
	int32 result_per_page = 3;
}
  • 文件中的第一行指定了你将使用 proto3 语法:如果你不添加这一行那么 Protocol Buffer 编译器将假设你要使用 proto2。 而且必须是文件中第一个非空行并且非注释的行。

  • SearchRequest 消息定义指定了三个字段(名/值对),每一个字段表示你想要包含在消息中数据的一部分。 每个字段都有名称和类型。

指定字段类型

在上面的示例中,每个字段都是 标量类型:两个整型( page_numberresult_per_page )和一个字符串( query )。 但你也可以为你的字段指定复合类型,包括枚举和其他消息类型。

分配字段编号

正如你所看到的,消息定义中的每个字段都有一个 唯一编号 。 这些字段编号用来在 消息二进制格式 中标识你的字段,并且当你的消息类型使用之后就不应再更改它们了。 需要注意的是在范围 1 到 15 的字段编号编码时消耗 1 字节,其中包括字段编号和字段类型(你可以在 Protocol Buffer 编码 中得到更多相关信息) 范围 16 到 2047 的字段号占用两个字节。 所以你应该为非常频繁出现的字段保留 1 到 15 的字段编号。 并且还要记得为未来可能频繁出现的元素留出一些空间。

可以使用的最小字段编号是 1,最大是 229 - 1,或 5,3687,0911。 另外你也不能使用 19000 到 19999 的编号( FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber ),因为他们是为 Protocol Buffers 实现保留的 —— 如果你在 .proto 文件中使用了其中任意编号编译器将报错。 同样,你也不能使用之前 保留 的字段编号。

指定字段规则

消息字段可以是以下两种之一:

  • 单一:一个符合语法的消息可以包含零个或一个此类字段(但是不能超过一个)。 并且这是 proto3 默认的语法规则。

  • 重复( repeated ):这种字段在一个符合语法的消息中可以重复任意次(包括零次)。 重复值的顺序将会被保留。

在 proto3 中,标量数字类型的重复( repeated )字段默认使用 packed 编码。 你可以在 Protocol Buffer Encoding 找到更多 packed 编码的信息。

添加更多消息类型

多个消息类型可以定义在单个 .proto 文件中。 如果你定义多个相关消息的话这非常有用 —— 比如,如果你想要定义 SearchResponse 消息类型对应的响应消息格式,你可以将其添加到同一个 .proto 文件中:

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

message SearchResponse {
 ...
}

添加注释

要在你的 .proto 文件中添加注释,可以使用 C/C++ 风格的 ///**/ 语法。

/* SearchRequest represents a search query, with pagination optionsto
 * indicate wich 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 文件的旧版本,会导致非常严重的问题,包括数据损坏,隐私问题等等。 确保这种情况不会发生的一种方法是将你删掉的字段编号(和/或字段名,这会导致 JSON 序列化出现问题)指定为保留( reserved )字段。 如果未来的任何用户尝试使用此字段标识,Protocol Buffer 编译器将会报错。

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

注意你不能在同一个 reserved 语句中混用字段名称和字段编号。

你的 .proto 文件生成了什么

当你使用 Protocol Buffer 编译器 编译 .proto 文件时, 编译器将根据你选择的语言生成代码,你要使用文件中描述的消息类型,包括获取和设置字段的值、将消息序列化为输出流,以及从输入流中解析消息。

  • 对于 C++ ,编译器从每个 .proto 文件生成 .h.cc 文件,并为文件中定义的每个消息类型提供一个类。

  • 对于 Java ,编译器生成一个 .java 文件,其中包含每个消息类型的类,以及一个用来创建消息类实例的特殊 Builder 类。

  • 对于 Kotlin ,除了生成的 Java 代码,编译器为每个消息类型都生成了包含用来简化消息实例创建 DSL 的 .kt 文件。

  • 对于 Python 略有不同 —— Python 编译器会生成一个模块,其中包含 .proto 文件中每种消息类型的静态描述,这些描述将和元类一起在运行时创建所需的数据访问类。

  • 对于 Go ,编译器会生成一个 .pb.go 文件,其中包含文件中每种消息的类型。

  • 对于 Ruby ,编译器会生成一个 .rb 文件,其中包含一个含有你所定义消息类型的模块。

  • 对于 Objective-C ,编译器为每个 .proto 文件生成一个 pbobjc.hpbobjc.m 文件,并为你文件中描述的每种消息类型提供一个类。

  • 对于 C# ,编译器为每个 .proto 文件生成一个 .cs 文件,为文件中描述的每种消息类型提供一个类。

  • 对于 Dart ,编译器会生成一个 .pb.dart 文件,其中包含你所定义的每个消息类型的类。

你可以按照你所选语言的教程(proto3 版本即将推出)了解有关使用每种语言的API的更多信息。 有关 API 的更多详细信息,请参阅相关 API 参考文献 (同样 proto3 版本即将推出)。

标量数据类型

一个标量消息字段可以又有下列类型之一 —— 下表现实了 .proto 文件中指定的类型,以及生成类中对应的类型:

.proto Type Notes C++ Type Java/Kotlin Type [1] Python Type[3] Go Type Ruby Type C# Type PHP Type Dart Type

double

double

double

float

float64

Float

double

float

double

float

float

float

float

float32

Float

float

float

double

int32

Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.

int32

int

int

int32

Fixnum or Bignum (as required)

int

integer

int

int64

Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.

int64

long

int/long[4]

int64

Bignum

long

integer/string[6]

Int64

uint32

Uses variable-length encoding.

uint32

int[2]

int/long[4]

uint32

Fixnum or Bignum (as required)

uint

integer

int

uint64

Uses variable-length encoding.

uint64

long[2]

int/long[4]

uint64

Bignum

ulong

integer/string[6]

Int64

sint32

Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.

int32

int

int

int32

Fixnum or Bignum (as required)

int

integer

int

sint64

Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.

int64

long

int/long[4]

int64

Bignum

long

integer/string[6]

Int64

fixed32

Always four bytes. More efficient than uint32 if values are often greater than 228.

uint32

int[2]

int/long[4]

uint32

Fixnum or Bignum (as required)

uint

integer

int

fixed64

Always eight bytes. More efficient than uint64 if values are often greater than 256.

uint64

long[2]

int/long[4]

uint64

Bignum

ulong

integer/string[6]

Int64

sfixed32

Always four bytes.

int32

int

int

int32

Fixnum or Bignum (as required)

int

integer

int

sfixed64

Always eight bytes.

int64

long

int/long[4]

int64

Bignum

long

integer/string[6]

Int64

bool

bool

boolean

bool

bool

TrueClass/FalseClass

bool

boolean

bool

string

A string must always contain UTF-8 encoded or 7-bit ASCII text, and cannot be longer than 232.

string

String

str/unicode[5]

string

String (UTF-8)

string

string

String

bytes

May contain any arbitrary sequence of bytes no longer than 232.

string

ByteString

str (Python 2)bytes (Python 3)

[]byte

String (ASCII-8BIT)

ByteString

string

List

当你使用 Protocol Buffer Encoding 序列化你的消息时,你可以在这里找到更多有关类型编码的信息。

[1] Kotlin 使用对应的 Java 类型,甚至无符号类型也和 Java 保持一致,来确保与 Java 代码混用时的兼容性。

[2] 在 Java 中,无符号 32 位和 64 位整数使用对应的有符号表示,最高位简单的存储在最高位中。

[3] 在所有情况下,位字段赋值都将会执行类型检查来确保值的有效性。

[4] 64 位或无符号 32 位整数在解码时始终表示为长整型,但当位字段赋值时如果需要整型则可以是整型 。 在任何情况下,设置的值应该与表示的类型相匹配。

[5] Python 字符串在解码是表示位 unicode 但如果是一个 ASCII 字符串也可以表示位 str

[6] 整型使用于 64 位机器上,string 用在 32 位机器上。

默认值

当一个消息被解析后,如果解码后的消息不包含某些单例元素,解析后对象中对应的值将被设置为此字段的默认值。 默认值是特定于类型的:

  • 对于字符串,默认值是空字符串。

  • 对于字节,默认值是空字节。

  • 对于布尔值,默认值是 false。

  • 对于数值类型,默认值是零。

  • 对于 枚举 类型,默认值是 定义的首个枚举值 ,其编号必须为 0。

  • 对一消息类型,该字段没有设置。确切的值于语言相关。详见 代码生成指南

重复字段的默认值为空(通常是对应语言中的空值)

对于消息中的标量字段需要注意,消息一经解析就再也无法得知一个字段是显式设置为默认值(例如对于布尔值将设置为 false )还是直接没有设置: 当你定义消息类型时这一点你应该铭记于心。 所以,当你不希望某些行为默认发生时,不要使用布尔值来切换某些行为。 同时注意如果标量消息字段设置为其默认值时,这个值将不会被序列化到线上[1]

查看你选择语言的 代码生成指南 获得更多关于生成的代码中默认值的工作细节。

[1] “线上”格式是指一个可解析消息的物理表示,更多可参考 XML wire format

枚举

当你定义一个消息类型时,你可能希望其中一个字段的值是一个预定义列表中某一个值。 例如,假设你想要为每个 SearchRequest 添加一个 corpus 字段,这里 corpus 可以是 UNIVERSALWEBIMAGESLOCALNEWSPRODUCTSVIDEO 。 这可以通过在你定义的消息中添加一个包含每种可能常量值的 enum 轻松搞定。

下面的示例中我们添加了一个名为 Corpusenum 以及一个类型为 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;
}

如你所见, Corpus 枚举的第一个常量映射到了编号 0:每个枚举的定义都 必须 包含一个映射到编号 0 的常量作为其首个元素。 这是因为:

  • 必须有一个为零的值,这样我们就能将 0 作为数字默认值。

  • proto2 中第一个枚举值总是被作为默认值,为了保持与其语义的兼容,这里零值必须是第一个元素。

你可以通过定义别名来分配相同的值到不同的枚举常量。 为此,你需要将 allow_alias 选项设置为 true ,否则当 protocol 编译器发现别名时将会抛出错误信息。

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 位整型的范围内。 因为 enum 值处理时使用 Varint 编码,因为对负数进行编码效率低下因此不推荐使用。 你可以将 enum 定义在消息定义内部(就像上面例子中展示的)或外部 —— 这种 enum 可以在整个 .proto 文件中的所有消息定义中使用。 你也可以使用 MessageType.EnumType 这种语法来将一个消息中定义的 enum 类型作为其它消息的字段类型。

当你使用 Protocol Buffer 编译器编译一个包含 enum 定义的 .proto 文件时, 对于 Java、Kotlin 或 C++ 来说生成的代码中将会包含对应的 enum , 而对 Python 来说将会生成一个用来在运行时生成的类中创建常量符号与整型值集合 的特殊类 EnumDescriptor

警告 生成的代码可能会受到特定语言的枚举数限制(low thousands for one language)。 所以请检查你所使用语言的限制。

在反序列化时,无法识别的枚举值将会被保留在消息中, 尽管消息反序列化时如何进行表示是特定于语言的。 在支持值可超出指定符号范围之外的开放枚举类型的语言比如 C++ 和 Go, 未知的枚举值被简单的存储为其底层整数表示。 在封闭枚举类型的语言中例如 Java,枚举中的一个用例被用来存储无法识别的值,并且底层的整数可以通过特殊的访问器进行访问。 在这两种情况下,如果消息被序列化,那么无法识别的值也会和消息一起进行序列化。

关于消息中的 enum 在你的应用中是如何工作的可以查看你所使用语言的 代码生成指南

保留值

如果你通过直接删除或注释掉的形式完全移除了一个枚举条目来更新枚举类型,将来的用户可以在进行自己的重新时使用这个数字值。 如果他们之后又使用了同一 .proto 文件的旧版本,这可能会导致严重的问题,包括数据损坏,隐私问题等。 确保这不会发生的一种方式是将你删除的条目的数字值指定为预留( reserved )。如果将来有用户尝试使用这些标识符 Protocol Buffer 编译器将会抛出错误。 你可以通过使用 max 关键字指定保留的数字值范围达到最大可能值。

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

注意你不能在一个 reserved 语句中混用字段名和数字值。

使用其它消息类型

你可以使用其他消息类型作为字段类型。 比如说,你想要将 Results 消息放到每个 SearchResponse 消息中 —— 你可以在同一个 .proto 文件中定义一个 Result 消息类型然后在 SearchResponse 指定一个 Result 类型的字段:

message SearchResponse {
  repeated Result results = 1;
}

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

导入定义

在上面的示例中,Result 消息类型和 SearchResponse 定义在同一个文件中 —— 那如果你想用一个定义在另一个 .proto 文件中的消息类型作为字段类型那?

你可以通过 导入 他们来使用定义在其他 .proto 文件中的定义。 为了导入其他 .proto 定义,你需要在你的文件头部添加一个导入语句:

import "myproject/other_protos.proto";

默认情况下,你只能使用直接导入的 .proto 文件中的定义。 但无论如何,有时你可能需要将 .proto 文件移动到一个新的位置。 相比于直接移动 .proto 文件然后一次性修改所有引用,你可以在旧的位置放一个占位用的 .proto 文件, 使用 import public 标记重定向所有导入到新的位置。

注意 public import 功能目前在 Java 中暂时还不支持

`import public` dependencies can be transitively relied upon by any code importing the proto containing the `import public` statement.
// 这里是在不知道该怎么翻译(编)了

import public 的依赖可以通过任何导入包含 import public 语句的 proto 的代码进行传递。 例如:

// new.proto
// All defintions 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 defintions from old.proto and new.proto, but not other.proto

Protocol 编译器使用在命令行中使用 -I / --proto_path 标志指定的目录集合中搜索导入的文件。 如果没有指定此标志,编译器在被调用的目录下查找。 通常你应该将 --proto_path 标志设置为项目根目录并在所有导入而地方使用全限定名。

使用 proto2 消息类型

可以导入 proto2 消息类型并用在 proto3 消息中,反过来也是这样。 无论如何,proto2 枚举无法直接用在 proto3 语法中(如果导入的 proto2 消息使用那没有问题)。

嵌套类型

你可以定义并将消息类型用在其他消息类型中,如下所示 —— 这里 Result 消息定义在 SearchResponse 消息中:

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

如果你想在父消息类型外重用此消息类型,你可以像 Parent.Type 这样应用它:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

你还可以按照你的需求对消息进行嵌套:

message Outer {      // Level 0
  message MiddleAA {   // Level1
    message Inner {      // Level2
      int64 ival = 1;
      bool booly = 2;
    }
  }
  message MiddleBB {   // Level1
    message Inner {      // Level2
      int32 ival = 1;
      bool booly = 2;
    }
  }
}

更新一个消息类型

如果现有而消息类型无法满足你所有的需求 —— 比如, 你希望为消息格式添加一个附加字段 —— 但你还想使用旧格式创建的代码,别慌! 要做到更新消息类型而不损坏任何之前已经存在的代码真的非常简单。 只要记住下面这几个规则就可以:

  • 不要修改任何已有字段的字段编号

  • 如果你添加了一个新的字段,任何使用你的“旧”消息格式序列化的消息仍然可以被新生成的代码解析。 你应该将这些元素的默认值铭记于心,从而保证新代码可以与旧代码生成的消息正确交互。 同样,新代码创建的消息也可以被旧代码解析:旧的二进制文件只是在解析时简单的将新字段忽略掉。 详情查看 未知字段 这一章。

  • 只要更新的消息类型中不再使用这个字段号,就可以删除这个字段。 你可能想要重命名这个字段,也许是添加前缀 “OBSOLETE_” 或者让字段编号成为被 预留 的,这样将来的用户在你的 .proto 文件中就不会意外重用这些编号了。

  • int32uint32 , int64 , uint64 以及 bool 都是兼容的 —— 这意味着你可以修改一个字段从这些类型中的一个类型到另一个, 而不破坏向后或向前的兼容性。 如果从线上解析出一个数字但其并不与对应的类型匹配,你将得到与你在 C++ 中手动强转为该类型相同的效果 (比如,一个 64 位的数字被读取为 32 位,其将被截断为 32 位)。

  • sint32sint64 之间是相互兼容的,但与其他整数类型不兼通。

  • 对于 stringbytes 来说,只要自己诶是有效的 UTF-8 彼此之间就是兼容的。

  • 如果字节包含消息的编码版本,那么潜入消息和字节兼容。

  • fixed32sfixed32 兼容,fixed64sfixed64 兼容。

  • 对于 stringbytes 及消息字段, optionalrepeated 是兼容的。 给出一个重复字段的序列化数据作为输入,如果对应字段是原始类型的,那么希望获取一个可选字段的客户端将会使用最后一个输入值, 或者对应字段是一个消息类型字段,那么将会合并所有的输入。 需要注意的是,这对于数字类型(包括布尔值和枚举)通常是 安全的。 数字类型的重复字段将以 [packed] 格式进行打包,当期待获得一个 optional 字段时将会无法正确解析。

  • enumint32uint32int64uint64 在物理表示上是兼容的(切记, 如果类型不匹配,值将会被截断), 但还是要注意,消息反序列化时客户端可能以不同的方式处理他们: 例如,无法识别的枚举类型将被暴露在消息中,但是当消息被反序列化时如何表示则是特定于语言的。 整型字段总是只保留他们的值。

  • TODO: 将单个值改为新 oneof 的成员是安全且二进制兼容的。Changing a single value into a member of a new oneof is safe and binary compatible. Moving multiple fields into a new oneof may be safe if you are sure that no code sets more than one at a time. Moving any fields into an existing oneof is not safe.

未知字段

未知字段是协议良好(well-formed)的 Protocol Buffer 序列化数据,表示解析器无法识别的字段。 例如:当旧的二进制解析一个带有新字段的新二进制数据时,这些新的字段在就的二进制中就是未知字段。

最初,proto3 消息在解析时总是丢弃未知字段,但在版本 3.5 中我们又重新引入了对未知字段的保留一次来匹配 proto2 的行为。 在版本 3.5 及更高的版本中,未知字段在解析时保留并包含在序列化输出中。

Any

Any 消息类型可以让你将消息作为嵌入类型而无需定义他们的 .protoAny 可以包含任意序列化为 bytes 的消息,并附加一个作为全局唯一标识符用来解析消息类型的 URL。 要使用 Any 类型,你需要 导入 google/protobuf/any.proto

import "google/protobuf/any.proto";

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

给定消息类型的默认 URL 是 type.googleapis.com/packagename.messagename

不同语言实现支持使用运行时库从而以类型安全的形式来辅助打包或拆包 Any 值 —— 例如: 在 Java 中 Any 类型会有 pack()unpack() 访问器,而在 C++ 中则有 PackFrom()UnpackTo() 方法:

// 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 可以保存任何 proto3 消息,这和 可以允许扩展的 proto2 消息类似。

Oneof

如果你的消息有许多字段但同时只会设置一个字段,你可以使用 oneof 特性强制保证此行为来节省内存。

除了在 oneof 中所有字段共享内存并且同时只能设置一个值之外,其他方面 oneof 字段与普通字段没有什么不同。 设置任何 oneof 的成员都将会晴空其他成员的值。 取决于你所选择的语言你可以使用 case()WhichOneof() 等特殊方法来检查 oneof 中设置了那个值。

使用 Oneof

要在你的 .proto 文件中定义一个 oneof 你可以像下列示例中 test_oneof 那样使用 oneof 关键字后跟 oneof 的名称:

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

之后将你的 oneof 字段添加到定义中就可以了。 你可以添加除 maprepeated 字段外的任何字段。

在你生成的代码中,oneof 字段拥有和普通字段一样的 getters 和 setters。 你也会获得一个用来检查那个值(if any)在 oneof 中被设置的特殊方法。 你可以在你所选语言的相关 API 参考文献 中获得更多 oneof API 相关信息。

Oneof 特性

  • 在 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

  • 在 oneof 上可以使用反射接口

  • 如果你将一个 oneof 字段设置为默认值(比如将 int32 设置为 0), 那么这个 oneof 字段的“case”将被设置,并且值将被序列化到线上。

  • 如果你使用 C++,请确保你的代码不会导致内存泄漏/崩溃。 下边这个简单示例将会导致崩溃,因为 sub_message 已经在调用 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++ 中,如果你使用 oneof 的 Swap() 方法交换两个消息,那么两个消息最终会变为另一个 oneof 用例:在下面的示例中, 最终 msg1 将会拥有 sub_messagemsg2 会拥有 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:你可能会在消息序列化和解析时丢失掉某些信息(某些字段将会被清空)。 However, you can safely move a single field into a new oneof and may be able to move multiple fields if it is known that only one is ever set.

  • 删除一个 oneof 字段然后再添加回来:这可能会在消息被序列化和解析后清除你当前设置的 oneof 字段。

  • 拆分或合并 oneof:这和移动普通字段有类似的问题。

Map

如果你想创建一个关联映射作为你数据定义的一部分,Protocol Buffers 提供了方便快捷的语法:

map<key_type, value_type> map_field = N;

这里 key_type 可以是任何整数或字符串类型(也就是说,可以是除了浮点类型和字节类型之外的任何类型)。 要注意的是枚举并不是有效的 key_value 。 而 value_type 可以是除了另一个映射之外的任何类型。

所以,比如,当你想要创建一个项目映射其中每个 Project 消息都和一个字符串键相关联,那么你可以像下面这样进行定义:

map<string, Project> projects = 3;
  • 映射字段不以是可重复的( repeated )。

  • 线上格式的顺序和映射的遍历顺序对于映射值来说都是不明确的,因此你不能依赖你的映射条目是有特定顺序的。

  • 当为 .proto 生成文本格式时,映射按键排序,数据键按数字排序。

  • 当从线上解析或合并时,如果有重复的映射键,那么使用最后的键。 当从文本格式解析映射时,如果有重复的键解析可能会失败。

  • 如果你为一个映射提供了键但没有值,字段的序列化行为是特定于语言的。 在 C++,Java,Kotlin 和 Python 中对应类型的默认值被序列化,而在其他语言中没有任何东西被序列化。

当前生成的映射 API 已在所有支持 proto3 的语言中可用。 你可以查看你所选语言的 API 参考文档 查看更映射 API 的信息。

向后兼容性

映射的语法在线上等同于下列定义,所以不支持映射的 Protocl Buffer 实现仍然可以处理你的数据:

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

repeated MapFieldEntry map_field = N;

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

你可以在 .proto 文件中添加一个可选的 package 说明符来避免 Protocol 消息类型的命名冲突。

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

同样你也可以在定义你的消息类型时使用包说明符:

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

包说明符对生成代码的影响方式是特定于语言的:

  • 在 *C* 中生成的类被包装在一个 C 明明空间中。 例如, Open 将会在 foo::bar 命名空间下。

  • JavaKotlin 中,包说明符被用作 Java 的包,除非你在你的 .proto 文件中明确提供了 option java_package 选项。

  • Python 中,包指令是被忽略的,因为 Python 是根据他们在文件系统中的位置组织的。

  • Go 中,包被用于 Go 的包名,除非你在你的 .proto 文件中明确提供了 option go_package 选项。

  • Ruby 中,生成的类包裹在嵌套的 Ruby 命名空间中,被转换为 Ruby 要求的大写风格(首字母大写;如果第一个字符不是字母, 将会使用 PB_ 前缀修饰)。 例如, Open 将会在命名空间 Foo::Bar 中。

  • C# 中包名被转化为 PascalCase 后被用做命名空间,除非你在你的 .proto 文件中明确提供了 option csharp_package 选项。 例如, Open 将会在命名空间 Foo.Bar 下。

包和名称解析

类型名称解析在 Protocol Buffer 语言中以类似 C++ 的方式工作:首先在最内部的空间中搜寻,然后是次内部的,以此类推,每个包都被认为是其父包的“内部”。 一个开头的“.”(例如, .foo.bar.Baz )表示从最外部范围开始。

定义服务

如果你想将你的消息类型用于 RPC(Remote Procedure Call)系统, 你可以在一个 .proto 文件中定义 RPC 服务接口, 之后 Protocol Buffer 编译器将会为你所选的语言生成服务接口的代码和存根(stubs)。 所以,比如,你想定义一个带有一个接收 SearchRequest 并返回一个 SearchResponse 方法的RPC服务, 你可以在你的 .proto 文件中做如下定义:

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

与 Protocol Buffer 一起使用最直接的 RPC 系统是 gRPC: 一个由谷歌开发的语言和平台中立的开源 RPC 系统。 gRPC 对于 Protocol Bufer 非常合适, 并且可以让你通过一个特殊的 Protocol Buffer 编译插件从你的 .proto 文件中直接生成相关的 RPC 代码。

如果你不想使用 gRPC,也可以将 Protocol Buffer 和你自己的 RPC 实现一起使用。 你可以在 Proto2 语言指南 中看到更多相关信息。

还有许多发展中的第三方开元项目在为 Protocol Buffer 开发 RPC 实现。 关于我们已知的项目链接列表,可以查看 第三方插件 Wiki 页面。

JSON 映射

Proto3 支持 JSON 中的编码规范,从而让系统间的数据共享变得更加轻松。 下标按类型分类对编码进行了描述:

如果 JSON 编码中的数据缺少某个值或者其值为 null,那么在解析到 Protocol Buffer 时会被解析为适当的默认值。 如果某个字段在 Protocol Buffer 中有默认值,那么在在 JSON 编码的数据中默认将其省略来节省空间。 有的实现可能会提供选项一个选项用来 JSON 编码中输出有默认值的字段。

proto3 JSON JSON example Notes

message

object

{"fooBar": v, "g": null, …}

Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys. If the json_name field option is specified, the specified value will be used as the key instead. Parsers accept both the lowerCamelCase name (or the one specified by the json_name option) and the original proto field name. null is an accepted value for all field types and treated as the default value of the corresponding field type.

enum

string

"FOO_BAR"

The name of the enum value as specified in proto is used. Parsers accept both enum names and integer values.

map<K,V>

object

{"k": v, …}

All keys are converted to strings.

repeated V

array

[v, …]

null is accepted as the empty list [].

bool

true, false

true, false

string

string

"Hello World!"

bytes

base64 string

"YWJjMTIzIT8kKiYoKSctPUB+"

JSON value will be the data encoded as a string using standard base64 encoding with paddings. Either standard or URL-safe base64 encoding with/without paddings are accepted.

int32, fixed32, uint32

number

1, -10, 0

JSON value will be a decimal number. Either numbers or strings are accepted.

int64, fixed64, uint64

string

"1", "-10"

JSON value will be a decimal string. Either numbers or strings are accepted.

float, double

number

1.1, -10.0, 0, "NaN", "Infinity"

JSON value will be a number or one of the special string values "NaN", "Infinity", and "-Infinity". Either numbers or strings are accepted. Exponent notation is also accepted. -0 is considered equivalent to 0.

Any

object

{"@type": "url", "f": v, … }

If the Any contains a value that has a special JSON mapping, it will be converted as follows: {"@type": xxx, "value": yyy}. Otherwise, the value will be converted into a JSON object, and the "@type" field will be inserted to indicate the actual data type.

Timestamp

string

"1972-01-01T10:00:20.021Z"

Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3, 6 or 9 fractional digits. Offsets other than "Z" are also accepted.

Duration

string

"1.000340012s", "1s"

Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision, followed by the suffix "s". Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision and the suffix "s" is required.

Struct

object

{ … }

Any JSON object. See struct.proto.

Wrapper types

various types

2, "2", "foo", true, "true", null, 0, …

Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer.

FieldMask

string

"f.fooBar,h"

See field_mask.proto.

ListValue

array

[foo, bar, …]

Value

value

Any JSON value. Check google.protobuf.Value for details.

NullValue

null

JSON null

Empty

object

{}

An empty JSON object

JSON 选项

一个 proto3 JSON 实现可能会提供下列选项:

  • 发出带有默认值的字段 :为默认值的字段在 proto3 JSON 输出中默认被忽略。 有的实现可能会提供一个配置项来覆盖这一行为并输出字段与其默认值。

  • 忽略未知字段 :Proto3 JSON 解析器默认情况下应该拒绝未知字段,但许多都提供来配置项来忽略未知字段。

  • 使用 proto 字段名而不是小驼峰命名 :默认情况下 proto3 JSON 打印器应该将字段名转换为小驼分并将其用作 JSON 字段名。 有的实现可能会提供一个使用 proto 字段名作为 JSON 字段名的配置项。 Proto3 解析器应该可以接受转换后的小驼分和 proto 字段名两种形式。

  • 将枚举值作为整数而不是字符串输出 :枚举值的名称默认被用于 JSON 输出。 可能会提供一个配置项从而使用数字作为枚举的值。