乐趣区

关于protocol-buffer:Protocol-Buffers-系列-3-proto2-proto语法指南

本文介绍如何应用 Protocol Buffers 语言来结构协定缓冲区数据,包含 .proto 文件 语法以及如何从 .proto 文件 生成数据拜访类。它涵盖了 proto2 版本的协定缓冲区语言。

本文只是一个参考指南,后续会出 Java 语言的教程。
如何应用本指南?在工作中遇到时,通过查问关键字来查找须要的知识点。

定义一个 Message 类型

首先咱们开始为一个简略的例子,如果咱们要构建一个搜寻的申请音讯格局。
每个搜寻的音讯包含三个参数:

  1. 要查问的字符串
  2. 指定的页码
  3. 以及后果数

上面则就是这个.proto 对应的 message 格局

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

SearchRequest 音讯 定义指定了三个字段(也可称为名称或者键值对),每个字段都有一个名称和一个类型。

指定的字段类型

下面的例子中,能够看到有两个类型:两个整数(页码和每页后果数量)、字符串(查问条件)。
当然,咱们也能够定义字段为复合类型,包含枚举和其余的音讯类型。

调配字段编号

下面的例子中,能够看到每个字段都有一个举世无双的编号,这些编号用于在二进制音讯中标识咱们的字段,也就是相当于字段的别名。
1 ~ 15 范畴内的字段应用的是 一个字节 来示意。
16 ~ 2047 范畴内的字段应用 两个字节 来示意。
所以,为了晋升性能,咱们尽可能的将呈现十分频繁的字段保留到 1~15 的范畴中。

最小的编号是 1,最大的编号是 2^29 - 1,也就是 536,870,911。
留神,不能应用数字 19000 到 19999,因为它们是为协定缓冲区实现保留的 – 如果在 .proto 中应用这些保留数字之一,协定缓冲区编译器会报错。
同一个音讯内的编号不能反复,这个也是须要留神的。

指定字段规定
  • required:简略了解为必选字段,数量为 1。
  • optional:可抉择的,数量不超过 1。
  • repeated:所润饰的字段能够任意次数反复,包含 0 次。反复值的程序也会被记录。

因为历史起因,标量数字类型的反复字段(例如,int32、int64、enum)的编码效率没有达到应有的程度。新代码应该应用非凡选项 [packed=true] 来取得更高效的编码。例如:

repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];

packed 为压缩字段,这个在后续的文章会解说。

required 也就象征了 forever,因而咱们在设置字段时,须要尽可能的多去考虑一下字段的作用范畴。

增加更多的音讯类型

咱们能够在一个.proto 文件中,设置多个音讯类型。


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

message SearchResponse {...}

尽管能够在单个 .proto 文件中定义多种音讯类型(例如音讯、枚举和服务),但在单个文件中定义大量具备不同依赖关系的音讯时,也会导致依赖关系收缩。官网倡议在每个 .proto 文件中蕴含尽可能少的音讯类型,然而这个数值并为给出具体范畴,须要依据理论状况评定。

保留字段

当咱们在更新音讯构造时,删除了某个字段或者正文掉了某个字段,未来的某一个用户能够应用咱们删除了的这个字段对应的编号,这是没有问题的。然而一旦在前期加载到了.proto 批改前的旧版本,就会因为编号抵触而产生问题,导致数据错乱的状况,因而能够引入保留字段。

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

保留字段编号范畴包含:2、15、9、10、11。也可应用 9 to max,以全副保留后续的编号。
请留神,不能在同一个保留语句中混合应用字段名和字段号。

依据.proto 文件主动生成了什么?

对于 java 来说,编译器生成一个 .java 文件,其中蕴含每个音讯类型的类,以及用于创立音讯类实例的非凡 Builder 类。

类型对照表

.proto 类型 形容 java 类型
double double
float float
int32 应用可变长度编码。对正数进行编码效率低下——如果您的字段可能有负值,请改用 sint32。 int
int64 应用可变长度编码。对正数进行编码效率低下——如果您的字段可能有负值,请改用 sint64。 long
uint32 应用可变长度编码。 int
uint64 应用可变长度编码。 long
sint32 应用可变长度编码。带符号的 int 值。这些比惯例 int32 更无效地编码正数。 int
sint64 应用可变长度编码。带符号的 int 值。这些比惯例 int64 更无效地编码正数。 long
fixed32 总是 4 个字节。如果值通常大于 2^28,则比 uint32 更无效。 int
fixed64 总是 8 个字节。如果值通常大于 2^56,则比 uint64 更无效。 long
sfixed32 总是 4 个字节。 int
sfixed64 总是 8 个字节。 long
bool boolean
string 字符串必须始终蕴含 UTF-8 编码的文本。 String
bytes 能够蕴含任意字节序列。 ByteString

可选字段和默认值

当字段设置为可选的时候,咱们的音讯可蕴含也能够不蕴含该字段。
当不蕴含时,咱们能够为可选字段设置默认值。

optional int32 result_per_page = 3 [default = 10];

如果没有指定默认值,则为零碎默认值。
字符串为空串,bool 为 false,整型为 0,枚举则为枚举类型的第一个值。
因而在设置枚举类型时,要分外留神。

枚举

咱们能够在 message 外部定义枚举,并为其设置编号,比方咱们设置一个 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];
}

在同一个 enum 中。如果咱们想要多个枚举值对应一个编号,能够应用别名的形式。
上面的实例中,下面的实例不会有问题,然而上面的实例会有谬误提醒。
留神:枚举名称不同,然而枚举值雷同,也会报错,但如果在不同的 message 中,则不会报错。

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}
enum EnumNotAllowingAlias {
  UNKNOWN = 0;
  STARTED = 1;
  // RUNNING = 1;  // 勾销正文此行将导致 Google 外部呈现编译谬误,并在内部呈现正告音讯。}

应用其余 message 类型

您能够应用其余音讯类型作为字段类型。
例如,假如您想在每个 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;
}
import 定义

当咱们须要应用另一个.proto 文件中的 message 时,咱们能够通过导入其余 .proto 文件中的定义来应用它们。要导入另一个 .proto 的定义,请在文件顶部增加一个 import 语句:

import "myproject/other_protos.proto";

默认状况下,咱们只能应用间接 import 的 .proto 文件中的定义。然而,有时咱们可能须要将 .proto 文件挪动到新地位。
咱们能够在旧地位搁置一个占位符 .proto 文件,以应用 import 公共文件的概念将所有导入转发到新地位,而不是间接挪动 .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

任何导入蕴含 import public 语句的 proto 的代码都能够传递依赖 import public 依赖项。

嵌套类型

咱们能够在其余音讯类型中定义和应用音讯类型,如下例所示 – 这里 Result 音讯在 SearchResponse 音讯中定义:

message SearchResponse {
  message Result {
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 1;
}

如果咱们须要在别的 message,援用 result,咱们须要指定号 result 外围类(父类)。

message SomeOtherMessage {optional SearchResponse.Result result = 1;}

您能够随便嵌套音讯。在上面的示例中,请留神名为 Inner 的两个嵌套类型是齐全独立的,因为它们是在不同的音讯中定义的:

message Outer {       // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      optional int64 ival = 1;
      optional bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      optional string name = 1;
      optional bool   flag = 2;
    }
  }
}

更新 message

如果现有的音讯类型不再满足咱们的所有需要 – 例如,咱们心愿音讯格局有一个额定的字段 – 但咱们心愿应用之前旧格局创立的代码,请不要放心!在不毁坏任何现有代码的状况下更新音讯类型非常简单。只需记住以下规定:

  • 不要更改任何现有字段的字段编号。
  • 您增加的任何新字段都应该是 可选的或反复的 。这意味着应用“旧”音讯格局的代码序列化的任何音讯都能够由新生成的代码解析,因为它们不会失落任何必须的元素。您应该 为这些元素设置正当的默认值,以便新代码能够与旧代码生成的音讯正确交互。

    • 相似地,新代码创立的音讯能够由旧代码解析:旧二进制文件在解析时会疏忽新字段。
    • 然而,未知字段不会被抛弃,如果音讯稍后被序列化,未知字段也会随之序列化——因而,如果将消息传递给新代码,新字段依然可用。
  • 如果在更新的音讯类型中不再应用某些字段编号,就能够删除非必填字段。您可能想要重命名该字段,可能增加前缀“OBSOLETE_”,或保留字段编号,以便您的 .proto 的将来用户不会意外重用该编号。
  • 只有类型和编号放弃不变,非必填字段能够转换为扩展名(extension 前面会讲),反之亦然。
  • int32、uint32、int64、uint64 和 bool 都是兼容的——这意味着您能够将字段从其中一种类型更改为另一种类型,而不会毁坏前向或后向兼容性。
  • sint32 和 sint64 互相兼容,但与其余整数类型不兼容。
  • 只有字节是无效的 UTF- 8 编码格局,字符串和字节就兼容。
  • fixed32 与 sfixed32 兼容,fixed64 与 sfixed64 兼容。
  • 对于字符串、字节和音讯字段,optional 与 repeated 兼容。给定一个 repeated 字段的序列化数据作为输出,如果它是一个根本类型字段,那么冀望这个字段是 optional 的客户端将采纳最初一个的输出值,或者如果它是一个 message 类型字段,则合并所有输出元素。

    • 请留神,这对于数字类型(包含布尔值和枚举)通常不平安。数字类型的反复字段能够以打包 (packed 后续会讲) 格局序列化,当须要可选字段时,将无奈正确解析。
  • 更改默认值通常是能够的,但要记住默认值永远不会通过网络发送。因而,如果程序接管到未设置特定字段的音讯,则程序将看到在该程序的协定版本中定义的默认值。它不会看到发送方代码中定义的默认值。
  • 尽量不要批改枚举值,否则会呈现一些奇怪的问题。
  • 在 map<K, V> 和相应的反复音讯字段之间更改字段是二进制兼容的(无关音讯布局和其余限度,请参见上面的 Maps)。然而,更改的安全性取决于应用程序:在反序列化和从新序列化音讯时,应用反复字段定义的客户端将产生语义雷同的后果;然而,应用映射字段定义的客户端可能会从新排序条目并删除具备反复键的条目,因为 map 的 key 不能反复。

扩大 Extensions

扩大容许咱们在 message 中的申明一系列字段编号,用于第三方扩大。
扩大是原始 .proto 文件中未定义类型的字段的占位符。这容许其余 .proto 文件通过应用这些字段编号来定义字段的类型,最终增加到咱们的 message 定义中。让咱们看一个例子:

message Foo {
  // ...
  extensions 100 to 199;
}

这示意 Foo 中的字段编号范畴 [100, 199] 是为扩大保留的。其余用户当初能够在他们本人的 .proto 文件中向 Foo 增加新字段,这些文件导入咱们的 .proto,应用咱们指定范畴内的字段编号 – 例如:

extend Foo {optional int32 bar = 126;}

这会在 Foo 的原始定义中增加一个名为 bar 且字段编号为 126 的字段。
具体的操作请查看后续的 Java 开发指南。
请留神,扩大能够是任何字段类型,包含音讯类型,但不能是 oneofs 或 Maps。

嵌套扩大

咱们在 message 内申明扩大

message Baz {
  extend Foo {optional int32 bar = 126;}
  ...
}

惟一的区别是 bar 被定义在 Baz 的范畴内。
这是一个常见的混同起源:申明一个在 message 中的嵌套扩大块并不意味着内部类型和扩大类型之间有任何关系。特地是,下面的例子并不意味着 Baz 是 Foo 的任何子类。这意味着符号 bar 是在 Baz 范畴内申明的;它只是一个动态成员。

一种常见的模式是在扩大的字段类型范畴内定义扩大——例如,这是对 Baz 类型的 Foo 的扩大,其中扩大被定义为 Baz 的一部分:

message Baz {
  extend Foo {optional Baz foo_ext = 127;}
  ...
}

然而,并没有要求必须在该类型内定义具备该 message 类型的扩大。你也能够这样做:

message Baz {...}

// This can even be in a different file.
extend Foo {optional Baz foo_baz_ext = 127;}

事实上,为了防止混同,最好应用这种语法。如上所述,嵌套语法常常被不相熟扩大的用户误认为是子类化。

Oneof

当咱们的 message 蕴含多个可选的字段,并且最多只赋值一个,咱们能够应用 oneOf。

除共享内存中的所有字段外,其中 oneof 字段与可选字段相似,最多能够同时设置一个字段。设置 oneof 其中的一个成员会主动革除所有其余成员。
能够应用非凡的 case() 或 WhichOneof() 办法查看 oneof 中设置的值(如果有),具体取决于咱们抉择的语言。
要在 .proto 中定义 oneof,请应用 oneof 关键字,后跟 oneof 名称,在本例中为 test_oneof:

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

留神不能应用 required、optional、repeated 润饰 oneof 字段。如果须要向 oneof 增加反复字段,能够应用蕴含反复字段的音讯
生成的代码中,oneof 字段具备与惯例可选办法雷同的 getter 和 setter。
咱们还能够取得一种非凡的办法来查看 oneof 中设置了哪个值(如果有)。

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 不反对 Extensions。
  • oneof 不能反复。
  • 反射 API 实用于 oneof 字段。
  • 如果设置了默认值,则会在序列化时进行解析。
  • 增加或者删除 oneof 字段时须要留神,如果查看 oneof 字段返回 none 或者 not_set,则意味着咱们没有设置 oneof 字段,或者曾经设置不同版本的 oneof 字段,此时无奈辨别这个字段是存在被革除还是说不存在。
    oneof 在特定场景时,比方这几个条件满足一个不为空时,会很不便。因而思考场景,防止批改,并管制好版本。

Maps

提供了一种能够实现键值对映射的快捷预发

map<key_type, value_type> map_field = N;

其中 key_type 能够是任何整数或字符串类型(因而,除了浮点类型和字节之外的任何标量类型)。请留神,枚举不是无效的 key_type。value_type 能够是除另一个映射之外的任何类型。
因而,例如,如果您想创立一个我的项目映射,其中每个我的项目音讯都与一个字符串键相关联,您能够这样定义它:

map<string, Project> projects = 3;
Map 个性
  • 不反对扩大
  • 不能被 repeated, optional, required 润饰。
  • 大部分状况下是乱序的,不能以其作为排序规范。
  • 当生成文本格式的.proto 文件时,则会依照 key 进行排序。
  • 反复的 key,则应用最初看到的 key。

罕用的性能简直涵盖了,如果遇到未提及的用法,能够参加评论,我会更新~ 感激大家反对~

退出移动版