Protocol Buffers,通常称为Protobuf,是一种由Google开发的轻量级、高效的数据序列化格式。它可以用于在不同的系统之间传输和存储结构化数据,如配置文件、网络通信、持久化存储等场景。
1. 数据格式定义
Protobuf使用一种类似于结构体的方式来定义数据的格式,这些定义通常在一个名为.proto
的文件中。在这个文件中,你可以定义消息类型、字段名称、数据类型和一些其他的元信息。
例如,一个简单的Protobuf消息类型定义如下:
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
repeated string hobbies = 3;
}
2. 优势
a. 紧凑性和高效性
Protobuf的编码格式非常紧凑,相较于XML和JSON等文本格式,它占用的空间更小,传输效率更高。同时,Protobuf在序列化和反序列化过程中的性能也很出色,适用于高性能的应用场景。
b. 跨语言支持
Protobuf定义了数据格式和编解码规则,因此你可以使用不同的编程语言来读写这些数据。Google为多种编程语言提供了Protobuf的支持库,如C++、Java、Python、Go等。
c. 可扩展性
当你的数据格式需要进行修改或添加新字段时,你可以在不破坏已有数据的情况下进行扩展。这是通过向消息中添加新的字段,并保持现有字段的编号不变来实现的。
3. 使用场景
Protobuf适用于许多不同的场景,包括:
- 网络通信:用于在不同的计算机之间传输数据,例如在分布式系统中。
- 持久化存储:可用于将数据序列化后存储在磁盘上,以便以后检索和读取。
- API设计:很多Web API会使用Protobuf来定义请求和响应的数据格式,从而提高通信效率。
- 配置文件:一些应用程序使用Protobuf来定义配置文件格式,这可以帮助确保配置文件的一致性和易读性。
4. Protobuf的局限性
a. 人类可读性差
相对于JSON和XML这样的文本格式,Protobuf的二进制编码在直接查看时不太容易被人类阅读和理解。
b. 不适用所有场景
虽然Protobuf非常高效,但在某些简单场景下可能过于繁重,特别是对于只需要传输少量数据的应用。
c. 缺乏动态性
在某些需要在运行时动态处理消息结构的情况下,Protobuf的静态定义可能会有限制。
总的来说,Protobuf是一种强大的数据序列化格式,适用于许多不同的应用领域。它的高效性、跨语言支持和可扩展性使其成为许多大规模系统中的首选数据交换格式之一。
定义消息类型
首先,让我们看一个非常简单的例子。假设想要定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串、感兴趣的特定结果页以及每页的结果数量。以下是用于定义消息类型的.proto文件示例:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
第一行指定正在使用proto3语法:如果不这样做,协议缓冲编译器将默认为使用proto2。这必须是文件中第一个非空、非注释行。
SearchRequest消息定义指定了三个字段(名称/值对),分别用于在此类型的消息中包含的每个数据部分。每个字段都有一个名称和一个类型。
指定字段类型
在前面的例子中,所有字段都是标量类型:两个整数(page_number和results_per_page)和一个字符串(query)。还可以指定枚举和复合类型,就像为字段定义其他消息类型一样。
分配字段编号
必须为消息定义中的每个字段分配1到536,870,911之间的唯一编号,并遵循以下限制:
- 给定的编号在该消息的所有字段中必须是唯一的。
- 字段编号19,000至19,999保留供协议缓冲实现使用。如果在消息中使用了这些保留的字段编号,协议缓冲编译器将发出警告。
- 不能使用任何先前保留的字段编号或已分配给扩展的字段编号。
- 一旦消息类型在使用中,就无法更改此编号,因为它标识了消息的字段在消息线格式中的位置。 "更改"字段编号等同于删除该字段,然后创建具有相同类型但新编号的新字段。
指定字段标签
消息字段可以是以下之一:
-
optional
:可选字段处于两种可能状态之一:- 该字段已设置,并包含显式设置或从线中解析的值。它将序列化到线中。
- 该字段未设置,将返回默认值。它不会序列化到线中。可以检查值是否被显式设置。
-
repeated
:此字段类型在合格的消息中可以重复零次或多次。重复值的顺序将被保留。 -
map
:这是一种成对的键/值字段类型。有关此字段类型的更多信息,请参阅映射。
如果未应用明确的字段标签,则假定默认字段标签称为“隐式字段存在”。(不能显式地将字段设置为此状态。)合格的消息可以有零个或一个此字段(但不超过一个)。还不能确定此类型的字段是否从线中解析。除非是默认值,否则将序列化隐式存在字段到线中。有关此主题的更多信息,请参阅字段存在。
在proto3中,标量数字类型的重复字段默认使用压缩编码。
添加更多消息类型
可以在单个.proto文件中定义多个消息类型。如果正在定义多个相关消息,这将非常有用。例如,如果想要定义与SearchResponse消息类型对应的回复消息格式,可以将其添加到同一个.proto文件中:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
添加注释
要为.proto文件添加注释,可以使用C/C++风格的 // 和 /* ... */ 语法。
/* SearchRequest代表搜索查询,带有分页选项来
* 指示在响应中包含哪些结果。 */
message SearchRequest {
string query = 1;
int32 page_number = 2; // 我们想要哪一页?
int32 results_per_page = 3; // 每页返回的结果数。
}
删除字段
如果不正确地删除字段,可能会导致严重问题。
当不再需要非必需字段,并且已从客户端代码中删除了所有引用时,可以从消息中删除字段定义。但是,必须保留已删除的字段编号。如果不保留字段编号,未来的开发人员可能会在将来重复使用该编号。
还应保留字段名称,以便继续解析消息的JSON和TextFormat编码。
保留字段
如果通过完全删除字段或将其注释掉来更新消息类型,未来的开发人员在对类型进行自己的更新时可以重复使用字段编号。这可能会引起严重问题,如“重用字段编号的后果”中所述。
为确保不会发生这种情况,请将已删除的字段编号添加到保留列表中。为确保仍然可以解析消息的JSON和TextFormat实例,还应将已删除的字段名称添加到保留列表中。
如果未来的开发人员尝试使用这些保留字段编号或名称,协议缓冲编译器将发出警告。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
保留字段编号范围是包容性的(9到11与9、10、11相同)。请注意,不能在同一个保留语句中混合字段名称和字段编号。
从.proto文件生成的内容
当在.proto文件上运行协议缓冲编译器时,编译器会生成你选择的语言中使用的代码,以便处理在文件中描述的消息类型,包括获取和设置字段值、将消息序列化到输出流以及从输入流解析消息。
- 对于C++,编译器从每个.proto生成一个.h和.cc文件,其中包含文件中描述的每个消息类型的类。
- 对于Java,编译器生成一个.java文件,其中包含每个消息类型的类,以及用于创建消息类实例的特殊Builder类。
- 对于Kotlin,除了Java生成的代码外,编译器还会生成一个.kt文件,其中包含一个DSL,用于简化创建消息实例。
- 对于Python,稍有不同 - Python编译器生成一个模块,其中包含.proto中每个消息类型的静态描述符,然后使用元类在运行时创建必要的Python数据访问类。
- 对于Go,编译器生成一个.pb.go文件,其中包含文件中每个消息类型的类型。
- 对于Ruby,编译器生成一个.rb文件,其中包含一个Ruby模块,其中包含消息类型。
- 对于Objective-C,编译器从每个.proto生成一个pbobjc.h和pbobjc.m文件,其中包含文件中描述的每个消息类型的类。
- 对于C#,编译器从每个.proto生成一个.cs文件,其中包含文件中描述的每个消息类型的类。
- 对于Dart,编译器从每个.proto生成一个.pb.dart文件,其中包含文件中每个消息类型的类。
默认值
解析消息时,如果编码的消息不包含特定的单个元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是类型特定的:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为false。
- 对于数值类型,默认值为零。
- 对于枚举,默认值是第一个定义的枚举值,必须为0。
- 对于消息字段,字段未设置。其确切值因语言而异。有关详情,请参阅生成的代码指南。
重复字段的默认值是空的(通常在适当的语言中是空列表)。
需要注意的是,对于标量消息字段,一旦解析了消息,就无法判断字段是否显式设置为默认值(例如,布尔值是否设置为false),或者根本没有设置。在定义消息类型时需要考虑这一点。例如,如果不希望默认情况下也发生某种行为,请勿使用布尔值开关该行为。还请注意,如果将标量消息字段设置为其默认值,则该值将不会在线上序列化。