C++高性能序列化:ProtoBuf
本文介绍图像标注劳务统计系统项目用到的关键技能Protobuf
简历描述
· 使用Protobuf作为数据序列化协议,统一了客户端与服务端的数据格式,提升系统通信效率和可维护性。
项目使用点
当客户端向服务器发送请求时,将请求数据序列化为 Protobuf 格式,由于其数据体积小,能够更快地在网络中传输;服务器接收到数据后,通过反序列化快速解析数据,处理请求并返回响应。
Protobuf与其他序列化协议的对比(必问)
- Protobuf :
更适合在对性能、空间占用要求较高,数据结构相对稳定的场景中使用,如分布式系统内部通信、数据存储等。
- JSON:
良好的可读性和通用性, JSON 是文本格式
在序列化和反序列化时需要进行大量的字符串解析和处理操作,导致其解析速度较慢。
在空间占用上,JSON 的数据体积通常较大,因为它包含了大量的文本字符,
JSON是一种弱类型的数据格式,它对数据类型的检查相对宽松 。
- XML:
自描述性和结构化
数据冗余度较高,数据体积通常比 Protobuf 大很多
传输和存储相同数据时,XML 需要占用更多的带宽和存储空间
在解析速度方面,XML 的解析过程相对复杂
在语法复杂度上,XML 的语法规则较为严格
XML 更适用于对数据自描述性要求高、需要遵循特定标准规范(如 SOAP 协议)的场景 ,例如在企业级应用中的数据交换和文档存储。
特点
JSON 可能会因为文本格式和键值对的描述方式而占用较大的空间
XML 则会更加冗长,标签的重复使用会增加数据量
使用ProtoBuf表示的数据能更加高效,并且将数据压缩得更小,大约是json格式的1/10,xml格式的1/20。速度可比 JSON 快 5 到 10 倍 ,这使得它在对性能要求极高的场景中表现出色,如实时数据传输、大数据处理等领域。大大减少网络带宽的占用,提高通信效率。
ProtoBuf还跨平台且支持多种主流编程语言
ProtoBuf还有扩展性和兼容性
- protobuf的数据类型主要有三种:基本数据类型,复合类,Map类型(proto3版本后支持)。
Protobuf 的使用步骤
1定义数据结构(.proto 文件)
使用 Protobuf 的第一步是定义数据结构,这通过创建一个.proto文件来完成。.proto文件使用一种特殊的语法来描述数据的结构,这种语法简洁明了,易于理解和编写。
下面是一个简单的例子,假设我们要定义一个表示用户信息的消息结构:
syntax = "proto3"; // 指定使用proto3语法
package user_info; // 定义包名,用于避免命名冲突
message User {
string name = 1; // 字段类型 字段名 = 字段编号
int32 age = 2;
string email = 3;
}
在这个例子中:
syntax = “proto3”;:声明了使用的 Protobuf
语法版本,目前常用的是proto3,它相较于proto2有一些改进,如语法更简洁、默认字段规则的改变等 。 package
user_info;:定义了包名user_info。包名在大型项目中非常重要,它可以防止不同模块间消息类型的命名冲突,类似于编程语言中的命名空间。
message User:定义了一个名为User的消息类型,这是我们自定义的数据结构,类似于类的概念。 string name =
1;、int32 age = 2;、string email =
3;:这些是User消息中的字段定义。每个字段都有自己的数据类型(如string、int32)、字段名(如name、age、email)和唯一的字段编号(如1、2、3)。字段编号在消息的二进制编码中用于标识字段,一旦确定就不应轻易更改,因为这会影响到数据的兼容性
2生成代码
定义好.proto文件后,就需要使用 Protobuf 编译器(protoc)来生成对应编程语言的代码。protoc编译器可以将.proto文件转换为多种语言的源代码,包括 C++、Java、Python 等 。
以 Python 为例,假设我们的.proto文件名为user.proto,并且已经安装好了protoc编译器和 Python 的 Protobuf 库。在命令行中执行以下命令来生成 Python 代码:
protoc --python_out=. user.proto
其中,–python_out=.表示将生成的 Python 代码输出到当前目录,user.proto是我们定义的数据结构文件。执行这个命令后,会在当前目录下生成一个user_pb2.py文件,这个文件包含了根据.proto文件生成的 Python 类和函数,用于对User消息进行序列化和反序列化操作。
生成的代码中包含了与.proto文件中定义的消息类型对应的类,这些类具有一系列方法,如用于设置字段值的set_xxx方法、获取字段值的get_xxx方法,以及用于序列化和反序列化的SerializeToString和ParseFromString方法等 。
使用生成的代码进行序列化和反序列化
生成代码后,就可以在项目中使用这些代码对数据进行序列化和反序列化操作了。下面以 Python 为例,展示如何使用生成的代码:
import user_pb2
# 创建一个User对象并赋值
user = user_pb2.User()
user.name = "李四"
user.age = 25
user.email = "lisi@example.com"
# 序列化
serialized_data = user.SerializeToString()
print("Serialized data:", serialized_data)
# 反序列化
new_user = user_pb2.User()
new_user.ParseFromString(serialized_data)
print("Deserialized user:")
print("Name:", new_user.name)
print("Age:", new_user.age)
print("Email:", new_user.email)
在这段代码中:
首先导入生成的user_pb2模块,这个模块包含了User类的定义。 然后创建一个User对象,并设置其各个字段的值。
使用user.SerializeToString()方法将User对象序列化为字节串,得到的serialized_data就是序列化后的数据。
接着创建一个新的User对象new_user,使用new_user.ParseFromString(serialized_data)方法将字节串反序列化,恢复出原始的User对象的状态,并输出其各个字段的值。
Protobuf 的应用场景
- 分布式系统通信
Google 的分布式文件系统(GFS),GFS 是一个大型的分布式文件系统
Apache Dubbo,是一个高性能的 Java RPC 框架 - 数据存储和持久化
LevelDB ,LevelDB 是一个高效的键值对存储引擎 - 网络通信
可以作为 RESTful API 的替代方案,在一些对性能要求极高的场景中发挥作用。
项目用到的
typedef struct Food
{
char table[5]; //完成人姓名
char number[10];// 标注任务序号
char food[100]; //标注任务内容标识(文件夹名称 序列号。。。。)
char price[10];// 标注图片单价
char quatity[10];// 标注图片数量
char post[200];// 个人备注
}FoodInfo;
//表示菜单中的一个菜品项
typedef struct Menu
{
char number[10];// 标注任务序号;number < 20000 是类型A;number >= 20000 是类型B,以此类推
char food[100]; //标注任务内容标识(文件夹名称 序列号。。。。)
char quatity[10];// 标注图片数量 (新加的)
char price[10];// 标注图片单价
char state[20];// 状态 (用来筛选有没有人领走)
}MenuInfo;
message FoodInfo {
string table = 1;
string number = 2;
string food = 3;
string price = 4;
string quatity = 5;
string post = 6;
}
message FoodList {
repeated FoodInfo foods = 1; // repeated表示可以有多个FoodInfo
}
std::vector<FoodInfo> food_vector; // 业务数据
FoodList food_list_pb; // protobuf对象
//序列化流程:
for (const auto& food : food_vector) {
FoodInfo* food_pb = food_list_pb.add_foods();
food_pb->set_table(food.table);
// ... 其他字段
}
std::string out;
food_list_pb.SerializeToString(&out); // 这一步是自动生成的
//反序列化流程:
food_list_pb.ParseFromString(in); // 这一步是自动生成的
for (const auto& food_pb : food_list_pb.foods()) {
FoodInfo food;
strncpy(food.table, food_pb.table().c_str(), sizeof(food.table));
// ... 其他字段
food_vector.push_back(food);
}
C++使用protobuf实现序列化的示例(了解)
在protobuf源码中的 /examples 目录下有官方提供的protobuf使用示例:addressbook.proto;参考官方示例实现C++使用protobuf进行序列化和反序列化addressbook.proto :
syntax = "proto3";
package tutorial;
option optimize_for = LITE_RUNTIME;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
生成的addressbook.pb.h 文件内容摘要:
namespace tutorial {
class Person;
class Person_PhoneNumber;
};
class Person_PhoneNumber : public MessageLite {
public:
Person_PhoneNumber();
virtual ~Person_PhoneNumber();
public:
//string number = 1;
void clear_number();
const string& number() const;
void set_number(const string& value);
//int32 id = 2;
void clear_id();
int32 id() const;
void set_id(int32 value);
//string email = 3;
//...
};
add_person.cpp :
#include <iostream>
#include <fstream>
#include <string>
#include "pbs/addressbook.pb.h"
using namespace std;
void serialize_process() {
cout << "serialize_process" << endl;
tutorial::Person person;
person.set_name("Obama");
person.set_id(1234);
person.set_email("1234@qq.com");
tutorial::Person::PhoneNumber *phone1 = person.add_phones();
phone1->set_number("110");
phone1->set_type(tutorial::Person::MOBILE);
tutorial::Person::PhoneNumber *phone2 = person.add_phones();
phone2->set_number("119");
phone2->set_type(tutorial::Person::HOME);
fstream output("person_file", ios::out | ios::trunc | ios::binary);
if( !person.SerializeToOstream(&output) ) {
cout << "Fail to SerializeToOstream." << endl;
}
cout << "person.ByteSizeLong() : " << person.ByteSizLong() << endl;
}
void parse_process() {
cout << "parse_process" << endl;
tutorial::Person result;
fstream input("person_file", ios::in | ios::binary);
if(!result.ParseFromIstream(&input)) {
cout << "Fail to ParseFromIstream." << endl;
}
cout << result.name() << endl;
cout << result.id() << endl;
cout << result.email() << endl;
for(int i = 0; i < result.phones_size(); ++i) {
const tutorial::Person::PhoneNumber &person_phone = result.phones(i);
switch(person_phone.type()) {
case tutorial::Person::MOBILE :
cout << "MOBILE phone : ";
break;
case tutorial::Person::HOME :
cout << "HOME phone : ";
break;
case tutorial::Person::WORK :
cout << "WORK phone : ";
break;
default:
cout << "phone type err." << endl;
}
cout << person_phone.number() << endl;
}
}
int main(int argc, char *argv[]) {
serialize_process();
parse_process();
google::protobuf::ShutdownProtobufLibrary(); //删除所有已分配的内存(Protobuf使用的堆内存)
return 0;
}
输出结果:
[serialize_process]
person.ByteSizeLong() : 39
[parse_process]
Obama
1234
1234@qq.com
MOBILE phone : 110
HOME phone : 119
protobuf提供的序列化和反序列化的API接口函数:
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成员函数,因为序列化不会改变类对象的内容, 而是将序列化的结果保存到函数入参指定的地址中。
.proto文件中的 option 选项:
.proto文件中的option选项用于配置protobuf编译后生成目标语言文件中的代码量,可设置为 SPEED, CODE_SIZE, LITE_RUNTIME 三种。
默认option选项为 SPEED,常用的选项为 LITE_RUNTIME。
三者的区别在于:
① SPEED(默认值):
表示生成的代码运行效率高,但是由此生成的代码编译后会占用更多的空间。
② CODE_SIZE:
与SPEED恰恰相反,代码运行效率较低,但是由此生成的代码编译后会占用更少的空间,
通常用于资源有限的平台,如Mobile。
③ LITE_RUNTIME:
生成的代码执行效率高,同时生成代码编译后的所占用的空间也非常少。
这是以牺牲Protobuf提供的反射功能为代价的。
因此我们在C++中链接Protobuf库时仅需链接libprotobuf-lite,而非protobuf。
SPEED 和 LITE_RUNTIME相比,在于调试级别上,例如 msg.SerializeToString(&str); 在 SPEED 模式下会利用反射机制打印出详细字段和字段值,但是 LITE_RUNTIME 则仅仅打印字段值组成的字符串。
因此:可以在调试阶段使用 SPEED 模式,而上线以后提升性能使用 LITE_RUNTIME 模式优化。
最直观的区别是使用三种不同的 option 选项时,编译后产生的 .pb.h 中自定义的类所继承的 protobuf类不同:
//1. SPEED模式:(自定义的类继承自 Message 类)
// .proto 文件:
option optimize_for = SPEED;
// .pb.h 文件:
class Person : public ::PROTOBUF_NAMESPACE_ID::Message {};
//2. CODE_SIZE模式:(自定义的类继承自 Message 类)
// .proto 文件:
option optimize_for = CODE_SIZE;
// .pb.h 文件:
class Person : public ::PROTOBUF_NAMESPACE_ID::Message {};
//3. LITE_RUNTIME模式:(自定义的类继承自 MessageLite 类)
// .proto 文件:
option optimize_for = LITE_RUNTIME;
// .pb.h 文件:
class Person : public ::PROTOBUF_NAMESPACE_ID::MessageLite {};
更多推荐



所有评论(0)