Protobuf

protobuf 的使用
proto3

本篇文章为 proto3 为非严格翻译自 protobuf 官方 proto3 的文档。这里会介绍 proto3 的语法,以及如何生成 proto3 的方法。 如果需要看 proto2 请参考 https://developers.google.com/protocol-buffers/docs/proto .

定义消息类型(Defining A Message Type)

首先,我们来看一个非常简单的例子。 这是一个请求消息,这个请求消息里面包含了一个 query 字符串,用于检索你中意的消息。以及你希望找到多少页,每页多少条这样的消息。

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 该文件的文件名一般以 。proto 结尾。 该文件的第一行用于申明当前文件遵从的协议版本号,这里是 protoc3. 如果你在此处不指明, protocol buffer 的编码器可能假设你当前是以 proto2 的协议来编写的。 而且首行不能为空,也不能是注释行。
  • 这个 SearchRequest 定义了三个键值对,每一条定义都指定了一个数据的名字和该数据的对应的数据类型。

在上面的例子中,所有的字段都为 scalar types( 标量类型 ),但是如果如果你想,也可以定义一些 composite types( 复合类型 ), 比如说 enumerations ( 枚举 ) 或者其他。

Assigning Field Numbers(字段号),每一个字段包含一个唯一的字段号,该字段号在二进制数据结构中标识了你所定义的字段。 而且一旦字段号定义下来了,最好就不要更改了。 另外数值为 1-15 的字段采用一个 byte 存储,数值为 16-2047 的字段会采用两个 byte 存储,因此您应该根据此规则合理的安排您的字段。 比如说最常用的字段数值应该在 1-15 之间。 而且要注意,最好预留一些字段,以免将来使用。

字段号最小为 1,最大为 2^29 -1 (即 536,870,911)。 但是你不能使用 [19000, 19999] ( [ FieldDescriptor::kFirstReservedNumber, FieldDescriptor::kLastReservedNumber ] ) 之间的数值,因为这是 proto 的保留号。 当然,你也不能使用你自己定义的保留字段号(见下面说明)。

Specifying Field Rules(指定字段的规则),字段规则可以为下面的两者之一:

  • singular: proto3 的默认规则,在一个消息包中,一个字段的个数不能大于 1 。
  • repeated: 在一个消息包中,一个字段的个数可以大于 1 ,而且出现在消息中的顺序将会被保留下来。

在 proto3 中,repeated 的字段将会采用 packed encoding (打包编码), 该编码规范可以参考: https://developers.google.com/protocol-buffers/docs/encoding#packed

添加更多消息类型

多条消息可以定义在一个 .proto 类型文件中,如下面的例子(定义了一个请求消息,以及一个与之相对应的响应消息):

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

message SearchResponse {
...
}
添加注释

添加注释的方法和 C/C++ 中的做法类似。(使用 // 或者 /* ... */

/* SearchRequest represents a search query, with pagination options to
* indicate which 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.
}
保留域

如果你通过过删除一个字段或者注释一个字段去更新你的消息,那么在将来如果有用户重用了该字段去与支持老版本的消息通信则有可能导致数据泄露或者比较隐秘的 Bug。 一个解决该问题的方法是将该字段设置成保留字段,并加以注释。 在以后如果其他人想用该字段的时候编译器将会提醒用户。

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

注意:你不能在一行里面既使用字段名,又使用字段标号。

你可以使用 protocol buffer 编译器编译你的 .proto 文件, 编译成你指定的语言文件中包含了该属性的,get 和 set 方法,以及序列化( serializing ),和反序列化( parsing )方法。

你可以在官网的各个语言的说明文档中找到更多帮助信息。

scalar types -- 标量类型

消息中的标量类型包括如下类型: double, float, int32, int64, uint32, uint64, sint32, sint64, fixed32, fiexd64 sfixed32, sfixed64, bool, string, bytes 标量类型中在各个语言中对应的数值类型请参考如下文章https://developers.google.com/protocol-buffers/docs/proto3#scalar

在解释 protocol 编码的那篇文章中你能找到更多关于这些类型的信息:https://developers.google.com/protocol-buffers/docs/encoding

  1. 在Java中,无符号的32位和64位整数使用带符号的对等体表示,最高位仅存储在符号位中。
  2. 在所有情况下,给子段赋值的时候最好要检查其正确性,已确保其在生产环境下是可用的。
  3. 64-bit or unsigned 32-bit integers are always represented as long when decoded, but can be an int if an int is given when setting the field. In all cases, the value must fit in the type represented when set. See [2].
  4. Python strings are represented as unicode on decode but can be str if an ASCII string is given (this is subject to change).
  5. Integer is used on 64-bit machines and string is used on 32-bit machines.

 

默认值

在消息的 parse 阶段,如果在消息中某个字段未被指定,那么该字段将会默认的设置成下面所描述的值:

  • string 类型,默认值为空串。
  • bytes 类型,默认值为空 bytes.
  • bool 类型,默认值为 false.
  • numeric 类型,默认为 0.
  • enums 类型,为枚举定义中的第一个字段的值,而且必须为 0
  • message 字段,该值将会是是视编译语言而定,具体请参考: https://developers.google.com/protocol-buffers/docs/reference/overview

当标记为 repeat 的字段为空,则会在相应的语言中标记为空的 List.

对于标量类型,在消息被解析的时候,如果发现消息解析的值和默认值一样,解析端也无法知道是否该值是被设置成该值还是因为没有设置而解析器给的默认值。 因此你在使用这些字段的时候,自己应该要预先想好处理此类现象的应对方法。

如果想知道更多和默认值相关的信息请参考: generated code guide

Enumerations -- 枚举类型

在你定义消息的时候,可能你定义的字段仅仅在一个列表中。如:你定义了一个搜索请求消息,其中 corpus 字段的值在 UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS or VIDEO 列表中, 你可以很简单的将该字段添加到你的 proto 消息中。如下所示:

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 值在 proto 设计的时候就被设计成了默认值,而且必须存在的这种形式。

你可以为一个为一个枚举值设置不同的别名。 但是在为一个值设置多个别名之前,你先需要将 allow_alias option (选项) 设置为 true. 否则编译器会报错。

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-Bit 整形范围内。由于 enum 在序列化时使用 varint 编码,因此负数值是效率不高的,也是不推荐的做法。 你可以将一个枚举值定义到消息体中,也可以定义到消息体外。如果定义子啊消息体外,则该枚举变量可以应用到多个消息的定义中。 当然,你也可以用 _MessageType_._MessageType_ 的形式从另外的消息体中引入枚举定义。

当你在你的消息中声明了一个枚举变量的时候,该枚举变量会在你的 Java / C++ 语言中表示成相应的媒体结构, 而在 Python 中将会生成一个包含一系列整形数值的特殊的类(EnumDescriptor)。

**Caution:** 生成的代码可能会受到特定于枚举器数量的语言的限制(一种语言的下限为几千)。请查看您计划使用的语言的限制。

在解序列化(反序列化,deserialization )的时候, 无法识别的枚举消息将不会翻译到解析的消息体。 在支持开放枚举类型且其值超出指定符号范围的语言(例如C ++和Go)中,未知枚举值只是作为其基础整数表示形式存储。 在具有封闭枚举类型的语言(例如Java)中,枚举中的大小写用于表示无法识别的值,并且可以使用特殊的访问器访问基础整数。 无论哪种情况,如果消息被序列化,则无法识别的值仍将与消息一起序列化。

更多关于应用中枚举的信息请参考如下网站: generated code guide.

Reserved Values

如上文介绍的情况,如果你以注释或者删除的方式更新一个枚举类型,那么有可能会因为其在后续的版本中重用该字段(字段 ID,或者字段名 )而导致程序的异常或者崩溃。 而解决这一问题的办法就是将这一字段 ID 或者字段名指定为保留( reserved )值。 表示字段值的时候可以用 to 关键字表示一个字段范围,也可用 max 表示最大值( int32 最大值 == 2147483647 )。

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

注意,你不能将名字和值放在统一申明语句中。

其他消息类型

你可以使用其他消息类型作为字段类型。 如下所示:

message SearchResponse {
  repeated Result results = 1;
}

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

注意,这个特性在 Java 中不可用。

在上面的例子中,Result 消息和 SearchResponse 定义在同一文件中, 而在不同文件中如果想重用,则应该使用 import 关键字将之倒入,而 import 导入语句应该位于文件的顶部:(如下所示)

import "myproject/other_protos.proto";

一般情况下,你可以使用直接 import 的消息,而不能使用 import 文件中 import 的消息。 但是,有时候你可能想引用一个文件,而该文件的位置将会有所变更,而你又不想一个一个去修改 import 该文件的引用语句。 此时,你可以在引用位置的文件中申明新文件为 public 类型,这时你便可以使用从原来引用处间接的获得另一个文件中的消息类型申明。 具体示例如图所示:

// 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 buffer 的编译器在编译的时候会从 -I/--proto_path 指定的一组路径中搜寻需要 import 的文件。 如果用户没有使用 -I/--proto_path 指定 .proto 的源目录,则会默认在调用编译器的目录下寻找。 但一般情况下,你应该使用 --proto_path 指定你 proto 工程的根目录,以及指定完整有效的 import 文件。

Using proto2 Message Types

在 proto3 中 import proto2 中的消息或者反过来都是有可能的, 然而,proto2 中的 enums 类型并不能直接用在 proto3 中。 (而 proto2 中的可以使用 proto3 的)

Nested Types

你可以在一个消息类型中定义另外的一个消息类型(即实现消息类型的嵌套)。 如下所示:

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

如果你想重用父级以外的消息作为自己的一个消息类型,则可以使用如下形式:

message SomeOtherMessage {
  SearchResponse.Result result = 1;
}

You can nest messages as deeply as you like:

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;
    }
  }
}
Updating A Message Type

如果现有的消息不满足你的需求了,例如你可能需要增加一个额外的字段,但是你不想破坏原来的格式,如果你几下一下几点,你能很容易的做到更新你的协议,而不破坏你原有的协议。

  1. 不变更现有字段以及其对应的 ID 值。
  2. 如果你增加一个新的字段,你任然可以用旧代码来解析新代码生成的编码序列。 而旧的代码在解析二进制序列的时候会忽略不认识的字段(新增加的字段),详情请见 未知序列 章节。 当然你也应该记住原来代码中的值的默认值,以便新旧代码能都交互使用
  3. 字段可以被移除,但是要确保历史使用的字段 ID 不再被用于新的字段定义。 你可能也想要重命名一个字段,但最好的方法是在字段前加上 OBSOLETE_ 前缀, 或者将该字段 ID 设置成 reserved, 这样做是为了防止以后的用户意外的使用了你作废的字段。
  4. int32,uint32,int64,uint64 和 bool 都是兼容的–这意味着您可以将字段从这些类型中的一种更改为另一种,而不会破坏向前或向后的兼容性。 如果从二进制流中解析出了一个数字,其类型与您协议中的类型不匹配,你将获得与 C++ 中类似的类型转换的效果。 (比如: 一个 64bit 的数字被读成了 int32 类型,那么他将截断成 32bit 的一个数据。
  5. sint32 和 sint64 彼此兼容,但是他们与其他整数类型不兼容。(待验证)
  6. 只要 bytes 是服从 UTF-8 编码的,string 类型和 bytes 类型也是兼容的;
  7. 如果 bytes 包含解码版本消息的二进制流,那么 bytes 和 messages 也是兼容的;
  8. fixed32 与 sfixed32 兼容;
  9. fixed64 与 sfixed64 兼容;
  10. 对于 string, bytes, message 类型字段, optional 和 repeated 特性是兼容的。 比如给定一个 repeated 字段的二进制序列,客户端在解析的时候如果以 optional 的方式解析,则会选择最后一个输入的值作为最终值。 如果是 message 类型字段则会 merge 所有的输入元素作为字段最终值。
    注意,这个属性对于数值类型来说是不安全的(包括 bool enum), 数值类型的 repeated 字段将会序列化成 packed format, 自己将使解析器无法识。
  11. 枚举类型与 int32, uint32, int64, uint64 兼容。 但是请注意,反序列化消息时,客户端代码可能会以不同的方式对待它们 例如,无法识别的 proto3 枚举类型将保留在消息中,但是在反序列化消息时如何表示它取决于语言。 Int 字段始终只是保留其值。
  12. 将单个值更改为新的 oneof 的成员是安全且二进制兼容的。 如果您确定没有一个代码一次设置多个字段,那么将多个字段移动到新的 oneof 字段中可能是安全的。 将任何字段移动到现有 oneof 字段中都是不安全的。
Unknown Fields

未知字段,即解析器在解析(反序列化)的时候遇到的格式正确但是无法识别的字段。比如新的 proto 协议,编码的数据传输给支持旧 proto 协议的解析器解析,此时存在新协议中但不存在旧协议中的字段,即为未知字段(Unknown Field).

原来, proto3 遇到未知字段的时候总是丢弃之,但是在 3.5 版本之后,为了保持与 proto2 的行为一致。 在 3.5 以及以后,未知字段在解析的时候将会被保留,并包含在序列化输出中。

Any

Any message 类型允许你嵌入事先未定义的 message type 到消息体中。 Any 包含任意序列化的消息(以字节为单位),以及 URL, URL 作为该消息的类型并解析为该消息的类型的全局唯一标识符。 要使用 Any 类型,您需要导入 google/protobuf/any.proto

import "google/protobuf/any.proto";

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

Any 消息类型的 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 network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

注意目前运行时的打包库还正在开发当中。

如果您已经熟悉 proto2 语法,则 Any 可以保存任意 proto3 消息,类似于可以扩展的 proto2 消息。

Oneof

如果你有一条消息拥有多个字段,但是同一时间只存在同一个字段,则你可以对该消息设置成 oneof 属性,以节省内存。

Oneof 字段和普通字段一样,只是所有的 oneof 自动共享一段内存,而且在同一时刻,只有一个字段能设置值。 当字段中一个值被设置的时候,该消息体中其他值都会被清除。 而通过 case() 或者 whichOneof() 方法可以查询到底是那个字段被设置了值。 至于是哪个方法还要具体参考你使用的语言。

Using Oneof

使用 oneof 关键字后跟消息体名字则可以定义一个 oneof 消息,如下所示:

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

然后你可以在 oneof 的消息体中定义除了 map 类型或者 repeasted 类型的字段。

在你生成的代码中,oneof 修饰的字段同样拥有 getters 和 setters 方法。 你同样可以获得一个特殊的方法用于确定 oneof 消息中的哪个字段被设置了值。 你可以在 这个 链接中找到哦啊更多关于 oneof 消息的 API。

Oneof Features
  • 设置 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;
  • Reflection APIs work for oneof fields.
  • 如果你使用的是 C++, 请确保你的代码不会造成内存崩溃。 比如下面的代码在 message.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 两个包含 oneofs 消息的 messages,那么这两个消息中的 oneof 消息中的值也会交换(这不废话吗?)
    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
    
Backwards-compatibility issues

在增加或者删除 oneof 字段的时候,如果你检查 oneof 的值得到的返回是 None/NOT_SET, 那么这可能代表 oneof 没有被设置,或者他被设置了字段值但是在另外一个版本的 oneof 消息中(不太明白另外一个版本的意思)。 其根因是解析器无法从序列化后的二进制代码中区分 unknown 字段是否属于某个 oneof 字段。

Tag Reuse Issues
  • Move fields into or out of a oneof: You may lose some of your information (some fields will be cleared) after the message is serialized and parsed. 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.
  • Delete a oneof field and add it back: This may clear your currently set oneof field after the message is serialized and parsed.
  • Split or merge oneof: This has similar issues to moving regular fields.
Maps

如果你想在你的数据中定义 map 数据。 protocol buffer 提供了如下简单的语法:

map<key_type, value_type> map_field = N;

...where the key_type can be any integral or string type (so, any scalar type except for floating point types and bytes). Note that enum is not a valid key_type. The value_type can be any type except another map. 这里的 key_type 可以是任何的整形或者字符串类型数据(即任何除浮点类型的标量类型和 bytes 类型)。 注意 enum 类型也不能作为 key_type。 而 value_type 则可以是任何除了 map 类型的值。

下面是一个 map 类型的例子:

map<string, Project> projects = 3;

map 类型有如下值得关注的属性:

  • Map 类型不能被设置 repeated 属性。
  • Map 类型在序列化的二进制序列中没有明确的顺序,因此解码端不应该对 map 对象的顺序有任何假定操作。
  • 当将 .proto 文件转换成 text 格式的时候, map 将会对 key 的字面值做排序处理。(例如如果 key 是数值,则会按照数值从小到大排序)
  • 当在对 wire 数据进行解析的时候,如果在解析序列中发现有重复的 map 键值,则会选择后者。 而如果是对 text 数据进行解析的时候遇到这种情况则直接会报错。
  • 如果你为一个 map 对象提供了 key 但是没有给之赋值,那么在序列化的时候,C++ / java / python 将会将值序列化成值类型的默认值。 而其他语言将不会对之进行序列化。

目前关于 map 的 API 已经适应于支持的所有编程语言。 你可以从 这个 文档找到更多关于 map API 的参考信息。

Backwards compatibility

wire 数据中 map 的的表示方式与下面的这种形式等效,因此,即便 protobuf 的实现没有支持 map, 也可以处理 map 发送过来的数据。

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

repeated MapFieldEntry map_field = N;

同时,任何 protobuf 的实现在处理 map 的时候都应该产生( produce )或者能够处理(handle)成上叙这种形式。

Packages

你可以在 .proto 中添加包明,以防止字段名冲突。如下所示:

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

然后你还可以用一下的方式来快速定义新的字段。

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

package specifier 究竟会生成何种代码则会取决于你选择的语言。

  • 在 C++ 中生成的类将会被包含在 namespace 中。比如你可能这样使用你的类: foo:bar
  • 在 Java 中则会使用 java 的包机制。除非你在你的 .proto 文件中利用 java_package 属性指定了包名。
  • 在 Python 中,会直接忽略 package 属性,因为 python 中是以目录结构来区分模块的。
  • 在 Go 语言中,会使用 Go package 的方式,除非你使用 go_package 明确指定你想要的包名;
  • (不熟) In Ruby, the generated classes are wrapped inside nested Ruby namespaces, converted to the required Ruby capitalization style (first letter capitalized; if the first character is not a letter, PB_ is prepended). For example, Open would be in the namespace Foo::Bar.
  • (不熟) In C# the package is used as the namespace after converting to PascalCase, unless you explicitly provide an option csharp_namespace in your .proto file. For example, Open would be in the namespace Foo.Bar.
Packages and Name Resolution

Protobuf 中的类型名称的解析方式类似与 C++: 首先搜索最里面的作用域,然后搜索前一层的作用域, 依此类推,每个包都被认为是其父包的 “内部“。 以 '.' 来衔接父子(包含)关系。

protobuf 编译器通过解析导入的 .proto 文件来解析所有类型名称。 每种语言的代码生成器都知道如何引用该语言中的每种类型,即使它具有不同的范围规则。

Defining Services

如果你是将 protobuf 用于 RPC,你可以直接在 .proto 文件中定义 RPC 的接口,并会更具你所选择的语言生成对应的代码。 下面是一个例子:

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

大部分使用 protobuf 作为 RPC 通讯协议的系统都使用一个叫 gRPC 的协议: 该协议有 Google 开发,是一个跨平台的通讯学医。 gRPC 可以完美的兼容 protobuf 协议,你使用特殊的编译插件即可从 .proto 文件编译出 RPC 相关的代码。

如果你不想使用 gRPC, 而想自己实现自己的 RPC 程序,你可以参考 proto2 的 User Guide.

这里还有一些第三方给予 protobuf 实现的方案,请参考 third-party add-ons wiki page.

JSON Mapping

Proto3 支持 JSON 中的规范编码,从而使在系统之间共享数据更加容易。下表按类型对编码进行了描述。

如果 JSON 编码的数据中缺少某个值,或者该值为 null,则在解析为协议缓冲区时,它将被解释为适当的默认值。 如果字段在 protobuf 中具有默认值,则默认情况下会在 JSON 编码数据中将其省略以节省空间。 有些实现方案可能会提供选项,以在 JSON 编码的输出中默认值的字段。

proto3 的官方文档中有个表格定义了 JSON 的类型和, protobuf 消息类型的对应关系。 详情请请参考 https://developers.google.com/protocol-buffers/docs/proto3#json

JSON options

proto3 JSON 的实现可能提供如下选项:

  • Emit fields with default values 默认情况下,proto3 JSON 输出中会省略具有默认值的字段。 实现可能(可以)提供一个选项,以使用默认值覆盖此行为和输出字段。
  • Ignore unknown fields Proto3 JSON 解析器默认情况下应拒绝未知字段,但可以提供在解析时忽略未知字段的选项。
  • Use proto field name instead of lowerCamelCase name: 默认情况下,proto3 JSON 输出应将字段名称转换为 lowerCamelCase 并将其用作 JSON 名称。 但也可能(可以)提供一个选项,改为使用原型字段名称作为 JSON 名称。 Proto3 JSON 解析器必须接受转换后的 lowerCamelCase 名称和原型字段名称。
  • Emit enum values as integers instead of strings 默认情况下,JSON 输出中使用枚举值的名称。可以提供一个选项来代替使用枚举值的数字值。
Options

.proto 文件中的各个声明可以使用多个选项进行修饰。 选项不会改变声明的整体含义,但可能会影响在特定上下文中处理声明的方式。 可用选项的完整列表在 google/protobuf/descriptor.proto 中有定义。

一些选项是文件级选项,这意味着它们应在 top( head ) 范围内编写,而不是在任何消息,枚举或服务定义内。 一些选项是消息级别的选项,这意味着它们应该写在消息定义中。 一些选项是字段级选项,这意味着它们应在字段定义中编写。 选项也可以写在枚举类型,枚举值,字段,服务类型和服务方法中; 但是,目前对于这些功能都不存在有用的选项。

以下是一些最常用的选项:

  • java_package 该 Option 指定生成 java 代码使用的包名。 如果在 proto 文件中没有显示指定 java_package, 那么 protobuf 的编译器将会以默认的的方式生成 java 包名(包名由 package 字段指定)。 但是,proto packages 包通常不能成为良好的 Java package 名,因为 proto 软件包不应以反向域名开头。 如果未生成Java代码,则此选项无效。
    option java_package = "com.example.foo";
    
  • java_multiple_files 使 top level 的 messages, enums,和 services 在程序包级别定义,而不是在以 .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(file option) 可以为如下的值。这个选项作用于生成 C++ 和 JAVA 代码的时候。
    • SPEED( default ) protobuf 编译器将生成代码,用于对消息类型进行序列化,解析和执行其他常见操作。 此代码已高度优化。
    • CODE_SIZE protobuf 编译器将生成最少的类,并将依赖于基于反射的共享代码来实现序列化,解析和其他各种操作。 因此,生成的代码将比使用 SPEED 的代码小得多,但是操作会更慢。 类仍将实现与 SPEED 模式下完全相同的公共 API。 此模式在包含大量 .proto 文件且不需要所有文件都快速达到要求的应用程序中最有用。
    • LITE_RUNTIME protobuf 编译器将生成仅依赖于 “lite” 运行时库的类(libprotobuf-lite 而非 libprotobuf)。 精简版运行时比完整库要小得多(大约小一个数量级),但省略了某些功能,例如描述符和反射。 这对于在受限平台(例如手机)上运行的应用程序特别有用。 编译器仍将像在 SPEED 模式下一样快速生成所有方法的实现。 生成的类将仅以每种语言实现 MessageLite 接口,这些接口是完整 Message 接口的方法的子集。
    option optimize_for = CODE_SIZE;
    
  • cc_enable_arenas (file option) 使能 C++ arenas allocation 能力
  • objc_class_prefix (file option) 设置 Objective-C 类的前缀,该前缀是所有 Objective-C 生成的类以及此 .proto 枚举的前缀。 没有默认值。您应该使用 Apple 推荐的 3-5 个大写字符之间的前缀。 请注意,Apple 保留所有 2 个字母前缀。
  • deprecated (field option) 如果设置为 true,则表明该字段已弃用,并且不应由新代码使用。 在大多数语言中,这没有实际效果。 在 Java 中,成为 @Deprecated 注解。 将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这反过来将导致在编译尝试使用该字段的代码时发出警告。 如果该字段未被任何人使用,并且您想阻止新用户使用该字段,请考虑使用保留语句替换该字段声明。
    int32 old_field = 6 [deprecated = true];
    
Custom Options

协议缓冲区还允许您定义和使用自己的选项。 这是大多数人不需要的高级功能。 如果您确实需要创建自己的选项, 请参阅 Proto2 Language Guide 以了解详细信息。 请注意,自定义的选项使用时需要用 extension 关键字,而且其仅适用于 proto3 中的自定义选项。

Generating Your Classes

要生成 Java,Python,C ++,Go,Ruby,Objective-C 或 C# 代码,您需要使用 .proto 文件中定义的消息类型, 您需要在 .proto 上运行 protobuf 编译器。 如果尚未安装编译器,请下载 软件包 并按照 README 中的说明进行操作。 对于 Go,您还需要为编译器安装一个特殊的代码生成器插件:您可以在 GitHub 上的 golang/protobuf 存储库中找到此代码以及安装说明。

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 protobuf 编译器将会在该指定目录中寻找 .proto 的文件。 如果省略则会在当前目录中查找。 可以通过多次设置 --proto_path 来指定多个导入目录。 编译器会按照你设置的目录顺序搜索编译文件。 -I--proto_path 的缩写形式。
  • 你需要提供至少一个输出参数:
    • --cpp_out 生成 C++ 的目录,参考 C++ Generated Code Refrence generates C++ code in DST_DIR. See the C++ generated code reference for more.
    • --java_out 生成 C++ 的目录,参考 java Generated Code Refrence -- generates Java code in DST_DIR. See the Java generated code reference for more.
    • --python_out 生成 C++ 的目录,参考 python Generated Code Refrence -- generates Python code in DST_DIR. See the Python generated code reference for more.
    • --go_out 生成 C++ 的目录,参考 Go Generated Code Refrence -- generates Go code in DST_DIR. See the Go generated code reference for more.
    • --ruby_out 生成 C++ 的目录,参考 Ruby Generated Code Refrence -- generates Ruby code in DST_DIR. Ruby generated code reference is coming soon!
    • --objc_out 生成 C++ 的目录,参考 objective-c Generated Code Refrence -- generates Objective-C code in DST_DIR. See the Objective-C generated code reference for more.
    • --csharp_out 生成 C++ 的目录,参考 C# Generated Code Refrence -- generates C# code in DST_DIR. See the C# generated code reference for more.
    • php_out 生成 C++ 的目录,参考 PHP Generated Code Refrence

    为了更加方便,如果 DST_DIR 以 .zip 或 .jar 结尾,则编译器会将输出写入具有给定名称的单个 ZIP 格式的存档文件。 根据 Java JAR 规范的要求,还将为 .jar 输出提供清单文件。 但请注意,如果输出存档已经存在,它将被覆盖; 编译器不够智能,无法将文件添加到现有存档中。
  • 您必须提供一个或多个.proto 文件作为输入。 可以一次指定多个 .proto 文件。 尽管这些文件是相对于当前目录命名的, 但是每个文件都必须位于 IMPORT_PATH 指定的目录中,同时也为了方便编译器可以确定其规范名称。
Protobuf 的编码
参考文档
参考文档
  • [link]机器学习即服务之BigML特性介绍和入门教程
  • [link]【深度开源 open经验】机器学习平台、框架、库和软件集合
  • [link]软件形式化方法概述
  • [link]形式化方法--百度百科
  • [link]敏捷开发中的持续集成
  • [link]github开源项目-算法实现之路

机器学习 开发pingtai 机器人开发平台

原创文章,版权所有,转载请获得作者本人允许并注明出处
我是留白;我是留白;我是留白;(重要的事情说三遍)
posted @ 2021-02-28 22:30  Mojies  阅读(876)  评论(0编辑  收藏  举报