大纲
前言
学习资源
版本说明
本文使用各软件的版本如下表所示:
软件 | 版本 | 说明 |
---|
C++ 标准 | 17 | 高版本的 Protobuf 库依赖 C++ 17 |
Protobuf | 31.1 | Protobuf 库,核心代码主要是用 C++ 开发 |
G++(GCC) | 12.2.0 | 建议使用 9 版本的 G++(GCC) 编译器 |
CMake | 3.25.1 | C/C++ 项目构建工具 |
Linux | Debian 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.sh
和 configure
构建系统,转而使用 CMake 作为主要构建系统。 - Protobuf 从源码编译后,默认只会生成
.a
静态库文件,若希望生成 .so
动态库文件,需要在编译时添加 CMake 参数 -DBUILD_SHARED_LIBS=ON
。
1
| sudo apt-get -y install cmake g++ make git wget
|
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
cmake .. -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local
make -j2
sudo make install
sudo ldconfig /usr/local/lib/
|
1 2 3 4 5 6 7 8
| protoc --version
ls -al /usr/local/include/google/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
| protoc userservice.proto --cpp_out=./
|
protoc
命令编译生成的 C++ 源文件和头文件如下所示:
1 2 3
| ├── userservice.pb.cc ├── userservice.pb.h └── userservice.proto
|
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() { user::LoginRequest req1; req1.set_name("jim"); req1.set_pwd("12345");
string serialize_str; if (req1.SerializeToString(&serialize_str)) { cout << serialize_str.c_str() << endl; }
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
| g++ main.cc userservice.pb.cc -o main -lprotobuf -labsl_log_internal_check_op -labsl_log_internal_message -labsl_log_internal_nullguard
g++ main.cc userservice.pb.cc -o main $(pkg-config --cflags --libs protobuf)
|
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
| 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() { friends::GetFriendListResponse resp;
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;
string serialize_str; if (!resp.SerializeToString(&serialize_str)) { cerr << "protobuf serialize failed" << endl; }
return 0; }
|
- 使用
g++
编译器编译 C++ 代码,生成可执行文件
1 2 3 4 5
| g++ main.cc friendservice.pb.cc -o main -lprotobuf -labsl_log_internal_check_op -labsl_log_internal_message -labsl_log_internal_nullguard
g++ main.cc friendservice.pb.cc -o main $(pkg-config --cflags --libs protobuf)
|
1 2 3 4 5
| ├── main // 编译生成的可执行文件 ├── main.cc // C++ 测试源文件 ├── friendservice.pb.cc // Protobuf 生成的 C++ 源文件 ├── friendservice.pb.h // Protobuf 生成的 C++ 头文件 └── friendservice.proto // Protobuf 文件
|
- 执行
g++
编译生成的可执行文件,程序的运行结果如下:
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
| 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() { group::GetGroupResponse resp;
group::ResultCode *result = resp.mutable_result(); result->set_errormsg("server error"); result->set_errorno(500); result->set_result(false);
group::Group *group = resp.mutable_group(); group->set_id(1001); group->set_groupname("AI Team"); group->set_groupdesc("A group for AI");
group::User user1; user1.set_id(1); user1.set_name("Jim"); user1.set_sex(group::User::MAN);
group::User user2; user2.set_id(2); user2.set_name("Tom"); user2.set_sex(group::User::MAN);
(*group->mutable_users())[user1.id()] = user1; (*group->mutable_users())[user2.id()] = user2;
cout << "group user map size: " << resp.group().users().size() << endl;
string serialize_str; if (!resp.SerializeToString(&serialize_str)) { cerr << "protobuf map serialize failed" << endl; }
return 0; }
|
- 使用
g++
编译器编译 C++ 代码,生成可执行文件
1 2 3 4 5
| g++ main.cc groupservice.pb.cc -o main -lprotobuf -labsl_hash -labsl_log_internal_check_op -labsl_log_internal_message -labsl_log_internal_nullguard
g++ main.cc groupservice.pb.cc -o main $(pkg-config --cflags --libs protobuf)
|
1 2 3 4 5
| ├── main // 编译生成的可执行文件 ├── main.cc // C++ 测试源文件 ├── groupservice.pb.cc // Protobuf 生成的 C++ 源文件 ├── groupservice.pb.h // Protobuf 生成的 C++ 头文件 └── groupservice.proto // Protobuf 文件
|
- 执行
g++
编译生成的可执行文件,程序的运行结果如下:
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
| 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() { 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;
string serialize_str; if (!addressBook.SerializeToString(&serialize_str)) { cerr << "address book serialize failed" << endl; }
return 0; }
|
- 使用
g++
编译器编译 C++ 代码,生成可执行文件
1 2 3 4 5
| g++ main.cc addressbook.pb.cc -o main -lprotobuf -labsl_log_internal_check_op -labsl_log_internal_message -labsl_log_internal_nullguard
g++ main.cc addressbook.pb.cc -o main $(pkg-config --cflags --libs protobuf)
|
1 2 3 4 5
| ├── main // 编译生成的可执行文件 ├── main.cc // C++ 测试源文件 ├── addressbook.pb.cc // Protobuf 生成的 C++ 源文件 ├── addressbook.pb.h // Protobuf 生成的 C++ 头文件 └── addressbook.proto // Protobuf 文件
|
- 执行
g++
编译生成的可执行文件,程序的运行结果如下:
参考资料