ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Protobuf语法 (proto3)

2020-12-26 10:32:12  阅读:288  来源: 互联网

标签:文件 oneof Protobuf proto 语法 proto3 消息 类型 message


本文是一篇译文,原文地址为:https://developers.google.com/protocol-buffers/docs/proto3

在阅读本篇文章之前可参考我的另一篇博文:Protobuf语法指南(proto2)

定义一个消息类型

先来看一个非常简单的例子。假设你想定义一个“搜索请求”的消息格式,每一个请求含有一个查询字符串、你感兴趣的查询结果所在的页数,以及每一页多少条查询结果。可以采用如下的方式来定义消息类型的.proto文件:

syntax = "proto3";

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
}
  • 文件的第一行指定你正在使用 proto3 语法: protocol buffer 编译器默认使用的是 proto2 。 这必须是文件的非空、非注释的第一行。
  • 这个 SearchRequest 消息定义了三个字段(名称/值对),每一条 SearchRequest 消息类型的数据都包含这三个字段定义的数据。每个字段包含一个名称和类型。

指定字段类型

在上面的例子中,所有字段都是标量类型:两个整型(page_number和result_per_page),一个string类型(query)。当然,你也可以为字段指定其他的合成类型,包括枚举(enumerations)或其他消息类型。

分配标识号

我们可以看到在上面定义的消息中,给每个字段都定义了唯一的数字值。这些数字是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。

最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf协议实现中对这些进行了预留。如果非要在.proto文件中使用这些预留标识号,编译时就会报警。类似地,你不能使用之前保留的任何标识符。

指定字段规则

消息的字段可以是一下情况之一:

  • singular(默认):一个格式良好的消息可以包含该段可以出现 0 或 1 次(不能大于 1 次)。
  • repeated:在一个格式良好的消息中,这种字段可以重复任意多次(包括0次)。重复的值的顺序会被保留。

默认情况下,标量数值类型的repeated字段使用packed的编码方式。

关于 packed 编码的信息,请查看 Protocol Buffer Encoding

在一个.proto文件中可以定义多个消息类型。在定义多个相关的消息的时候,这一点特别有用——例如,如果想定义与SearchResponse消息类型对应的回复消息格式的话,你可以将它添加到相同的.proto文件中,如:

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

message SearchResponse {
  ...
}

添加注释

向.proto文件添加注释,可以使用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.
}

保留字段

如果通过将字段完全删除或将其注释来更新消息类型,则在将来,当用户更新其消息类型时,他们可以重用那些字段的编号。 如果以后加载相同.proto文件的旧版本,这可能会导致严重问题,包括数据损坏,隐私错误等。 确保不会发生这种情况的一种方法是指定已删除字段的字段编号为“reserved”。 如果将来的任何用户尝试使用这些字段标识符,协议缓冲编译器将会发出抱怨。

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

注意:不能在同一 “reserved” 语句中将字段名称和字段编号混合在一起指定。

从.proto文件生成了什么?

当用protocolbuffer编译器来运行.proto文件时,编译器将生成所选择语言的代码,这些代码可以操作在.proto文件中定义的消息类型,包括获取、设置字段值,将消息序列化到一个输出流中,以及从一个输入流中解析消息。

  • 对C++来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。
  • 对Java来说,编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。
  • 对Python来说,有点不太一样——Python编译器为.proto文件中的每个消息类型生成一个含有静态描述符的模块,,该模块与一个元类(metaclass)在运行时(runtime)被用来创建所需的Python数据访问类。
  • 对于 Go 语言,针对每一个定义的消息类型编译器会创建一个带类型的.pb.go 文件。
  • 对于 Ruby 语言,编译器会创建一个带 Ruby 模块的.rb 文件,其中包含了所有你定义的消息类型。
  • 对于 JavaNano,编译器会创建 Java 语言类似的输出文件,但是没有 Builder 构造类。
  • 对于 Ojective-C,编译器会创建一个 pbobjc.h 和一个 pbobjc.m 文件,为每一个消息类型都创建一个类来操作。
  • 对于 C#语言,编译器会为每一个.proto 文件创建一个.cs 文件,为每一个消息类型都创建一个类来操作。

标量类型

一个标量消息字段可以含有一个如下的类型——该表格展示了定义于.proto文件中的类型,以及与之对应的、在自动生成的访问类中定义的类型:

.proto TypeNotesC++ TypeJava TypePython Type[2]Go Type
double doubledoublefloat*float64
float floatfloatfloat*float32
int32使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint32int32intint*int32
int64使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint64int64longint/long[3]*int64
uint32使用可变长度编码uint32int[1]int/long[3]*uint32
uint64使用可变长度编码uint64long[1]int/long[3]*uint64
sint32使用可变长度编码。有符号的 int 值。这些比常规 int32 对负数能更有效地编码int32intint*int32
sint64使用可变长度编码。有符号的 int 值。这些比常规 int64 对负数能更有效地编码int64longint/long[3]*int64
fixed32总是四个字节。如果值通常大于 228,则比 uint32 更有效。uint32int[1]int/long[3]*uint32
fixed64总是八个字节。如果值通常大于 256,则比 uint64 更有效。uint64long[1]int/long[3]*uint64
sfixed32总是四个字节int32intint*int32
sfixed64总是八个字节int64longint/long[3]*int64
bool boolbooleanbool*bool
string字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本stringStringstr/unicode[4]*string
bytes可以包含任意字节序列stringByteStringstr[]byte

在 Protocol Buffer 编码 中你可以找到有关序列化 message 时这些类型如何被编码的详细信息。
[1] 在 Java 中,无符号的 32 位和 64 位整数使用它们对应的带符号表示,第一个 bit 位只是简单的存储在符号位中。
[2] 在所有情况下,设置字段的值将执行类型检查以确保其有效。
[3] 64 位或无符号 32 位整数在解码时始终表示为 long,但如果在设置字段时给出 int,则可以为int。在所有情况下,该值必须适合设置时的类型。见 [2]。
[4] Python 字符串在解码时表示为 unicode,但如果给出了 ASCII 字符串,则可以是 str(这条可能会发生变化)。
[5] 整型用于64位的机器,字符串用于32位的机器

默认值

当一个消息被解析的时候,如果在编码后的消息结构中某字段没有初始值,相应的字段在被解析的对象中会被设置默认值。这些默认值都是类型相关的。

  • 字符串默认值为空字符串。
  • 字节类型默认值是空字节。
  • 布尔类型默认值为 false。
  • 数值类型默认值为 0。
  • 枚举类型默认值是其定义中的第一个值,它必须为 0。
  • 消息类型的默认值没有设置。它的具体值与使用的编程语言有关。

repeated字段的默认值为空(通常是相应编程语言的空列表)。

注意:对于标量消息字段,当消息被解析时,我们没有办法知道某个字段是否被显示地设定为默认值(例如一个布尔类型的字段值是否被设置为 false),也许这个字段压根就没有被设定值。当我们定义一个消息类型时,我们需要牢记这点。例如,如果一个布尔类型的字段在其值被设置为false时,会导致某种行为的发生,而我们并不想让这种行为在默认情况下也会发生,那么我们就不要定义这个bool类型的字段。 还要注意的是,在序列化的时候,如果标量消息字段的值设为默认值,这个值是不会被序列化的。

枚举

当需要定义一个消息类型的时候,可能想为一个字段指定某“预定义值序列”中的一个值。例如,假设要为每一个SearchRequest消息添加一个 corpus字段,而corpus的值可能是UNIVERSAL,WEB,IMAGES,LOCAL,NEWS,PRODUCTS或VIDEO中的一个。 其实可以很容易地实现这一点:通过向消息定义中添加一个枚举(enum)就可以了。一个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,每个枚举类型的定义中,它的第一个元素都应该是一个等于 0 的常量。 这是因为:

  • 枚举类型中必须包含值为0的元素,这样我们才可以使用0作为数值型字段的默认值。
  • 这个为 0 的元素必须是第一个元素,是为了兼容 proto2 语法(proto2 中枚举类型的第一个元素总是默认值)。
    你可以为枚举常量定义别名。 需要设置option allow_alias为 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的语法格式。

使用其他的Message类型

你可以将其他message类型用作字段类型。例如,假设在每一个SearchResponse消息中包含Result消息,此时可以在同一.proto文件中定义一个Result消息类型,然后在SearchResponse消息中指定一个Result类型的字段,如:

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}
  • 导入定义

在上面的例子中,Result消息类型与SearchResponse是定义在同一文件中的。如果想要使用的消息类型已经在其他.proto文件中已经定义过了呢?

你可以通过导入(importing)其他.proto文件中的定义来使用它们。要导入其他.proto文件的定义,你需要在你的文件中添加一个导入声明,如:

import "myproject/other_protos.proto";

默认情况下你只能使用直接导入的.proto文件中的定义。然而, 有时候你需要移动一个.proto文件到一个新的位置。现在,你可以在旧位置放置一个虚拟 .proto 文件,以使用命令 import public将所有导入转发到新位置,而不是直接移动 .proto 文件并在一次更改中更新所有调用点。导入包含 import public 语句的 proto 的任何人都可以导入公共依赖项。例如:

// 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

使用命令 -I/–proto_path 让 protocol 编译器在指定的一组目录中搜索要导入的文件。如果没有给出这个命令选项,它将查找调用编译器所在的目录。通常,你应将 --proto_path 设置为项目的根目录,并对所有导入使用完全限定名称。

使用proto2消息类型

我们有可能在proto3消息中导入并使用proto2消息类型,反之亦然。然而在proto3语法中不能直接使用proto2的枚举类型字段(如果是被导入的proto2消息使用的,这是可以的)

嵌套类型

你可以在其他 message 类型中定义和使用 message 类型,如下例所示 - 此处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;
    }
  }
}

更新 message 类型

如果现有的 message 类型不再满足你的所有需求 - 例如,你希望 message 格式具有额外的字段 - 但你仍然希望使用基于旧的message格式产生的代码,请不要担心!在不破坏任何现有代码的情况下更新 message 类型非常简单。请记住以下规则:

  • 请勿更改任何现有字段的字段编号
  • 如果添加了新的字段,基于“旧的”消息格式的代码而序列化的任何消息仍可以被新生成的代码解析。你应该为这些元素设置合理的默认值,以便新代码可以正确地与旧代码生成的 message 进行交互。同样,你的新代码创建的 message 可以由旧代码解析:旧的二进制文件在解析时只是忽略新字段。但是未丢弃这个新字段,如果稍后序列化消息,则将新字段与其一起序列化。因此,如果将消息传递给新代码,则新字段仍然可用。(兼容性强
  • 可以删除字段,只要在新的 message 类型中不再使用该字段的编号。也许你希望的是重命名该字段,那么可以添加前缀 “OBSOLETE_”,或者将字段编号保留(reserved),以便将来你的 .proto 文件的用户不会不小心重用这个编号。
  • int32,uint32,int64,uint64 和 bool 都是兼容的 - 这意味着你可以将字段从这些类型更改为另一种类型,而不会破坏向前或向后兼容性。如果从中解析出一个不符合相应类型的数字,你将获得与在 C++ 中将该数字转换为该类型时相同的效果(例如,如果将 64 位数字作为 int32 读取,它将被截断为 32 位)。
  • sint32 和 sint64 彼此兼容,但与其他整数类型不兼容。
  • 只要bytes是有效的 UTF-8,string 和 bytes 就是兼容的。
  • 如果bytes包含 message 的编码版本,则内嵌的 message 与 bytes 兼容。
  • fixed32 与 sfixed32 兼容,fixed64 与 sfixed64 兼容。
  • enum 与 int32,uint32,int64 和 uint64兼容(注意,如果它们不匹配,值将被截断)。但请注意,在反序列化消息时,客户端代码可能会以不同方式对待它们:例如,无法识别的proto3枚举类型将保留在消息中,但在反序列化消息时如何表示它,这是与语言相关的。 Int字段总是保留它们的值。
  • 将单个值更改为新的oneof 的成员是安全且二进制兼容的。如果你确定没有代码一次设置多个,则将多个 字段移动到新的 oneof 中可能是安全的。但是将任何字段移动到现有的 oneof 是不安全的。

未知字段

未知字段是格式良好的protocol buffer序列化数据中解析器无法识别的字段。 例如,当旧二进制文件解析具有新字段的新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。

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

Any

Any消息类型允许您可以像使用嵌入类型一样使用消息,而无需拥有其.proto定义。 一个Any型的消息包含任意字节的序列化消息,以及一个URL,它作为全局唯一标识符来解析消息的类型。为了使用 Any 类型的消息,你需要import 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()和 PackTo()方法。

// 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 语法 ,Any 类型就是替代了 proto2 中的 extensions 。

Oneof

如果你的 message 包含许多可选字段,并且最多只能同时设置其中一个字段,则可以使用 oneof 功能强制执行此行为并节省内存。

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

使用 Oneof

要在 .proto 中定义 oneof,请使用 oneof 关键字,后跟你的 oneof 名称,在本例中为 test_oneof:

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

然后,将 oneof 字段添加到 oneof 定义中。你可以添加任何类型的字段,但不能使用 required,optional 或 repeated 关键字。如果需要向 oneof 添加重复字段,可以使用包含重复字段的 message。

在生成的代码中,oneof 字段与常规 optional 方法具有相同的 getter 和 setter。你还可以使用特殊方法检查 oneof 中的值(如果有)。

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 不支持扩展
  • oneof 不能使用 repeated
  • 反射 API 适用于 oneof 字段
  • 如果你使用的是 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() 交换了两条 oneofs 消息,则每条消息将以另一条消息的 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());

向后兼容性问题

添加或删除其中一个字段时要小心。如果检查 oneof 的值返回 None/NOT_SET,则可能意味着 oneof 尚未设置或已设置为 oneof 的另一个字段。这种情况是无法区分的,因为无法知道未知字段是否是 oneof 成员。

标签重用问题

  • 将字段移入或移出 oneof:在序列化和解析 message 后,你可能会丢失一些信息(某些字段将被清除)。但是,你可以安全地将单个字段移动到新的 oneof 中,并且如果已知只有一个字段被设置,则可以移动多个字段。

  • 删除 oneof 字段并将其重新添加回去:在序列化和解析 message 后,这可能会清除当前设置的 oneof 字段。

  • 拆分或合并 oneof:这与移动常规的字段有类似的问题。

Maps

如果要在数据定义中创建关联映射,protocol buffers 提供了一种方便快捷的语法:

map<key_type, value_type> map_field = N;

其中 key_type 可以是任何整数或字符串类型(任何标量类型除浮点类型和 bytes)。请注意,枚举不是有效的 key_type。value_type 可以是除 map 之外的任何类型。

因此,举个例子,如果要创建项目映射,其中每个 “Project” message 都与字符串键相关联,则可以像下面这样定义它:

map<string, Project> projects = 3;

Maps 特性

  • maps 不能是 repeated
  • map 值的网络序和 map 迭代序未定义,因此你不能依赖于特定顺序的 map 项
  • 生成 .proto 的文本格式时,maps 按键排序。数字键按数字排序
  • 当解析或合并时,如果有重复的 map 键,则使用最后看到的键。从文本格式解析 map 时,如果存在重复键,则解析可能会失败。
  • 如果为映射字段提供键但没有值,则字段序列化时的行为取决于语言。 在C ++,Java和Python中,类型的默认值是序列化的,而在其他语言中,键值均不是序列化的。

向后兼容性

map 语法等效于以下内容,因此不支持 map 的 protocol buffers 实现仍可处理你的数据:

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

repeated MapFieldEntry map_field = N;

任何支持 maps 的 protocol buffers 实现都必须生成和接受上述定义所能接受的数据。

Packages

你可以将可选的package说明符添加到 .proto 文件,以防止 protocol message 类型之间的名称冲突。

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

然后,你可以在定义 message 类型的字段时使用package说明符:

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

package 对生成的代码的影响取决于你所选择的语言:

  • 在 C++ 中,生成的类包含在 C++ 命名空间中。例如,Open 将位于命名空间 foo::bar 中。
  • 在 Java 中,除非在 .proto 文件中明确提供选项 java_package,否则该包将用作 Java 包
  • 在 Python 中,package 指令被忽略,因为 Python 模块是根据它们在文件系统中的位置进行组织的
  • 在Go 中,package 指令被忽略,生成的.pb.go文件位于以相应的go_proto_library规则命名的包中。
  • 在 Ruby 中,所生成的类被包裹在嵌套的 Ruby 命名空间中,包名被转换为 Ruby 大写样式(第一个字母大写,如果第一个不是字母字符,添加PB_前缀)。例如,Open 类会出现在命名空间 Foo::Bar 中。
  • 在 JavaNano 中,除非你在.proto 文件中显示声明了 option java_package,否则这个包名会被作为 Java 包名使用。

Packages 和名称解析

protocol buffer 语言中的类型名称解析与 C++ 类似:首先搜索最里面的范围,然后搜索下一个范围,依此类推,每个包被认为是其父包的 “内部”。开头的 ‘.’(例如 .foo.bar.Baz)意味着从最外层的范围开始。

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

定义服务

如果要将 message 类型与 RPC(远程过程调用)系统一起使用,则可以在 .proto 文件中定义 RPC 服务接口,protocol buffer 编译器将以你选择的语言生成服务接口和stub(桩)。因此,例如,如果要定义一个 RPC 服务,其中包含一个根据 SearchRequest 返回 SearchResponse 的方法,可以在 .proto 文件中定义它,如下所示:

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

与 ProtoBuf 直接搭配使用的 RPC 系统是 gRPC :一个 Google 开发的平台无关语言无关的开源 RPC 系统。gRPC 和 ProtoBuf 能够非常完美的配合,你可以使用专门的 ProtoBuf 编译插件直接从.proto 文件生成相关 RPC 代码。

如果你不想使用 gRPC,你也可以用自己的 RPC 来实现和 ProtoBuf 协作。 更多的关于RPC 的信息请参考[译]Protobuf语法指南(proto2) 。

JSON 映射

Proto3 支持标准的 JSON 编码,使得在不同的系统直接共享数据变得简单。下表列出的是基础的类型对照。

在 JSON 编码中,如果某个值被设置为 null 或丢失,在映射为 ProtoBuf 的时候会转换为相应的 默认值 。 在 ProtoBuf 中如果一个字段是默认值,在映射为 JSON 编码的时候,这个默认值会被忽略以节省空间。可以通过选项设置,使得 JSON 编码输出中字段带有默认值。

proto3JSONJSON exampleNotes
messageobject{"fooBar": v, "g": null, …}生成JSON对象。 消息字段名称映射到lowerCamelCase并成为JSON对象键。 如果指定了json_name字段选项,则将指定的值用作键。解析器接受 lowerCamelCase名称(或json_name选项指定的名称)和原始proto字段名称。 null是所有字段类型的可接受值,并被视为相应字段类型的默认值。
enumstring"FOO_BAR"使用proto中指定的枚举值的名称。 解析器接受枚举名称和整数值。
map<K,V>object{"k": v, …}所有键都转换为字符串。
repeated Varray[v, …]null被转换为空列表[]
booltrue, falsetrue, false 
stringstring"Hello World!" 
bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"JSON值将是使用带填充的标准base64编码编码为字符串的数据。 接受带有/不带填充的标准或URL安全base64编码。
int32, fixed32, uint32number1, -10, 0JSON值将是十进制数。 接受数字或字符串。
int64, fixed64, uint64string"1", "-10"JSON值将是十进制字符串。 接受数字或字符串。
float, doublenumber1.1, -10.0, 0, "NaN", "Infinity"JSON值将是一个或多个特殊字符串值“NaN”,“Infinity”和“-Infinity”。 接受数字或字符串。 指数表示法也被接受。
Anyobject{"@type": "url", "f": v, … }如果Any包含具有特殊JSON映射的值,则它将按如下方式转换: {“@ type”:xxx,“value”:yyy} 。 否则,该值将转换为JSON对象,并将插入 “@ type” 字段以指示实际数据类型。
Timestampstring"1972-01-01T10:00:20.021Z"使用RFC 3339,其中生成的输出将始终被Z标准化并使用0,3,6或9个小数位。 也接受“Z”以外的偏移。
Durationstring"1.000340012s", "1s"生成的输出始终包含0,3,6或9个小数位,具体取决于所需的精度,后跟后缀“s”。 接受的是任何小数位(也没有),只要它们符合纳秒精度并且需要后缀“s”。
Structobject{ … }任意JSON对象
Wrapper typesvarious types2, "2", "foo", true, "true", null, 0, …Wrappers在JSON中使用与包装基元类型相同的表示形式,除了在数据转换和传输期间允许并保留 null .
FieldMaskstring"f.fooBar,h"field_mask.proto.
ListValuearray[foo, bar, …] 
Valuevalue 任意JSON 值
NullValuenull JSON null

选项 Options

.proto 文件中的各个声明可以使用一些选项进行诠释。选项不会更改声明的含义,但可能会影响在特定上下文中处理它的方式。可用选项的完整列表在 google/protobuf/descriptor.proto中定义。

一些选项是文件级选项,这意味着它们应该在更高层的范围内编写,而不是在任何消息,枚举或服务定义中。一些选项是 message 消息级选项,这意味着它们应该写在 message 消息定义中。一些选项是字段级选项,这意味着它们应该写在字段定义中。选项也可以写在枚举类型、枚举值、服务类型和服务方法上,但是,目前在这几个项目上并没有任何有用的选项。

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

  • java_package(文件选项):要用于生成的 Java 类的包。如果 .proto 文件中没有给出显式的 java_package 选项,那么默认情况下将使用 proto 包(使用 .proto 文件中的 “package” 关键字指定)。但是,proto 包通常不能生成好的 Java 包,因为 proto 包不会以反向域名开头。如果不生成Java 代码,则此选项无效。
option java_package = "com.example.foo";
  • java_outer_classname(文件选项):要生成的最外层 Java 类(以及文件名)的类名。如果 .proto 文件中没有指定显式的 java_outer_classname,则通过将 .proto 文件名转换为 camel-case 来构造类名(因此 foo_bar.proto 变为 FooBar.java)。如果不生成 Java 代码,则此选项无效。
option java_outer_classname = "Ponycopter";
  • optimize_for(文件选项):可以设置为 SPEED,CODE_SIZE 或 LITE_RUNTIME。这会以下列方式影响 C++和 Java 的代码生成器(可能还有第三方生成器):

    • SPEED(默认值):protocol buffer 编译器将生成用于对 message 类型进行序列化、解析和执行其他常见操作的代码。此代码经过高度优化。

    • CODE_SIZE:protocol buffer 编译器将生成最少的类,并依赖于基于反射的共享代码来实现序列化,解析和各种其他操作。因此,生成的代码将比使用 SPEED 小得多,但操作会更慢。类仍将实现与 SPEED 模式完全相同的公共 API。此模式在包含大量 .proto 文件的应用程序中最有用,并且不需要所有这些文件都非常快。

    • LITE_RUNTIME:protocol buffer 编译器将生成仅依赖于 “lite” 运行时库(libprotobuf-lite 而不是libprotobuf)的类。精简版的运行时间比整个库小得多(大约小一个数量级),但省略了描述符和反射等特定功能。这对于在移动电话等受限平台上运行的应用程序尤其有用。编译器仍将生成所有方法的快速实现,就像在 SPEED 模式下一样。生成的类将仅实现每种语言的 MessageLite 接口,该接口仅提供完整 Message 接口的方法的子集。

option optimize_for = CODE_SIZE;
  • 1
  • cc_generic_services,java_generic_services,py_generic_services(文件选项):protocol buffer 编译器应根据服务定义判断是否生成 C++,Java 和 Python 抽象服务代码。由于遗留原因,这些默认为 “true”。但是,从版本 2.3.0(2010年1月)开始,RPC 实现最好提供代码生成器插件,以生成每个系统的具体代码,而不是依赖于 “抽象” 服务。
// This file relies on plugins to generate service code.
option cc_generic_services = false;
option java_generic_services = false;
option py_generic_services = false;
  • cc_enable_arenas(文件选项):为 C++ 生成的代码启用 arena allocation
  • objc_class_prefix(文件级选项):这个选项用来设置编译器从.proto 文件生成的类和枚举类型的名称前缀。这个选型没有默认值。您应该使用 3–5 个大写字母作为前缀。 请注意:所有的 2 个字母的前缀由苹果公司保留使用。
  • deprecated (字段选项):这个选项如果设置为 true ,表示该字段已被废弃,你不应该在后续的代码中使用它。 在大多数语言中这没有任何实际的影响。在 Java 中,它会变成废弃的注释。将来,其他特定的语言的代码生成器在为被标注为 deprecated 的字段生成操作函数的时候,编译器会在尝试使用该字段的代码时发出警告。如果不希望将来有人使用使用这个字段,请考虑用 reserved 关键字 声明该字段。
int32 old_field = 6 [deprecated=true];
  • 1

自定义选项

Protocol Buffers 甚至允许你定义和使用自己的选项。请注意,这是高级功能,大多数人不需要。更多信息参考[译]Protobuf语法指南(proto2)

生成类

要生成 Java, Python, C++, Go, Ruby, Objective-C, 或者r C#代码,你需要使用 .proto 文件中定义的 message 类型,你需要在 .proto 上运行 protocol buffer 编译器 protoc。

Protocol 编译器的调用如下:

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++ 代码。
  • –java_out在DST_DIR中生成 Java 代码。
  • –python_out 在 DST_DIR 中生成 Python 代码。
  • –go_out 在 DST_DIR 中 生成 Go 代码 。
  • –ruby_out 在 DST_DIR 中生成 Ruby 代码。
  • –javanano_out 在 DST_DIR 中 生成 JavaNano 代码 。
  • –objc_out 在 DST_DIR 中生成 Objective-C 代码。
  • –csharp_out 在DST_DIR 中生成 C#代码。

为了方便起见,如果 DST_DIR 以 .zip 或 .jar 结尾,编译器会将输出写入到具有给定名称的单个 ZIP 格式的存档文件。.jar 输出还将根据 Java JAR 规范的要求提供清单文件。请注意,如果输出存档已存在,则会被覆盖;编译器不够智能,无法将文件添加到现有存档中。

你必须提供一个或多个 .proto 文件作为输入。可以一次指定多个 .proto 文件。虽然文件是相对于当前目录命名的,但每个文件必须驻留在其中一个 IMPORT_PATH 中,以便编译器可以确定其规范名称。

标签:文件,oneof,Protobuf,proto,语法,proto3,消息,类型,message
来源: https://blog.csdn.net/weixin_37989267/article/details/111716559

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有