Protobuf3学习笔记

日之朝矣

观前提醒

本文大部分内容来自于官方文档Language Guide (proto 3)

因为是英文版的,大部分使用谷歌翻译,然后将内容排序为本人看起来更舒服一点的样子

定义消息类型

==该文件的第一行指定您使用的是 proto3 语法:如果不这样做,protocol buffer 编译器将假定您使用的是 proto2。==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// test.proto
syntax = "proto3"

// 定义消息类型
message SearchRequest{
string query = 1;
int32 page_num = 2;
int32 result_per_page = 3;
}

// 定义消息类型
message ResponseRequest{
...
}
1
2
3
4
5
6
7
8
9
syntax = "proto3" // 声明版本

// 定义消息类型
message 消息类型名{
字段规则 字段类型 字段名 = 字段编号;
字段规则 字段类型 字段名 = 字段编号;
...
}

字段规则

不写默认为singular

  • singular: 格式正确的消息可以有这个字段的0或1个,也是默认规则
  • optional: 在singular基础之上检查是否显式设置了值
  • repeated: 格式正确的消息可以重复任意次数,包括0次,重复值的顺序也会被保留

字段类型

上面的例子中,int32与string都是标量类型(scalar type) ,也可以使用枚举类型或者嵌套消息类型

标量类型

下面表格列出了所有标量类型与其他语言类型的对应关系

.proto TypeNotesC++ TypeJava/Kotlin Type[1]Python Type[3]Go TypeRuby TypeC# TypePHP TypeDart Type
doubledoubledoublefloatfloat64Floatdoublefloatdouble
floatfloatfloatfloatfloat32Floatfloatfloatdouble
int32使用可变长度编码。 编码负数效率低下——如果您的字段可能有负值,请改用 sint32。int32intintint32Fixnum or Bignum (as required)intintegerint
int64使用可变长度编码。 编码负数效率低下——如果您的字段可能有负值,请改用 sint64。int64longint/longint64Bignumlonginteger/stringInt64
uint32使用可变长度编码。uint32intint/longuint32Fixnum or Bignum (as required)uintintegerint
uint64使用可变长度编码。uint64longint/longuint64Bignumulonginteger/stringInt64
sint32使用可变长度编码。 有符号的 int 值。 这些比常规的 int32 更有效地编码负数。int32intintint32Fixnum or Bignum (as required)intintegerint
sint64使用可变长度编码。 有符号的 int 值。 这些比常规的 int64 更有效地编码负数。int64longint/longint64Bignumlonginteger/stringInt64
fixed32总是四个字节。 如果值通常大于,则比 uint32 更有效。uint32intint/longuint32Fixnum or Bignum (as required)uintintegerint
fixed64总是四个字节。 如果值通常大于,则比 uint64 更有效。uint64longint/longuint64Bignumulonginteger/string[6]Int64
sfixed32总是4个字节int32intintint32Fixnum or Bignum (as required)intintegerint
sfixed64总是8个字节int64longint/longint64Bignumlonginteger/stringInt64
boolboolbooleanboolboolTrueClass/FalseClassboolbooleanbool
string字符串必须始终包含 UTF-8编码的或7位 ASCII 文本,且不能长于stringStringstr/unicodestringString (UTF-8)stringstringString
bytes可以包含任何不超过字节的任意字节序列stringByteStringstr (Python 2) bytes (Python 3)[]byteString (ASCII-8BIT)ByteStringstringList

枚举类型

当您定义消息类型时,您可能希望其字段之一仅具有预定义值列表之一。 例如,假设您想为每个 SearchRequest 添加一个语料库字段,其中语料库可以是 UNIVERSAL、WEB、IMAGES、LOCAL、NEWS、PRODUCTS 或 VIDEO。 您可以通过在消息定义中添加一个枚举来非常简单地完成此操作,每个可能的值都有一个常量。

在下面的示例中,我们添加了一个名为 Corpus 的枚举,其中包含所有可能的值,以及一个 Corpus 类型的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}

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

每个枚举定义都必须包含一个映射到零的常量作为其第一个元素。这是因为:

  • 必须有一个零值,以便我们可以使用 0 作为数字默认值。
  • 零值需要是第一个元素,以便与第一个枚举值始终是默认值的 proto2 语义兼容。

可以通过将相同的值分配给不同的枚举常量来定义别名。 为此,需要将 allow_alias 选项设置为 true,否则协议编译器会在发现别名时生成警告消息。 尽管在反序列化期间所有别名值都有效,但在序列化时始终使用第一个值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum EnumAllowingAlias {
option allow_alias = true;
EAA_UNSPECIFIED = 0;
EAA_STARTED = 1;
EAA_RUNNING = 1; // 别名
EAA_FINISHED = 2;
}

enum EnumNotAllowingAlias {
ENAA_UNSPECIFIED = 0;
ENAA_STARTED = 1;
// ENAA_RUNNING = 1; // 取消注释此行将导致warning message
ENAA_FINISHED = 2;
}

枚举数常量必须在 32 位整数范围内。 由于枚举值在线上使用 varint 编码,因此负值效率低下,因此不推荐使用。 您可以在消息定义中定义枚举,如上例所示,也可以在消息定义之外定义枚举——这些枚举可以在您的.proto文件中的任何消息定义中重复使用。 您还可以使用语法 _MessageType_._EnumType_,将一条消息中声明的枚举类型用作另一条消息中字段的类型。

当您在使用枚举的 .proto 上运行协议缓冲区编译器时,生成的代码将具有对应的 Java、Kotlin 或 C++ 枚举,或用于 Python 的特殊 EnumDescriptor 类,用于创建一组具有整数的符号常量 运行时生成的类中的值。

注意:生成的代码可能会受到特定于语言的枚举数限制(单种语言的数量低于千)。请检查你计划使用的语言的限制。

在反序列化过程中,不可识别的枚举值将保留在消息中,尽管当消息被反序列化时,这种值的表示方式依赖于语言。在支持值超出指定符号范围(如 C++ 和 Go)的开放枚举类型的语言中,未知枚举值仅存储为其底层的整数表示形式。在具有闭合枚举类型(如 Java)的语言中,枚举中的一个类型将用于表示一个无法识别的值,并且可以使用特殊的访问器访问底层的整数。在这两种情况下,如果消息被序列化,那么不可识别的值仍然会与消息一起被序列化。

有关如何在应用程序中使用消息enum的详细信息,请参阅为所选语言生成的代码指南

预留值

如果通过完全删除枚举条目或注释掉枚举类型来更新枚举类型,那么未来的用户在自己更新该类型时可以重用该数值。这可能会导致严重的问题,如果以后有人加载旧版本的相同.proto文件,包括数据损坏,隐私漏洞等等。确保不发生这种情况的一种方法是指定已删除条目的数值(和/或名称,这也可能导致 JSON 序列化问题)为 reserved。如果任何未来的用户试图使用这些标识符,protocol buffer 编译器将报错。你可以使用 max关键字指定保留的数值范围最大为可能的值。

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

注意,不能在同一个保留语句中混合字段名和数值。

嵌套消息类型

你可以使用其他消息类型作为字段类型。例如,假设你希望在每个 SearchResponse消息中包含UI个 Result消息——为了做到这一点,你可以在同一个.proto文件中定义 Result消息类型。然后在 SearchResponse中指定 Result 类型的字段。

1
2
3
4
5
6
7
8
9
message SearchResponse {
repeated Result results = 1;
}

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

当然也可以将消息类型定义到消息类型中,如

1
2
3
4
5
6
7
8
message FirstM{
message SecondM{
message ThridM{
string name = 1;
bool IsBool = 2;
}
}
}

导入定义

在上面的示例中,Result消息类型定义在与 SearchResponse相同的文件中——如果你希望用作字段类型的消息类型已经在另一个.proto文件中定义了,该怎么办?

你可以通过 import 来使用来自其他 .proto 文件的定义。要导入另一个.proto 的定义,你需要在文件顶部添加一个 import 语句:

1
import "myproject/other_protos.proto";

默认情况下,只能从直接导入的 .proto 文件中使用定义。但是,有时你可能需要将 .proto 文件移动到新的位置。你可以在旧目录放一个占位的.proto文件使用import public 概念将所有导入转发到新位置,而不必直接移动.proto文件并修改所有的地方。

使用proto2消息类型

导入 proto2消息类型并在 proto3消息中使用它们是可能的,反之亦然。然而,proto2 enum 不能直接在 proto3语法中使用(如果一个导入的 proto2消息使用了它们,那没问题)。

默认值

  • 对于字符串,默认值为空字符串。
  • 对于字节,默认值为空字节。
  • 对于布尔值,默认值为 false。
  • 对于数值类型,默认值为零。
  • 对于枚举,默认值是第一个定义的枚举值,该值必须为0。
  • 对于消息字段,未设置该字段。其确切值与语言有关。

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

其他字段

Maps

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

1
map<key_type, value_type> map_field = N;

…其中key_type可以是任何整型或字符串类型(因此,除了浮点类型和字节以外的任何标量类型) 。注意,枚举不是有效的key_typevalue_type可以是除另一个映射以外的任何类型。

例如,如果你想创建一个项目映射,其中每个Project消息都与一个字符串键相关联,你可以这样定义:

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

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

向后兼容性

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

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

repeated MapFieldEntry map_field = N;

任何支持映射的protocol buffers实现都必须生成并接受上述定义可以接受的数据。

未知字段

未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件解析新二进制文件发送的具有新字段的数据时,这些新字段将成为旧二进制文件中的未知字段。
最初,proto3 消息在解析过程中总是丢弃未知字段,但在 3.5 版本中我们重新引入了未知字段的保留以匹配 proto2 行为。在 3.5 及更高版本中,未知字段在解析期间保留并包含在序列化输出中。

Any

Any 消息类型允许您将消息用作嵌入式类型,而无需其 .proto 定义。 Any 包含作为字节的任意序列化消息,以及充当该消息类型的全局唯一标识符并解析为该消息类型的 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_

不同的语言实现将支持运行库助手以类型安全的方式打包和解包 Any值。例如在java中,Any类型会有特殊的pack()unpack()访问器,在C++中会有PackFrom()UnpackTo()方法。

目前正在开发用于处理任何类型的运行时库.

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

Oneof

如果你有一条包含多个字段的消息,并且最多同时设置其中一个字段,那么你可以通过使用oneof来实现并节省内存。

举个例子,比如单分类查询某个物品,列出了一个分类表,但每次只能选择其中的一个分类。

oneof字段类似于常规字段,只不过oneof中的所有字段共享内存,而且最多可以同时设置一个字段。设置其中的任何成员都会自动清除所有其他成员。根据所选择的语言,可以使用特殊 case()WhichOneof() 方法检查 one of 中的哪个值被设置(如果有的话)。

使用oneof

要定义 oneof 字段需要在你的.proto文件中使用oneof关键字并在后面跟上名称,在下面的例子中字段名称为test_oneof

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

然后将其中一个字段添加到该字段的定义中。你可以添加任何类型的字段,除了map字段和repeated字段。

在生成的代码中,其中一个字段具有与常规字段相同的 getter 和 setter。你还可以获得一个特殊的方法来检查其中一个设置了哪个值(如果有的话)。你可以在相关的 API 参考文献 中找到更多关于所选语言的 API。

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 不支持repeated
  • 反射 api 适用于 oneof 字段。
  • 如果将 oneof 字段设置为默认值(例如将 int32 oneof 字段设置为0) ,则将设置该字段的“ case”,并在连接上序列化该值。

向后兼容性问题

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

标签重用问题

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

字段编号

==消息类型中的字段编号是唯一的==,1-15仅需要一个字节进行编码,包括字段编号和字段类型,16到2047采用两个字节,也就是说,尽量将1-15留给常用的消息字段,也预留一些给未来

字段编号范围为1 到也就是536870911,但是 19000 到 19999 同样不能使用,这些是预留给Protocol Buffers协议实现的

Packages

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

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

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

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

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

  • 对于**C++**,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中;
  • 对于JavaKotlin,包声明符会变为java的一个包,除非在.proto文件中提供了一个明确有option java_package
  • 对于 Python,这个包声明符是被忽略的,因为Python模块是按照其在文件系统中的位置进行组织的
  • 对于Go,包可以被用做Go包名称,除非你显式的提供一个option go_package在你的.proto文件中。
  • 对于Ruby,生成的类可以被包装在内置的Ruby名称空间中,转换成Ruby所需的大小写样式 (首字母大写;如果第一个符号不是一个字母,则使用PB_前缀),例如Open会在Foo::Bar名称空间中。
  • C# 中,包在转换到 PascalCase 后被用作名称空间,除非你在.proto文件中提供option csharp_namespace。例如,Open 将位于Foo.Bar名称空间中。

package和名称解析

在 protocol buffer 语言中,类型名称解析的工作原理类似于 C++ : 首先搜索最内层的作用域,然后搜索下一个最内层的作用域,依此类推,每个包都被认为是其父包的“ inner”。前导的“ .”(例如,.foo.bar.Baz)表示从最外侧的范围开始。

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

JSON 映射

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

当将 JSON 编码的数据解析到协议缓冲区中时,如果缺少一个值或者它的值为 null,它将被解释为相应的默认值。

从协议缓冲区生成 JSON 编码的输出时,如果 protobuf 字段具有默认值并且该字段不支持字段存在,则默认情况下它将从输出中省略。 实现可以提供选项以在输出中包含具有默认值的字段。

使用 optional 关键字定义的 proto3 字段支持字段存在。 设置了值且支持字段存在的字段始终在 JSON 编码输出中包含字段值,即使它是默认值。

proto3JSONJSON exampleNotes
messageobject{"fooBar": v, "g": null, …}生成 JSON 对象。消息字段名映射到 lowerCamelCase 并成为 JSON 对象键。如果指定了 json_name 字段选项,则将使用指定的值作为键。解析器接受 lowerCamelCase 名称(或 json_name 选项指定的名称)和原始 proto 字段名称。 null 是所有字段类型的接受值,并被视为相应字段类型的默认值。
enumstring"FOO_BAR"使用 proto 中指定的枚举值的名称。解析器接受枚举名称和整数值。
mapobject{"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”。接受数字或字符串。也接受指数表示法。-0被认为等效于0。
Anyobject{"@type": "url", "f": v, … }如果Any包含一个具有特殊 JSON 映射的值,它将被转换如下: {"@type": xxx, "value": yyy}. 否则,该值将转换为 JSON 对象,并插入"@type"字段以指示实际的数据类型。
Timestampstring"1972-01-01T10:00:20.021Z"使用 RFC3339,其中生成的输出将始终是 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, …Wrappers 使用与包装原语类型相同的 JSON 表示,只是在数据转换和传输期间允许并保留 null
FieldMaskstring"f.fooBar,h"请参见 field_mask.proto.
ListValuearray[foo, bar, …]
Valuevalue任何 JSON 值。请检查 google.protobuf.Value 以获取详细信息。
NullValuenullJSON null
Emptyobject{}一个空的JSON对象

JSON选项

一个proto3协议 JSON 实现可能提供以下选项:

  • 提供默认值的字段:在proto3 JSON 输出中,值为默认值的字段被省略。可以提供一个选项,用默认值覆盖此行为和输出字段。
  • 忽略位置字段:在缺省情况下,Proto3 JSON 解析器应该拒绝未知字段,但在解析过程中可能会提供一个忽略未知字段的选项。
  • 使用 proto 字段名而不是小驼峰名称:默认情况下,proto3 JSON 打印机应该将字段名转换为 lowerCamelCase,并使用它作为 JSON 名称。可以提供一个选项,用原型字段名作为 JSON 名。需要协议3 JSON 解析器同时接受转换后的 lowerCamelCase 名称和原始字段名称。
  • 以整数而不是字符串形式展示枚举值:在 JSON 输出中,默认情况下使用枚举值的名称。可以提供一个选项来代替使用枚举值的数值。

参考内容:

Language Guide (proto 3)

Protocol Buffers V3中文语法指南[翻译]

  • 标题: Protobuf3学习笔记
  • 作者: 日之朝矣
  • 创建于 : 2023-04-17 19:42:21
  • 更新于 : 2024-08-18 09:25:27
  • 链接: https://blog.rzzy.fun/2023/04/17/Protobuf3Learn/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论