概述

翻译自:overview

本指南描述了如何使用协议缓冲区语言来构造你的协议缓冲区数据,包括 .proto 文件的语法和如何从你的 .proto 文件生成数据访问类。它涵盖了协议缓冲区语言的 proto2 版本:关于 proto3 语法的信息,请参阅 Proto3 语言指南

这是一个参考指南——关于使用本文档中描述的许多功能的入门的例子,请参阅你所选择的语言的 教程

定义消息类型

首先让我们看一个非常简单的例子。假设你想定义一个搜索请求的消息格式,每个搜索请求都有一个查询字符串,你感兴趣的特定结果页,以及每页的结果数量。这里是你用来定义消息类型的 .proto 文件:

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

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

指定字段规则

你指定消息字段是以下之一:

  • required:一封格式良好的消息必须有一个这样的字段。

  • optional:一份格式良好的消息可以有零个或一个这个字段(但不能超过一个)。

  • repeated:这个字段可以在一条格式良好的信息中重复任何次数(包括零)。重复值的顺序将被保留。

由于历史原因,标量数字类型的重复字段并没有得到有效的编码。新的代码应该使用特殊选项 [packed = true] 来获得更有效的编码。比如说:

repeated int32 samples = 4 [packed = true];

你可以在 协议缓冲区编码 中找到更多关于 packed 编码的信息。

警告

Required Is Forever 你应该非常小心地把字段标记为 required。如果在某个时候你想停止编写或发送一个 required 字段,那么将该字段改为 optional 字段就会有问题——旧的阅读器会认为没有这个字段的消息是不完整的,并可能无意中拒绝或放弃它们。你应该考虑为你的缓冲区编写特定应用的自定义验证例程。谷歌的一些工程师已经得出结论,使用必填字段弊大于利;他们更倾向于只使用可选的和重复的。然而,这种观点并不普遍。

  • optional:该字段可以设置也可以不设置。如果一个可选字段的值没有设置,就会使用一个默认值。调用访问器(accessor)来获取一个没有明确设置的可选(或 required)字段的值,总是返回该字段的默认值。

  • repeated:该字段可以重复任何次数(包括零)。repeated 值的顺序将在协议缓冲区中被保留下来。可以把 repeated 字段看作是动态大小的数组。

  • required:必须提供该字段的值,否则该消息将被视为 “未初始化”。序列化一个未初始化的消息将引发一个异常。解析一个未初始化的消息将失败。除此以外,required 字段的行为与可选字段完全一样。

小心

Required Is Forever 你应该非常小心地把字段标记为 required 字段。如果在某个时候你想停止编写或发送一个 required 字段,那么将该字段改为 optional 字段就会有问题——旧的阅读器会认为没有这个字段的消息是不完整的,并可能无意中拒绝或放弃它们。建议考虑为你的缓冲区编写特定应用的自定义验证例程作为替代方案。在 Google 内部,强烈反对 required 字段;大多数用 proto2 语法定义的消息只使用可选和重复。(Proto3 根本不支持 required 字段)。

可选字段和默认值

如上所述,消息描述中的元素可以被标记为 optional。一个格式良好的消息可能包含也可能不包含可选元素。当一条消息被解析时,如果它不包含一个可选元素,解析对象中的相应字段就会被设置为该字段的默认值。默认值可以作为消息描述的一部分来指定。例如,假设你想为 SearchRequestresult_per_page 值提供一个 10 的默认值。

optional int32 result_per_page = 3 [default = 10];

如果没有为一个可选元素指定默认值,则会使用一个特定类型的默认值:对于字符串,默认值是空字符串。对于字节,默认值是空的字节字符串。对于 Bools,默认值是false。对于数字类型,默认值是 0。对于枚举,默认值是枚举的类型定义中列出的第一个值。这意味着在向枚举值列表的开头添加一个值时必须小心。关于如何安全地改变定义的指南,请参见 更新消息类型 部分。

使用 proto3 消息类型

可以导入 proto3 消息类型并在你的 proto2 消息中使用它们,反之亦然。然而,proto2 的枚举不能用于 proto3 的语法。

更新消息类型

如果一个现有的消息类型不再满足你的所有需求——例如,你希望消息格式有一个额外的字段——但你仍然想使用用旧格式创建的代码,不要担心!更新消息类型非常简单,不会破坏你现有的任何代码。只要记住以下规则:

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

  • 你添加的任何新字段都应该是可选的或重复的。这意味着任何由使用你的 “旧” 消息格式的代码序列化的消息可以被你的新生成的代码解析,因为它们不会缺少任何 required 元素。你应该为这些元素设置合理的默认值,这样新代码就可以与旧代码生成的消息正确地互动。同样,由你的新代码创建的消息可以被你的旧代码解析:旧的二进制文件在解析时只需忽略新字段。然而,未知字段不会被丢弃,如果消息后来被序列化,未知字段也会被一起序列化——所以如果消息被传递给新代码,新字段仍然可用。

  • required 字段可以被删除,只要字段号不会在你更新的消息类型中再次使用。你可能想重新命名这个字段,也许加上前缀 “OBSOLETE_”,或者让这个字段号保留下来,这样你的 .proto 的未来用户就不能意外地重复使用这个号码。

  • 一个非 required 字段可以转换为 extension 字段,反之亦然,只要类型和数字保持不变。

  • int32uint32int64uint64bool 都是兼容的——这意味着你可以将一个字段从这些类型中的一个改为另一个,而不会破坏向前或向后的兼容。如果从 wire 上解析出的数字不符合相应的类型,你会得到与你在 C++ 中把数字投到该类型的相同效果(例如,如果一个 64 位的数字被读成 int32,它将被截断为 32 位)。

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

  • 如果 bytes 包含信息的编码版本,则嵌入式信息与字节兼容。

  • fixed32sfixed32 兼容,而 fixed64sfixed64 兼容。

  • 对于 stringbytes 和消息字段,optionalrepeated 兼容。给出重复字段的序列化数据作为输入,如果是原始类型的字段,期望这个字段是 optional 客户端将采取最后的输入值,如果是消息类型的字段,将合并所有输入元素。请注意,这对于数字类型,包括 bool 和 enum,通常是不安全的。数字类型的重复字段可以用 packed 的格式进行序列化,当期望有一个 optional 字段时,这将不能被正确解析。

  • 改变默认值一般是可以的,只要你记住默认值是不会在 wire 发送的。因此,如果一个程序收到一个没有设置特定字段的消息,该程序将看到默认值,因为它是在该程序的协议版本中定义的。它不会看到发件人代码中定义的默认值。

  • enumint32uint32int64uint64 在 wire 格式上是兼容的(注意,如果数值不合适,会被截断),但是要注意,当消息被反序列化时,客户端代码可能会对它们进行不同的处理。值得注意的是,当消息被反序列化时,未被识别的 enum 值会被丢弃,这使得字段的 has.. 访问器返回 false,其 getter 返回 enum 定义中列出的第一个值,或者默认值(如果指定了一个)。在重复 enum 字段的情况下,任何未被识别的值都会从列表中剥离出来。然而,一个整数字段将始终保留其值。正因为如此,当把一个整数升级为 enum 时,你需要非常小心,以免在电线上收到超界的 enum 值。

  • 在当前的 Java 和 C++ 实现中,当未识别的 enum 值被剥离出来时,它们会与其他未知字段一起被存储。如果这些数据被序列化,然后被识别这些值的客户端重新解析,这可能导致奇怪的行为。在可选字段的情况下,即使在原始消息被反序列化后写了一个新的值,旧的值仍会被识别它的客户端读取。在重复字段的情况下,旧值将出现在任何已识别的和新添加的值之后,这意味着顺序将不会被保留下来。

  • 将一个单一的可选值改成一个新的 oneof 的成员是安全的,而且二进制兼容。将多个可选字段移入一个新的 oneof 中可能是安全的,如果你确信没有代码同时设置多个字段的话。将任何字段移入一个现有的 oneof 中是不安全的。

  • map<K, V> 和相应的重复消息字段之间改变字段是二进制兼容的(关于消息布局和其他限制,见下文的 Maps)。然而,改变的安全性取决于应用:当反序列化和重新序列化消息时,使用重复字段定义的客户端将产生一个语义上相同的结果;然而,使用 map 字段定义的客户端可能会重新排序条目并放弃有重复键的条目。

extensions

扩展允许你声明信息中的一系列字段号码可用于第三方扩展。扩展是一个字段的占位符,其类型未被原始 .proto 文件定义。这允许其他 .proto 文件通过定义这些字段号的部分或全部字段的类型来增加你的消息定义。让我们看一个例子:

message Foo {
  // ...
  extensions 100 to 199;
}

这说明 Foo 中字段号 [100, 199] 的范围是保留给扩展的。其他用户现在可以在他们自己的 .proto 文件中向 Foo 添加新的字段,这些文件可以导入你的 .proto,使用你指定范围内的字段号 - 例如:

extend Foo {
  optional int32 bar = 126;
}

这在 Foo 的原始定义中增加了一个名为 bar 的字段,字段号为 126

当你的用户的 Foo 消息被编码时,wire 的格式与用户在 Foo 内部定义新字段的情况完全相同。然而,你在应用程序代码中访问扩展字段的方式与访问普通字段略有不同——你生成的数据访问代码有特殊的访问器用于处理扩展字段。因此,例如,这里是你如何在 C++ 中设置 bar 的值。

Foo foo;
foo.SetExtension(bar, 15);

同样,Foo 类定义了模板化的访问器 HasExtension()ClearExtension()GetExtension()MutableExtension()AddExtension()。所有的语义都与正常字段的相应生成访问器相匹配。关于使用扩展的更多信息,请参见你所选择的语言的生成代码参考。

扩展可以是任何字段类型,包括消息类型,但不能是 oneofs 或 maps。

嵌套扩展

你可以在另一个类型的范围内声明扩展:

message Baz {
  extend Foo {
    optional int32 bar = 126;
  }
  ...
}

在这种情况下,访问这个扩展的 C++ 代码是:

Foo foo;
foo.SetExtension(Baz::bar, 15);

换句话说,唯一的效果是,bar 被定义在 Baz 的范围内。

这是一个常见的混淆源。声明一个嵌套在消息类型内部的扩展块并不意味着外部类型和扩展类型之间的任何关系。特别是,上面的例子并不意味着 BazFoo 的任何种类的子类。它只意味着符号 bar 是在 Baz 的范围内声明的;它只是一个静态成员。

一个常见的模式是在扩展的字段类型的范围内定义扩展–例如,这里是对 Baz 类型的 Foo 的扩展,其中扩展被定义为 Baz 的一部分:

message Baz {
  extend Foo {
    optional Baz foo_ext = 127;
  }
  ...
}

然而,并不要求带有消息类型的扩展被定义在该类型内。你也可以这样做:

message Baz {
  ...
}

// This can even be in a different file.
extend Foo {
  optional Baz foo_baz_ext = 127;
}

事实上,为了避免混淆,这种语法可能是首选。如上所述,嵌套语法经常被那些还不熟悉扩展的用户误认为是子类。

选择扩展编号

确保两个用户不使用相同的字段号为同一消息类型添加扩展名是非常重要的——如果一个扩展名被意外地解释为错误的类型,就会导致数据损坏。你可能想考虑为你的项目定义一个扩展编号惯例,以防止这种情况发生。

如果你的编号惯例可能涉及到扩展有非常大的字段号,你可以使用 max 关键字指定你的扩展范围,直到可能的最大字段号。

message Foo {
  extensions 1000 to max;
}

max\(2^{29} - 1\)

如同在选择字段号时,你的编号惯例也需要避免字段号 19000 到 19999(FieldDescriptor::kFirstReservedNumberFieldDescriptor::kLastReservedNumber),因为它们被保留给协议缓冲区的实现。你可以定义一个包括这个范围的扩展范围,但是协议编译器不允许你用这些数字定义实际的扩展。