1.Protocol Buffers(简称 Protobuf)
是一种语言无关、平台无关的可扩展的序列化结构数据格式,由 Google 开发并开源。它主要用于高效地存储和传输结构化数据,广泛应用于网络通信、数据存储、微服务架构等多个领域。
1. 核心概念
-
序列化:将数据结构或对象状态转换为可存储或可传输的格式的过程。Protobuf 将数据序列化为二进制格式,这种格式紧凑且高效,适合在网络中传输或存储到磁盘上。
-
反序列化:将序列化后的数据重新转换为原始数据结构或对象的过程。Protobuf 提供了高效的反序列化机制,能够快速恢复数据的原始状态。
-
结构化数据:Protobuf 用于处理结构化数据,例如对象、记录、消息等。它通过定义数据的结构(称为
.proto
文件),来指定数据的字段、类型和关系。
2. 主要特点
-
高效性:Protobuf 的二进制格式比 JSON、XML 等文本格式更加紧凑,占用空间更小,解析速度更快。例如,对于相同的数据,Protobuf 的序列化结果通常只有 JSON 的三分之一大小。
-
可扩展性:Protobuf 的数据结构定义支持字段的添加、删除和修改,而不会破坏已有的数据格式。这使得它非常适合在快速迭代的系统中使用,例如微服务架构中的接口定义。
-
语言无关性:Protobuf 提供了多种语言的库支持,包括 C++、Java、Python、Go、C# 等。开发者可以在不同语言的系统之间无缝交换数据。
-
强类型:Protobuf 在定义数据结构时需要明确指定字段的类型(如整数、浮点数、字符串等),这使得数据的结构更加清晰,减少了因类型不匹配而导致的错误。
3. 工作原理
-
定义数据结构:开发者通过
.proto
文件定义数据的结构。例如:syntax = "proto3"; message Person { string name = 1; int32 id = 2; repeated string email = 3; }
在这个例子中,
Person
是一个消息类型,包含三个字段:name
(字符串类型)、id
(整数类型)和email
(字符串列表)。 -
生成代码:使用 Protobuf 编译器(
protoc
)根据.proto
文件生成目标语言的代码。例如,对于上述.proto
文件,protoc
可以生成 Java、Python 或 C++ 的代码。 -
序列化与反序列化:在应用程序中,使用生成的代码将数据对象序列化为二进制格式,或者将二进制格式的数据反序列化为对象。
4. 应用场景
-
网络通信:在客户端和服务器之间传输数据时,Protobuf 可以显著提高通信效率。例如,Google 的许多内部服务都使用 Protobuf 进行 RPC(远程过程调用)通信。
-
数据存储:将结构化数据序列化后存储到文件或数据库中,节省存储空间并提高读写效率。
-
微服务架构:在微服务之间传递数据时,Protobuf 提供了高效且灵活的数据交换格式,支持服务的快速迭代和扩展。
2. 序列化反序列化组件
以下是对 IDL(接口描述语言)、IDL编译器、Client/Server、Stub/Skeleton库以及它们与序列化、反序列化的关系的详细解释:
1. IDL(接口描述语言)
IDL(Interface Definition Language,接口定义语言)是一种用于定义软件组件接口的通用语言。它允许开发者以一种独立于具体编程语言的方式描述接口、数据类型、方法和属性。这些定义可以被转换成多种编程语言的代码,从而实现不同编程语言之间的无缝通信
2. IDL编译器
IDL编译器的作用是将IDL文件转换为特定编程语言的代码。这些代码通常包括:
-
Stub(客户端存根):客户端使用的代码,负责将方法调用和参数序列化后发送到服务端,并接收服务端返回的序列化结果进行反序列化。
-
Skeleton(服务端骨架):服务端使用的代码,负责接收客户端发送的序列化请求,反序列化后调用实际的服务逻辑,并将结果序列化后返回给客户端。
-
协议库(Protocol Library):定义了接口的公共部分,供客户端和服务端共享。
3. protobuf使用
1. 编写 .proto
文件
首先,需要定义一个 .proto
文件,描述你的数据结构。例如:
syntax = "proto3";
message Person {
int32 id = 1;
string name = 2;
repeated string phone = 3;
}
这个文件定义了一个 Person
消息,包含一个整数 id
、一个字符串 name
和一个字符串数组 phone
。
2. 使用 protoc
生成 C++ 代码
运行以下命令,将 .proto
文件编译为 C++ 代码:
$ protoc .proto文件路径 --cpp_out=输出路径(存储生成的c++文件)
这会生成两个文件:your_file.pb.h
和 your_file.pb.cc
。
3. 在 C++ 代码中使用 Protobuf
1.序列化反序列化
bool SerializeToString(std::string* output) const;
bool ParseFromString(const std::string& data);
bool SerializeToArray(void* data, int size) const;
MyMessage message;
// 设置消息的字段
char buffer[1024];
if (message.SerializeToArray(buffer, sizeof(buffer))) {
// 序列化成功,buffer包含二进制数据
}
bool ParseFromArray(const void* data, int size);
bool SerializeToOstream(std::ostream* output) const;
MyMessage message;
// 设置消息的字段
std::ofstream output("message.bin", std::ios::binary);
if (message.SerializeToOstream(&output)) {
// 序列化成功,数据已写入文件
}
bool ParseFromIstream(std::istream* input);
MyMessage message;
std::ifstream input("message.bin", std::ios::binary);
if (message.ParseFromIstream(&input)) {
// 解析成功,message包含解析后的数据
}
序列化
在 C++ 代码中,包含生成的头文件,并使用 Protobuf 提供的 API 进行序列化。例如:
#include "your_file.pb.h"
#include <fstream>
#include <iostream>
int main() {
// 创建一个 Person 对象并设置字段
Person person;
person.set_id(123);
person.set_name("Alice");
person.add_phone("1234567890");
// 序列化到字符串
std::string serialized_data;
if (!person.SerializeToString(&serialized_data)) {
std::cerr << "Failed to serialize data." << std::endl;
return 1;
}
// 或者序列化到文件
std::ofstream output("person_data.bin", std::ios::binary);
if (!person.SerializeToOstream(&output)) {
std::cerr << "Failed to serialize data to file." << std::endl;
return 1;
}
return 0;
}
反序列化
反序列化是将序列化后的数据还原为 Protobuf 对象。例如:
#include "your_file.pb.h"
#include <fstream>
#include <iostream>
int main() {
// 从字符串反序列化
Person person;
std::string serialized_data = ...; // 获取序列化后的数据
if (!person.ParseFromString(serialized_data)) {
std::cerr << "Failed to parse data." << std::endl;
return 1;
}
// 或者从文件反序列化
std::ifstream input("person_data.bin", std::ios::binary);
if (!person.ParseFromIstream(&input)) {
std::cerr << "Failed to parse data from file." << std::endl;
return 1;
}
// 使用反序列化后的对象
std::cout << "ID: " << person.id() << std::endl;
std::cout << "Name: " << person.name() << std::endl;
for (const auto& phone : person.phone()) {
std::cout << "Phone: " << phone << std::endl;
}
return 0;
}
2.操作消息字段
1. 设置字段值
ProtoBuf 提供了 set_<field_name>
方法,用于设置字段的值。
MyMessage message;
message.set_name("Alice"); // 设置字符串字段
message.set_age(30); // 设置整数字段
message.set_height(1.75); // 设置浮点字段
2. 获取字段值
ProtoBuf 提供了 <field_name>
方法,用于获取字段的值。
std::string name = message.name(); // 获取字符串字段
int age = message.age(); // 获取整数字段
double height = message.height(); // 获取浮点字段
3. 检查字段是否已设置
ProtoBuf 提供了 has_<field_name>
方法,用于检查字段是否已设置。
if (message.has_name()) {
std::cout << "Name is set: " << message.name() << std::endl;
}
4. 清除字段值
ProtoBuf 提供了 clear_<field_name>
方法,用于清除字段的值。
message.clear_name(); // 清除字符串字段
message.clear_age(); // 清除整数字段
message.clear_height(); // 清除浮点字段
5. 操作重复字段
ProtoBuf 提供了多种方法来操作重复字段(数组)。
int32_t* value_ptr = message.add_values(); // 获取指向新添加元素的指针
*value_ptr = 10; // 设置新添加元素的值
int size = message.values_size(); // 获取重复字段的大小
int first_value = message.values(0); // 获取第一个元素
message.set_values(0, 100); // 设置第一个元素的值
message.clear_values(); // 清除所有元素
6. 操作嵌套消息
ProtoBuf 提供了 mutable_<field_name>
方法通常用于访问并修改一个字段,这个字段的类型是嵌套类型或引用类型(例如一个 message
类型)。mutable_<field_name>
会返回该字段的指针,允许你直接修改其内容。
void ModifyLoadAvg1(MonitorInfo* monitor_info) {
// 获取 CpuLoad 字段的可修改引用
CpuLoad* cpu_load = monitor_info->mutable_cpu_load();
// 使用 set_ 方法修改 load_avg_1
cpu_load->set_load_avg_1(1.23f);
}
4.CMake项目中集成Protobuf和gRPC
步骤 1: 查找所需的包
首先,使用 find_package
命令来查找所需的库。这里查找了protobuf和gRPC库,以及可选的c-ares库和线程库(如果需要)。
find_package(protobuf CONFIG REQUIRED)
find_package(gRPC CONFIG REQUIRED)
步骤 2: 定义 .proto
文件列表
定义一个变量 PROTO_FILES
,列出所有需要编译的 .proto
文件。
set(PROTO_FILES
monitor_info.proto
cpu_load.proto
cpu_softirq.proto
cpu_stat.proto
mem_info.proto
net_info.proto
)
步骤 3: 添加库目标
添加一个库目标 monitor_proto
,并将 .proto
文件作为源文件添加到该目标。
add_library(monitor_proto ${PROTO_FILES})
步骤 4: 链接库
为目标 monitor_proto
链接所需的库,包括protobuf和gRPC的库。
target_link_libraries(monitor_proto
PUBLIC
protobuf::libprotobuf
gRPC::grpc
gRPC::grpc++
)
步骤 5: 设置包含目录
为目标 monitor_proto
设置包含目录,以便编译器可以找到protobuf和gRPC的头文件,以及生成的头文件。
target_include_directories(monitor_proto PUBLIC
${PROTOBUF_INCLUDE_DIRS}
${CMAKE_CURRENT_BINARY_DIR})
步骤 6: 生成 protobuf 和 gRPC 代码
protobuf_generate
是一个在 CMake 脚本中使用的命令,它用于从 .proto
文件生成 C++ 源代码和头文件。这个命令是 protobuf_generate_cpp
和 protobuf_generate_grpc
命令的封装,它根据 .proto
文件的内容自动决定是仅生成 protobuf 代码还是同时生成 gRPC 代码。
protobuf_generate(
[TARGET target_name]
[LANGUAGE <language>]
[PROTO_SRCS variable]
[PROTO_HDRS variable]
[GENERATE_EXTENSIONS extension1 [extension2...]]
[DESCRIPTOR_SET output_file]
[PLUGIN plugin]
[input.proto files...]
)
-
TARGET
target_name
:指定一个目标名称,用于接收生成的源文件和头文件。这是可选的,如果不指定,你需要手动处理生成的文件。 -
LANGUAGE
<language>
:指定要生成代码的语言。对于 C++,通常使用cpp
。 -
PROTO_SRCS
variable
:指定一个变量,用于存储生成的源文件路径。 -
PROTO_HDRS
variable
:指定一个变量,用于存储生成的头文件路径。 -
GENERATE_EXTENSIONS
extension1
[extension2...]
:指定生成文件的扩展名。默认情况下,会生成.pb.h
和.pb.cc
文件。 -
DESCRIPTOR_SET
output_file
:指定一个输出文件,用于保存描述符集。 -
PLUGIN
plugin
:指定一个插件,用于处理特定的生成任务,例如 gRPC。 -
input.proto
files...
:指定一个或多个.proto
文件,作为代码生成的输入
get_target_property
是 CMake 中的一个命令,它用于查询指定目标(target)的属性(property),并将查询结果存储在变量中。目标可以是库(library)、可执行文件(executable)或自定义目标(custom target)等。属性可以是目标的任何元数据,比如位置、源文件、链接库等。
-
get_target_property(<variable> <target> <property> [发电机表达式])
-
<variable>
:存储查询结果的变量名称。 -
<target>
:要查询的目标名称。 -
<property>
:要查询的目标属性名称。 -
[发电机表达式]
:(可选)当指定此参数时,属性值将在构建系统生成时计算。
使用 protobuf_generate
命令为目标 monitor_proto
生成C++源文件和头文件。这包括protobuf的C++代码和gRPC的C++存根代码。
get_target_property(grpc_cpp_plugin_location gRPC::grpc_cpp_plugin LOCATION)
protobuf_generate(TARGET monitor_proto LANGUAGE cpp)
protobuf_generate(TARGET monitor_proto LANGUAGE grpc GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc PLUGIN "protoc-gen-grpc=${grpc_cpp_plugin_location}")
这里,get_target_property
用于获取gRPC C++插件的位置,然后 protobuf_generate
用于生成C++代码。LANGUAGE cpp
和 LANGUAGE grpc
指定了要生成的代码类型,GENERATE_EXTENSIONS
指定了生成文件的扩展名,PLUGIN
指定了用于生成gRPC代码的插件。
5.在项目中protobuf具体使用
1.proto文件的导入
将相关的消息类型定义分散到多个文件中,从而提高代码的可维护性和可读性
import "要使用的proto文件的名字";
2.protobuf 包的使用
package monitor.proto
是 Protobuf 文件中定义的一个包名(package)声明,它用于指定当前 Protobuf 文件所属的命名空间。以下是对它的详细解释:
- 在 C++ 中,
package monitor.proto;
会被映射为命名空间monitor::proto
。
3.repeated
是一个限定修饰符,用于表示某个字段可以包含多个值,类似于数组。repeated
字段可以存储零个或多个相同类型的元素,在 C++ 中,repeated
字段会被映射为 std::vector
syntax = "proto3";
package monitor.proto;
message ServerInfo {
string ip = 1;
int32 port = 2;
}
syntax = "proto3";
package monitor.proto;
import "ServerInfo.proto"
message MonitorRequest {
repeated ServerInfo servers = 1; // repeated 字段
string query = 2;
}
#include "monitor.pb.h"
#include <iostream>
int main() {
// 创建 MonitorRequest 消息
monitor::proto::MonitorRequest request;
// 添加服务器信息
monitor::proto::ServerInfo* server1 = request.add_servers();
server1->set_ip("192.168.1.1");
server1->set_port(8080);
monitor::proto::ServerInfo* server2 = request.add_servers();
server2->set_ip("192.168.1.2");
server2->set_port(8081);
// 遍历 repeated 字段
for (int i = 0; i < request.servers_size(); ++i) {
const monitor::proto::ServerInfo& server = request.servers(i);
std::cout << "Server IP: " << server.ip() << ", Port: " << server.port() << std::endl;
}
// 清空 repeated 字段
request.clear_servers();
return 0;
}