0.1. Protocol buffers如何工作 0.2. 为什么不直接用XML 0.3. 如何开始使用 0.4. proto3简介 0.5. 一点小历史 Protocol buffers是一种灵活,高效,自动化的机制,用于序列化结构化的数据(如XML),但是它更小,更快,更简单。可以定义数据如何被结构化,然后使用特定的生成的源代码轻松地将结构化数据在各种数据流中写入和读取,这支持各种编程语言。甚至可以更新数据结构,而不会破坏根据“旧”格式编译的已部署程序。 0.1. Protocol buffers如何工作 通过在.proto文件中定义protocol buffers消息类型来指定希望如何构建序列化信息。每个protocol buffers消息都是一个小的逻辑信息记录,包含一系列名称-值对。以下是.proto文件的一个非常基本的示例,该文件定义了包含有关人员信息的消息: message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phone = 4; } 如上所示,消息格式很简单: 每种消息类型都有一个或多个唯一编号的字段, 每个字段都有一个名称和一个值类型, 其中值类型可以是: 数字(整数或浮点数) 布尔值 字符串 原始字节 甚至(如上例所示)其他protocol buffers消息类型,允许分层次地构建数据 可以指定: 可选字段(optional) 必填字段(required) 重复字段(repeated) 可以在proto3指南中找到有关编写.proto文件的更多信息。 一旦定义了消息,就可以在.proto文件上运行相应编程语言的protocol buffers编译器来生成数据访问类,他们为每个字段提供了简单的访问器,如name()和set_name(),以及将整个结构序列化或解析为原始字节的方法。 例如,如果选择的语言是C++,在上面的例子上运行编译器将生成一个名为Person的类。然后,可以在应用程序中使用此类来填充,序列化和检索Person的protocol buffers消息。如下所示的代码。 Person person; person.set_name("John Doe"); person.set_id(1234); person.set_email("jdoe@example.com"); fstream output("myfile", ios::out | ios::binary); person.SerializeToOstream(&output); 可以在以下位置阅读消息: fstream input("myfile", ios::in | ios::binary); Person person; person.ParseFromIstream(&input); cout << "Name: " << person.name() << endl; cout << "E-mail: " << person.email() << endl; 可以在不破坏向后兼容性的情况下为邮件格式添加新字段,旧的二进制文件在解析时只是忽略新字段。因此,如果通信协议使用protocol buffers作为数据格式,则可以扩展协议,而无需担心破坏现有代码。 将在API参考部分找到有关使用生成的protocol buffers代码的完整参考,在protocol buffers编码中找到有关protocol buffers消息如何编码的更多信息。 0.2. 为什么不直接用XML 对于序列化结构化数据,protocol buffers比XML具有许多优点: 更简单 缩小3~10倍 快20~100倍 更少歧义 生成更易于编程的数据访问类型 例如,假设要为具有name和email的Person建模。在XML中,需要: <person> <name>John Doe</name> <email>jdoe@example.com</email> </person> 而相应的protocol buffers消息(protocol buffers文本格式)是: # protocol buffer的文本表示 # 这不是在实际传输中的二进制格式 person { name: "John Doe" email: "jdoe@example.com" } 当此消息被编码为protocol buffers二进制格式(上面的文本格式只是方便人类可读的表示形式,用于调试和编辑)时,它可能是28字节长并且需要大约100-200纳秒来解析。如果删除空格,XML版本至少为69个字节,并且需要大约5000-10000纳秒才能解析。 此外,操作protocol buffers要容易得多: cout << "Name: " << person.name() << endl; cout << "E-mail: " << person.email() << endl; 而使用XML,必须执行以下操作: cout << "Name: " << person.getElementsByTagName("name")->item(0)->innerText() << endl; cout << "E-mail: " << person.getElementsByTagName("email")->item(0)->innerText() << endl; 但是,protocol buffers并不总是比XML更好的解决方案。例如: protocol buffers不是使用标记对基于文本的文档(例如HTML)建模的好方法,因为无法轻松地将结构与文本交错。 XML是人类可读和可编辑的; protocol buffers在它原生的格式中不是人类可读和可编辑的。 XML在某种程度上也是自我描述的。只有拥有消息定义(如.proto文件)时,protocol buffers才有意义。 0.3. 如何开始使用 下载这个包,它包含Java,Python和C++版本的protocol buffers编译器的完整源代码,以及I/O和测试所需的类。要构建和安装编译器,请按照自述文件中的说明进行操作。 完成所有设置后,请尝试按照所选语言的教程进行操作,这将指导你创建一个使用protocol buffers的简单应用程序。 0.4. proto3简介 最新的版本3发布了一个新的语言版本:Protocol Buffers语言版本3(简称proto3),以及现有语言版本(简称proto2)中的一些新功能。Proto3简化了protocol buffers语言,既易于使用,又可以在更广泛的编程语言中使用:当前的版本允许使用Java,C++,Python,Java Lite,Ruby,JavaScript,Objective和C#来生成protocol buffers代码。此外,可以使用最新的Go protoc插件为Go生成proto3代码,该插件可从golang/protobuf Github存储库获得。更多的语言正在筹备中。 请注意,两种语言版本的API不完全兼容。为避免给现有用户带来不便,将继续在新protocol buffers版本中支持以前的语言版本。 可以在发行说明中看到与当前默认版本的主要差异,并了解Proto3语言指南中的proto3语法。proto3的完整文档即将推出! (如果名称proto2和proto3看起来有点令人困惑,那是因为最初开源protocol buffers时,它实际上是Google的第二版语言,也称为proto2,这也是开源版本号从v2开始的原因。 0.5. 一点小历史 protocol buffers最初是在Google开发的,用于处理索引服务器请求/响应协议。在protocol buffers之前,有一种请求和响应的格式,它使用请求和响应的手动编组/解组,并支持许多版本的协议。 这导致一些非常丑陋的代码,如: if (version == 3) { ... } else if (version > 4) { if (version == 5) { ... } ... } 明确格式化的协议也使新协议版本的推出变得复杂,因为开发人员必须确保请求的发起者和处理请求的实际服务器之间的所有服务器都能理解新协议,然后才能切换以开始使用新协议。 protocol buffers开发用于解决这些问题: 可以轻松引入新字段,而中间服务器不需要检查数据就可以简单地解析它并传递数据而无需了解所有字段。 格式更具自我描述性,可以用各种语言(C++,Java等)处理。 但是,用户仍然需要自己手写解析代码。 随着系统的发展,它获得了许多其他功能和用途: 自动生成序列化和反序列化代码避免了手动解析的需要。 除了用于短生命周期的RPC(远程过程调用)请求之外,大家已经开始使用protocol buffers作为一种方便的自描述格式用于持久存储数据(例如,在Bigtable中)。 首先服务的RPC接口被声明为protocol文件的一部分,然后使用protocol编译器生成stub类,用户可以通过服务接口的实际实现来覆盖这些stub类。 现在protocol buffers是Google的数据通用语言,在撰写本文时,Google代码树中定义了306,747种不同的消息类型,跨348,952个.proto文件。它们既可用于RPC系统,也可用于各种存储系统中的数据持久存储。
0.1. 定义消息类型 0.1.1. 指定字段类型 0.1.2. 分配字段编号 0.1.3. 自定字段规则 0.1.4. 添加更多消息类型 0.1.5. 添加注释 0.1.6. 保留字段 0.1.7. .proto文件将生成什么 0.2. 标量值类型 0.3. 默认值 0.4. 枚举 0.4.1. 保留值 0.5. 使用其他消息类型 0.5.1. 导入定义 0.5.2. 使用proto2消息类型 0.6. 嵌套类型 0.7. 更新消息类型 0.8. 未知字段 0.9. Any 0.10. Oneof 0.10.1. 使用Oneof 0.10.2. Oneof的功能 0.10.3. 向后兼容性问题 0.10.3.1. 标签重用问题 0.11. Maps 0.11.1. 向后兼容性 0.12. Packages 0.12.1. 包和名称解析 0.13. 定义服务 0.14. JSON映射 0.14.1. JSON选项 0.15. 可用选项 0.15.1. 自定义选项 0.16. 生成自定义的类 本指南介绍如何使用protocol buffers语言构建protocol buffers数据,包括: .proto文件语法 如何从.proto文件生成数据访问类 它涵盖了protocol buffers语言的proto3版本:有关较早的proto2语法的信息,请参阅Proto2语言指南。 这是一个参考指南,对于使用本文档中描述的许多功能的分步示例,请参阅所选语言的教程(目前仅限proto2,更多proto3文档即将推出)。 0.1. 定义消息类型 首先看一个非常简单的例子。假设要定义搜索请求消息格式,其中每个搜索请求都有: 一个字符串类型的查询 所查询的特定页码 每页返回的结果数 这是用于定义消息类型的.proto文件。 // 指定正在使用proto3语法,默认使用proto2,必须是文件的第一个非空注释行 syntax = "proto3";message SearchRequest { // 消息格式以名称-值对的形式指定三个字段 string query = 1; // 每个字段有一个名称和类型 int32 page_number = 2; int32 result_per_page = 3;} 0.1.1. 指定字段类型 在上面的例子中,所有的字段都是标量类型:两个整型一个字符串类型。同时也可以给字段指定组合类型(包括枚举或其他类型)。 0.1.2. 分配字段编号 如上所示,消息定义中的每个字段都定义一个唯一的编号。这些字段的编号用于在消息的二进制格式中标识字段,一旦消息类型被使用就不能再更改。请注意: 1到15范围内的字段编号需要一个字节进行编码,包括字段的编号和字段的类型(可以在protocol buffers编码中找到更多相关信息) 16到2047范围内的字段编号占用两个字节。 因此,应该为非常频繁出现的消息元素保留数字1到15,请记住为将来可能添加的频繁出现的元素留出一些空间 可以指定的最小字段数为1,最大字段数为536,870,911(2的29次方-1)。 不能使用数字19000到19999(FieldDescriptor::kFirstReservedNumber到FieldDescriptor::kLastReservedNumber),因为它们是为protocol buffers实现而保留的。 如果在.proto中使用这些保留数字之一,protocol buffers编译器会发出警告。同样,不能使用任何以前保留的字段编号。 0.1.3. 自定字段规则 消息的字段可以是以下之一: 单数:格式良好的消息可以包含零个或一个(但不超过一个)这样的字段。这是proto3语法的默认字段规则。 重复:该字段可以在格式良好的消息中重复任意次数(包括零)。将保留重复值的顺序。 在proto3中,标量数字类型的重复字段默认使用压缩编码。 在Protocol Buffer Encoding中找到有关压缩编码的更多信息。 0.1.4. 添加更多消息类型 可以在单个.proto文件中定义多种消息类型。如果要定义多个相关消息,这非常有用。例如,如果要定义与SearchResponse消息类型对应的回复消息格式,则可以将其添加到相同的.proto文件中: message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3;}message SearchResponse { ...} 0.1.5. 添加注释 要为.proto文件添加注释,请使用C/C++样式//和/* ... */语法。 /* SearchRequest 代表一个查询请求, * 带有分页选项以指示要包含在响应中的结果。*/message SearchRequest { string query = 1; int32 page_number = 2; // 我们需要的页码 int32 result_per_page = 3; // 每页返回的结果数 } 0.1.6. 保留字段 如果通过完全删除字段或将其注释来更新消息类型,未来的用户可以在对类型进行更新时再次使用该字段编号。如果以后加载相同.proto文件的旧版本,这可能会导致严重问题,包括数据损坏,隐私错误等。确保不会发生这种情况的一种方法是指定已删除字段或字段的编号为保留的(否则可能导致JSON序列化问题)。如果将来的任何用户尝试使用这些字段标识符,protocol buffers编译器将会发出警告。 message Foo { reserved 2, 15, 9 to 11; reserved "foo", "bar";} 请注意,不能在同一保留语句中混合字段名称和字段编号。 0.1.7. .proto文件将生成什么 在.proto文件上运行protocol buffers编译器时,编译器会根据文件中的描述生成所选语言的代码,这些代码是需要使用的消息类型,包括:获取和设置字段值,将消息序列化为输出流,并从输入流中解析消息。 对于C++,编译器会从每个.proto生成一个.h和.cc文件,并为文件中描述的每种消息类型提供一个类。 对于Java,编译器生成一个.java文件,其中包含每种消息类型的类,以及用于创建消息类实例的特殊Builder类。 Python有点不同:Python编译器生成一个模块,其中包含.proto中每种消息类型的静态描述符,然后与元类一起使用,以在运行时创建必要的Python数据访问类。 对于Go,编译器会生成一个.pb.go文件,其中包含文件中每种消息类型的类型。 对于Ruby,编译器生成一个带有包含消息类型的Ruby模块的.rb文件。 对于Objective-C,编译器从每个.proto生成一个pbobjc.h和pbobjc.m文件,其中包含文件中描述的每种消息类型的类。 对于C#,编译器从每个.proto生成一个.cs文件,其中包含文件中描述的每种消息类型的类。 对于Dart,编译器会生成一个.pb.dart文件,其中包含文件中每种消息类型的类。 可以按照所选语言的教程(即将推出的proto3版本)了解有关为每种语言使用API的更多信息。有关更多API详细信息,请参阅相关API参考(proto3版本即将推出)。 0.2. 标量值类型 标量消息字段可以具有以下类型之一:该表显示.proto文件中指定的类型,以及自动生成的类中的相应类型: .proto 类型 注释 Go类型 double float64 float float32 int32 使用可变长度编码,编码负数的效率低(如果字段可能有负值,改用sint32) int32 int64 使用可变长度编码,编码负数的效率低(如果字段可能有负值,改用sint64) int64 uint32 使用可变长度编码 uint32 uint64 使用可变长度编码 uint64 sint32 使用可变长度编码,有符号int值(这比常规int32更有效地编码负数) int32 sint64 使用可变长度编码,有符号int值(这比常规int64更有效地编码负数) int64 fixed32 总是四个字节,如果值大于2的28次方则比uint32更有效 uint32 fixed64 总是八个字节,如果值大于2的56次方则比uint32更有效 uint64 sfixed32 总是四个字节 int32 sfixed64 总是八个字节 int64 bool bool string 字符串必须始终包含UTF-8编码或7位ASCII文本,且不能超过2的32次方 string bytes 可以包含不超过2的32次方的任意字节序列 []bytes 在protocol buffers编码中可以找到更多关于在序列化消息时这些类型是如何被编码的信息。 0.3. 默认值 在解析消息时,如果编码消息不包含某个特定的单数元素,则解析对象中相应的字段将被设置为该字段的默认值。这些默认值根据类型而不同: 对于字符串,默认值为空字符串。 对于字节,默认值为空字节。 对于布尔型,默认值为false。 对于数字类型,默认值为零。 对于枚举,默认值是第一个定义的枚举值,该值必须为0。 对于消息字段,未设置该字段。它的确切值取决于编程语言。有关详细信息,请参阅生成代码指南。 重复(repeated)字段的默认值为空(通常是相应编程语言的空列表)。 请注意,对于标量消息字段,一旦解析了消息,就无法确定字段是否显式设置为默认值(例如,布尔值是否设置为false)或根本没有设置值,因此,在定义消息类型时要注意。例如,如果不希望默认情况下也发生这种行为,那么当设置为false时,没有一个布尔值可以打开某些行为。另外请注意,如果标量消息字段设置为其默认值,则该值不会在传输时序列化。 有关默认值如何在生成的代码中工作的更多详细信息,请参阅所选语言的生成代码指南。 0.4. 枚举 在定义消息类型时,可能希望其中一个字段只有一个预定义的值列表。例如,假设要为每个SearchRequest添加语料库(corups)字段,其中语料库可以是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;} 如上所示,Corpus枚举的第一个常量映射为零:每个枚举定义必须包含一个映射到零的常量作为其第一个元素。这是因为: 必须有一个零值,以便可以使用0作为数字默认值。 零值必须是第一个元素,以便与proto2语义兼容,其中第一个枚举值始终是默认值。 可以通过为不同的枚举常量指定相同的值来定义别名。为此,需要将allow_alias选项设置为true,否则proto编译器将在找到别名时生成错误消息。 enum EnumAllowingAlias { option allow_alias = true; // 开启配置 UNKNOWN = 0; STARTED = 1; RUNNING = 1;}enum EnumNotAllowingAlias { UNKNOWN = 0; STARTED = 1; // RUNNING = 1; // 取消注释此行将导致Google内部的编译错误和外部的警告消息。 } 枚举常量必须在32位整数范围内。由于枚举值在传输时使用varint编码,因此,负值效率低,不建议使用。可以在消息定义中定义枚举,如上例所示,也可以在外部定义枚举,这些枚举可以在.proto文件中的任何消息定义中重用。还可以使用语法MessageType.EnumType将一个消息中声明的枚举类型用作不同消息中字段的类型。 在使用枚举的.proto文件上运行protocol buffers编译器时,生成的代码将具有相应的Java或C++枚举类型,在Python中使用一个特殊的EnumDescriptor类,用于在运行时生成类中创建一组带有整数值的符号常量。 在反序列化期间,无法识别的枚举值将保留在消息中,如何表示这种值取决于具体的编程语言。 在支持具有超出指定符号范围的值的开放枚举类型的编程语言中(例如C++和Go),未知的枚举值仅作为其基础整数表示存储。 在支持封闭枚举类型(如Java)的编程语言中,枚举中的大小写用于表示无法识别的值,并且可以使用特殊访问器访问基础整数。 在任何一种情况下,如果消息被序列化了,那么无法识别的值仍然会和消息一起被序列化。 有关如何在应用程序中使用消息枚举的详细信息,请参阅所选语言的生成代码指南。 0.4.1. 保留值 如果通过完全删除枚举条目或将其注释掉来更新枚举类型,那么未来的用户可以在对类型进行更新时重用该数值。如果后面又加载相同.proto文件的旧版本,这可能会导致严重问题(包括数据损坏,隐私错误等)。确保不会发生这种情况的一种方法是设置已删除条目的数值(和/或名称,也可能导致JSON序列化问题))为reserved。如果将来的任何用户尝试使用这些标识符,protocol buffers编译器将会发出警告。可以使用max关键字指定保留(reserved)的数值范围达到最大可能值。 enum Foo { reserved 2, 15, 9 to 11, 40 to max; reserved "FOO", "BAR";} 请注意,不能在同一保留(reserved)语句中混合字段名称和数值。 0.5. 使用其他消息类型 可以使用其他消息类型作为字段类型。例如,假设在每个SearchResponse消息中包含Result消息,为此,可以在同一.proto中定义Result消息类型,然后在SearchResponse中指定Result类型的字段: message SearchResponse { repeated Result results = 1;}message Result { string url = 1; string title = 2; repeated string snippets = 3;} 0.5.1. 导入定义 在上面的示例中,Result消息类型在与SearchResponse相同的文件中定义,如果要用作字段类型的消息类型在另一个.proto文件中定义,可以通过导入来使用其他.proto文件中的定义。 要导入另一个.proto的定义,请在文件顶部添加一个import语句: import "myproject/other_protos.proto"; 默认情况下,只能使用直接导入的.proto文件中的定义。但是,有时可能需要将.proto文件移动到新位置。那么,可以在旧位置放置一个虚拟.proto文件,以使用import public将所有导入转发到新位置,而不是直接移动.proto文件并在一次更改中更新所有导入它的文件。import public的依赖可以被任何包含import public语句的proto文件传递。 例如: // new.proto // 所有定义都移动到了这里。 // old.proto // 这是proto文件,所有的clients都导入了它。 import public "new.proto";import "other.proto";// client.proto import "old.proto";// 使用old.proto和new.proto中的定义,但不使用other.proto proto编译器使用-I/--proto_path标志在编译器命令行中指定的一组目录中搜索导入的文件。如果没有给出标志,它将查找调用编译器的目录。通常,应将--proto_path标志设置为项目的根目录,并对所有导入使用完全限定名称。 0.5.2. 使用proto2消息类型 可以导入proto2消息类型并在proto3消息中使用它们,反之亦然。但是,proto2枚举不能直接用于proto3语法(如果已导入的proto2消息使用它们就没关系)。 0.6. 嵌套类型 可以在其他消息类型中定义和使用消息类型,如下例所示:此处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; } }} 0.7. 更新消息类型 如果现有的消息类型不再满足需求,例如,希望消息格式具有额外的字段,但仍然希望使用旧格式创建的代码。在不破坏任何现有代码的情况下更新消息类型非常简单。请记住以下规则: 请勿更改任何现有字段的字段编号。 如果添加新字段,则使用“旧”消息格式序列化的任何消息仍可由新生成的代码进行解析。应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。同样的新代码创建的消息可以由旧代码解析,旧的二进制文件在解析时只是忽略新字段。有关详细信息,请参阅“未知字段”部分。 在更新的消息类型中不再使用的字段编号就可以删除。 有时想要重命名该字段,可能添加前缀“OBSOLETE_”,或者将字段编号设置为保留(reserved),以便.proto的未来用户不会意外地重复使用该编号。 int32,uint32,int64,uint64和bool都是兼容的,这意味着可以将字段从这些类型之一更改为另一种类型,而不会破坏向前或向后兼容性。如果在传输中解析出一个不适合相应类型的数字,将获得与在C++中将该数字转换为该类型相同的效果(例如,如果将64位数字作为int32读取,它将被截断为32位)。 sint32和sint64彼此兼容,但与其他整数类型不兼容。 只要byte是有效的UTF-8,string和byte是兼容的。 如果byte包含消息的编码版本,则嵌入消息与byte兼容。 fixed32与sfixed32兼容,fixed64与sfixed64兼容。 enum在传输格式中与int32,uint32,int64和uint64兼容(请注意,如果值不合适,将截断值)。但请注意,在反序列化消息时,客户端代码可能会以不同方式对待它们:例如,无法识别的proto3枚举类型将保留在消息中,但在反序列化消息时如何表示它是依赖于编程语言的。Int字段总是保留它们的值。 将单个值更改为新oneof的成员是安全且二进制兼容的。如果确保没有代码一次设置多个字段,那么将多个字段移动到新的oneof可能是安全的。将任何字段移动到某个现有的oneof中都是不安全的。 0.8. 未知字段 未知字段是格式良好的protocol buffers序列化数据,它表示解析器无法识别的字段。例如,当旧二进制文件解析具有新字段的新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。 最初,proto3消息在解析期间总是丢弃未知字段,但在3.5版本中,重新引入了未知字段的保存以匹配proto2行为。在版本3.5及更高版本中,未知字段在解析期间保留并包含在序列化输出中。 0.9. Any Any消息类型允许将消息用作嵌入类型,而无需使用这些消息的.proto定义。Any包含任意序列化消息(如byte),并带有一个URL作为该消息类型的全局唯一标识符用于表示和解析它。要使用Any类型,需要导入google/protobuf/any.proto。 import "google/protobuf/any.proto";message ErrorStatus { string message = 1; repeated google.protobuf.Any details = 2;} type.googleapis.com/packagename.messagename是给定消息类型的默认类型URL。 不同的编程语言实现将支持运行时库来帮助程序以类型安全的方式打包和解压缩Any值。例如,在Java中,Any类型将具有特殊的pack()和unpack()访问器,而在C++中则有PackFrom()和UnpackTo ()方法: // 在Any中存储任意消息类型。 NetworkErrorDetails details = ...; ErrorStatus status; status.add_details()->PackFrom(details); // 从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类型将替换扩展。 0.10. Oneof 如果有一个包含许多字段的消息,并且最多只能同时设置一个字段,则可以使用oneof来强制执行此操作同时还能节省内存。 oneof字段与正常的字段一样,只是在同一个oneof中的所有字段共享内存,并且最多可以同时设置一个字段。设置oneof中的任何一个成员时都会自动清除所有其他成员。可以使用case()或WhichOneof()方法检查oneof中的哪个值(如果有)被设置了,具体使用哪个取决于选择的编程语言。 0.10.1. 使用Oneof 要在.proto中定义oneof,请使用oneof关键字,并在后面跟着oneof名称,在本例中为test_oneof: message SampleMessage { oneof test_oneof { string name = 4; SubMessage sub_message = 9; }} 然后,将oneof字段添加到oneof定义中。可以添加任何类型的字段,但不能使用repeated字段。 在生成的代码中,oneof字段与常规字段具有相同的getter和setter。还可以获得一种特殊方法来检查oneof中设置了哪个值(如果有)。可以在相关API参考中找到有关所选语言的oneof API的更多信息。 0.10.2. 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字段设置为默认值(例如将int32 oneof字段设置为0),那么该oneof字段的“case”将会被设置,并且这些字段的值将在传输时序列化。 如果使用的是C++,请确保代码不会导致内存崩溃。以下示例代码将崩溃,因为通过调用set_name()方法删除了sub_message。 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的消息,则每条消息都将以另一条消息的“case”作为结束:在下面的示例中,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()); 0.10.3. 向后兼容性问题 添加或删除oneof字段时要小心。如果在检查oneof的值返回None/NOT_SET,这可能意味着oneof尚未设置或已设置为oneof的另一个不同版本。没有办法区分,因为没有办法知道传输中的未知字段是否是oneof的成员。 0.10.3.1. 标签重用问题 将字段移入或移出oneof:在序列化或解析消息后,可能会丢失一些信息(某些字段将被清除)。但是,可以安全地将单个字段移动到新的oneof字段中,并且如果已知只有一个字段被设置,则可以移动多个字段。 删除oneof字段并将其添加回:在序列化或解析消息后,这可能会清除当前设置的oneof字段。 拆分或合并oneof:这与移动常规字段有类似的问题。 0.11. Maps 如果要在数据定义中创建关联映射,protocol buffers提供了一种方便的快捷方式语法: map<key_type, value_type> map_field = N; 其中key_type可以是任何整数或字符串类型(除了浮点类型和字节之外的任何标量类型)。请注意,枚举不是有效的key_type。 value_type可以是map之外的任何类型。 如果要创建一个map,其中每个Project消息与字符串键相关联,则可以像下面这样定义它: map<string, Project> projects = 3; map的字段不能是repeated。 传输格式的顺序和map值的迭代顺序是未定义的,因此不能依赖map中的项目按特定顺序排序。 从.proto文件中生成文本格式时,map按键排序,数字键按数字排序。 在传输或合并时进行解析,如果有重复的map键,那么就使用最后的那个键。从文本格式解析map时,如果存在重复键,则解析可能会失败。 如果给map提供了键却没有提供值,那么字段序列化的具体行为就取决于具体的编程语言。在c++,Java,Python中使用类型的默认值进行序列化,在其他编程语言中,没有值被序列化。 目前,所有proto3支持的编程语言都能生成mapAPI,更多关于所选语言的mapAPI的参考查看API参考文档。 0.11.1. 向后兼容性 在传输时,map的语法等效于下面的示例,因此不支持map的protocol buffers实现仍然可以处理传输的数据: message MapFieldEntry { key_type key = 1; value_type value = 2;}repeated MapFieldEntry map_field = N; 支持map的任何protocol buffers实现都必须生成和接受上述定义所表示的可接受的数据。 0.12. Packages 可以将可选的package说明符添加到.proto文件,以防止protocol buffers消息类型之间的名称冲突。 package foo.bar;message Open { ... } 然后,可以在定义消息类型的字段时使用package说明符: message Foo { ... foo.bar.Open open = 1; ...} package说明符影响生成代码的方式取决于选择的编程语言: 在C++中,生成的类包含在C++命名空间中。例如,Open将位于命名空间foo::bar中。 在Java中,除非在.proto文件中明确提供选项java_package,否则该包将用作Java包。 在Python中,将忽略package指令,因为Python模块是根据它们在文件系统中的位置进行组织的。 在Go中,除非在.proto文件中明确提供选项go_package,否则该包将用作Go包名称。 在Ruby中,生成的类包含在嵌套的Ruby命名空间中,转换为所需的Ruby大写形式(首字母大写;如果第一个字符不是字母,则PB_前置)。例如,Open将位于名称空间Foo::Bar中。 在C#中,转换为PascalCase后,包将用作命名空间,除非在.proto文件中明确提供选项csharp_namespace。例如,Open将位于名称空间Foo.Bar中。 0.12.1. 包和名称解析 protocol buffers语言中的类型名称解析与C++类似:首先搜索最里面的范围,然后搜索下一个范围,依此类推,每个包被认为是其父包的“内部”。一个'.' (例如.foo.bar.Baz)意味着从最外层的范围开始。 protocol buffers编译器通过解析导入的.proto文件来解析所有类型名称。每种编程语言的代码生成器都知道如何引用该语言中的每种类型,即使它具有不同的范围规则。 0.13. 定义服务 如果要在RPC(远程过程调用)系统中使用自定义消息类型,可以在.proto文件中定义RPC服务接口protocol buffers编译器将以选择的编程语言生成服务接口代码和stub。 例如,要定义一个RPC服务,该服务获取SearchRequest请求并返回SearchResponse响应消息,可以在.proto文件中定义它,如下所示: service SearchService { rpc Search (SearchRequest) returns (SearchResponse);} 与protocol buffers一起使用的最简单的RPC系统是gRPC:一种由Google开发的语言平台中立的开源RPC系统。gRPC特别适用于protocol buffers,并允许使用特定的protocol buffers编译器插件直接从.proto文件生成相关的RPC代码。 如果不想使用gRPC,也可以将protocol buffers与自定义的RPC实现一起使用。可以在Proto2语言指南中找到更多相关信息。 还有一些正在进行的第三方项目为Protocol Buffers开发RPC实现。有关我们了解的项目的链接列表,请参阅第三方加载项wiki页面。 0.14. JSON映射 Proto3支持JSON中的规范编码,使得在系统之间共享数据变得更加容易。在下表中逐个类型地描述编码。 如果JSON编码数据中缺少某个值,或者其值为null,则在解析为protocol buffers时,它将被解释为相应的默认值。如果某个字段在protocol buffers中具有默认值,则默认情况下将在JSON编码的数据中省略该字段以节省空间。一种实现方式是提供可选选项在JSON编码的输出中输出字段和它的默认值。 proto3 JSON JSON example Notes message object {"fooBar": v, "g": null, …} 生成JSON对象。message字段名称映射到小驼峰命名并成为JSON对象的key。如果指定了json_name字段这个选项,则将指定的值作为key。解析器接受小驼峰命名的名称(或json_name选项指定的名称)和原始的proto字段名称。null是所有字段类型都可接受的值,并被视为相应字段类型的默认值。 enum string "FOO_BAR" 使用proto中指定的枚举值的名称。解析器接受枚举名称和整数值。 map object {"k": v, …} 所有键都转换为字符串。 repeated V array [v, …] null被接受为空的list[]。 bool true, false true, false string string "Hello World!" bytes base64 string "YWJjMTIzIT8kKiYoKSctPUB+" JSON值将是使用带填充的标准base64编码方式编码的字符串数据。带有/不带填充的标准或URL安全的base64编码方式也是可接受的。 int32, fixed32, uint32 number 1, -10, 0 十进制数形式的JSON值,接受数字或字符串。 int64, fixed64, uint64 string "1", "-10" 十进制字符串形式的JSON值,接受数字或字符串。 float, double number 1.1, -10.0, 0, "NaN", "Infinity" 一个或多个特殊字符串“NaN”,“Infinity”和“-Infinity”形式的JSON值,接受数字或字符串,指数表示法也被接受。 Any object {"@type": "url", "f": v, … } 如果Any包含具有特殊JSON映射的值,则它将按如下方式转换:{“@ type”:xxx,“value”:yyy}。 否则,该值将转换为JSON对象,并将插入“@type”字段以指示实际数据类型。 Timestamp string "1972-01-01T10:00:20.021Z" 使用RFC 3339,其中生成的输出将始终被Z-标准化并使用0,3,6或9个小数位。也接受“Z”以外的偏移。 Duration string "1.000340012s", "1s" 生成的输出始终包含0,3,6或9个小数位,具体取决于所需的精度,后跟后缀“s”。接受任何小数位(也可以没有小数位),只要它们符合纳秒精度并且需要后缀“s”。 Struct object { … } 任意JSON对象,查看struct.proto文件 Wrapper types various types 2, "2", "foo", true, "true", null, 0, … Wrappers在JSON中使用与包装基元类型相同的表示形式,除了在数据转换和传输期间允许和保留null。 FieldMask string "f.fooBar,h" 查看field_mask.proto文件 ListValue array [foo, bar, …] Value value 任意JSON值 NullValue null JSON中的null Empty object {} 空的JSON对象 0.14.1. JSON选项 proto3 JSON实现可以提供以下可用选项: 输出字段的默认值:在proto3 JSON的输出中默认省略字段的默认值。有一个选项可以提供覆盖此行为并输出字段的默认值。 忽略未知字段:默认情况下,proto3 JSON解析器会拒绝未知字段,可以提供一个选项来忽略解析未知字段。 使用proto字段名称而不是小驼峰命名的名称:默认情况下,proto3 JSON会将字段名称转换为小驼峰命名并将其用作JSON的名称。有一个选项可以提供使用proto字段名称作为JSON的名称。proto3 JSON解析器需要接受转换后的小驼峰命名的名称和proto字段名称。 将枚举值作为整数而不是字符串输出:默认情况下,在JSON输出中使用枚举值的名称。可以提供一个选项以使用枚举值的数值。 0.15. 可用选项 .proto文件中的各个声明可以使用许多选项进行注释。选项不会更改声明的整体含义,但可能会影响该声明在特定上下文中被处理的方式。可用选项的完整列表在google/protobuf/descriptor.proto中定义。 一些选项是文件级选项,这意味着它们应该在顶级范围内编写,而不是在任何message、enum或service的定义中。 一些选项是消息级选项,这意味着它们应该写在message定义中。 一些选项是字段级选项,这意味着它们应该写在字段定义中。 可用选项也可以写在枚举类型,枚举值,服务类型和服务方法上,但是,目前没有任何支持这些的可用选项。 以下是一些最常用的选项: java_package(文件级选项):用于生成的Java类的包。如果.proto文件中没有给出显式的java_package选项,那么默认情况下将使用proto包(.proto文件中的“package”关键字指定的包)。但是,proto包通常不能生成好的Java包,因为proto包不会以反向域名开头。如果不生成Java代码,则此选项无效。 option java_package = "com.example.foo"; java_multiple_files(文件级选项):生成在包级别中定义的顶级message、enum和service,而不是在.proto文件之后命名的外部类中。 option java_multiple_files = true; java_outer_classname(文件级选项):生成最外层的Java类的类名(以及文件名)。如果.proto文件中没有指定显式的java_outer_classname,则通过将.proto文件名转换为驼峰命名来构造类名(因此foo_bar.proto变为FooBar.java)。如果不生成Java代码,则此选项无效。 option java_outer_classname = "Ponycopter"; optimize_for(文件级选项):可以设置为SPEED,CODE_SIZE或LITE_RUNTIME。这会影响C++和Java代码生成器(可能还有第三方生成器),以下列方式: option optimize_for = CODE_SIZE; SPEED(default):protocol buffers编译器将生成用于对消息类型进行序列化、解析和执行其他常见操作的代码。此代码经过高度优化。 CODE_SIZE:protocol buffers编译器将生成最小的类,并依赖于基于反射的共享代码来实现序列化、解析和各种其他操作。因此生成的代码将比使用SPEED小得多,但操作会更慢。生成的类仍将实现与SPEED模式完全相同的公共API。此模式在包含大量.proto文件的应用程序中最有用,并且不需要所有这些文件都非常快速。 LITE_RUNTIME:protocol buffers编译器将生成仅依赖于“lite”的运行时库的类(即依赖于libprotobuf-lite而不是libprotobuf)。lite运行时库比完整库小得多(大约小一个数量级),其中省略了描述符和反射等功能。这对于在移动电话等受限平台上运行的应用程序尤其有用。编译器仍将生成所有方法的快速实现,就像在SPEED模式下那样。生成的类将仅实现每种语言的MessageLite接口,该接口仅提供完整Message接口的方法的子集。 ` cc_enable_arenas(文件级选项):为C++生成的代码启用竞技场分配。 objc_class_prefix(文件级选项):设置Objective-C类前缀,该前缀由此.proto文件提供给所有Objective-C生成的类和枚举,没有默认值。应该使用Apple建议的3-5个大写字符之间的前缀。请注意,Apple保留所有2个字母的前缀。 deprecated(文件级选项):如果设置为true,则表示该字段已弃用,新代码不应使用该字段。在大多数语言中,这没有实际效果。在Java中,这将成为@Deprecated注释。将来,其他特定语言的代码生成器可能会在字段的访问器上生成弃用注释,这将导致在编译尝试使用该字段的代码时发出警告。如果任何人都没有使用该字段,并且想要阻止新用户使用该字段,请考虑使用保留语句替换字段声明。 int32 old_field = 6 [deprecated=true]; 0.15.1. 自定义选项 Protocol Buffers还允许定义和使用自定义的选项。这是大多数人不需要的高级功能。如果确实认为需要创建自定义的选项,请参阅proto2语言指南以获取详细信息。请注意,创建自定义的选项使用的扩展仅允许用于proto3中的自定义的选项。 0.16. 生成自定义的类 需要使.proto文件中定义的消息类型来生成Java,Python,C++,Go,Ruby,Objective-C或C#代码,这需要在.proto上运行protocol buffers编译器protoc。如果尚未安装编译器,请下载该软件包并按照自述文件中的说明进行操作。对于Go,还需要为编译器安装一个特殊的代码生成器插件,可以在GitHub上的golang/protobuf存储库中找到这个插件和安装说明。 协议编译器的调用如下: 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指定解析导入指令时查找.proto文件的目录,如果省略,则使用当前目录。可以通过多次传递--proto_path选项来指定多个导入目录,将按顺序搜索。-I=IMPORT_PATH可以用作--proto_path的缩写形式。 可以提供一个或多个输出指令: --cpp_out在DST_DIR中生成C++代码。有关更多信息,请参阅C++生成代码参考。 --java_out在DST_DIR中生成Java代码。有关更多信息,请参阅Java生成代码参考。 --python_out在DST_DIR中生成Python代码。有关更多信息,请参阅Python生成代码参考。 --go_out在DST_DIR中生成Go代码。有关更多信息,请参阅Go生成代码参考。 --ruby_out在DST_DIR中生成Ruby代码。Ruby生成的代码参考即将推出! --objc_out在DST_DIR中生成Objective-C代码。有关更多信息,请参阅Objective-C生成的代码参考。 --csharp_out在DST_DIR中生成C#代码。有关更多信息,请参阅C#生成代码参考。 --php_out在DST_DIR中生成PHP代码。有关更多信息,请参阅PHP生成代码参考。 为方便起见,如果DST_DIR以.zip或.jar结尾,编译器会将输出写入具有给定名称的单个ZIP格式存档文件。.jar输出还将根据Java JAR规范的要求提供清单文件。请注意,如果输出存档已存在,则会被覆盖; 编译器不够智能,无法将文件添加到现有存档中。 必须提供一个或多个.proto文件作为输入。可以一次指定多个.proto文件。虽然文件是相对于当前目录命名的,但每个文件必须驻留在其中一个IMPORT_PATH中,以便编译器可以确定其规范名称。
0.1. 标准文件格式 0.2. 文件结构 0.3. 包 0.4. 消息和字段名称 0.5. Repeated 字段 0.6. 枚举 0.7. Servies(服务) 0.8. 要避免的事情 本文档提供.proto文件的样式指南。通过遵循这些约定,将使protocol buffers消息定义及其相应的类保持一致且易于阅读。 请注意,protocol buffers样式随着时间的推移而发展,因此可能会看到以不同约定或样式编写的.proto文件。修改这些文件时请尊重现有样式,一致性是关键。但是,在创建新的.proto文件时,最好采用当前最佳样式。 0.1. 标准文件格式 保持行长度为80个字符。 使用2个空格的缩进。 0.2. 文件结构 文件应命名为lower_snake_case.proto(小蛇式) 文件或变量命令方式: 大驼峰式:CamelCase 小驼峰式:camelCase 大蛇式:GET_USER_NAME 小蛇式:get_user_name 烤肉串式:get-user-name 按照以下方式排序所有文件: 许可证标题(如果适用) 文件概述 句法(syntax) 包(package) 导入(分类,import) 文件选项 其他 0.3. 包 包名称应为小写,并且应与目录层次结构相对应。例如,如果文件在my/package/中,那么包名应该是my.package。 0.4. 消息和字段名称 将CamelCase(带有初始大写)用于消息名称。例如,SongServerRequest。 将underscore_separated_names用于字段名称(包括oneof字段和扩展名称)。例如,song_name。 message SongServerRequest { required string song_name = 1;} 对字段名称使用此命名约定可提供以下访问器: C++: const string& song_name() { ... } void set_song_name(const string& x) { ... } Java: public String getSongName() { ... } public Builder setSongName(String v) { ... } 如果字段名称包含数字,则该数字应显示在字母后面而不是下划线之后。例如,使用song_name1而不是song_name_1。 0.5. Repeated 字段 对repeated字段使用复数名称。 repeated string keys = 1; ... repeated MyMessage accounts = 17; 0.6. 枚举 对于枚举类型名称使用CamelCase(带有初始大写),对值名称使用CAPITALS_WITH_UNDERSCORES(大蛇式): enum Foo { FOO_UNSPECIFIED = 0; FOO_FIRST_VALUE = 1; FOO_SECOND_VALUE = 2;} 每个枚举值应以分号结束,而不是逗号。更喜欢为枚举值添加前缀,而不是将其包围在封闭消息中。零值枚举应具有后缀UNSPECIFIED(缺省)。 0.7. Servies(服务) 如果.proto定义了RPC服务,则应该对服务名称和任何RPC方法名称使用CamelCase(带有初始大写): service FooService { rpc GetSomething(FooRequest) returns (FooResponse);} 0.8. 要避免的事情 Required字段(仅适用于proto2) Groups(仅适用于proto2)
0.1. 一个简单的消息 0.2. Base 128 Varint 0.3. 消息结构 0.4. 更多值类型 0.4.1. 有符号整数 0.4.2. 非varint数字 0.4.3. Strings 0.5. 嵌入式消息 0.6. 可选和重复元素 0.6.1. 压缩重复字段 0.7. 字段顺序 0.8. 启示 0.1. 一个简单的消息 本文档描述了protocol buffers消息的二进制传输格式。不需要了解这一点就可以在应用程序中使用protocol buffers,但了解不同的protocol buffers格式如何影响编码消息的大小是非常有用。 假设有以下非常简单的消息定义: message Test1 { optional int32 a = 1; } 在应用程序中,创建一个Test1消息并将a设置为150,然后将消息序列化为输出流。如果能够检查编码的消息,会看到三个字节: 08 96 01 到目前为止,这么小的数字,它是什么意思?继续阅读...... 0.2. Base 128 Varint 要了解简单的protocol buffers编码,首先需要了解varints,它是一种使用一个或多个字节序列化整数的方法。较小的数字占用较少的字节数。 varint中的每个字节(最后一个字节除外)都设置了最高有效位(msb,most significant bit):这表示还有很多字节。每个字节的低7位用于存储7位组中的二进制补码表示,最低有效组优先。 因此,如下所示: 0000 0001 // 这里是数字1,它是单个字节,因此`msb`未设置 1010 1100 0000 0010 // 这是300,有点复杂: 怎么知道这是300?首先从每个字节中删除msb,因为这只是告诉我们是否已到达数字的末尾(如所见,它在第一个字节中设置,因为varint中有多个字节) : 1010 1100 0000 0010 → 010 1100 000 0010 可以反转这两组7位,因为varints会将具有最低有效组的数字存储起来。然后连接它们以获得最终的值: 000 0010 010 1100 → 000 0010 ++ 010 1100 → 100101100 → 256 + 32 + 8 + 4 = 300 0.3. 消息结构 protocol buffers消息是一系列键值对。消息的二进制版本只使用字段的数字编号作为键值。每个字段的名称和声明的类型只能通过引用消息类型的定义(即.proto文件)在解码端确定。 对消息进行编码时,键和值将连接成字节流。在解码消息时,解析器需要能够跳过它无法识别的字段。这样,可以将新字段添加到消息中,而不会破坏不了解它们的旧程序。因此,在传输格式消息中每对的“键”实际上是两个值: 来自.proto文件的字段编号 提供足够信息以查找以下值的长度的传输类型 在大多数语言实现中,该键称为标记。 可用的传输类型如下: Type Meaning Used For 0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum 1 64-bit fixed64, sfixed64, double 2 Length-delimited string, bytes, embedded messages, packed repeated fields 3 Start group groups (deprecated) 4 End group groups (deprecated) 5 32-bit fixed32, sfixed32, float 流式消息中的每个键都是带有值(field_number<<3)| wire_type 的varint。换句话说,数字的最后三位存储传输类型。 现在让再看一下上面的例子。现在知道流中的第一个数字总是一个varint键,这里是08,或者(删除msb): 000 1000 取最后三位得到传输类型(0),然后右移三次得到字段编号(1)。所以现在知道字段编号是1,以及下一个值是varint。使用上一节中的varint-decoding知识,可以看到接下来的两个字节存储值150。 96 01 = 1001 0110 0000 0001 → 000 0001 ++ 001 0110 (drop the msb and reverse the groups of 7 bits) → 10010110 → 128 + 16 + 4 + 2 = 150 0.4. 更多值类型 0.4.1. 有符号整数 正如在上一节中看到的,与传输类型0关联的所有protocol buffers类型都被编码为varints。但是,在编码负数时,signed int类型(sint32和sint64)与“standard”int类型(int32和int64)之间存在重要差异。如果使用int32或int64作为负数的类型,则生成的varint总是十个字节长,实际上,它被视为一个非常大的无符号整数。如果使用其中一种有符号类型,则生成的varint使用ZigZag编码,这样效率更高。 ZigZag编码将有符号整数映射到无符号整数,因此具有较小绝对值(例如,-1)的数字也具有较小的varint编码值。它通过正负整数来回“zig-zags”的方式做到这一点,因此-1被编码为1,1被编码为2,-2被编码为3,依此类推,可以在下表中看到: Signed Original Encoded As 0 0 -1 1 1 2 -2 3 2147483647 4294967294 -2147483648 4294967295 换句话说,每个值n被编码: (n << 1) ^ (n >> 31) 成为sint32s,或者: (n << 1) ^ (n >> 63) 成为64为的版本。 注意,第二个移位(n >> 31)部分是算术移位。因此,移位的结果是一个全为零的数字(如果n为正)或全部为一位(如果n为负)。 0.4.2. 非varint数字 非varint数字类型很简单:double和fixed64有传输类型1,它告诉解析器期望一个固定的64位数据块; 类似地,float和fixed32具有传输类型5,这告诉它期望32位。在这两种情况下,值都以little-endian字节顺序存储。 0.4.3. Strings 传输类型为2(长度分隔)表示该值是varint编码长度,后跟指定的数据字节数。 message Test2 { optional string b = 2;} 将b的值设置为“testing”可以得到: 12 07 74 65 73 74 69 6e 67 第三个开始的字节是“testing”的UTF8编码。这里的关键是0x12→字段编号=2,类型=2.值中的长varint是7,它后面的七个字节是所需要的字符串。 0.5. 嵌入式消息 这是一个消息定义,带有示例类型Test1的嵌入消息: message Test3 { optional Test1 c = 3;} 这是编码版本,再次将Test1的a字段设置为150: 1a 03 08 96 01 如上所示,最后三个字节与第一个示例(08 96 01)完全相同,并且它们前面是数字3,嵌入式消息的处理方式与字符串完全相同(传输类型= 2) 。 0.6. 可选和重复元素 如果proto2消息定义具有repeated元素(没有[packed = true]选项),则编码消息具有零个或多个具有相同字段编号的键值对。这些重复值不必连续出现; 它们可能与其他字段交错。解析时保留元素相对于彼此的顺序,尽管丢失了关于其他字段的顺序。 在proto3中,repeated字段使用压缩编码,可以在下面阅读。 对于proto3中的任何非repeated字段或proto2中的optional字段,编码消息可能具有或不具有该字段编号的键值对。 通常,编码消息永远不会有多个非repeated字段的实例。但是,解析器应该处理这些情况。 对于数字类型和字符串,如果多次出现相同的字段,则解析器接受它看到的最后一个值。 对于嵌入式消息字段,解析器合并同一字段的多个实例,就像使用Message::MergeFrom方法一样。也就是说,后一个实例中的所有单个标量字段都替换前者,单个嵌入消息被合并,并且连接重复字段。这些规则的作用是解析两个连接的编码消息产生的结果与分别解析两个消息后并合解析结果所得到的结果完全相同。就是这样: MyMessage message; message.ParseFromString(str1 + str2); 相当于: MyMessage message, message2; message.ParseFromString(str1); message2.ParseFromString(str2); message.MergeFrom(message2); 此属性偶尔会有用,因为它允许合并两条消息,即使不知道它们的类型。 0.6.1. 压缩重复字段 版本2.1.0引入压缩重复字段,在proto2中声明为重复字段,但具有特殊的[packed = true]选项。在proto3中,默认情况下会压缩标量数字类型的重复字段。这些功能类似于重复的字段,但编码方式不同。包含零元素的压缩重复字段不会出现在编码消息中。否则,该字段的所有元素都被压缩到一个键值对中,其中传输类型为2(长度分隔)。每个元素的编码方式与正常情况相同,只是前面没有键。 例如,假设有如下消息类型: message Test4 { repeated int32 d = 4 [packed=true];} 现在假设构造一个Test4,为重复字段d提供值3,270和86942。然后,编码的形式将是: 22 // key (字段编号4, 传输类型2) 06 // payload size (6 bytes) 03 // first element (varint 3) 8E 02 // second element (varint 270) 9E A7 05 // third element (varint 86942) 只有原始数字类型的重复字段(使用varint、32位或64位传输类型的类型)才能声明为“packed”。 请注意,虽然通常没有理由为压缩重复字段编码多个键值对,但编码器必须准备好接受多个键值对。在这种情况下,应该连接有效负载(payloads)。每个键值对必须包含大量元素。 protocol buffers解析器必须能够解析压缩的重复字段,就好像它们没有压缩一样,反之亦然。这允许以向前和后向兼容的方式将[packed = true]添加到现有字段。 0.7. 字段顺序 字段编号可以在.proto文件中以任何顺序使用。选择的顺序对消息的序列化方式没有影响。 当序列化消息时,不能保证其已知或未知字段的写入顺序。序列化顺序是一个实现细节,任何特定实现的细节可能在将来发生变化。因此,protocol buffers解析器必须能够以任何顺序解析字段。 0.8. 启示 不要假设序列化消息的字节输出是稳定的。对于具有表示其他序列化protocol buffers消息的传递字节字段的消息尤其如此。 默认情况下,在同一protocol buffers消息实例上重复调用序列化方法可能不会返回相同的字节输出;即默认序列化不是确定性的。 确定性序列化仅保证特定对于特定的二进制文件输出相同的字节。字节输出可能会在不同版本的二进制文件中发生变化。 对于protocol buffers消息实例foo,以下检查可能会失败。 foo.SerializeAsString() == foo.SerializeAsString() Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString()) CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString()) FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString()) 以下是一些示例场景,其中逻辑上等效的protocol buffers消息foo和bar可以序列化为不同的字节输出。 bar由旧服务器序列化,将某些字段视为未知。 bar由服务器序列化,该服务器以不同的编程语言实现,并按不同顺序序列化字段。 bar有一个以非确定性方式序列化的字段。 bar有一个字段,用于存储protocol buffers消息的序列化字节输出,该消息以不同方式序列化。 bar由新服务器序列化,该服务器由于实现更改因此以不同顺序序列化字段。 foo和bar都是单个消息的串联,但顺序不同。
0.1. 流式传输多条消息 0.2. 大数据集 0.3. 自描述消息 本页描述了一些常用的处理protocol buffers的设计模式。还可以将设计和使用问题发送到protocol buffers讨论组。 0.1. 流式传输多条消息 如果要将多条消息写入单个文件或流,则需要跟踪一条消息的结束位置和下一条消息的开始位置。 protocol buffers传输格式不是自定界限的,因此protocol buffers解析器无法确定消息自身的结束位置。 解决此问题的最简单方法是:在写入消息之前写入每条消息的大小。当重新读取消息时,读取大小,然后将字节读入单独的缓冲区,然后从该缓冲区解析。 如果想避免将字节复制到一个单独的缓冲区,请查看CodedInputStream类(在C++和Java中),该类可以将读取限制为一定的字节数。 0.2. 大数据集 protocol buffers不是为处理大型消息而设计的。作为一般经验法则,如果正在处理大于每兆字节的消息,则可能需要考虑替代策略。 也就是说,protocol buffers非常适合处理大型数据集中的单个消息。通常,大型数据集实际上只是一些小部分的集合,其中每个小部分可能是结构化的数据。尽管protocol buffers无法同时处理整个集合,但使用protocol buffers对每个部分进行编码可以极大地简化问题:现在只需要处理一组字节组成的字符串而不是一整个结构。 protocol buffers不包括对大型数据集的任何内置支持,因为不同的情况需要不同的解决方案。有时是一个简单的记录列表,而有时可能想要更像数据库的东西。每个解决方案都应该作为一个单独的库开发,以便只有需要它的人才需要支付成本。 0.3. 自描述消息 protocol buffers不包含其自身类型的描述。因此,只给出没有定义其类型的原始消息而没有相应的定义它类型的.proto文件,很难提取任何有用的数据。 但是,请注意.proto文件的内容本身可以使用protocol buffers表示。源代码包中的文件src/google/protobuf/descriptor.proto定义了所涉及的消息类型。protoc可以使用--descriptor_set_out选项输出FileDescriptorSet(表示一组.proto文件)。有了这个,可以定义一个自描述协议消息,如下所示: syntax = "proto3";import "google/protobuf/any.proto";import "google/protobuf/descriptor.proto";message SelfDescribingMessage { // Set of FileDescriptorProtos which describe the type and its dependencies. google.protobuf.FileDescriptorSet descriptor_set = 1; // The message and its type, encoded as an Any message. google.protobuf.Any message = 2;} 通过使用DynamicMessage(可在C++和Java中使用)这样的类,可以编写可以操作SelfDescribingMessages的工具。 总而言之,这个功能未包含在protocol buffers库中的原因是因为从未在Google内部使用它。 此技术需要使用描述符支持动态消息。在使用自描述消息之前,请检查平台是否支持此功能。
许多开源项目都试图在Protocol Buffers之上添加有用的功能。 有关的项目的链接列表,查看这里。
0.1. 为何使用Protocol Buffers 0.2. 在哪里查看样例代码 0.3. 自定义协议格式 0.4. 编译Protocol Buffers 0.5. Protocol Buffers API 0.6. 写一条消息 0.7. 读一条消息 0.8. 扩展一个Protocol Buffers 本教程使用proto3版本的protocol buffers语言,提供了一个基本的Go程序员使用protocol buffers的介绍。通过创建一个简单的示例应用程序,展示如何: 在.proto文件中定义消息格式 使用protocol buffers编译器 使用 Go protocol buffers API来编写和读取消息 这不是在Go中使用protocol buffers的综合指南。有关更详细的参考信息,请参阅“protocol buffers语言指南”,“Go API参考”,“生成代码指南”和“编码参考”。 0.1. 为何使用Protocol Buffers 将要使用的示例是一个非常简单的“地址簿”应用程序,可以在文件中读取和写入人员的详细信息。地址簿中的每个人都有姓名,ID,电子邮件地址和联系电话号码。 如何序列化和检索这样的结构化数据?有几种方法可以解决这个问题: 使用gobs序列化Go数据结构。这是Go特定环境中的一个很好的解决方案,但如果需要与为其他平台编写的应用程序共享数据,它将无法正常工作。 可以发明一种特殊的方法将数据项编码为单个字符串,例如将4个整数编码为“12:3:-23:67”。这是一种简单而灵活的方法,虽然它确实需要编写一次性编码和解析代码,并且解析会产生较小的运行时成本。这最适合编码非常简单的数据。 将数据序列化为XML。这种方法非常有吸引力,因为XML是人类可读的,并且对许多编程语言都有绑定库。如果想与其他应用程序/项目共享数据,这可能是一个不错的选择。然而,XML是空间密集型,并且编码/解码它会对应用程序造成巨大的性能损失。此外,遍历一棵XML的DOM树比像通常那样在类中遍历简单字段要复杂得多。 protocol buffers是灵活,高效,自动化的解决方案,可以解决这个问题。使用protocol buffers编写要存储的数据结构的.proto描述文件。然后protocol buffers编译器创建一个类,该类使用有效的二进制格式实现protocol buffers数据的自动编码和解析。生成的类为构成protocol buffers的字段提供getter和setter,并生成一个protocol buffers,并将protocol buffers作为一个单元读取和写入详细信息。重要的是,protocol buffers格式支持扩展格式,使得代码仍然可以读取用旧格式编码的数据。 0.2. 在哪里查看样例代码 我们的示例是一组用于管理地址簿数据文件的命令行应用程序,使用protocol buffers进行编码。命令add_person_go向数据文件添加新条目。命令list_people_go解析数据文件并将数据打印到控制台。 可以在GitHub存储库的examples目录中找到完整的示例。 0.3. 自定义协议格式 要创建地址簿应用程序,需要从.proto文件开始。.proto文件中的定义很简单:为要序列化的每个数据结构添加消息,然后为消息中的每个字段指定名称和类型。在示例中,定义消息的.proto文件是addressbook.proto。 .proto文件以包声明开头,这有助于防止不同项目之间的命名冲突。 syntax = "proto3";package tutorial;import "google/protobuf/timestamp.proto"; 在Go中,包名称用作Go包,除非指定了go_package。即使确实提供了go_package,仍然应该定义一个普通的包,以避免在Protocol Buffers名称空间和非Go语言中发生名称冲突。 接下来是消息定义。消息只是包含一组类型字段的聚合。许多标准的简单数据类型都可用作字段类型,包括bool,int32,float,double和string。还可以使用其他消息类型作为字段类型,为消息添加更多结构。 message Person { string name = 1; int32 id = 2; // Unique ID number for this person. string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phones = 4; google.protobuf.Timestamp last_updated = 5;}// Our address book file is just one of these. message AddressBook { repeated Person people = 1;} 在上面的示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。 甚至可以定义嵌套在其他消息中的消息类型,如上所示,PhoneNumber类型在Person中定义。 如果希望其中一个字段具有预定义的值列表之一,还可以定义枚举类型,此处要指定电话号码可以是MOBILE,HOME或WORK之一。 每个元素上的“=1”,“=2”标识该字段在二进制编码中使用的唯一“标记”。标签号1-15相对于更大数字的标签号需要少于一个字节来编码,因此作为优化,可以决定将这些标签用于常用或重复的元素,将标签16和更大数字的标签号留给不太常用的可选元素。重复字段中的每个元素都需要重新编码标记号,因此重复字段特别适合此优化。 如果未设置字段的值,则使用默认值:数字类型为零,字符串为空字符串,布尔型为false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,其中没有设置其字段。调用访问器以获取尚未显式设置的字段的值始终返回该字段的默认值。 对于重复字段,该字段可以重复任意次数(包括零)。重复值的顺序将保留在protocol buffers中。将重复字段视为动态数组。 在Protocol Buffer指南中找到编写.proto文件的完整指南,包括所有可能的字段类型。不要去寻找类继承这样的工具,protocol buffers不会这样做。 0.4. 编译Protocol Buffers 既然有一个.proto文件,需要做的下一件事是生成读取和写入AddressBook(以及Person和PhoneNumber)消息所需的类。为此,需要在.proto上运行protocol buffers编译器protoc: 如果尚未安装编译器,请下载该软件包并按照自述文件中的说明进行操作。 运行以下命令安装Go protocol buffers 插件 go get -u github.com/golang/protobuf/protoc-gen-go 编译器插件protoc-gen-go将被安装在$GOBIN中,默认为$GOPATH/bin。该路径必须在$PATH中,协议编译器protoc才能找到它。 现在运行编译器: 指定源目录(应用程序的源代码所在的位置,如果不提供值,则使用当前目录), 指定目的目录(希望生成的代码存放的位置,通常与$SRC_DIR相同), 指定.proto的路径。 如下所示: protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto 因为需要Go类,所以使用--go_out选项,其他支持的语言也可以提供类似的选项。 这会在指定的目标目录($DST_DIR)中生成addressbook.pb.go。 0.5. Protocol Buffers API 生成的addressbook.pb.go提供以下有用类型: 具有People字段的AddressBook结构体。 具有Name,Id,Email和Phones字段的Person结构体。 Person_PhoneNumber结构体,包含Number和Type字段。 Person_PhoneType类型和为Person.PhoneType枚举中的每个值定义的值。 可以阅读更多有关“生成代码”指南中生成的内容的详细信息,但在大多数情况下,可以将这些视为完全普通的Go类型。 这是list_people命令关于如何创建Person实例的单元测试的示例: p := pb.Person{ Id: 1234, Name: "John Doe", Email: "jdoe@example.com", Phones: []*pb.Person_PhoneNumber{ {Number: "555-4321", Type: pb.Person_HOME}, }, } 0.6. 写一条消息 使用protocol buffers的目的是序列化数据,以便可以在其他地方解析它。在Go中,使用proto库的Marshal函数来序列化protocol buffers数据。指向protocol buffers消息的struct的指针实现了proto.Message接口。调用proto.Marshal会返回以传输格式编码的protocol buffers。例如,在add_person命令中使用此函数: book := &pb.AddressBook{} // ... // Write the new address book back to disk. out, err := proto.Marshal(book) if err != nil { log.Fatalln("Failed to encode address book:", err) } if err := ioutil.WriteFile(fname, out, 0644); err != nil { log.Fatalln("Failed to write address book:", err) } 0.7. 读一条消息 要解析编码的消息,使用proto库的Unmarshal函数。调用它将buf中的数据解析为protocol buffers,并将结果放在pb中。因此,要在list_people命令中解析文件,我们使用: // Read the existing address book. in, err := ioutil.ReadFile(fname) if err != nil { log.Fatalln("Error reading file:", err) } book := &pb.AddressBook{} if err := proto.Unmarshal(in, book); err != nil { log.Fatalln("Failed to parse address book:", err) } 0.8. 扩展一个Protocol Buffers 一段时间后,会释放使用protocol buffers的代码,毫无疑问会想要“改进”protocol buffers的定义。如果希望新缓冲区向后兼容,并且旧缓冲区是向前兼容的,那么需要在新得到protocol buffers中遵循一些规则: 不得更改任何现有字段的标记号。 可以删除字段。 可以添加新字段,但必须使用新的标记号(即从未在此协议缓冲区中使用的标记号,甚至不包括已删除的字段)。 这些规则有一些例外,但它们很少使用。 如果遵循这些规则,旧代码将很乐意阅读新消息并简单地忽略任何新字段。对于旧代码,已删除的单个字段将只具有其默认值,删除的重复字段将为空。新代码也将透明地读取旧消息。 但是,请记住旧消息中不会出现新字段,因此需要使用默认值执行合理的操作。使用特定于类型的默认值: 对于字符串,默认值为空字符串 对于布尔值,默认值为false 对于数字类型,默认值为零
0.1. 编译器调用 0.2. 包 0.3. 消息 0.3.1. 嵌套类型 0.3.2. 知名类型 0.4. 字段 0.4.1. 单个标量字段 (proto2) 0.4.2. 单个标量字段 (proto3) 0.4.3. 单个消息字段 0.4.4. Repeated字段 0.4.5. Map字段 0.4.6. Oneof字段 0.5. 枚举 0.6. 扩展(proto2) 0.7. 服务 此页面准确描述了protocol buffers编译器为任何给定协议定义生成的Go代码。proto2和proto3生成的代码之间的任何差异都会突出显示。请注意,这些差异在本文档中描述的生成代码中,而不是基本API,两个版本中的API相同。在阅读本文档之前,应该阅读proto2语言指南和或proto3语言指南。 0.1. 编译器调用 protocol buffers编译器需要一个插件生成Go代码。执行如下命令来安装: go get github.com/golang/protobuf/protoc-gen-go 在命令行中使用--go_out标志时,由protoc编译器调用protoc-gen-go二进制文件。--go_out标志告诉编译器Go源文件输出位置。编译器为每个.proto文件创建单个源文件。 输出文件的名称是根据.proto文件的名称得来的,在两个地方可以更改: 扩展名(.proto)替换为.pb.go。例如,名为player_record.proto的文件会生成一个名为player_record.pb.go的输出文件。 proto路径(使用--proto_path或-I命令行标志指定)将替换为输出路径(使用--go_out标志指定)。 当像下面这样运行proto编译器时: protoc --proto_path=src --go_out=build/gen src/foo.proto src/bar/baz.proto 编译器将读取src/foo.proto和src/bar/baz.proto文件。它产生两个输出文件:build/gen/foo.pb.go和build/gen/bar/baz.pb.go。 如有必要,编译器会自动创建build/gen/bar目录,但不会创建build或build/gen目录,所以这两个目录必须已经存在。 0.2. 包 如果.proto文件中有package声明,则生成的代码使用proto的package声明作为其Go包名称,其中将"."转换为"_"。例如,example.high_score的proto的package名称生成的Go包名称为example_high_score。 可以使用.proto文件中的go_package选项覆盖特定.proto默认生成的包。如下.proto文件,生成的Go包名为hs。 package example.high_score;option go_package = "hs"; 否则,如果.proto文件不包含package声明,则生成的代码使用文件名(减去扩展名)作为Go包名称,并将"."转换为"_“。例如,一个名为high.score.proto的proto包,其中的没有package声明,将生成在high_score包中生成一个名为high.score.pb.go的文件。 0.3. 消息 给出一个简单的消息声明: message Foo {} protocol buffers 编译器生成一个名为Foo的结构和一个实现了Message*Foo的接口。有关详细信息,请参阅内联注释。 type Foo struct { } // Reset sets the proto's state to default values. func (m *Foo) Reset() { *m = Foo{} } // String returns a string representation of the proto. func (m *Foo) String() string { return proto.CompactTextString(m) } // ProtoMessage acts as a tag to make sure no one accidentally implements the // proto.Message interface. func (*Foo) ProtoMessage() {} 请注意,所有这些成员始终存在, optimize_for选项不会影响Go代码生成器的输出。 0.3.1. 嵌套类型 可以在一个条消息中嵌套声明另一个消息。例如: message Foo { message Bar { }} 在这种情况下,编译器生成两个结构:Foo和Foo_Bar。 0.3.2. 知名类型 Protobufs 带有一组预定义的消息,称为知名类型(WKT)。这些类型可以用于与其他服务的互操作性,或者仅仅因为它们简洁地表示常见的有用模式。 例如,Struct消息表示任意C风格的结构体。 WKT的预生成Go代码作为Go protobuf库的一部分进行分发,如果使用WKT,则生成的消息的Go代码会引用此代码。例如,给出如下消息: import "google/protobuf/struct.proto"import "google/protobuf/timestamp.proto"message NamedStruct { string name = 1; google.protobuf.Struct definition = 2; google.protobuf.Timestamp last_modified = 3;} 生成的Go代码如下所示: import google_protobuf "github.com/golang/protobuf/ptypes/struct" import google_protobuf1 "github.com/golang/protobuf/ptypes/timestamp" ... type NamedStruct struct { Name string Definition *google_protobuf.Struct LastModified *google_protobuf1.Timestamp } 一般来说,不需要将这些类型直接导入代码中。但是,如果需要直接引用其中一种类型,只需导入github.com/golang/protobuf/ptypes/[TYPE]包,并正常使用该类型。 0.4. 字段 protocol buffers编译器为消息中定义的每个字段生成结构体的字段。该字段的确切性质取决于它的类型以及它是否是singular, repeated, map或者oneof字段。 请注意,生成的Go字段名称始终使用大驼峰式(CamelCase)命名,即使.proto文件中的字段名称使用带有下划线的小写(应该如此)。大小写转换的工作原理如下: 第一个字母是导入的。如果第一个字符是下划线,则将其删除并添加大写字母X。 如果内部下划线后跟小写字母,则删除下划线,并将后面的字母大写。 因此: 原字段foo_bar_baz在Go中变为FooBarBaz 原字段_my_field_name_2在GO中变为XMyFieldName_2 0.4.1. 单个标量字段 (proto2) 对于以下任一字段定义: optional int32 foo = 1;required int32 foo = 1; 编译器生成一个结构,其中包含一个名为Foo的*int32字段和一个访问器方法GetFoo()它返回Foo中的int32值或默认值(如果该字段未设置)。如果未显式设置默认值,则使用该类型的零值(0表示数字,空字符串表示字符串)。 对于其他标量字段类型(包括bool,bytes和string),*int32将根据[标量值类型]..(/Protocol-Buffers/02-proto3指南.md)表替换为相应的Go类型。 0.4.2. 单个标量字段 (proto3) 对于此字段定义: int32 foo = 1; 编译器将生成一个带有名为Foo的int32字段和一个访问器方法GetFoo()的结构体,该方法返回Foo中的int32值或该字段的零值,如果字段未设置(0表示数字,字符串为空字符串)。 对于其他标量字段类型(包括bool,bytes和string),根据标量值类型将int32替换为相应的Go类型。proto中的未设置值将表示为该类型的零值(0表示数字,空字符串表示字符串)。 0.4.3. 单个消息字段 给定消息类型: message Bar {} 对于带有Bar字段的消息: // proto2 message Baz { optional Bar foo = 1; // The generated code is the same result if required instead of optional. }// proto3 message Baz { Bar foo = 1;} 编译器将生成Go结构: type Baz struct { Foo *Bar } 消息字段可以设置为nil,这意味着该字段未设置,有效的清除该字段。这不等同于将值设置为消息结构体的“空”实例。 编译器还生成一个func(m *Baz) GetFoo() *Bar辅助函数。这使得可以在没有中间nil检查的情况下链接获取调用。 0.4.4. Repeated字段 每个repeated字段生成一个T字段的切片(slice)在Go结构中,其中T是字段的元素类型。如下消息带有repeated字段: message Baz { repeated Bar foo = 1;} 编译器生成Go结构: type Baz struct { Foo []*Bar } 同样: 对于字段定义repeated bytes foo = 1;编译器将生成一个带有名为Foo的[][]bytes字段的Go结构 对于重复的枚举repeated MyEnum bar = 2;编译器将生成一个带有名为Bar的[]MyEnum字段的Go结构 0.4.5. Map字段 每个map字段以类型map[TKey]TValue在结构体中生成一个字段,其中TKey是字段的键类型,TValue是字段的值类型。如下消息带有map字段: message Bar {}message Baz { map<string, Bar> foo = 1;} 编译器生成Go结构: type Baz struct { Foo map[string]*Bar } 0.4.6. Oneof字段 对于oneof字段,protobuf编译器生成具有接口类型isMessageName_MyField的单独字段。并且还为oneof中的每个单独字段生成一个结构体。这些都实现了这个isMessageName_MyField接口。 对于带有oneof字段的此消息: package account;message Profile { oneof avatar { string image_url = 1; bytes image_data = 2; }} 编译器生成的结构体: type Profile struct { // Types that are valid to be assigned to Avatar: // *Profile_ImageUrl // *Profile_ImageData Avatar isProfile_Avatar `protobuf_oneof:"avatar"` } type Profile_ImageUrl struct { ImageUrl string } type Profile_ImageData struct { ImageData []byte } *Profile_ImageUrl和*Profile_ImageData都通过提供空的isProfile_Avatar()方法来实现isProfile_Avatar。 以下示例显示如何设置字段: p1 := &account.Profile{ Avatar: &account.Profile_ImageUrl{"http://example.com/image.png"}, } // imageData is []byte imageData := getImageData() p2 := &account.Profile{ Avatar: &account.Profile_ImageData{imageData}, } 要访问该字段,可以使用值上的类型开关来处理不同的消息类型。 switch x := m.Avatar.(type) { case *account.Profile_ImageUrl: // Load profile image based on URL // using x.ImageUrl case *account.Profile_ImageData: // Load profile image based on bytes // using x.ImageData case nil: // The field is not set. default: return fmt.Errorf("Profile.Avatar has unexpected type %T", x) } 编译器还生成get方法func (m *Profile) GetImageUrl() string和func (m *Profile) GetImageData() []byte。每个get函数返回该字段的值,如果未设置则返回零值。 0.5. 枚举 给出如下枚举: message SearchRequest { enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } Corpus corpus = 1; ...} protocol buffers编译器生成一个类型和一系列该类型的常量。 对于上面消息中的枚举,类型名称以消息名称开头: type SearchRequest_Corpus int32 对于包级别的枚举: enum Foo { DEFAULT_BAR = 0; BAR_BELLS = 1; BAR_B_CUE = 2;} Go生成的类型名称从未修改的proto枚举名称得来: type Foo int32 此类型具有String()方法,该方法返回给定值的名称。 Enum()方法使用给定值初始化新分配的内存并返回相应的指针: func (Foo) Enum() *Foo protocol buffers编译器为枚举中的每个值生成一个常量。对于消息中的枚举,常量以消息的名称开头: const ( SearchRequest_UNIVERSAL SearchRequest_Corpus = 0 SearchRequest_WEB SearchRequest_Corpus = 1 SearchRequest_IMAGES SearchRequest_Corpus = 2 SearchRequest_LOCAL SearchRequest_Corpus = 3 SearchRequest_NEWS SearchRequest_Corpus = 4 SearchRequest_PRODUCTS SearchRequest_Corpus = 5 SearchRequest_VIDEO SearchRequest_Corpus = 6 ) 对于包级别的枚举,常量以枚举名称开头: const ( Foo_DEFAULT_BAR Foo = 0 Foo_BAR_BELLS Foo = 1 Foo_BAR_B_CUE Foo = 2 ) protobuf编译器还生成从整数值到字符串名称的映射以及从名称到值的映射: var Foo_name = map[int32]string{ 0: "DEFAULT_BAR", 1: "BAR_BELLS", 2: "BAR_B_CUE", } var Foo_value = map[string]int32{ "DEFAULT_BAR": 0, "BAR_BELLS": 1, "BAR_B_CUE": 2, } 请注意,.proto允许多个枚举符号具有相同的数值,具有相同数值的符号是同义词。这些在Go中以完全相同的方式表示,多个名称对应于相同的数值。反向映射包含数值的单个条目,该条目第一个出现在.proto文件中。 0.6. 扩展(proto2) 扩展仅存在于proto2中。有关proto2扩展的Go生成代码API的文档,请参阅proto包doc。 0.7. 服务 默认情况下,Go代码生成器不会为服务生成输出。如果启用gRPC插件(请参阅gRPC Go快速入门指南),则会生成代码以支持gRPC。
0.1. 词汇元素 0.1.1. 字母和数字 0.1.2. 身份标识 0.1.3. 整数 0.1.4. 浮点数 0.1.5. 布尔 0.1.6. 字符串 0.1.7. 空声明 0.1.8. 常量 0.2. 句法 0.3. 导入语句 0.4. 包 0.5. 可用选项 0.6. 字段 0.6.1. 普通字段 0.6.2. Oneof集合oneof字段 0.6.3. Map字段 0.7. 保留的(Reserved) 0.8. 顶级定义 0.8.1. 枚举定义 0.8.2. 消息定义 0.8.3. 服务定义 0.9. Proto文件 这是Protocol Buffers语言(proto3)第3版的语言规范参考。使用Extended Backus-Naur Form(EBNF)指定语法: | alternation () grouping [] option (zero or one time) {} repetition (any number of times) 有关使用proto3的更多信息,请参阅语言指南。 0.1. 词汇元素 0.1.1. 字母和数字 letter = "A" … "Z" | "a" … "z" decimalDigit = "0" … "9" octalDigit = "0" … "7" hexDigit = "0" … "9" | "A" … "F" | "a" … "f" 0.1.2. 身份标识 ident = letter { letter | decimalDigit | "_" } fullIdent = ident { "." ident } messageName = ident enumName = ident fieldName = ident oneofName = ident mapName = ident serviceName = ident rpcName = ident messageType = [ "." ] { ident "." } messageName enumType = [ "." ] { ident "." } enumName 0.1.3. 整数 intLit = decimalLit | octalLit | hexLit decimalLit = ( "1" … "9" ) { decimalDigit } octalLit = "0" { octalDigit } hexLit = "0" ( "x" | "X" ) hexDigit { hexDigit } 0.1.4. 浮点数 floatLit = ( decimals "." [ decimals ] [ exponent ] | decimals exponent | "."decimals [ exponent ] ) | "inf" | "nan" decimals = decimalDigit { decimalDigit } exponent = ( "e" | "E" ) [ "+" | "-" ] decimals 0.1.5. 布尔 boolLit = "true" | "false" 0.1.6. 字符串 strLit = ( "'" { charValue } "'" ) | ( '"' { charValue } '"' ) charValue = hexEscape | octEscape | charEscape | /[^\0\n\\]/ hexEscape = '\' ( "x" | "X" ) hexDigit hexDigit octEscape = '\' octalDigit octalDigit octalDigit charEscape = '\' ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | '\' | "'" | '"' ) quote = "'" | '"' 0.1.7. 空声明 emptyStatement = ";" 0.1.8. 常量 constant = fullIdent | ( [ "-" | "+" ] intLit ) | ( [ "-" | "+" ] floatLit ) | strLit | boolLit 0.2. 句法 语法(syntax)语句用于定义protobuf版本。 syntax = "syntax" "=" quote "proto3" quote ";" # 例如 syntax = "proto3"; 0.3. 导入语句 import语句用于导入另一个.proto的定义。 import = "import" [ "weak" | "public" ] strLit ";" # 例如 import public "other.proto"; 0.4. 包 包说明符可用于防止协议消息类型之间的名称冲突。 package = "package" fullIdent ";" # 例如 package foo.bar; 0.5. 可用选项 选项可用于proto文件,消息,枚举和服务。可用选项可以是protobuf定义的选项或自定义选项。有关更多信息,请参阅语言指南中的选项。 option = "option" optionName "=" constant ";"optionName = ( ident | "(" fullIdent ")" ) { "." ident }// 例如 option java_package = "com.example.foo"; 0.6. 字段 字段是protocol buffers消息的基本元素。字段可以是普通字段,oneof字段或map字段。字段具有类型和字段编号。 type = "double" | "float" | "int32" | "int64" | "uint32" | "uint64" | "sint32" | "sint64" | "fixed32" | "fixed64" | "sfixed32" | "sfixed64" | "bool" | "string" | "bytes" | messageType | enumTypefieldNumber = intLit; 0.6.1. 普通字段 每个字段都有类型,名称和字段编号。它可能有字段选项。 field = [ "repeated" ] type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"fieldOptions = fieldOption { "," fieldOption }fieldOption = optionName "=" constant// 例如 foo.bar nested_message = 2;repeated int32 samples = 4 [packed=true]; 0.6.2. Oneof集合oneof字段 oneof由oneof字段和oneof名称组成。 oneof = "oneof" oneofName "{" { oneofField | emptyStatement } "}"oneofField = type fieldName "=" fieldNumber [ "[" fieldOptions "]" ] ";"// 例如 oneof foo { string name = 4; SubMessage sub_message = 9;} 0.6.3. Map字段 map字段具有键类型,值类型,名称和字段编号。键类型可以是任何整数或字符串类型。 mapField = "map" "<" keyType "," type ">" mapName "=" fieldNumber [ "[" fieldOptions "]" ] ";"keyType = "int32" | "int64" | "uint32" | "uint64" | "sint32" | "sint64" | "fixed32" | "fixed64" | "sfixed32" | "sfixed64" | "bool" | "string"// 例如 map<string, Project> projects = 3; 0.7. 保留的(Reserved) 保留语句声明了一系列不能在此消息中使用的字段编号或字段名称。 reserved = "reserved" ( ranges | fieldNames ) ";"ranges = range { "," range }range = intLit [ "to" ( intLit | "max" ) ]fieldNames = fieldName { "," fieldName }// 例如 reserved 2, 15, 9 to 11;reserved "foo", "bar"; 0.8. 顶级定义 0.8.1. 枚举定义 枚举定义由名称和枚举主体组成。枚举主体可以有选项和枚举字段。枚举定义必须以枚举值零开始。 enum = "enum" enumName enumBodyenumBody = "{" { option | enumField | emptyStatement } "}"enumField = ident "=" intLit [ "[" enumValueOption { "," enumValueOption } "]" ]";"enumValueOption = optionName "=" constant// 例如 enum EnumAllowingAlias { option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 2 [(custom_option) = "hello world"];} 0.8.2. 消息定义 消息由消息名称和消息正文组成。消息正文可以包含字段,嵌套枚举定义,嵌套消息定义,可用选项,oneof字段,map字段和保留语句。 message = "message" messageName messageBodymessageBody = "{" { field | enum | message | option | oneof | mapField | reserved | emptyStatement } "}"// 例如 message Outer { option (my_option).a = true; message Inner { // Level 2 int64 ival = 1; } map<int32, string> my_map = 2;} 0.8.3. 服务定义 service = "service" serviceName "{" { option | rpc | emptyStatement } "}"rpc = "rpc" rpcName "(" [ "stream" ] messageType ")" "returns" "(" [ "stream" ]messageType ")" (( "{" {option | emptyStatement } "}" ) | ";")// 例如 service SearchService { rpc Search (SearchRequest) returns (SearchResponse);} 0.9. Proto文件 proto = syntax { import | package | option | topLevelDef | emptyStatement }topLevelDef = message | enum | service 示例.proto文件 syntax = "proto3";import public "other.proto";option java_package = "com.example.foo";enum EnumAllowingAlias { option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 2 [(custom_option) = "hello world"];}message outer { option (my_option).a = true; message inner { // Level 2 int64 ival = 1; } repeated inner inner_message = 2; EnumAllowingAlias enum_field =3; map<int32, string> my_map = 4;}