0%

protobuf3语法指南02

7. 更新消息类型

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

  • 请勿更改任何现有字段的字段编号。
  • 如果添加新字段,则使用“旧”消息格式按代码序列化的任何消息仍可由新生成的代码进行解析。您应该记住这些元素的默认值,以便新代码可以正确地与旧代码生成的消息进行交互。同样,您的新代码创建的消息可以由旧代码解析:旧的二进制文件在解析时只是忽略新字段。有关详细信息,请参阅“ 未知字段”部分
  • 只要在更新的消息类型中不再使用字段编号,就可以删除字段。您可能希望重命名该字段,可能添加前缀“OBSOLETE_”,或者保留字段编号,以便您的未来用户.proto不会意外地重复使用该编号。
  • int32uint32int64uint64,和bool都是兼容的-这意味着你可以改变这些类型到另一个的一个场不破坏forwards-或向后兼容。如果从导线中解析出一个不符合相应类型的数字,您将获得与在C ++中将该数字转换为该类型相同的效果(例如,如果将64位数字作为int32读取,它将被截断为32位)。
  • sint32并且sint64彼此兼容但与其他整数类型兼容。
  • string``bytes只要字节是有效的UTF-8 ,它们是兼容的。
  • bytes如果字节包含消息的编码版本,则嵌入消息是兼容的。
  • fixed32与兼容sfixed32,并fixed64sfixed64
  • enum与兼容int32uint32int64,和uint64电线格式条款(注意,如果他们不适合的值将被截断)。但请注意,在反序列化消息时,客户端代码可能会以不同方式对待它们:例如,enum将在消息中保留未识别的proto3 类型,但在反序列化消息时如何表示这种类型取决于语言。Int字段总是保留它们的价值。
  • 将单个值更改为 成员oneof是安全且二进制兼容的。oneof如果您确定没有代码一次设置多个字段,则将多个字段移动到新字段可能是安全的。将任何字段移动到现有字段oneof并不安全。

8. 未知字段

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

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

9. 任何

Any消息类型,可以使用邮件作为嵌入式类型,而不必自己.proto定义。一个Any含有任意的序列化消息bytes,以充当一个全局唯一标识符和解析到该消息的类型的URL一起。要使用该Any类型,您需要导入google/protobuf/any.proto

1
2
3
4
5
6
import "google/protobuf/any.proto";

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

给定消息类型的默认类型URL是。 type.googleapis.com/*packagename*.*messagename*

不同的语言实现将支持运行时库佣工类型安全的方式打包和解包的任何值-例如,在Java中,任何类型都会有特殊pack()unpack()存取,而在C ++中有PackFrom()UnpackTo()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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类型将替换扩展

10. Oneof

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

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

10.1 使用Oneof

要在您中定义oneof,请.proto使用oneof关键字后跟您的oneof名称,在这种情况下test_oneof

1
2
3
4
5
6
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}

然后,将oneof字段添加到oneof定义中。您可以添加任何类型的字段,但不能使用repeated字段。

在生成的代码中,oneof字段与常规字段具有相同的getter和setter。您还可以使用特殊方法检查oneof中的值(如果有)。您可以在相关API参考中找到有关所选语言的oneof API的更多信息。

10.2 Oneof 特性
  • 设置oneof字段将自动清除oneof的所有其他成员。因此,如果您设置了多个字段,则只有您设置的最后一个字段仍然具有值。

    1
    2
    3
    4
    5
    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 API适用于其中一个字段。

  • 如果您使用的是C ++,请确保您的代码不会导致内存崩溃。以下示例代码将崩溃,sub_message已通过调用该set_name()方法删除了该代码。

    1
    2
    3
    4
    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,每个消息最终将与另一个消息结果:在下面的例子中,msg1将有一个sub_messagemsg2并将有一name

    1
    2
    3
    4
    5
    6
    7
    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
10.3 向后兼容性问题

添加或删除其中一个字段时要小心。如果检查oneof返回的值None/ NOT_SET,这可能意味着oneof尚未设置或已在不同版本的oneof的被设置为一个字段。没有办法区分,因为没有办法知道线上的未知字段是否是其中一个成员。

10.4 标签重用问题
  • 将字段移入或移出oneof:在序列化和解析消息后,您可能会丢失一些信息(某些字段将被清除)。但是,您可以安全地将单个字段移动到新的 oneof中,并且如果已知只有一个字段被设置,则可以移动多个字段。
  • 删除oneof字段并将其添加回:在序列化和解析消息后,这可能会清除当前设置的oneof字段。
  • 拆分或合并oneof:这与移动常规字段有类似的问题。

11. 地图

如果要在数据定义中创建关联映射,协议缓冲区提供了一种方便的快捷方式语法:

1
map < key_type ,value_type > map_field = N ;

…其中key_type可以是任何整数或字符串类型(因此,除了浮点类型之外的任何标量类型bytes)。请注意,枚举不是有效的key_type。的value_type可以是任何类型的除另一地图。

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

1
map < string ,Project > projects = 3 ;
  • 地图字段不能repeated
  • 地图值的有线格式排序和地图迭代排序未定义,因此您不能依赖于特定顺序的地图项目。
  • 为a生成文本格式时.proto,地图按键排序。数字键按数字排序。
  • 从线路解析或合并时,如果有重复的映射键,则使用最后看到的键。从文本格式解析映射时,如果存在重复键,则解析可能会失败。
  • 如果为映射字段提供键但没有值,则字段序列化时的行为取决于语言。在C ++,Java和Python中,类型的默认值是序列化的,而在其他语言中没有任何序列化。

生成的地图API目前可用于所有proto3支持的语言。您可以在相关API参考中找到有关所选语言的地图API的更多信息。

11.1 向后兼容性

映射语法在线上等效于以下内容,因此不支持映射的协议缓冲区实现仍可处理您的数据:

1
2
3
4
5
6
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}

repeated MapFieldEntry map_field = N;

任何支持映射的协议缓冲区实现都必须生成和接受上述定义可以接受的数据。

12. 包

您可以向.proto文件添加package可选说明符,以防止协议消息类型之间的名称冲突。

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

然后,您可以在定义消息类型的字段时使用包说明符:

1
2
3
4
5
message Foo {
...
foo.bar.Open open = 1;
...
}

包说明符影响生成的代码的方式取决于您选择的语言:

  • C ++中,生成的类包含在C ++命名空间中。例如,Open将在命名空间中foo::bar
  • Java中,该包用作Java包,除非您option java_package.proto文件中明确提供了该包。
  • Python中,package指令被忽略,因为Python模块是根据它们在文件系统中的位置进行组织的。
  • Go中,该包用作Go包名称,除非您option go_package.proto文件中明确提供。
  • Ruby中,生成的类包含在嵌套的Ruby命名空间内,转换为所需的Ruby大写形式(首字母大写;如果第一个字符不是字母,PB_则前置)。例如,Open将在命名空间中Foo::Bar
  • C#中,包转换为PascalCase后用作命名空间,除非您option csharp_namespace.proto文件中明确提供。例如,Open将在命名空间中Foo.Bar
12.1 包和名称解析

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

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

13. 定义服务

如果要将消息类型与RPC(远程过程调用)系统一起使用,则可以在.proto文件中定义RPC服务接口,protobuf 编译器将使用您选择的语言生成服务接口代码和存根。因此,例如,如果要定义RPC服务请求方法为:SearchRequest和返回方法为:SearchResponse,可以.proto按如下方式在文件中定义它:

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

与协议缓冲区一起使用的最简单的RPC系统是gRPC:一种由Google开发的,平台中立的开源RPC系统。gRPC特别适用于protobuf,并允许在您的.proto文件中使用特殊的protobuf 编译器插件直接生成相关的RPC代码。

如果您不想使用gRPC,也可以将protobuf与您自己的RPC实现一起使用。您可以在Proto2语言指南中找到更多相关信息。

还有一些正在进行的第三方项目使用Protocol Buffers开发RPC实现。有关我们了解的项目的链接列表,请参阅第三方加载项wiki页面

14. JSON映射

Proto3支持JSON中的规范编码,使得在系统之间共享数据变得更加容易。在下表中逐个类型地描述编码。

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

proto3JSONJSON示例笔记
messageobject{"fooBar": v, "g": null,…}生成JSON对象。消息字段名称映射到小写驼峰并成为JSON对象键。如果json_name指定了field选项,则指定的值将用作键。解析器接受小写驼峰名称(或json_name选项指定的名称)和原始proto字段名称。null是所有字段类型的可接受值,并将其视为相应字段类型的默认值。
eunmString"FOO_BAR"使用proto中指定的枚举值的名称。解析器接受枚举名称和整数值。
map<K,V>object{"k": v, …}所有键都转换为字符串。
repeated V.array[v, …]null 被接受为空列表[]。
booltrue,falsetrue, false
stringstring"Hello World!"
bytesbase64 string"YWJjMTIzIT8kKiYoKSctPUB+"JSON值将是使用带填充的标准base64编码编码为字符串的数据。接受带有/不带填充的标准或URL安全base64编码。
int32,fixed32,uint32string1, -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映射的值,则将按如下方式进行转换:。否则,该值将转换为JSON对象,并将插入该字段以指示实际的数据类型。{"@type": xxx, "value": yyy}``"@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对象。见。struct.proto
Wrapper typesvarious types2, "2", "foo", true,"true", null, 0, …包装器在JSON中使用与包装基元类型相同的表示形式,除了null在数据转换和传输期间允许和保留的表示形式。
FieldMaskstring"f.fooBar,h"见。field_mask.proto
ListValuearray[foo, bar, …]
Valuevalue任何JSON值
NullValuenullJSON null
13.1 JSON选项

proto3 JSON实现可以提供以下选项:

  • 使用默认值发出字段:默认情况下,proto3 JSON输出中省略了具有默认值的字段。实现可以提供覆盖此行为的选项,并使用其默认值输出字段。
  • 忽略未知字段:默认情况下,Proto3 JSON解析器应拒绝未知字段,但可以提供忽略解析中未知字段的选项。
  • 使用proto字段名称而不是小写驼峰名称:默认情况下,proto3 JSON打印机应将字段名称转换为小写驼峰并将其用作JSON名称。实现可以提供使用proto字段名称作为JSON名称的选项。Proto3 JSON解析器需要接受转换后的小写驼峰名称和proto字段名称。
  • 将枚举值发送为整数而不是字符串:默认情况下,在JSON输出中使用枚举值的名称。可以提供选项以使用枚举值的数值。

14. 选项

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

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

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

  • java_package(文件选项):用于生成的Java类的包。如果.proto文件中没有给出显式选项java_package,则默认情况下将使用proto包(使用文件中的“package”关键字指定 .proto )。但是,proto包通常不能生成好的Java包,因为proto包不会以反向域名开头。如果不生成Java代码,则此选项无效。

    1
    option java_package =“com.example.foo”;
  • java_multiple_files (文件选项):导致在包级别定义顶级消息,枚举和服务,而不是在.proto文件之后命名的外部类中。
1
option java_multiple_files = true;
  • java_outer_classname(file option):要生成的最外层Java类(以及文件名)的类名。如果 .proto文件中没有指定 java_outer_classname,则通过将.proto文件名转换为驼峰格式(因此 foo_bar.proto 成为FooBar.java)来构造类名。如果不生成Java代码,则此选项无效。
1
option java_outer_classname =“Ponycopter”;
  • optimize_for

    (文件选项):可以设置为SPEEDCODE_SIZELITE_RUNTIME。这会以下列方式影响C ++和Java代码生成器(可能还有第三方生成器):

    • SPEED(默认值):protobuf 编译器将生成用于对消息类型进行序列化,解析和执行其他常见操作的代码。此代码经过高度优化。
    • CODE_SIZE:protobuf 编译器将生成最少的类,并依赖于基于反射的共享代码来实现序列化,解析和各种其他操作。因此生成的代码比使用SPEED小得多,但操作会更慢。类仍将实现与SPEED模式完全相同的公共API 。此模式在包含非常大数量的.proto文件的应用程序中最有用,并且不需要所有文件都非常快速。
    • LITE_RUNTIME:protobuf 编译器将生成仅依赖于“lite”运行时库(libprotobuf-lite而不是libprotobuf)的类。精简版运行时比整个库小得多(大约小一个数量级),但省略了描述符和反射等特定功能。这对于在移动电话等受限平台上运行的应用程序尤其有用。编译器仍然会像在SPEED模式中一样生成所有方法的快速实现。生成的类将仅实现MessageLite每种语言的接口,该接口仅提供完整Message接口的方法的子集。
    1
    option optimize_for = CODE_SIZE;
  • cc_enable_arenas(文件选项):为C ++生成的代码启用竞技场分配

  • objc_class_prefix(文件选项):设置Objective-C类前缀,该前缀预先添加到此.proto的所有Objective-C生成的类和枚举中。没有默认值。您应该使用Apple建议的 3-5个大写字符之间的前缀。请注意,Apple保留所有2个字母的前缀。

  • deprecated(字段选项):如果设置为true,则表示该字段已弃用,新代码不应使用该字段。在大多数语言中,这没有实际效果。在Java中,这成为一个@Deprecated注释。将来,其他特定于语言的代码生成器可能会在字段的访问器上生成弃用注释,这将导致在编译尝试使用该字段的代码时发出警告。如果任何人都没有使用该字段,并且您希望阻止新用户使用该字段,请考虑使用保留语句替换字段声明。

    1
    int32 old_field = 6 [deprecated = true];
14.1 自定义选项

Protocol Buffers还允许您定义和使用自己的选项。这是大多数人不需要的高级功能。如果您确实认为需要创建自己的选项,请参阅Proto2语言指南以获取详细信息。请注意,创建自定义选项使用的扩展名仅允许用于proto3中的自定义选项。

15. 生成您的类

根据实际工作需要,生成以下对应语言的自定义消息类型Java,Python,C ++,Go, Ruby, Objective-C,或C#的.proto文件,你需要运行protobuf 编译器protoc.proto。如果尚未安装编译器,请下载该软件包并按照自述文件中的说明进行操作。对于Go,您还需要为编译器安装一个特殊的代码生成器插件:您可以在GitHub上的golang / protobuf存储库中找到这个和安装说明。

Protobuf 编译器的调用如下:

1
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解析import指令时在其中查找文件的目录。如果省略,则使用当前目录。可以通过--proto_path多次传递选项来指定多个导入目录; 他们将按顺序搜索。 可以用作简短的形式。 -I=*IMPORT_PATH*``--proto_path

  • 您可以提供一个或多个输出指令:

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

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

16. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!