ProtoBuf3语法指南(Protocol Buffers)_

2020-10-19  本文已影响0人  木木与呆呆

0.说明

ProtoBuf3语法指南,
又称为proto3,
是谷歌的Protocol Buffers第3个版本。
本文基于官方英文版本翻译,
加上了自己的理解少量修改,
一共分为上下两部分。

1.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语法,
可以使用Any替换任意的proto3消息,
类似proto2消息的扩展方式。

2.Oneof

如果消息中有很多可选字段,
且同时最多只会设置一个字段,
可以使用oneof特性加强这个行为,
并且可以节省内存.

Oneof字段类似于常规字段,
不仅共享内存中的所有字段之外,
而且最多可以同时设置一个字段。
设置其中任何一个字段会清除其它成员字段。
可以使用case()或者WhichOneof()方法检查哪个oneof字段被设置了,
这取决于选择使用的语言。

2.1.使用Oneof

为了在.proto中定义Oneof字段,
需要在字段名称前面加上oneof关键字,
比如下面例子中的test_oneof:

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

然后将oneof成员字段到test_oneof字段的定义中,
可以增加任意类型的字段,
但是不能使用map和repeated字段。

在生成的代码中,
oneof字段具有与常规字段相同的getter和setter,
有一个特殊的方法来检查oneof的哪个值被设置了,
你可以在相应的语言API参考中,
找到更多关于所选语言的oneof API介绍.

2.2.Oneof 特性

SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message();   // Will clear name field.
CHECK(!message.has_name());
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name");      // Will delete sub_message
sub_message->set_...            // Crashes here
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());

2.3.向后兼容性问题

当增加或者删除oneof字段时一定要小心,
如果检查oneof的值返回None/NOT_SET,
它意味着oneof字段没有被赋值,
或者在一个不同的版本中赋值了。
但是没有办法区分,
因为没有办法判断未知字段是否是一个oneof字段。

2.3.1.Tag重用问题:

3.Map

如果你希望创建一个关联映射,
作为数据定义的一部分,
protocol buffer提供了一种方便快捷的语法:

map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string类型,
即除了floating和bytes的任何标量类型,
value_type可以是出来map之外的任意类型。

如果想创建一个projects的映射,
每个Projecct使用一个string作为key,
可以这样定义:

map<string, Project> projects = 3;

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

3.1.向后兼容性

Map语法序列化后等同于如下内容,
因此即使是不支持Map语法的protocol buffer实现,
也是可以处理数据的:

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

repeated MapFieldEntry map_field = N;

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

4.包

可以为.proto文件添加一个可选的package声明,
用来防止不同的消息类型的命名冲突:

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

在定义其他的消息类型时,
可以使用包名+消息名的方式来声明字段类型:

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

包声明生成代码的方式取决于选择的语言:

4.1.包和名称解析

Protocol buffer语言中类型名称的解析类似于c++:
首先搜索最内层的作用域,
然后搜索下一层的作用域,
以此类推,
每个包会被看作是其父类包的"内部"。
对于以"."开头的意味着是从最外围开始的。
比如".foo.bar.Baz"。
Protocol Buffer编译器会解析.proto文件中定义的所有类型名称。
对于不同语言,
代码生成器都知道如何引用具体的类型,
即使它有不同的作用域规则。

5.定义服务

如果要将消息类型用在RPC(远程方法调用)系统中,
可以在.proto文件中定义RPC服务接口,
protocol buffer编译器将会根据所选的语言生成服务接口代码及存根。
例如定义一个RPC服务并具有一个方法,
该方法能够接收SearchRequest并返回SearchResponse,
此时可以在.proto文件中进行如下定义:

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

与protocol buffer一起使用的最直接的RPC系统是gRPC,
它是谷歌开发的与语言和平台无关的开放源码RPC系统。
gRPC在protocol buffer方面工作得特别好,
它允许使用一个特殊的protocol buffer编译器插件,
从.proto文件直接生成相关的RPC代码。
如果不想使用gRPC,
也可以在自己的RPC实现中使用protocol buffer。
可以在Proto2语言指南中找到更多相关信息。
还有许多正在进行的第三方项目,
使用protocol buffer开发RPC实现。
有关我们所知道的项目的链接列表,
请查看第三方项目wiki页面

6.JSON映射

Proto3支持JSON的编码规范,
使它更容易在不同系统之间共享数据,
在下表中按类型逐个描述对应编码。
如果json编码的数据中缺少一个值,
或者它的值为null,
这个数据在protocol buffer解析时,
会被表示成适当的默认值。
如果一个字段在protocol buffer中有默认值,
那么默认情况下它将在Json编码的数据中被省略,
以节省空间。
具体实现可以提供在JSON编码中提供选项,
输出具有默认值的字段。

proto3 JSON JSON example Notes
message object {"fooBar": v, "g": null, …} 生成JSON对象。消息字段名称映射为小驼峰,并成为JSON对象的key。如果指定了'json_name字段选项,则指定的值将被用作key。解析器同时接受小驼峰名称(或json_name选项指定的名称)和原始的原型字段名称。"null"是所有字段类型的可接受的值,并被作为对应字段类型的默认值。
enum string "FOO_BAR" 使用proto中指定的枚举值的名称。解析器同时接受枚举名和整数值。
map<K,V> object {"k": v, …} 所有的key都被转换成string。
repeated V array [v, …] null被视为空列表[]。
bool true, false true, false
string string "Hello World!"
bytes base64 string "YWJjMTIzIT8kKiYoKSctPUB+" JSON值是字符串编码的数据,使用带填充的标准base64编码。标准的或URL安全的base64编码,有没有填充都可以接受。
int32, fixed32, uint32 number 1, -10, 0 JSON值是一个十进制数。可以接受数字或字符串。
int64, fixed64, uint64 string "1", "-10" JSON值将是一个十进制字符串。可以接受数字或字符串。
float, double number 1.1, -10.0, 0, "NaN", "Infinity" JSON值是一个数字或特殊字符串值“NaN”、“Infinity”和“-Infinity”中的一个。可以接受数字或字符串。指数符号也被接受。-0被认为等于0。
Any object {"@type": "url", "f": v, … } 如果一个Any有一个特别的JSON映射,则它会转换成如下形式:{"@type": xxx, "value": yyy}。否则,该值会被转换成一个JSON对象,@type字段将被插入,以指示实际的数据类型。
Timestamp string "1972-01-01T10:00:20.021Z" 使用RFC 3339,其中生成的输出将始终是Z标准化的,并使用0、3、6或9位小数。也接受除Z以外的偏移量。
Duration string "1.000340012s", "1s" 生成的输出总是包含0、3、6或9个小数(取决于所需的精度),后面跟着后缀s。任何小数都可以接受,只要它们能满足纳米秒的精确度,并且需要后缀s。
Struct object { … } 任意的JSON对象,参见struct.proto。
Wrapper types various types 2, "2", "foo", true, "true", null, 0, … 包装器在JSON中的表示方式类似于原始类型,但是nulll在数据转换和传输期间被允许和保留。
FieldMask string "f.fooBar,h" 参见field_mask.proto.
ListValue array [foo, bar, …]
Value value 任意JSON值,细节请参见 google.protobuf.Value
NullValue null JSON null
Empty object {} 空JSON对象

6.1.JSON选项

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

7.选项

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

一些选项是文件级别的,
这意味着它们应该在顶级作用域中编写,
而不是在任何消息、枚举或服务定义中。
有些选项是消息级选项,
这意味着它们应该写入消息定义中。
有些选项是字段级选项,
这意味着它们应该写入字段定义中。
选项也可以写入枚举类型、枚举值、字段、服务类型和服务方法;
但是到目前为止,
并没有一种选项能作用于所有的类型。

option java_package = "com.example.foo"; 
option java_multiple_files = true;
option java_outer_classname = "Ponycopter";
option optimize_for = CODE_SIZE;
int32 old_field = 6 [deprecated = true];

7.1.自定义选项

Protocol Buffers允许定义和使用自己的选项。这是一个大多数人不需要的高级特性。如果您确实认为需要创建自己的选项,请参阅Proto2语言指南了解详细信息。注意创建自定义选项使用扩展,这只允许在proto3中自定义选项。

8.生成代码类

要生成Java、Python、C++、Go、Ruby、Objective-C或C#代码,
可以通过在.proto文件中定义消息、枚举、服务等,
需要在.proto上运行protocol buffer编译器protoc。
如果还没有安装编译器,
下载安装包并按照README中的说明操作。
对于Go,还需要为编译器安装一个特殊的代码生成器插件:
可以在GitHub上的golang/protobuf存储库中找到这个插件和安装说明。
protocol buffer调用方式如下:

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 --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto

9.参考文章

Language Guide (proto3)
Protobuf3教程
ProtoBuf v3 语法简介
gRPC之proto语法

上一篇下一篇

猜你喜欢

热点阅读