系列文章
ProtoBuf 语法(一)
ProtoBuf 语法(二)
.proto
文件中可以声明许多选项,使用 option
标注。选项能影响proto
编译器的某些处理方式。
选项的完整列表在google/protobuf/descriptor.proto
中定义。部分代码如下:
syntax = "proto2"; // descriptor.proto 使⽤ proto2 语法版本
message FileOptions { ... } // ⽂件选项 定义在 FileOptions 消息中
message MessageOptions { ... } // 消息类型选项 定义在 MessageOptions 消息中
message FieldOptions { ... } // 消息字段选项 定义在 FieldOptions 消息中
message OneofOptions { ... } // oneof字段选项 定义在 OneofOptions 消息中
message EnumOptions { ... } // 枚举类型选项 定义在 EnumOptions 消息中
message EnumValueOptions { .. } // 枚举值选项 定义在 EnumValueOptions 消息中
message ServiceOptions { ... } // 服务选项 定义在 ServiceOptions 消息中
message MethodOptions { ... } // 服务⽅法选项 定义在 MethodOptions 消息中
//...
由此可见,选项分为 文件级
、消息级
、字段级
等等,但并没有⼀种选项能作用于所有的类型。
optimize_for
:该选项为文件选项,可以设置protoc
编译器的优化级别,分别为 SPEED
、CODE_SIZE
、 LITE_RUNTIME
。受该选项影响,设置不同的优化级别,编译.proto
文件后生成的代码内容不同。
SPEED
:protoc
编译器将生成的代码是⾼度优化的,代码运行效率⾼,但是由此生成的代码编译后会占用更多的空间。 另外,SPEED
是默认选项。CODE_SIZE
:protoc
编译器将生成最少的类,会占用更少的空间,是依赖基于反射的代码来实现序列化、反序列化和各种其他操作。但和SPEED
恰恰相反,它的代码运行效率较低。这种方式适合用在包含大量的.proto
文件,而并不盲目追求速度的应用中。LITE_RUNTIME
:生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲Protocol Buffer
提供的反射功能为代价的,仅仅提供encoding+序列化
功能,所以我们在链接BP
库时仅需链接libprotobuf-lite
,而非libprotobuf
。这种模式通常用于资源有限的平台,例如移动手机平台中。
option optimize_for = LITE_RUNTIME;
allow_alias
:允许将相同的常量值分配给不同的枚举常量,用来定义别名。该选项为枚举选项。举个例子:enum PhoneType
{
option allow_alias = true;
MP = 0;
TEL = 1;
LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}
这里分别使用PB
与JSON
的序列化与反序列化,对值完全相同的⼀份结构化数据进行不同次数的性能测试。为了可读性,下面这⼀份文本使用JSON
格式展示了需要被进行测试的结构化数据内容:
{
"age" : 20,
"name" : "张珊",
"phone" :
[
{
"number" : "110112119",
"type" : 0
},
{
"number" : "110112119",
"type" : 0
},
{
"number" : "110112119",
"type" : 0
},
{
"number" : "110112119",
"type" : 0
},
{
"number" : "110112119",
"type" : 0
}
],
"qq" : "95991122",
"address" :
{
"home_address" : "四川省广安市",
"unit_address" : "四川省成都市"
},
"remark" :
{
"key1" : "value1",
"key2" : "value2",
"key3" : "value3",
"key4" : "value4",
"key5" : "value5"
}
}
开始进行测试代码编写,在新的目录下新建contacts.proto
文件,内容如下:
syntax = "proto3";
package compare_serialization;
import "google/protobuf/any.proto"; // 引⼊ any.proto ⽂件
// 地址
message Address
{
string home_address = 1; // 家庭地址
string unit_address = 2; // 单位地址
}
// 联系⼈
message PeopleInfo
{
string name = 1; // 姓名
int32 age = 2; // 年龄
message Phone
{
string number = 1; // 电话号码
enum PhoneType
{
MP = 0; // 移动电话
TEL = 1; // 固定电话
}
PhoneType type = 2; // 类型
}
repeated Phone phone = 3; // 电话
google.protobuf.Any data = 4;
oneof other_contact
{
// 其他联系⽅式:多选⼀
string qq = 5;
string weixin = 6;
}
map<string, string> remark = 7; // 备注
}
使用protoc
命令编译文件后,新建性能测试文件compare.cc
,分别对相同的结构化数据进行100 、1000 、10000 、100000次的序列化与反序列化,分别获取其耗时与序列化后的大小。
compare.cc
文件的内容如下:
#include
#include
#include
#include "contacts.pb.h"
using namespace std;
using namespace compare_serialization;
using namespace google::protobuf;
#define TEST_COUNT 100000
void createPeopleInfoFromPb(PeopleInfo *people_info_ptr);
void createPeopleInfoFromJson(Json::Value &root);
int main(int argc, char *argv[])
{
struct timeval t_start, t_end;
double time_used;
int count;
string pb_str, json_str;
// ------------------------------Protobuf 序列化------------------------------------
{
PeopleInfo pb_people;
createPeopleInfoFromPb(&pb_people);
count = TEST_COUNT;
gettimeofday(&t_start, NULL);
// 序列化count次
while ((count--) > 0)
{
pb_people.SerializeToString(&pb_str);
}
gettimeofday(&t_end, NULL);
time_used = 1000000 * (t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec -
t_start.tv_usec;
cout << TEST_COUNT << "次 [pb序列化]耗时:" << time_used / 1000 << "ms."
<< " 序列化后的⼤⼩:" << pb_str.length() << endl;
}
// ------------------------------Protobuf 反序列化------------------------------------
{
PeopleInfo pb_people;
count = TEST_COUNT;
gettimeofday(&t_start, NULL);
// 反序列化count次
while ((count--) > 0)
{
pb_people.ParseFromString(pb_str);
}
gettimeofday(&t_end, NULL);
time_used = 1000000 * (t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec -
t_start.tv_usec;
cout << TEST_COUNT << "次 [pb反序列化]耗时:" << time_used / 1000 << "ms."
<< endl;
}
// ------------------------------JSON 序列化------------------------------------
{
Json::Value json_people;
createPeopleInfoFromJson(json_people);
Json::StreamWriterBuilder builder;
count = TEST_COUNT;
gettimeofday(&t_start, NULL);
// 序列化count次
while ((count--) > 0)
{
json_str = Json::writeString(builder, json_people);
}
gettimeofday(&t_end, NULL);
// 打印序列化结果
// cout << "json: " << endl << json_str << endl;
time_used = 1000000 * (t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec -
t_start.tv_usec;
cout << TEST_COUNT << "次 [json序列化]耗时:" << time_used / 1000 << "ms."
<< " 序列化后的⼤⼩:" << json_str.length() << endl;
}
// ------------------------------JSON 反序列化------------------------------------
{
Json::CharReaderBuilder builder;
unique_ptr<Json::CharReader> reader(builder.newCharReader());
Json::Value json_people;
count = TEST_COUNT;
gettimeofday(&t_start, NULL);
// 反序列化count次
while ((count--) > 0)
{
reader->parse(json_str.c_str(), json_str.c_str() + json_str.length(),
&json_people, nullptr);
}
gettimeofday(&t_end, NULL);
time_used = 1000000 * (t_end.tv_sec - t_start.tv_sec) + t_end.tv_usec -
t_start.tv_usec;
cout << TEST_COUNT << "次 [json反序列化]耗时:" << time_used / 1000 << "ms."
<< endl;
}
return 0;
}
/**
* 构造pb对象
*/
void createPeopleInfoFromPb(PeopleInfo *people_info_ptr)
{
people_info_ptr->set_name("张珊");
people_info_ptr->set_age(20);
people_info_ptr->set_qq("95991122");
for (int i = 0; i < 5; i++)
{
PeopleInfo_Phone *phone = people_info_ptr->add_phone();
phone->set_number("110112119");
phone->set_type(PeopleInfo_Phone_PhoneType::PeopleInfo_Phone_PhoneType_MP);
}
Address address;
address.set_home_address("陕西省西安市⻓安区");
address.set_unit_address("陕西省西安市雁塔区");
google::protobuf::Any *data = people_info_ptr->mutable_data();
data->PackFrom(address);
people_info_ptr->mutable_remark()->insert({"key1", "value1"});
people_info_ptr->mutable_remark()->insert({"key2", "value2"});
people_info_ptr->mutable_remark()->insert({"key3", "value3"});
people_info_ptr->mutable_remark()->insert({"key4", "value4"});
people_info_ptr->mutable_remark()->insert({"key5", "value5"});
}
/**
* 构造json对象
*/
void createPeopleInfoFromJson(Json::Value &root)
{
root["name"] = "张珊";
root["age"] = 20;
root["qq"] = "95991122";
for (int i = 0; i < 5; i++)
{
Json::Value phone;
phone["number"] = "110112119";
phone["type"] = 0;
root["phone"].append(phone);
}
Json::Value address;
address["home_address"] = "陕西省西安市⻓安区";
address["unit_address"] = "陕西省西安市雁塔区";
root["address"] = address;
Json::Value remark;
remark["key1"] = "value1";
remark["key2"] = "value2";
remark["key3"] = "value3";
remark["key4"] = "value4";
remark["key5"] = "value5";
root["remark"] = remark;
}
测试结果如下:
[lhf@localhost test_pb_json]$ ./compare
100次 [pb序列化]耗时:0.319ms. 序列化后的⼤⼩:278
100次 [pb反序列化]耗时:0.445ms.
100次 [json序列化]耗时:2.031ms. 序列化后的⼤⼩:567
[lhf@localhost test_pb_json]$ ./compare
1000次 [pb序列化]耗时:3.245ms. 序列化后的⼤⼩:278
1000次 [pb反序列化]耗时:4.6ms.
1000次 [json序列化]耗时:19.747ms. 序列化后的⼤⼩:567
1000次 [json反序列化]耗时:11.42ms.
[lhf@localhost test_pb_json]$ ./compare
10000次 [pb序列化]耗时:29.65ms. 序列化后的⼤⼩:278
10000次 [pb反序列化]耗时:45.103ms.
10000次 [json序列化]耗时:235.906ms. 序列化后的⼤⼩:567
10000次 [json反序列化]耗时:108.5ms.
[lhf@localhost test_pb_json]$ ./compare
100000次 [pb序列化]耗时:282.857ms. 序列化后的⼤⼩:278
100000次 [pb反序列化]耗时:426.645ms.
100000次 [json序列化]耗时:1898.32ms. 序列化后的⼤⼩:567
100000次 [json反序列化]耗时:1087.88ms.
由实验结果可得:
JSON
高出 2 - 4 倍。ProtoBuf
的内存占用只有JSON
的 1/2 左右。注意:以上结论的数据只是根据该项实验得出。因为受不同的字段类型、字段个数等影响,测出的数据会有所差异。
序列化协议 | 通用性 | 格式 | 可读性 | 序列化大小 | 序列化性能 | 适用场景 |
---|---|---|---|---|---|---|
JSON | 通用(json、xml已成为多种行业标准的编写工具) | 文本格式 | 好 | 轻量(使用键值对方式,压缩了⼀定的数据空间) | 中 | web项目。因为浏览器对于JSON数据支持非常好,有很多内建的函数支持。 |
XML | 通用 | 文本格式 | 好 | 重量(数据冗余,因为需要成对的闭合标签) | 低 | XML作为⼀种扩展标记语言,衍生出了HTML、RDF/RDFS,它强调数据结构化的能力和可读性。 |
ProtoBuf | 独立(Protobuf只是Google公司内部的工具) | 二进制格式 | 差(只能反序列化后得到真正可读的数据) | 轻量(比JSON更轻量,传输起来带宽和速度会有优化) | 高 | 适合高性能,对响应速度有要求的数据传输场景。Protobuf比XML、JSON占用内存更小,速度更快。 |
通过以上表格,我们可以知道: