protobuf指南
protobuf
是 Google
开发了供内部使用的一种免费的开源跨平台数据格式,用于序列化结构化数据,并在开源许可下为多种语言提供了代码生成器。
protobuf
1. 网址
2. 介绍
protobuf
支持几种不同的编程语言。对于每种编程语言,您可以在Github
开源网址上相应的源目录中找到有关如何为该特定语言安装 protobuf
运行时的说明:
Language | Source |
---|---|
C++ (include C++ runtime and protoc) | src |
Java | java |
Python | python |
Objective-C | objectivec |
C# | csharp |
Ruby | ruby |
Go | protocolbuffers/protobuf-go |
PHP | php |
Dart | dart-lang/protobuf |
本文都是根据官方文件 https://developers.google.com/protocol-buffers/docs/proto3 翻译并只保留基本介绍和接口说明,版本适用于 proto3
。
2.1. 定义消息类型
2.1.1. 示例
首先让我们看一个非常简单的例子。假设您要定义一个搜索请求消息格式,其中每个搜索请求都有一个查询字符串、您感兴趣的特定结果页面以及每页的多个结果。这是.proto
您用来定义消息类型的文件。
1
2
3
4
5
6
7
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 该文件的第一行指定您使用的是
proto3
语法:如果您不这样做,协议缓冲区编译器将假定您使用的是proto2
。这必须是文件的第一个非空、非注释行。 SearchRequest
消息定义指定了三个字段(名称/值对),每个字段用于您希望包含在此类消息中的每条数据。每个字段都有一个名称和一个类型。
2.1.2. 指定字段类型
- 标量类型
- 复合类型:枚举、嵌套消息类型
2.1.3. 分配字段编号
消息定义中的每个字段都有一个唯一的编号。这些字段编号用于在消息二进制格式中标识您的字段,并且在使用您的消息类型后不应更改。
您可以指定的最小字段编号是 1,最大的是 $2^{29}-1$,即 536,870,911。您也不能使用数字 19000 到 19999 ( FieldDescriptor::kFirstReservedNumber
到 FieldDescriptor::kLastReservedNumber
),因为它们是为 Protocol Buffers 实现保留的。如果您在.proto
自定义,您不能使用任何以前保留的字段编号。
2.1.4. 指定字段规则
消息字段可以是以下之一:
- 单数:格式良好的消息可以有零个或一个此字段(但不能超过一个)。这是 proto3 语法的默认字段规则。
repeated
:该字段可以在格式良好的消息中重复任意次数(包括零次)。重复值的顺序将被保留。
在 proto3
中,repeated
标量数值类型的字段packed
默认使用编码。
2.1.5. 添加更多消息类型
可以在单个.proto
文件中定义多种消息类型。如果您要定义多个相关消息,这很有用——例如,如果您想定义与您的SearchResponse
消息类型相对应的回复消息格式,您可以将其添加到相同的.proto
:
1
2
3
4
5
6
7
8
9
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
2.1.6. 注释
要向.proto
文件添加注释,请使用 C/C++ 样式//
和/* ... */
语法。
1
2
3
4
5
6
7
/* SearchRequest 表示一个搜索查询,其分页选项为显示结果包含在响应中. */
message SearchRequest {
string query = 1;
int32 page_number = 2; // 我们想要哪个页码?
int32 result_per_page = 3; // 每页返回的结果数.
}
2.1.7. 保留字段
2.1.8. proto 生成文件
当你运行 protocol buffer compiler
(编译器)编译 .proto
文件,编译器会以您选择的语言生成代码,您需要使用文件中描述的消息类型,包括获取和设置字段值,将消息序列化到输出流,并从输入流中解析您的消息。
- 对于C++,编译器会将每个
.proto
生成一个.h
and.cc
文件,并为文件中描述的每种消息类型提供一个类。
2.2. 标量类型
标量消息字段可以具有以下类型之一,该表显示.proto
文件中指定的类型,以及自动生成的类中的相应类型:
.proto Type | Notes | C++ Type | Java/Kotlin Type[1] | Python Type[3] | C# Type |
---|---|---|---|---|---|
double | double | double | float | double | |
float | float | float | float | float | |
int32 | 使用可变长度编码。对负数进行编码效率低下——如果您的字段可能有负值,请改用 sint32。 | int32 | int | int | int |
int64 | 使用可变长度编码。对负数进行编码效率低下——如果您的字段可能有负值,请改用 sint64。 | int64 | long | int/long[4] | long |
uint32 | 使用可变长度编码。 | uint32 | int[2] | int/long[4] | uint |
uint64 | 使用可变长度编码。 | uint64 | long[2] | int/long[4] | ulong |
sint32 | 使用可变长度编码。带符号的 int 值。这些比常规 int32 更有效地编码负数。 | int32 | int | int | int |
sint64 | 使用可变长度编码。带符号的 int 值。这些比常规 int64 更有效地编码负数。 | int64 | long | int/long[4] | long |
fixed32 | 总是四个字节。如果值通常大于 $2^{28}$,则比 uint32 更有效。 | uint32 | int[2] | int/long[4] | uint |
fixed64 | 总是八个字节。如果值通常大于 $2^{56}$,则比 uint64 更有效。 | uint64 | long[2] | int/long[4] | ulong |
sfixed32 | 总是四个字节。 | int32 | int | int | int |
sfixed64 | 总是八个字节。 | int64 | long | int/long[4] | long |
bool | bool | boolean | bool | bool | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且不能超过 $2^{32}$。 | string | String | str/unicode[5] | string |
bytes | 可能包含不超过 $2^{32}$的任意字节序列。 | string | ByteString | str (Python 2) bytes (Python 3) | ByteString |
[1] Kotlin 使用 Java 中的相应类型,甚至是无符号类型,以确保在混合 Java/Kotlin 代码库中的兼容性。
[2] 在 Java 中,无符号 32 位和 64 位整数使用它们的有符号对应物表示,最高位简单地存储在符号位中。
[3] 在所有情况下,为字段设置值将执行类型检查以确保其有效。
[4] 64 位或无符号 32 位整数在解码时始终表示为 long,但如果在设置字段时给出 int,则可以是 int。在所有情况下,该值必须适合设置时表示的类型。见[2]。
[5] Python 字符串在解码时表示为 unicode,但如果给出 ASCII 字符串,则可以是 str(这可能会发生变化)。
[6] 整数用于 64 位机器,字符串用于 32 位机器。
2.3. 默认值
解析消息时,如果编码的消息不包含特定的奇异元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:
- 对于字符串,默认值为空字符串。
- 对于字节,默认值为空字节。
- 对于布尔值,默认值为 false。
- 对于数字类型,默认值为零。
- 对于enums,默认值是第一个定义的 enum value,它必须是 0。
- 对于消息字段,未设置该字段。它的确切值取决于语言。
重复字段的默认值为空(通常是相应语言的空列表)。
2.4. 枚举
在定义消息类型时,您可能希望其字段之一仅具有预定义的值列表之一。例如,假设您要corpus
为每个添加一个字段,SearchRequest
其中语料库可以是UNIVERSAL
、WEB
、IMAGES
、LOCAL
、NEWS
或。您可以通过在消息定义中添加一个非常简单的方法来做到这一点,并为每个可能的值添加一个常量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
如您所见,Corpus
枚举的第一个常量映射到零:每个枚举定义都必须包含一个映射到零的常量作为其第一个元素。这是因为:
- 必须有一个零值,以便我们可以使用 0 作为数字默认值。
- 零值必须是第一个元素,以便与第一个枚举值始终为默认值的
proto2
语义兼容。
您可以通过将相同的值分配给不同的枚举常量来定义别名。为此,您需要将allow_alias
选项设置为true
,否则协议编译器将在找到别名时生成错误消息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
message MyMessage1 {
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
message MyMessage2 {
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
}
2.4.1. 保留值
2.5. 使用其他消息类型
您可以使用其他消息类型作为字段类型。例如,假设您想Result
在每条SearchResponse
消息中包含消息——为此,您可以Result
在同一条消息中定义一个消息类型,.proto
然后指定一个类型为Result
的字段SearchResponse
:
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;
}
2.5.1. 导入定义
在上面的例子中,Result
消息类型定义在同一个文件中SearchResponse
——如果你想用作字段类型的消息类型已经在另一个.proto
文件中定义了怎么办?
您可以通过导入.proto
来自其他文件的定义来使用它们。要导入另一个的定义,请在文件顶部添加一个 import
语句:
1
import "myproject/other_protos.proto";
请注意,公共导入功能在 Java 中不可用。
import public
任何导入包含该语句的原型的代码都可以传递依赖依赖项。例如:
1
2
3
4
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
1
2
3
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
2.5.2. 使用 proto2 消息类型
可以导入proto2
消息类型并在您的 proto3
消息中使用它们,反之亦然。但是,proto2
枚举不能直接在 proto3
语法中使用
2.6. 嵌套类型
您可以在其他消息类型中定义和使用消息类型,如下例所示,这里Result
消息是在消息内部定义的SearchResponse
:
1
2
3
4
5
6
7
8
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果您想在其父消息类型之外重用此消息类型,请将其称为_Parent_._Type_
:
1
2
3
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
2.7. 包管理
您可以将可选package
说明符添加到.proto
文件中,以防止协议消息类型之间的名称冲突。
1
2
package foo.bar;
message Open { ... }
然后,您可以在定义消息类型的字段时使用包说明符:
1
2
3
4
5
message Foo {
...
foo.bar.Open open = 1;
...
}
2.8. JSON 映射
2.9. 生成类
使用 protoc
工具编译生成自己需要的编程语言。
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
。您可以提供一个或多个输出指令:
--cpp_out
生成 C++ 代码DST_DIR
。--java_out
生成 Java 代码DST_DIR
。--kotlin_out
生成 Kotlin 代码DST_DIR
。--python_out
生成 Python 代码DST_DIR
。--go_out
生成 Go 代码DST_DIR
。--objc_out
生成 C# 代码DST_DIR
。--php_out
生成 PHP 代码DST_DIR
。
请注意,如果输出存档已经存在,它将被覆盖
您必须提供一个或多个
.proto
文件作为输入。.proto
可以一次指定多个文件。尽管文件是相对于当前目录命名的,但每个文件必须位于其中一个IMPORT_PATH
中,以便编译器可以确定其规范名称。
3. 使用
3.1. proto 文件
我习惯进行文件分类,所以 proto
文件放入 proto_file
文件中。
3.1.1. enum.proto
1
2
3
4
5
6
7
8
9
10
syntax = "proto3";
package ENUM;
enum PhoneType
{
MOBILE = 0;
HOME = 1;
WORK = 2;
}
3.1.2. address_book.proto
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
syntax = "proto3";
package TEST;
import "enum.proto";
message Phone
{
string number = 1;
ENUM.PhoneType type = 2;
}
message Person
{
string name = 1;
int32 id = 2;
string email = 3;
int32 phone_num = 4;
repeated Phone phone = 5;
}
message AddressBook
{
int32 people_num = 1;
repeated Person people = 2;
}
3.1.3. 编译
可以写入 .bat
或 .sh
文件中
1
2
protoc ./proto_file/enum.proto --proto_path=./proto_file/ --cpp_out=.
protoc ./proto_file/address_book.proto --proto_path=./proto_file/ --cpp_out=.
会生成对应的 .pb.h
和.pb.cc
文件。
3.2. 测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <iostream>
#include "PbConvertor.h"
#include "address_book.pb.h"
void serializate(void*& data, size_t& size)
{
TEST::AddressBook address_book;
address_book.set_people_num(1);
TEST::Person* person = address_book.add_people();
person->set_name("august");
person->set_id(1);
person->set_email("123456789@qq.com");
person->set_phone_num(1);
TEST::Phone* phone = person->add_phone();
phone->set_number("12345678910");
phone->set_type(ENUM::PhoneType::MOBILE);
size = address_book.ByteSizeLong();
data = malloc(size);
address_book.SerializeToArray(data, size);
}
void deserializate(void* data, size_t size)
{
TEST::AddressBook address_book;
address_book.ParseFromArray(data, size);
for (int i = 0; i < address_book.people_size(); i++)
{
TEST::Person person = address_book.people(i);
std::cout << "The " << i + 1 << " Information" << std::endl;
std::cout << "name = " << person.name() << std::endl;
std::cout << "id = " << person.id() << std::endl;
std::cout << "email = " << person.email() << std::endl;
for (int j = 0; j < person.phone_size(); j++)
{
TEST::Phone phone = person.phone(j);
switch (phone.type())
{
case ENUM::PhoneType::MOBILE: std::cout << " Mobile phone = "; break;
case ENUM::PhoneType::HOME: std::cout << " Home phone = "; break;
case ENUM::PhoneType::WORK: std::cout << " Work phone = "; break;
}
std::cout << phone.number() << std::endl;
}
}
}
void demo_hand()
{
void* data = nullptr;
size_t size = 0;
serializate(data, size);
deserializate(data, size);
free(data);
}
// g++ -o main main.cpp address_book.pb.cc enum.pb.cc -lprotobuf
int main()
{
demo_hand();
return 0;
}
参考
[1] Google参考文档:https://developers.google.com/protocol-buffers/