企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[Protobuf2 语法指南](http://colobu.com/2015/01/07/Protobuf-language-guide/) [TOC] 英文:[Proto Buffers Language Guide](https://developers.google.com/protocol-buffers/docs/proto) 本指南描述了怎样使用protocol buffer 语法来构造你的protocol buffer数据,包括.proto文件语法以及怎样生成.proto文件的数据访问类。 (本文只针对proto2的语法) 本文是一个参考指南——如果要查看如何使用本文中描述的多个特性的循序渐进的例子,请在[http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/tutorials.html](http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/tutorials.html)中查找需要的语言的教程。 ## 定义一个消息类型 先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件了: ``` message SearchRequest { required string query = 1; optional int32 page_number = 2; optional int32 result_per_page = 3; } ``` SearchRequest消息格式有3个字段,在消息中承载的数据分别对应于每一个字段。其中每个字段都有一个名字和一种类型。 ### 指定字段类型 在上面的例子中,所有字段都是[标量类型](https://developers.google.com/protocol-buffers/docs/proto#scalar):两个整型(page\_number和result\_per\_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括[枚举](https://developers.google.com/protocol-buffers/docs/proto#enum)(enumerations)或其他消息类型。 ### 分配标识号 正如上述文件格式,在消息定义中,每个字段都有唯一的一个**数字标识符**。这些标识符是用来在消息的[二进制格式](https://developers.google.com/protocol-buffers/docs/encoding)中识别各个字段的,一旦开始使用就不能够再改变。注:\[1,15\]之内的标识号在编码的时候会占用一个字节。\[16,2047\]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 \[1,15\]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。 最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的\[19000-19999\]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。 ### 指定字段规则 所指定的消息字段修饰符必须是如下之一: * required:一个格式良好的消息一定要含有1个这种字段。表示该值是必须要设置的; * optional:消息格式中该字段可以有0个或1个值(不超过1个)。 * repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。表示该值可以重复,相当于java中的List。 由于一些历史原因,基本数值类型的repeated的字段并没有被尽可能地高效编码。在新的代码中,用户应该使用特殊选项\[packed=true\]来保证更高效的编码。如: ``` repeated int32 samples = 4 [packed=true]; ``` > required是永久性的:在将一个字段标识为required的时候,应该特别小心。如果在某些情况下不想写入或者发送一个required的字段,将原始该字段修饰符更改为optional可能会遇到问题——旧版本的使用者会认为不含该字段的消息是不完整的,从而可能会无目的的拒绝解析。在这种情况下,你应该考虑编写特别针对于应用程序的、自定义的消息校验函数。Google的一些工程师得出了一个结论:使用required弊多于利;他们更 愿意使用optional和repeated而不是required。当然,这个观点并不具有普遍性。 ### 添加更多消息类型 在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用——例如,如果想定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中,如: ``` message SearchRequest{ required string query = 1; optional int32 page_number = 2; optional int32 result_per_page = 3; } message SearchResponse{ ... } ``` ### 添加注释 向.proto文件添加注释,可以使用C/C++/java风格的双斜杠(//) 语法格式,如: ``` message SearchRequest{ required string query = 1; optional int32 page_number = 2;// Which page number do we want? optional int32 result_per_page = 3;// Number of results to return per page. } ``` ### 从.proto文件生成了什么? 当用protocolbuffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。 * 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。 * 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。 * 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。 你可以从如下的文档链接中获取每种语言更多API。[http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/overview.html](http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/overview.html) ### 标量数值类型 一个标量消息字段可以含有一个如下的类型——该表格展示了定义于.proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型: | .proto类型 | Java 类型| C++类型 | 备注 | | --- | --- |--- |--- | | double | double | double | | | float | float | float | int32 | int | int32 | | uint32 | int[1] | uint32 | Uses variable-length encoding. | | uint64 | long[1] | uint64 | Uses variable-length encoding.| | sint32| int | int32 | 使用可变长编码方式。有符号的整型值。编码时比通常的int32高效。 | sint64 | long | int64 | 使用可变长编码方式。有符号的整型值。编码时比通常的int64高效。 | fixed32 | int[1] | uint32 |总是4个字节。如果数值总是比总是比228大的话,这个类型会比uint32高效。 | fixed64 | long[1] | uint64 | 总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效。 | sfixed32 | int | int32 | 总是4个字节。 | sfixed64 | long| int64| 总是8个字节。 | bool| boolean| bool |string| String | string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 | bytes| ByteString| string | 可能包含任意顺序的字节数据。| 你可以在文章[http://code.google.com/apis/protocolbuffers/docs/encoding.html](http://code.google.com/apis/protocolbuffers/docs/encoding.html)中,找到更多“序列化消息时各种类型如何编码”的信息。 ### Optional的字段和默认值 如上所述,消息描述中的一个元素可以被标记为“可选的”(optional)。一个格式良好的消息可以包含0个或一个optional的元素。当解 析消息时,如果它不包含optional的元素值,那么解析出来的对象中的对应字段就被置为默认值。默认值可以在消息描述文件中指定。例如,要为*SearchRequest*消息的*result\_per\_page*字段指定默认值10,在定义消息格式时如下所示: ``` optional int32 result_per_page = 3 [default = 10]; ``` 如果没有为optional的元素指定默认值,就会使用与特定类型相关的默认值:对string来说,默认值是空字符串。对bool来说,默认值是false。对数值类型来说,默认值是0。对枚举来说,默认值是枚举类型定义中的第一个值。 ### 枚举 当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)就可以了。一个enum类型的字段只能用指定的常量集中的一个值作为其值(如果尝 试指定不同的值,解析器就会把它当作一个未知的字段来对待)。在下面的例子中,在消息格式中添加了一个叫做Corpus的枚举类型——它含有所有可能的值 ——以及一个类型为Corpus的字段: ``` message SearchRequest { required string query = 1; optional int32 page_number = 2; optional int32 result_per_page = 3 [default = 10]; enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } optional Corpus corpus = 4 [default = UNIVERSAL]; } ``` 你可以为枚举常量定义别名。 需要设置allow\_alias option 为 true, 否则 protocol编译器会产生错误信息。 ``` enum EnumAllowingAlias { option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 1; } 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值是使用可变编码方式的,对负数不够高效,因此不推荐在enum中使用负数。如上例所示,可以在 一个消息定义的内部或外部定义枚举——这些枚举可以在.proto文件中的任何消息定义里重用。当然也可以在一个消息中声明一个枚举类型,而在另一个不同 的消息中使用它——采用MessageType.EnumType的语法格式。 当对一个使用了枚举的.proto文件运行protocol buffer编译器的时候,生成的代码中将有一个对应的enum(对Java或C++来说),或者一个特殊的EnumDescriptor类(对 Python来说),它被用来在运行时生成的类中创建一系列的整型值符号常量(symbolic constants)。 关于如何在你的应用程序的消息中使用枚举的更多信息,请查看所选择的语言[http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/overview.html。](http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/overview.html%E3%80%82) ## 使用其他消息类型 你可以将其他消息类型用作字段类型。例如,假设在每一个SearchResponse消息中包含Result消息,此时可以在相同的.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如: ``` message SearchResponse{ repeated Result result = 1; } message Result{ required string url = 1; optional string title = 2; repeated string snippets = 3; } ``` ### 导入定义 在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢? 你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如: ``` import "myproject/other_protos.proto"; ``` 默认情况下你只能使用直接导入的.proto文件中的定义. 然而, 有时候你需要移动一个.proto文件到一个新的位置, 可以不直接移动.proto文件, 只需放入一个dummy .proto 文件在老的位置, 然后使用import转向新的位置: ``` // 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 ``` protocol编译器就会在一系列目录中查找需要被导入的文件,这些目录通过protocol编译器的命令行参数-I/–import\_path指定。如果不提供参数,编译器就在其调用目录下查找。 ### 嵌套类型 你可以在其他消息类型中定义、使用消息类型,在下面的例子中,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; } } } ``` ## 更新一个消息类型 如果一个已有的消息格式已无法满足新的需求——如,要在消息中添加一个额外的字段——但是同时旧版本写的代码仍然可用。不用担心!更新消息而不破坏已有代码是非常简单的。在更新时只要记住以下的规则即可。 * 不要更改任何已有的字段的数值标识。 * 如果你增加新的字段,使用旧格式的字段仍然可以被你新产生的代码所解析。你应该记住这些元素的默认值这样你的新代码就可以以适当的方式和旧代码产生的数据交互。相似的,通过新代码产生的消息也可以被旧代码解析:只不过新的字段会被忽视掉。注意,未被识别的字段会在反序列化的过程中丢弃掉,所以如果消息再被传递给新的代码,新的字段依然是不可用的(这和proto2中的行为是不同的,在proto2中未定义的域依然会随着消息被序列化) * 非required的字段可以移除——只要它们的标识号在新的消息类型中不再使用(更好的做法可能是重命名那个字段,例如在字段前添加“OBSOLETE\_”前缀,那样的话,使用的.proto文件的用户将来就不会无意中重新使用了那些不该使用的标识号)。 * int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。 * sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。 * string和bytes是兼容的——只要bytes是有效的UTF-8编码。 * 嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。 * fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。 * 枚举类型与int32,uint32,int64和uint64相兼容(注意如果值不相兼容则会被截断),然而在客户端反序列化之后他们可能会有不同的处理方式,例如,未识别的proto3枚举类型会被保留在消息中,但是他的表示方式会依照语言而定。int类型的字段总会保留他们的 ## Any Any类型消息允许你在没有指定他们的.proto定义的情况下使用消息作为一个嵌套类型。一个Any类型包括一个可以被序列化bytes类型的任意消息,以及一个URL作为一个全局标识符和解析消息类型。为了使用Any类型,你需要导入`import 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语法](https://developers.google.com/protocol-buffers/docs/proto),使用Any替换[扩展](https://developers.google.com/protocol-buffers/docs/proto#extensions)。 1. ## 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字段到 oneof 定义中. 你可以增加任意类型的字段, 但是不能使用repeated 关键字. 在产生的代码中, oneof字段拥有同样的 getters 和setters, 就像正常的可选字段一样. 也有一个特殊的方法来检查到底那个字段被设置. 你可以在相应的语言[API指南](https://developers.google.com/protocol-buffers/docs/reference/overview)中找到oneof API介绍. 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. * 反射API对oneof 字段有效. * 如果使用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++中,如果你使用Swap()两个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()); ``` 3. ### 向后兼容性问题 当增加或者删除oneof字段时一定要小心. 如果检查oneof的值返回None/NOT\_SET, 它意味着oneof字段没有被赋值或者在一个不同的版本中赋值了。 你不会知道是哪种情况,因为没有办法判断如果未识别的字段是一个oneof字段。 Tag 重用问题: * **将字段移入或移除oneof**:在消息被序列号或者解析后,你也许会失去一些信息(有些字段也许会被清除) * **删除一个字段或者加入一个字段**:在消息被序列号或者解析后,这也许会清除你现在设置的oneof字段 * **分离或者融合oneof**:行为与移动常规字段相似。 ## Map 如果你希望创建一个关联映射,protocol buffer提供了一种快捷的语法: ``` map<string, Project> projects = 3; ``` * Map的字段可以是repeated。 * 序列化后的顺序和map迭代器的顺序是不确定的,所以你不要期望以固定顺序处理Map * 当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。 * 从序列化中解析或者融合时,如果有重复的key则后一个key不会被使用,当从文本格式中解析map时,如果存在重复的key。 生成map的API现在对于所有proto3支持的语言都可用了,你可以从[API指南](https://developers.google.com/protocol-buffers/docs/reference/overview)找到更多信息。 1. ### 向后兼容性问题 map语法序列化后等同于如下内容,因此即使是不支持map语法的protocol buffer实现也是可以处理你的数据的: ``` message MapFieldEntry { key_type key = 1; value_type value = 2; } repeated MapFieldEntry map_field = N; ``` ## Package 当然可以为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。如: ``` package foo.bar; message Open { ... } ``` 在其他的消息格式定义中可以使用包名+消息名的方式来定义域的类型,如: ``` message Foo { ... required foo.bar.Open open = 1; ... } ``` 包的声明符会根据使用语言的不同影响生成的代码。 * 对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中; - 对于Java,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有java\_package; * 对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的。 * 对于Go,包可以被用做Go包名称,除非你显式的提供一个option go\_package在你的.proto文件中。 * 对于Ruby,生成的类可以被包装在内置的Ruby名称空间中,转换成Ruby所需的大小写样式 (首字母大写;如果第一个符号不是一个字母,则使用PB\_前缀),例如Open会在Foo::Bar名称空间中。 * 对于javaNano包会使用Java包,除非你在你的文件中显式的提供一个option java\_package。 * 对于C#包可以转换为PascalCase后作为名称空间,除非你在你的文件中显式的提供一个option csharp\_namespace,例如,Open会在Foo.Bar名称空间中 ### 包及名称的解析 Protocol buffer语言中类型名称的解析与C++是一致的:首先从最内部开始查找,依次向外进行,每个包会被看作是其父类包的内部类。当然对于 (foo.bar.Baz)这样以“.”分隔的意味着是从最外围开始的。 ProtocolBuffer编译器会解析.proto文件中定义的所有类型名。 对于不同语言的代码生成器会知道如何来指向每个具体的类型,即使它们使用了不同的规则。 ## 定义服务(Service) 如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收 SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义: ``` service SearchService { rpc Search (SearchRequest) returns (SearchResponse); } ``` 最直观的使用protocol buffer的RPC系统是[gRPC](https://github.com/grpc/grpc-experiments),一个由谷歌开发的语言和平台中的开源的PRC系统,gRPC在使用protocl buffer时非常有效,如果使用特殊的protocol buffer插件可以直接为您从.proto文件中产生相关的RPC代码。 如果你不想使用gRPC,也可以使用protocol buffer用于自己的RPC实现,你可以从[proto2语言指南](https://developers.google.com/protocol-buffers/docs/proto#services)中找到更多信息 还有一些第三方开发的PRC实现使用Protocol Buffer。参考[第三方插件wiki](https://github.com/google/protobuf/blob/master/docs/third_party.md)查看这些实现的列表。 ## JSON 映射 Proto3 支持JSON的编码规范,使他更容易在不同系统之间共享数据,在下表中逐个描述类型。 如果JSON编码的数据丢失或者其本身就是null,这个数据会在解析成protocol buffer的时候被表示成默认值。如果一个字段在protocol buffer中表示为默认值,体会在转化成JSON的时候编码的时候忽略掉以节省空间。具体实现可以提供在JSON编码中可选的默认值。 | proto3 | JSON | JSON示例 | 注意 | | --- | --- | --- | --- | | messag| eobject | {“fBar”: v, “g”: null, …} | 产生JSON对象,消息字段名可以被映射成lowerCamelCase形式,并且成为JSON对象键,null被接受并成为对应字段的默认值