Protobuf:原理、用法与 C++ 实践

在当今的软件开发领域,高效的数据序列化和通信协议是构建高性能、可扩展系统的关键。Protobuf(Protocol Buffers)作为一种流行的开源序列化框架,正因其出色的性能、简洁的语法和跨语言支持而备受青睐。本文将深入探讨 Protobuf 的原理和用法,并通过 C++ 代码进行实际操作演示。

一、Protobuf 原理

Protobuf 是一种轻便高效的结构化数据存储格式,类似于 XML 或 JSON,但具有更小的序列化后数据大小和更高的解析速度。它通过定义一个 .proto 文件来描述数据结构,然后使用 Protobuf 编译器将其生成特定语言(如 C++、Java、Python 等)的代码。

在序列化过程中,Protobuf 根据定义的字段规则,将数据转换为紧凑的二进制格式。在反序列化时,能够准确无误地还原出原始数据结构。

它具有以下特点:

• 语⾔⽆关、平台⽆关:即 ProtoBuf ⽀持 Java、C++、Python 等多种语⾔,⽀持多个平台

• ⾼效:即⽐ XML 更⼩、更快、更为简单

• 扩展性、兼容性好:你可以更新数据结构,⽽不影响和破坏原有的旧程序

使用流程:

• 编写.proto⽂件,⽬的是为了定义结构对象(message)及属性内容

 • 使⽤protoc编译器编译.proto⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中

 • 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对.proto⽂件中定义的字段进⾏ 设置和获取,和对message对象进⾏序列化和反序列化


二、Protobuf 用法

1、定义 .proto 文件
首先,我们需要创建一个 .proto 文件来定义我们的数据结构。以下是一个简单的示例:

//声明proto语法版本
syntax = "proto3";
 //声明代码的命名空间
package contacts;

//定义结构化对象:类型名 字段名 = 字段编号
message PeopleInfo {
 string name = 1; 
 int32 age = 2; 
}

在上述示例中,我们定义了一个名为 Person 的消息类型,包含了 name(字符串)、age(整数)和 is_married(布尔值)三个字段。

在message中我们可以定义其属性字段,字段定义格式为:字段类型字段名=字段唯⼀编号; 

• 字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤_连接。 

• 字段类型分为:标量数据类型和特殊类型(包括枚举、其他消息类型等)。

 • 字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。

该表格展⽰了定义于消息体中的标量数据类型,以及编译.proto⽂件之后⾃动⽣成的类中与之对应的 字段类型。在这⾥展⽰了与C++语⾔对应的类型。

2、编译 .proto 文件
使用 Protobuf 编译器(如 protoc)将 .proto 文件编译为目标语言的代码。

protoc cpp_out=output_directory student.proto

其中,--cpp_out 指定输出目录为 output_directory

3、在 C++ 中使用 Protobuf

对于编译生成的 C++ 代码,包含了以下内容:

• 对于每个 message ,都会生成一个对应的消息类
• 在消息类中,编译器为每个字段提供了获取和设置方法,以及一些其他能够操作字段的方法
• 编辑器会针对于每个 .proto 文件生成 .h 和 .cc 文件,分别用来存放类的声明与类的实现

contacts.pb.h 部分代码展示

class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
 public:
 using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
 1
 2
 3 void CopyFrom(const PeopleInfo& from);
 using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
 void MergeFrom( const PeopleInfo& from) {
 PeopleInfo::MergeImpl(*this, from);
 }
 static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
 return "PeopleInfo";
 }
 // string name = 1;
 void clear_name();
 const std::string& name() const;
 template <typename ArgT0 = const std::string&, typename... ArgT>
 void set_name(ArgT0&& arg0, ArgT... args);
 std::string* mutable_name();
 PROTOBUF_NODISCARD std::string* release_name();
 void set_allocated_name(std::string* name);
 // int32 age = 2;
 void clear_age();
 int32_t age() const;
 void set_age(int32_t value);
};

上述的例子中:

• 每个字段都有设置和获取的方法,getter 的名称与小写字段完全相同,setter 方法以 set_ 开头。
• 每个字段都有一个 clear_ 方法,可以将字段重新设置回 empty 状态。

contacts.pb.cc 中的代码就是对类声明方法的一些实现,在这里就不展开了

那之前提到的序列化和反序列化方法在哪里呢?在消息类的父类 MessageLite 中,提供了读写消息实例的方法,包括序列化方法和反序列化方法。

class MessageLite {
public:
 //序列化: 
 bool SerializeToOstream(ostream* output) const; // 将序列化后数据写入文件流 
 bool SerializeToArray(void *data, int size) const;
 bool SerializeToString(string* output) const;

 //反序列化: 
 bool ParseFromIstream(istream* input); // 从流中读取数据,再进行反序列化动作 
 bool ParseFromArray(const void* data, int size);
 bool ParseFromString(const string& data);
};

注意:

• 序列化的结果为二进制字节序列,而非文本格式。
• 以上三种序列化的方法没有本质上的区别,只是序列化后输出的格式不同,可以供不同的应用场景使用
• 序列化的 API 函数均为 const 成员函数,因为序列化不会改变类对象的内容,而是将序列化的结果保存到函数入参指定的地址中
• 详细 message API 可以参⻅ 完整列表

序列化与反序列化的使用

• 创建一个测试文件 info.cc ,方法中我们实现:
◦ 对一个联系人的信息使用 PB 进行序列化,并将序列化结果打印出来
◦ 对序列化后的内容使用 PB 进行反序列,解析出联系人信息并打印出来

#include <iostream> 
#include "contacts.pb.h" // 引入编译生成的头文件  
using namespace std; 

int main() { 
 string people_str; 
 // 序列化 
 {
 //.proto 文件声明的 package,通过 protoc 编译后,会为编译生成的 C++ 代码声明同名的命名空间 
 // 其范围是在.proto 文件中定义的内容 
 contacts::PeopleInfo people; 
 people.set_age(20); 
 people.set_name("张三"); 
 // 调用序列化方法,将序列化后的二进制序列存⼊ string 中 
 if (!people.SerializeToString(&people_str)) { 
 cout << "序列化联系人失败." << endl; 
 }
 // 打印序列化结果 
 cout << "序列化后的 people_str: " << people_str.size() << endl; 
 }

// 反序列化  
 {
 contacts::PeopleInfo people; 
 // 调用反序列化方法,读取 string 中存放的二进制序列,并反序列化出对象 
 if (!people.ParseFromString(people_str)) { 
 cout << "反序列化出联系人失败." << endl; 
 } 
 // 打印结果 
 cout << "Parse age: " << people.age() << endl; 
 cout << "Parse name: " << people.name() << endl; 
 }
 return 0;
} 

• 代码书写完成后,编译 info.cc ,生成可执行程序

1 g++ info.cc contacts.pb.cc -o info -std=c++11 -lprotobuf

◦ -lprotobuf:链接 protobuf 库文件
◦ -std=c++11:支持 C++11

• 执行可执行程序,可以看见 people 经过序列化和反序列化后的结果

序列化后的 people_str: 10
Parse age: 20
Parse name: 张三

由于 ProtoBuf 是把联系人对象序列化成了二进制序列,这里用 string 来作为接收二进制序列的容器。相对于 xml 和 JSON 来说,因为 PB 被编码成二进制,破解成本增大,ProtoBuf 编码是相对安全的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值