C++ 快速上手使用 Protobuf

大纲

前言

学习资源

版本说明

本文使用各软件的版本如下表所示:

软件版本说明
C++ 标准17高版本的 Protobuf 库依赖 C++ 17
Protobuf31.1Protobuf 库,核心代码主要是用 C++ 开发
G++(GCC)12.2.0建议使用 9 版本的 G++(GCC) 编译器
CMake3.25.1C/C++ 项目构建工具
LinuxDebian 12

Protobuf 介绍

  • Protocol Buffers(简称 Protobuf)是 Google 提出的一种高效、可扩展的结构化数据序列化格式,用于数据交换。它独立于平台和编程语言,具有良好的跨平台兼容性和扩展性。

  • Google 为多种主流编程语言提供了 Protobuf 的官方实现,包括 Java、C#、C++、Go 和 Python 等。每种语言的实现都包含相应的编译器插件(protoc)和运行时库,使得开发者可以在不同语言间无缝进行数据通信。

  • 由于 Protobuf 采用紧凑的二进制编码格式,其序列化和反序列化效率远高于基于文本的格式。相比 XML,Protobuf 的传输效率可提高约 20 倍;相比 JSON,也有近 10 倍的性能提升。这使得它特别适用于对性能要求高的场景。

  • Protobuf 广泛应用于分布式系统间的数据通信、异构平台的数据交换,也适合用作网络传输协议的数据格式、高效配置文件的载体、或用于数据持久化存储。作为一种兼具效率与可维护性的序列化方案,Protobuf 在大规模系统设计中具有极高的实用价值。

Protobuf 安装

提示

  • Protobuf 各个版本的源码包可以从 GitHub Release 下载得到。
  • Protobuf 从 3.21 版本开始,Google 官方已经弃用了 autogen.shconfigure 构建系统,转而使用 CMake 作为主要构建系统。
  • Protobuf 从源码编译后,默认只会生成 .a 静态库文件,若希望生成 .so 动态库文件,需要在编译时添加 CMake 参数 -DBUILD_SHARED_LIBS=ON
  • 安装依赖包
1
sudo apt-get -y install cmake g++ make git wget
  • 编译安装 Protobuf 库
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
# 下载源码
wget https://github.com/protocolbuffers/protobuf/archive/refs/tags/v31.1.tar.gz -O protobuf-v31.1.tar.gz

# 解压源码
tar -xvf protobuf-v31.1.tar.gz

# 进入解压目录
cd protobuf-v31.1

# 初始化子模块
git init && git submodule update --init --recursive

# 创建构建目录
mkdir build

# 进入构建目录
cd build

# 生成构建文件(Makefile)
cmake .. -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local

# 编译源码(耗时较长)
make -j2

# 执行安装(包括可执行文件、头文件和库)
sudo make install

# 更新系统的共享库缓存
sudo ldconfig /usr/local/lib/
  • 验证 Protobuf 库安装
1
2
3
4
5
6
7
8
# 查看 Protobuf 库的版本
protoc --version

# 查看 Protobuf 库的头文件
ls -al /usr/local/include/google/protobuf

# 查看 Protobuf 库的动态库
ls -al /usr/local/lib/libproto*

Protobuf 快速入门

Protobuf 协议文件

提示

  • 在 Protobuf 的协议文件中,消息(message)的字段定义格式为 <类型> <字段名> = <唯一编号>;,比如 int32 age = 3;
  • 在 Protobuf 的协议文件中,为了提高运行效率,建议使用 bytes 类型来替代 string 类型,比如 bytes name = 1;;之后在 C++ 项目中使用 Protobuf 自动生成的 C++ 代码时,跟使用 std::string 类型没有任何区别,不需要改动任何 C++ 代码。
  • 创建 userservice.proto 文件,其中 Protobuf 的协议内容如下:
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
// Protobuf 语法的版本
syntax = "proto3";

// 定义包名,便于在生成的代码中区分不同模块(类似 C++ 的命名空间)
package user;

// 允许生成通用的 C++ 服务接口(可选项)
option cc_generic_services = true;

// 用户登录请求消息结构体
message LoginRequest {
string name = 1;
string pwd = 2;
}

// 用户注册请求消息结构体
message RegRequest {
string name = 1;
string pwd = 2;
int32 age = 3;

// 枚举类型
enum SEX {
MAN = 0;
WOMAN = 1;
}

SEX sex = 4;
string phone = 5;
}

// 通用响应消息结构体
message Response {
int32 errorno = 1;
string errormsg = 2;
bool result = 3;
}

// 定义RPC服务接口类和服务函数
service UserServiceRpc {
// 用户登录函数,接收 LoginRequest 请求,返回 Response 响应
rpc login(LoginRequest) returns (Response);

// 用户注册函数,接收 RegRequest 请求,返回 Response 响应
rpc reg(RegRequest) returns (Response);
}

Protobuf 生成代码

  • 当定义了 .proto 文件(比如 userservice.proto)后,可以使用 protoc 命令根据 .proto 文件编译生成 C++ 源文件和头文件
1
2
# 根据 .proto 文件生成 C++ 源文件和头文件
protoc userservice.proto --cpp_out=./
  • protoc 命令编译生成的 C++ 源文件和头文件如下所示:
1
2
3
├── userservice.pb.cc   // Protobuf 生成的 C++ 源文件
├── userservice.pb.h // Protobuf 生成的 C++ 头文件
└── userservice.proto // Protobuf 文件

Protobuf 使用案例

  • 根据 ·proto 文件(比如 userservice.proto)编译生成 C++ 源文件和头文件后,就可以基于 Protobuf 实现序列化和反序列化,示例代码(main.cc)如下:
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
#include <iostream>
#include <string>

#include "userservice.pb.h"

using namespace std;

int main() {
// LoginRequest 类的代码由 protoc 命令根据 .proto 文件编译生成
user::LoginRequest req1;
req1.set_name("jim");
req1.set_pwd("12345");

// C++对象序列化
string serialize_str;
if (req1.SerializeToString(&serialize_str)) {
cout << serialize_str.c_str() << endl;
}

// C++对象反序列化
user::LoginRequest req2;
if (req2.ParseFromString(serialize_str)) {
cout << "name: " << req2.name() << ", pwd: " << req2.pwd() << endl;
}

return 0;
}
  • 然后使用 g++ 编译器编译 C++ 代码,生成可执行文件
1
2
3
4
5
# 精简版编译命令(只链接必要的 Protobuf 库,编译速度较快)
g++ main.cc userservice.pb.cc -o main -lprotobuf -labsl_log_internal_check_op -labsl_log_internal_message -labsl_log_internal_nullguard

# 完整版编译命令(链接 Protobuf 依赖的所有库,编译速度较慢)
g++ main.cc userservice.pb.cc -o main $(pkg-config --cflags --libs protobuf)
  • g++ 编译后完整的文件列表如下:
1
2
3
4
5
├── main                // 编译生成的可执行文件
├── main.cc // C++ 测试源文件
├── userservice.pb.cc // Protobuf 生成的 C++ 源文件
├── userservice.pb.h // Protobuf 生成的 C++ 头文件
└── userservice.proto // Protobuf 文件
  • 执行 g++ 编译生成的可执行文件,程序的运行结果如下:
1
2
jim12345
name: jim, pwd: 12345

Protobuf 进阶使用

数组的使用

在 Protobuf 中,repeated 是一个字段修饰符,用来表示一个字段可以出现多次,即是一个数组(或列表)。

  • 创建 friendservice.proto 文件,其中 Protobuf 的协议内容如下:
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
// Protobuf 语法的版本
syntax = "proto3";

// 定义包名,便于在生成的代码中区分不同模块
package friends;

message ResultCode {
int32 errorno = 1;
string errormsg = 2;
bool result = 3;
}

message User {
uint32 id = 1;
string name = 2;

// 枚举类型
enum SEX {
MAN = 0;
WOMAN = 1;
}

SEX sex = 3;
}

message GetFriendListRequest {
uint32 userid = 1;
}

message GetFriendListResponse {
ResultCode result = 1;
// 定义列表
repeated User friendList = 2;
}
  • 使用 protoc 命令根据 .proto 文件编译生成 C++ 源文件和头文件
1
2
# 根据 .proto 文件生成 C++ 源文件和头文件
protoc friendservice.proto --cpp_out=./
  • protoc 命令编译生成的 C++ 源文件和头文件如下所示:
1
2
3
├── friendservice.pb.cc   // Protobuf 生成的 C++ 源文件
├── friendservice.pb.h // Protobuf 生成的 C++ 头文件
└── friendservice.proto // Protobuf 文件
  • C++ 使用 Protobuf 进行序列化或者反序列化的代码
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
#include <iostream>
#include <string>

#include "friendservice.pb.h"

using namespace std;

int main() {
// GetFriendListResponse 类的代码由 protoc 命令根据 .proto 文件编译生成
friends::GetFriendListResponse resp;

// 设置 ResultCode
friends::ResultCode *res = resp.mutable_result();
res->set_errormsg("success");
res->set_errorno(200);
res->set_result(true);

// 往列表添加元素
friends::User *user1 = resp.add_friendlist();
user1->set_sex(friends::User::MAN);
user1->set_name("jim");
user1->set_id(1);

// 往列表添加元素
friends::User *user2 = resp.add_friendlist();
user2->set_sex(friends::User::MAN);
user2->set_name("tom");
user2->set_id(2);

// 获取列表的大小
cout << "friend list size: " << resp.friendlist_size() << endl;

// C++ 对象序列化
string serialize_str;
if (!resp.SerializeToString(&serialize_str)) {
cerr << "protobuf serialize failed" << endl;
}

return 0;
}
  • 使用 g++ 编译器编译 C++ 代码,生成可执行文件
1
2
3
4
5
# 精简版编译命令(只链接必要的 Protobuf 库,编译速度较快)
g++ main.cc friendservice.pb.cc -o main -lprotobuf -labsl_log_internal_check_op -labsl_log_internal_message -labsl_log_internal_nullguard

# 完整版编译命令(链接 Protobuf 依赖的所有库,编译速度较慢)
g++ main.cc friendservice.pb.cc -o main $(pkg-config --cflags --libs protobuf)
  • g++ 编译后完整的文件列表如下:
1
2
3
4
5
├── main                  // 编译生成的可执行文件
├── main.cc // C++ 测试源文件
├── friendservice.pb.cc // Protobuf 生成的 C++ 源文件
├── friendservice.pb.h // Protobuf 生成的 C++ 头文件
└── friendservice.proto // Protobuf 文件
  • 执行 g++ 编译生成的可执行文件,程序的运行结果如下:
1
friend list size: 2

Map 的使用

  • 创建 groupservice.proto 文件,其中 Protobuf 的协议内容如下:
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
// Protobuf 语法的版本
syntax = "proto3";

// 定义包名,便于在生成的代码中区分不同模块(类似 C++ 的命名空间)
package group;

message ResultCode {
int32 errorno = 1;
string errormsg = 2;
bool result = 3;
}

message User {
uint32 id = 1;
string name = 2;

// 枚举类型
enum SEX {
MAN = 0;
WOMAN = 1;
}

SEX sex = 3;
}

message Group {
uint32 id = 1;
string groupName = 2;
string groupDesc = 3;
// Map 类型
map<uint32, User> users = 4;
}

message GetGroupRequest {
uint32 userid = 1;
}

message GetGroupResponse {
ResultCode result = 1;
Group group = 2;
}
  • 使用 protoc 命令根据 .proto 文件编译生成 C++ 源文件和头文件
1
2
# 根据 .proto 文件生成 C++ 源文件和头文件
protoc groupservice.proto --cpp_out=./
  • protoc 命令编译生成的 C++ 源文件和头文件如下所示:
1
2
3
├── groupservice.pb.cc    // Protobuf 生成的 C++ 源文件
├── groupservice.pb.h // Protobuf 生成的 C++ 头文件
└── groupservice.proto // Protobuf 文件
  • C++ 使用 Protobuf 进行序列化或者反序列化的代码
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
#include <iostream>
#include <string>

#include "groupservice.pb.h"

using namespace std;

int main() {
// GetGroupResponse 类的代码由 protoc 命令根据 .proto 文件编译生成
group::GetGroupResponse resp;

// 设置 ResultCode
group::ResultCode *result = resp.mutable_result();
result->set_errormsg("server error");
result->set_errorno(500);
result->set_result(false);

// 设置 Group
group::Group *group = resp.mutable_group();
group->set_id(1001);
group->set_groupname("AI Team");
group->set_groupdesc("A group for AI");

// Map 中的 User
group::User user1;
user1.set_id(1);
user1.set_name("Jim");
user1.set_sex(group::User::MAN);

// Map 中的 User
group::User user2;
user2.set_id(2);
user2.set_name("Tom");
user2.set_sex(group::User::MAN);

// 向 Map 添加成员(注意:这里使用的是指针引用)
(*group->mutable_users())[user1.id()] = user1;
(*group->mutable_users())[user2.id()] = user2;

// 获取 Map 的大小
cout << "group user map size: " << resp.group().users().size() << endl;

// C++ 对象序列化
string serialize_str;
if (!resp.SerializeToString(&serialize_str)) {
cerr << "protobuf map serialize failed" << endl;
}

return 0;
}
  • 使用 g++ 编译器编译 C++ 代码,生成可执行文件
1
2
3
4
5
# 精简版编译命令(只链接必要的 Protobuf 库,编译速度较快)
g++ main.cc groupservice.pb.cc -o main -lprotobuf -labsl_hash -labsl_log_internal_check_op -labsl_log_internal_message -labsl_log_internal_nullguard

# 完整版编译命令(链接 Protobuf 依赖的所有库,编译速度较慢)
g++ main.cc groupservice.pb.cc -o main $(pkg-config --cflags --libs protobuf)
  • g++ 编译后完整的文件列表如下:
1
2
3
4
5
├── main                        // 编译生成的可执行文件
├── main.cc // C++ 测试源文件
├── groupservice.pb.cc // Protobuf 生成的 C++ 源文件
├── groupservice.pb.h // Protobuf 生成的 C++ 头文件
└── groupservice.proto // Protobuf 文件
  • 执行 g++ 编译生成的可执行文件,程序的运行结果如下:
1
group user map size: 2

Message 嵌套使用

  • 创建 addressbook.proto 文件,其中 Protobuf 的协议内容如下:
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
// Protobuf 语法的版本
syntax = "proto3";

// 定义包名,便于在生成的代码中区分不同模块(类似 C++ 的命名空间)
package address;

// 定义个人信息的消息类型
message Person {

// ID
int32 id = 2;

// 姓名
string name = 1;

// 邮箱地址,可选字段
optional string email = 3;

// 定义性别的枚举类型
enum SEX {
MAN = 0;
WOMAN = 1;
}

SEX sex = 4;

// 定义电话类型的枚举
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}

// 定义电话号码的嵌套消息类型
message PhoneNumber {
// 电话号码
string number = 1;
// 电话类型
PhoneType type = 2;
}

// 电话号码列表(一个人可以有多个电话号码)
repeated PhoneNumber phones = 5;
}

// 定义通讯录
message AddressBook {
// 通讯录中的人列表
repeated Person people = 1;
}
  • 使用 protoc 命令根据 .proto 文件编译生成 C++ 源文件和头文件
1
2
# 根据 .proto 文件生成 C++ 源文件和头文件
protoc addressbook.proto --cpp_out=./
  • protoc 命令编译生成的 C++ 源文件和头文件如下所示:
1
2
3
├── addressbook.pb.cc   // Protobuf 生成的 C++ 源文件
├── addressbook.pb.h // Protobuf 生成的 C++ 头文件
└── addressbook.proto // Protobuf 文件
  • C++ 使用 Protobuf 进行序列化或者反序列化的代码
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
#include <iostream>
#include <string>

#include "addressbook.pb.h"

using namespace std;

int main() {
// AddressBook 类的代码由 protoc 命令根据 .proto 文件编译生成
address::AddressBook addressBook;

// 添加第一个人
address::Person* person1 = addressBook.add_people();
person1->set_id(1001);
person1->set_name("jim");
person1->set_sex(address::Person::MAN);
person1->set_email("jim@example.com");

// 添加第一个人的手机号码
address::Person::PhoneNumber* phone1 = person1->add_phones();
phone1->set_number("1234567890");
phone1->set_type(address::Person::PHONE_TYPE_MOBILE);

// 添加第一个人的家庭电话
address::Person::PhoneNumber* phone2 = person1->add_phones();
phone2->set_number("0987 - 654321");
phone2->set_type(address::Person::PHONE_TYPE_HOME);

// 添加第二个人
address::Person* person2 = addressBook.add_people();
person2->set_name("Tom");
person2->set_id(1002);
person2->set_email("tom@example.com");

// 添加第二个人的手机号码
address::Person::PhoneNumber* phone3 = person2->add_phones();
phone3->set_number("9876543210");
phone3->set_type(address::Person::PHONE_TYPE_MOBILE);

// 添加第二个人的家庭电话
address::Person::PhoneNumber* phone4 = person2->add_phones();
phone4->set_number("0865 - 123456");
phone4->set_type(address::Person::PHONE_TYPE_HOME);

// 获取通讯录里的人数
cout << "people size: " << addressBook.people_size() << endl;

// C++ 对象序列化
string serialize_str;
if (!addressBook.SerializeToString(&serialize_str)) {
cerr << "address book serialize failed" << endl;
}

return 0;
}
  • 使用 g++ 编译器编译 C++ 代码,生成可执行文件
1
2
3
4
5
# 精简版编译命令(只链接必要的 Protobuf 库,编译速度较快)
g++ main.cc addressbook.pb.cc -o main -lprotobuf -labsl_log_internal_check_op -labsl_log_internal_message -labsl_log_internal_nullguard

# 完整版编译命令(链接 Protobuf 依赖的所有库,编译速度较慢)
g++ main.cc addressbook.pb.cc -o main $(pkg-config --cflags --libs protobuf)
  • g++ 编译后完整的文件列表如下:
1
2
3
4
5
├── main                  // 编译生成的可执行文件
├── main.cc // C++ 测试源文件
├── addressbook.pb.cc // Protobuf 生成的 C++ 源文件
├── addressbook.pb.h // Protobuf 生成的 C++ 头文件
└── addressbook.proto // Protobuf 文件
  • 执行 g++ 编译生成的可执行文件,程序的运行结果如下:
1
people size: 2

参考资料