Proto 3

一些简介
Protocol Buffer是一种与语言无关,与平台无关的可扩展机制,用于序列化结构化数据

Style Guide

原文

Enum Behavior

在GO中,enum会被编码为int32。

枚举有两种不同的风格:openclosed
除了处理未知值意外,其他行为相同。

1
2
3
4
5
6
7
8
enum Enum {
A = 0;
B = 1;
}

message Msg {
optional Enum enum = 1;
}

openclosed的区别可以概括为一个问题
当程序解析包含enum的Msg时,其值为2,会发生什么?

  • open enums 将解析值 2 并将其直接存储在字段中。访问器将报告该字段已设置,并将返回代表 2 的内容
  • closed enums 将解析值 2 并将其存储在消息的未知字段集中。访问器将报告该字段未设置,并将返回枚举的默认值。

而所有已知的GO版本都不符合要求。GO将所有枚举视为open

Well-Known Types and Common Types

Well-Known Types

  • duration 带符号的固定长度时间跨度(例如:42S)
  • timestamp 独立于任何时区或日历的时间点 (例如:2017-01-15T01:30:15.01Z)
  • field_mask 一组符号字段路径 (例如:f.b.d

Common Types

  • interval 独立于时区或日历的时间间隔 (例如:2017-01-15T01:30:15.01Z - 2017-01-16T02:30:15.01Z)
  • date 整个日期时间 (例如:2005-09-19)
  • dayofweek 一周的日子 (例如: Monday)
  • timeofday 一天的时间 (例如:10:22:23)
  • latlng 是纬度/经度对(例如,37.386051 纬度和 -122.083855 经度
  • money 具有货币类型的金额 (例如, 42USD)
  • postal_address 是邮政地址(例如,1600 Amphitheatre Parkway Mountain View, CA 94043 USA)
  • color RGBA颜色空间中的颜色
  • month 一年的月份 (例如,April)

定义一个消息类型

1
2
3
4
5
6
7
syntax = "proto3";    // 指定使用的是proto3协议

message SearchRequest { // 定义一个有三个字段的消息
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}

你必须给消息中的每个字段分配一个在1536,870,911之间的数字,同时还有下面这些限制:

  • 分配的数字在这个消息体中必须是唯一的
  • 19,00019,999之间的数字分配给了Protocol Buffer实现,不可用
  • 你不能用任何之前保留的数字或是分配给扩展的数字

一旦信息类型在使用,该数字就无法更改,因为它标识消息传输格式中的字段。“更改”字段编号相当于删除该字段并创建一个具有相同类型但新编号的新字段。查看删除字段了解如何正确执行此操作的信息。

字段数字永远不应该重复使用。不要将保留列表中的数字取出供新字段重复使用。查阅重复使用字段编号的后果

你应该将字段编号1到15用在最常用的字段。较低的字段数字会占用较少的空间。例如,1到15范围内的字段编号需要一个字节进行编码,而16到2047范围的编号需要两个字节。你可以在Protocol Buffer编码中了解更多。

重复使用字段编号的后果

重复使用字段编号会使用解码消息变得不明确。

Protobuf消息格式很精简,并没有提供一种方法来检测使用一种定义编码并使用另一种定义解码的字段。

使用一个定义对字段进行编码,再使用另一个不同的定义对同一字段进行解码会导致:

  • 开发人员因调试而损失的时间
  • 解析/合并错误(最好的情况)
  • PII/SPII泄漏
  • 数据损坏

字段号重复使用的常见原因:

  • 对字段重新编号(有时是为了使字段编号顺序更美观)。重新编号实际上会删除并重新添加新编号中涉及的所有字段,从而导致不兼容的格式更改。
  • 删除字段且不保留编号以防止将来重复使用

最大字段为29位(bit),而不是更常见的32位,因为有3个较低位用于有线格式。了解更多可查看编码主题.

指定字段标签

消息字段可以增加以下的标签:

  • optional 一个optional字段可能是以下两种状态
    • 字段存在,并包含一个显式设置或从线路解析的值。它将被序列化到线路上
    • 字段不存在,会返回默认值。不会被序列化到线路上。
      可以检查该值是否明确设置
  • repeated 该字段类型可以在格式正确的消息中重复零次或多次。会保留重复值的顺序。
  • map 这是成对的key:value字段类型。查看Maps了解更多。
  • 如果没有使用显式字段标签,则假定使用默认字段标签,称为“隐式字段存在”。(无法显式将字段设置为该状态)。格式良好的消息可以用零个或一个次字段(但不能超过一个)。你也无法确定是否从线路中解析了该类型的字段。隐式存在字段将被序列化到线路,除非它是默认值。查看字段存在了解更多。

在proto3,标量数值类型的repeated字段默认使用packed编码。可以在Protocol Buffer编码中了解更多packed

添加更多消息类型

在单个.proto文件中可以定义多个消息类型。这对于定义多个相关的消息很有用。例如,如果你想定义与SearchRequest对应的消息类型SearchResponse,可以直接在同文件中添加。

1
2
3
4
5
6
7
message SearchRequest {
...
}

message SearchResponse {
...
}

添加注释

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

1
2
3
4
5
6
7
8
/* Comment
* Multiple line comments
*
*/

message SearchRequest { // single line comment
...
}

删除字段

如果操作不当,删除字段可能会导致严重问题。

当你不再需要一个字段且客户端相关的代码已经删掉了,可以从消息中删除该字段定义。但是,你必须保留已删除的字段编号。如果你不保留字段编号,开发人员将来可以重复使用该编号。

你还应该保留字段名称,以允许消息的JSON和TextFormat编码继续解析。

保留字段

如果你通过删除一个字段或是注释掉来更新一个消息类型,后来的开发可能会在更新类型时重用字段编号。这会导致很多问题,就像在重复使用字段编号中描述的。

为了确保这不会发生,将你删除的字段添加到reserved列表。为了确保消息的JSON和TextFormat实例依然可以被解析,将字段名也添加到reserved列表。

Protocol buffer编译器会在新开发尝试使用这些保留的字段或是字段名时报错。

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

保留字段编号可以使用范围(9 to 11就是9, 10, 11)。
注意:不能在同一个reserved语句中混用字段名和字段编号。

从proto文件可以生成什么?

当使用Protocol Buffer编译器编译proto文件时,编译器会根据你选择的语言生成在文件中描述的消息类型,包括gettingsetting字段值,将消息序列化到输出流,从输入流解析数据。

  • 对于GO,编译器生成.pb.go文件,包含了proto文件中定义的每个消息类型

有关更多API详细信息,参阅相关API参考

标量值类型

原文

可以在Protobuf Buffer编码中查看更多关于在编码中序列化消息。

默认值

解析消息时,如果编码的消息不包含特定的单一元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:

  • 对于strings,默认值就是空字符串
  • 对于bytes,默认值就是空的字节
  • 对于bools,默认值就是false
  • 对于数值类型,默认值就是0
  • 对于enums,默认值是第一个定义的enum值,也就是0
  • 对于消息字段,未设置该字段。明确值取决于语言。查看生成代码向导了解更多细节。

对于repeated字段的空默认值(通常是相关语言的空列表)。

注意:对于标量消息字段,一旦解析消息,就无法判断字段是否已显式设置为默认值(例如布尔值是否设置为false)或根本未设置: 在定义消息类型时应该注意这一点。例如,如果你不希望默认情况下也发生某些行为,则没有一个布尔值可以在设置为false时开启某些行为。另请注意:如果标量消息字段设置为其默认值,则该值将不会在线上序列化。

有关默认值如何在生成的代码中工作的更多详细信息,参阅所用语言的生成代码指南

枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}

message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
Corpus corpus = 4;
}

Corpus枚举的第一个常量映射到0:每个枚举值定义必须包含一个映射到0的值作为第一个常量,这是因为:

  • 必须有0值,这样我们可以作默认值
  • 0值需要时第一个值,为了和proto2兼容

可以通过将相同的值分配给不同的枚举常量来定义别名。为此,需要将allow_alias选项设置为true,否则编译器会发出警告。尽管所有别名值在反序列化期间都有效,但在序列化时始终使用第一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum EnumAlloingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1;
EAA_FINISHED = 2;
}

enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // 不注释这行会导致警告信息
ENAA_FINISHED = 2;
}

枚举常量必须在32位整数范围内。因为枚举值使用的varint编码,负值效率低下,因此不推荐。

保留值

如果通过完全删除枚举条目或将其注释掉来更新枚举类型,则将来的用户在对该类型进行自己的更新时可以重用该数值。如果他们稍后加载同一 .proto 的旧版本,这可能会导致严重问题,包括数据损坏、隐私错误等。确保不会发生这种情况的一种方法是指定保留已删除条目的数值(和/或名称,这也可能导致 JSON 序列化问题)。如果任何未来的用户尝试使用这些标识符,Protocol Buffer编译器将会警告。您可以使用 max 关键字指定保留的数值范围达到最大可能值。

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

使用其他消息类型

可以使用其他消息类型作为字段类型。例如,假设你想在每个SearchResponse消息中包含Result消息,你可以在同一文件中定义Result消息,然后在SearchResponse中指定Result类型的字段。

1
2
3
4
5
6
7
8
9
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}

message SearchResponse {
repeated Result results = 1;
}

导入定义

在上面的例子中,Result消息和SearchResponse定义在一个文件中 - 如果你想用已经在其他文件中定义的消息作为字段类型呢?

你可以导入它们再使用。语法是这样:

import "myproject/other_protos.proto";

默认情况下,只能使用直接导入的proto文件中的定义。但是,有时候可能需要将proto文件移动到新位置。可以将占位符proto文件放在旧位置,使用导入公共概念将所有导入转发到新位置,而不是直接移动proto文件并在更改中更新所有调用。

导入包含import public语句的原型的任何代码都可以传递依赖import public依赖项。例如:

1
2
// new.proto
// All definitions are moved here
1
2
3
4
// old.proto
// This is the proto that all clients are importing
import public "new.proto";
import "other.proto";
1
2
3
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto

Protocol编译器使用-I/--proto_path标志在命令行指定的一组目录中搜索导入的文件。如果没有指定位置,它会在调用的目录下查找。一般来说,应该将--proto_path标志设置为项目的根目录,并为所有导入使用完全限定名称。

嵌套类型

你可以在其他消息中定义并使用消息,就像下面的例子 - 在SearchResponse中定义Result消息:

1
2
3
4
5
6
7
8
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}

如果你想在其他消息中复用这个内部定义的消息,可以这样_Parent_._Type_:

1
2
3
message SomeOtherMessage {
SearchResponse.Result result = 1;
}

你可以按照自己的喜好随意嵌套消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;
}
}
}

更新消息类型

如果一个已存在的消息不满足需要了,例如,你希望消息有额外的字段,但你仍然希望使用旧格式创建的代码,不要担心。使用二进制有线格式时,更新消息类型很简单,不会破坏任何现有代码。

注意:
如果你使用JSON或proto文本格式来存储protocol buffer消息,则你可以在proto定义中进行的更改会由所不同。

检查Proto最佳实践和以下规则:

  • 不要更改任何现有字段的字段编号
  • 如果添加新字段,则使用旧消息格式的代码序列化的任何消息仍可以由新生成的代码进行解析。你应该记住这些元素的默认值,以便新代码可以和旧代码生成的消息正确交互。同样,新代码创建的消息旧代码也可解析:旧的二进制文件解析时只是忽略新字段。
  • 只要在更新的消息类型中不再使用字段编号,就可以删除字段。你可能想重命名该字段,也许添加前缀OBSOLETE_,或者保留字段编号,以便未来的开发不会意外的重复使用该编号。
  • int32, uint32, int64, uint64, bool都是兼容的。这意味着可以将这些类型的字段更改为另一个类型而不会破坏兼容性。如果从线路中解析的数字不适应该类型,将会获得在C++中将该数字转换为该类型的效果。
  • sint32, sint64彼此兼容,但与其他整数类型不兼容
  • string, bytes只要字节是有效的UTF8,就可以兼容
  • 如果字节包含消息的编码版本,则嵌入消息和字节兼容
  • fixed32sfixed32, fixed64, sfixed64兼容
  • 对于string, bytes,以及消息字段,optionalrepeated兼容。
    给定重复字段的序列化数据作为输入,希望该字段为可选的客户端会采用最后一个输入值(如果它是原始类型字段)或合并所有输入元素(如果是消息类型字段)。
    注意: 这对于数字类型(包括枚举和布尔)通常并不安全。数字类型的重复字段可以packed格式序列化,当需要可选字段时,将无法正确解析。
  • enumint32, uint32, int64, uint64在有线格式方面兼容(注意:如果不合适,值会被截断)。
  • 将单个optional字段或extension更改为新的oneof的成员是二进制兼容的,但对于某些语言(尤其是GO),生成的代码的API将以不兼容的方式发生变化。

未知字段

未知字段是格式正确的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件使用新字段解析新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。

最初,proto3 消息在解析过程中总是丢弃未知字段,但在 3.5 版本中,我们重新引入了保留未知字段以匹配 proto2 行为。在 3.5 及更高版本中,未知字段在解析期间被保留并包含在序列化输出中。

Any

Any消息类型允许你将消息用作嵌入类型,而无需其proto定义。Any包含作为字节的任意序列化消息,以及充当该消息类型的全局唯一标识符并解析为该消息类型的URL。要使用Any类型,需要导入google/protobuf/any.proto

1
2
3
4
5
6
import "google/protobuf/any.proto";

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

不同的语言实现将支持运行时库助手以类型安全的方式打包和解包任何值。

Oneof

如果您的消息包含多个字段,并且最多同时设置一个字段,则可以使用 oneof 功能强制执行此行为并节省内存。

oneof 字段与常规字段类似,只是所有字段都位于 oneof 共享内存中,并且最多可以同时设置一个字段。设置 oneof 的任何成员都会自动清除所有其他成员。您可以使用特殊的case()WhichOneof()方法检查 oneof 中设置了哪个值(如果有),具体取决于您选择的语言。

Go应该不支持 TODO

Maps

如果您想创建关联映射作为数据定义的一部分,protocol buffer提供了一种方便的快捷语法:

map<key_type, value_type> map_field = N;

其中key_type可以是任何整数类型或字符串类型(因此,除浮点类型和字节之外的任何标量类型)。注意,枚举不行。value_type可以是除另一个映射外的任何类型。

例如,如果你想创建一个项目映射,其中每个项目消息都和一个字符串键关联,可以这样定义:

1
2
3
4
5
message Project {
...
}

map<string, Project> projects = 3;
  • Map字段不能repeated
  • map值的顺序未定义,因此不能依赖特定顺序的映射项
  • 为proto生成文本格式时,map按键排序,数字键按数字排序
  • 当从线路解析或合并时,如果存在重复的map键,使用最后收到的键。从文本解析map时,如果存在重复的键,解析可能失败
  • 如果为map字段提供键但未提供值,则序列化字段时的行为取决于语言。

向后兼容性

Map语法相当于以下内容,因此不支持map的protocol buffer实现依然可以处理你的数据:

1
2
3
4
5
6
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支持map的protocol buffer实现都必须生成并接受上述定义可以接受的数据

你可以想proto文件添加可选的包说明符,以防止protocol消息类型间发生名称冲突

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

你可以在定义消息字段时使用包说明符:

1
2
3
4
5
message Foo {
...
foo.bar.Open open = 1;
...
}

包说明符影响生成代码的方式取决于使用的语言:

  • GO 除非在proto文件中显式指定了go_package,否则会被用作包名

定义服务

如果要在RPC(Remote Procedure Call)系统中使用消息类型,可以在proto文件中定义一个RPC服务接口,protocol buffer编译器会根据选择的语言生成服务接口代码。例如,如果要定义一个接收SearchRequest并返回SearchResponse的RPC服务,可以像下面这样定义:

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

使用protocol buffer的最直接的RPC系统就是GRPC。

还有很多正在进行的三方项目来开发protocol buffer的RPC实现,查看三方附加组件wiki页面

JSON映射

原文

生成代码

对于 Go,您还需要为编译器安装一个特殊的代码生成器插件:您可以在 GitHub 上的 golang/protobuf 存储库中找到此插件和安装说明。