protobuf使用

虽然protobuf的版本管理比较拉跨,然而其使用指南可以说非常详尽与方便,具体可参考:

本文记录了其中proto3使用和C++构建和使用proto较重点的部分,只有少量proto2部分(少用;

proto 3基础语法

第一行必须非空、非注释行,表示版本信息,如:syntax = "proto3";缺省默认为proto2

第二行一般使用package xxx;指明包名,防止不同protobuf定义的冲突;

数据类型

标量类型(Scalar Value Types)

一般数据类型,和C++数据类型类似;

类型 说明 类型 说明
double 64浮点 float 32浮点
uint32 相当于uint32,可变长度编码 fixed32 相当于uint32,定长4字节,值大于\(2^{28}\)更高效
uint64 相当于uint64,可变长度编码 fixed32 相当于uint64,定长8字节,值大于\(2^{56}\)更高效
int32 相当于int32,可变长度编码 sint32 相当于int32,可变长度编码,对负数编码更高效
int64 相当于int64,可变长度编码 sint64 相当于int64,可变长度编码,对负数编码更高效
sfixed32 相当于int32,定长4字节 bool 布尔:false/true
sfixed64 相当于int32,定长8字节 string 仅UTF-8字符
bytes 任意二进制编码

stringbytes的区别是string会在序列化时检查UTF-8格式,UTF-8对二进制编码是有要求的,其变长编码开头几位是固定头,因此如果任意读取文本、音频等的二进制编码不应该使用string

枚举类型enum

自定义枚举类型第一个值必须为0值,其也充当了默认值;枚举类型最多支持32位,为了编码效率,不应该使用负数;

1
2
3
4
5
6
7
8
9
10
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}

枚举类型定义在message里面或者外面,都能够被重新复用,如果在某message内,使用message_.enum_访问对应的类型;

enum的字段并不要求一一对应,因为enum存在别名机制两个枚举字段可映射到同一个编号,两字段互为别名,因此尽管弃用0编号别名,也能保证新字段能够利用该编号:

1
2
3
4
5
6
enum Color{
PINK = 0[deprecated=true]; //若不弃用,PINK和YELLOW互为别名
YELLOW = 0;
WHITE = 1;
BLACK = 2;
}

自定义消息类型message

最常用类型,定义了要序列化结构体的信息,在proto3中,只有枚举类型支持设置默认值

1
2
3
4
5
6
7
8
9
10
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}

message PhoneNumber {
string number = 1;
PhoneType type = 2[default = HOME];
}

message可嵌套,其不同嵌套层级的命名也是相对独立的,如下的Inner;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message Outer {            // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2,OK
int32 ival = 1;
bool booly = 2;
}
}
}

删除字段

必须要保证message名称和编号是一一对应的,因为protobuf的编码不是根据名称,而是编号,编号范围为1536870911,其中1900019999被protobuf保留,若使用会告警;

此外,对于编号需要谨慎处理,当我们删除字段时不要直接注释或者删除该字段应该使用关键字以声明字段被弃用

法1:通过[deprecated = true],如string deprecated_field = 20 [deprecated = true];

法2(推荐):reserved关键字,如reserved 20;reserved "deprecated_field"; 若多个:reserved 200 to 299;

编号被reserved新增字段时不应该被重新使用避免编号冲突导致数据损坏、隐私泄露;

此外,一旦消息类型被使用对应的编号不应该发生更改更改相当于删除该编号重新创建名称和编号);

最常用的字段应该设置为0到15,因为它们在序列化中只占据1个字节,而更高编号会占据更多字节;

新增一个字段生成的代码仍然可以解析旧格式数据

Specifying Field Cardinality

proto3采用了若干种关键字指定字段基数(即结构体某个字段在实际信息中出现次数),常用为optional(缺省)/repeated/map等;

optional

proto3初始版本丢弃了proto2的optional,更名为Singular,但在3.19后重新引入,optional是缺省类型时的默认选择,表示该字段在message中只会出现0次或1次,它具有两种状态:

    1. 没有被设置,其不会出现在序列化中,反序列化时返回默认值
    1. 被设置,将被序列化到线路中

对于proto3语法,repeated的字段可缺省类型,默认就是singular,这里singular隐式地指向optional,另一种同样表示singular的关键字是implicit它并不推荐使用,因为它不含“显式基数标签”,因此在序列化时如果一个值被设置成默认值,它无法分辨究竟是没有设置该字段,还是设置成默认值,尽管设置了默认值使用has_xxx函数判断都会返回false,这也是proto2中optional的特性;

即如果需要区分一个值是否被设置,有两种方法

    1. 显式指定optional,此时会对应生成has_xxx函数进行判断;
    1. 缺省类型,此时仍然是optional,但是不会生成has_xxx接口,此时还需要区分只能将简单字段封装成message才会生成has_xxx接口

显然前者是更方便的。

特别的,optional如果被用于修饰message不会改变其存在状态message类型是恒定存在的(包括空message);

repeated

表示该字段在message中可出现0次或者多次,即类似数组一样;

map

1
map<key_type, value_type> map_field = N;

key_type不能是浮点数bytes类型,也不能直接用enum类型定义,但因为enumwire format中和int32/int64/uint32/uint64兼容,因此对其传值是OK的,底层用整数存储。而value_type可以是任意数据类型(不能是另一个map类型;

map本身已经有数组的意味了,不能再使用repeated进行修饰,实际上从Encoding中,序列化上map类型实际上是复写了一种嵌套

1
2
3
4
5
6
7
8
map<string, int32> g = 7;

//上述等效于:
message g_Entry {
optional string key = 1;
optional int32 value = 2;
}
repeated g_Entry g = 7;

map序列化时如果序列化成wire格式,则是无序的,若序列化为text格式键是数字的,则按数字排序;此外****反序列化****时,若是wire格式存在相同的键使用最后序列化的,如果是text格式序列化有可能失败

数据类型兼容性

兼容性是指一种数据类型转换到另一种数据类型,也不会破坏数据的表达意义,因为其截断策略是相同的,只是可能需要使用C++的方法将类型重新转换到原来类型,兼容关系如下:

    1. int32, uint32, int64, uint64, bool均是兼容的(不同长度会截断);
    1. sint32sint64兼容,与其他均不兼容;
    1. stringbytesUTF-8数据上是兼容的;
    1. fixed32sfixed32兼容,fixed64sfixed64兼容;
    1. wire format下,即一般二进制编码,enumint32/int64/uint32/uint64均是兼容的;但需要注意,当反序列化遇到未定义的enum值时,该值一般会保留在消息中,后续如何表示,不同的语言会有不同的策略,例如C++提供相关方法,来判断其是否为未定义的enum值,而Go提供一个带原始数值的特殊类型;对于int等整数类型没有这种现象数值总能被保留并且表示; **
    1. bytes和嵌套的message是兼容的:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      message test{
      ...
      }
      message test1{
      message test = 1;

      //编码上等效于:
      bytes test = 1;
      }
    1. repeatedsingular兼容性说明:对于stringbytesmessage类型,它们的单数重复数兼容的,即在旧版本上声明repeated,也不会影响数据本身解析,但是具体接收行为有所不同,对于前二者,旧客户端按照单数读取,会取重复序列化的最后一个对象;对于message类型,旧客户端会将所有repeated message全部打包成一个单数message

      对于此外的整数、enum、bool等类型,其repeatedsingular是不兼容的,无法正确解析数据:这个原因在encoding规则可以了解,数值类型均会被声明为packed(在proto2中需要[packed=true]显示指定,在proto3是数值类型自动的),所谓的packed,即多个repeated数值传输时会序列化一个包,如:repeated int32 f = 6 [packed=true]将序列化为3206038e029ea705二进制码6: {3 270 86942}打包的键值文本,因此无法正确兼容repeatedsingular

等等;

Unknown Fields

基于兼容性和非统一格式读写,会产生消息中未知字段,只需要知道这些字段仍然会保留在消息中,但一些行为可能导致其永久丢失:例如将proto转换成json迭代message以设置新的字段使用文本而不是wire进行读写等;避免未知消息丢失还应使用CopyFrom()MergeFrom()面对消息操作,而不是逐个字段解析。

高阶数据类型

Any

Any允许使用某个字段代替任何的数据类型,而不提前声明这个字段的具体类型,需要import "google/protobuf/any.proto",例如:

1
2
3
4
5
6
import "google/protobuf/any.proto";

message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2; ///声明为Any
}

但打包和解包时,需要使用相关方法对该数据指明具体类型并进行序列化和解析,如C++的PackFrom()和UnpackTo();

Oneof

一个message中具有多个singular成员,且最多只会设置一个成员的值时,可以考虑使用Oneof节省内存,因为在其内部内存是共享的,类似Union; 且注意:Oneof的成员不能是repeated、map字段,但不得不使用重复字段时使用包含重复字段的message代替**,如:

1
2
3
4
5
6
7
Oneof{ ///最多一个成员被设置值
int32 id1 = 3;
int32 id2 = 5;
message{
repeated string name = 1; ///禁止直接使用重复
}
}

import定义

proto允许使用另一个proto文件定义的自定义类型,使用import导入相关的包:

1
import "myproject/other_protos.proto";

在编译时,可以使用-I/--proto_path参数指定一个搜索目录,否则默认编译器所在目录搜索

序列化与反序列化实例

可能阅读规则已经看懵了,这里我引入了一个实例,能够辅助上面内容的理解,在能够看懂proto后,进行C++的序列化写入和读取,对于一般使用就够了。因为代码比较多且涉及多文件关系,提高观感,以下实例上传到代码仓库:ProtoBuf_Test

构造proto

为了了解导入语义实例分成三个proto文件,且基本涵盖了常用的嵌套关系,结构如下: 定义了AddressBookProto为地址本消息,其中包含一个Admin管理员和含若干家庭Familycommunity,clarify是为了介绍Any特别引入的,它在后面的实例中会分别解析成不同的数据类型;该社区以肤色Color进行map组织,即community = map<Color,Family>,Family的成员是多个人,每个人又是一个结构体消息,其中一个信息是电话号码,一个人可以有0个或者多个电话号码,电话号码信息成员是电话类型和电话号,如下:

addressBook.proto:

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
syntax = "proto3";
package info.addressbook;

import "google/protobuf/any.proto";
import "phone.proto";
import "person.proto";

enum Color{ //肤色枚举:community的int32使用
YELLOW = 0;
WHITE = 1;
BLACK = 2;
}

message Family{ //家庭成员
repeated info.person.PersonProto people = 1;
}

//地址本消息:一个管理员 + 人种分类家庭消息 + 说明
message AddressBookProto{
message Admin{
info.phone.PhoneNumberProto admin_phone= 1;
optional string name = 2; //显式optional:生成has_name接口判断是否被set
}

map <int32,Family>community = 1; //多个家庭,分为黄人、白人、黑人家庭
repeated google.protobuf.Any clarify = 2; //说明字段,我们的目标将是把Any解释成string\int\message等测试
Admin admin = 3; //实例化一个管理员
}
person.proto:
1
2
3
4
5
6
7
8
9
10
11
syntax = "proto3";
package info.person;
import "phone.proto";

message PersonProto{
string name = 1;
int32 id = 2;
string email = 3;

repeated info.phone.PhoneNumberProto phones = 4;
}
phone.proto:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
syntax = "proto3";

package info.phone;

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}

message PhoneNumberProto {
string number = 1;
PhoneType type = 2;
}

使用protoc生成三个C++对应的头文件源文件并且添加到项目:-I表示import proto搜索目录

1
protoc .\phone.proto .\person.proto .\addressBook.proto -I . --cpp_out .
动态库,每个头文件中记得声明使用动态库#define PROTOBUF_USE_DLLS;

写入信息

新增初始化信息规律:

  • mutable_和add_均能返回对象的指针,然后使用set_设置或者修改;

  • 其中add_是针对新增带repeated的复杂类型(message);

  • mutable_则是针对新增singular复杂类型;

  • 对于singular简单类型,如string、int32等,直接使用set_即可初始化设置;如果带repeated既可以使用add_也可以使用mutable_进行获取,

  • Any写入简单数据类型,使用StringValue等封装在packfrom;写入message,先new一个对象按照上面填充message的方式再packfrom;

  • map写入,采用operator[] = xxx的方法;

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
struct familyInfo {
string name;
int id;
string email;
};

void map_wirte(info::addressbook::AddressBookProto* addressBookProto) {
/*
* message AddressBookProto{
* message Admin{
* info.phone.PhoneNumberProto admin_phone= 1;
* string name = 2;
* }
* Admin admin = 3; //一个管理员
* }
*
* ---------------------------------
* message PhoneNumberProto {
* optional string number = 1;
* PhoneType type = 2;
* }
*/
info::addressbook::AddressBookProto::Admin* admin = addressBookProto->mutable_admin();
admin->set_name("AdMin_Lucy");
info::phone::PhoneNumberProto* admin_phone = admin->mutable_admin_phone();
admin_phone->set_type(info::phone::WORK);
admin_phone->set_number("13888888");

//每个家庭会添加三位成员:生成三个家庭\九个人基本信息:
auto getFamily = [](string name, int id, string email) {
vector<familyInfo> vp;
for(int i=0; i<3; i++)
vp.push_back({name + (char)(i+'0'), id + i ,email + (char)(i + '0')});
return vp;
};
vector<familyInfo>vpYellow = getFamily("name_A", 100, "email_A");
vector<familyInfo>vpWhite = getFamily("name_B", 200, "email_B");
vector<familyInfo>vpBlack = getFamily("name_C", 300, "email_C");

/*
* message AddressBookProto{
* map <int32,Family>community = 1; //enum不能直接用于key,使用int32代替
* }

* enum Color{
* YELLOW = 0;
* WHITE = 1;
* BLACK = 2;
* }

* message Family{ //家庭成员
* repeated info.person.PersonProto people = 1;
* }
* -----------------------------------------------------
* message PersonProto{
* string name = 1;
* int32 id = 2;
* string email = 3;
* repeated info.phone.PhoneNumberProto phones = 4; //PhoneNumberProto见前
* }
*/


//将家庭信息填入Proto结构,只有第一个人有两个电话,其余人无电话
auto getProtoFamily = [](vector<familyInfo>& vp, string tele1, string tele2) {
info::addressbook::Family family;
for (int i = 0; i < vp.size(); i++) {
info::person::PersonProto* people = family.add_people();
if (i == 0) { //每家只有第一个人有两个电话
info::phone::PhoneNumberProto* phones = people->add_phones();
phones->set_number(tele1);
phones->set_type(info::phone::HOME);
phones = people->add_phones();
phones->set_number(tele2);
phones->set_type(info::phone::MOBILE);
}
people->set_name(vp[i].name);
people->set_id(vp[i].id);
people->set_email(vp[i].email);
}
return family;
};
//三个家庭填入Proto
info::addressbook::Family yFamily = getProtoFamily(vpYellow, "123", "234");
info::addressbook::Family wFamily = getProtoFamily(vpWhite, "345", "456");
info::addressbook::Family bFamily = getProtoFamily(vpBlack, "567", "678");
//与肤色enum对应,生成map community
(*addressBookProto->mutable_community())[info::addressbook::Color::YELLOW] = yFamily;
(*addressBookProto->mutable_community())[info::addressbook::Color::WHITE] = wFamily;
(*addressBookProto->mutable_community())[info::addressbook::Color::BLACK] = bFamily;


/*
* message AddressBookProto{
* repeated google.protobuf.Any clarify = 2; //说明字段,我们的目标将是把Any解释成string\int等测试
* }
*/

//Any为string
google::protobuf::Any* clarify = addressBookProto->add_clarify();
google::protobuf::StringValue str;
str.set_value("Happy Life");
clarify->PackFrom(str);

//Any为整数、浮点数、bool等,但不能直接用enum
clarify = addressBookProto->add_clarify();
google::protobuf::Int32Value num;
num.set_value(100);
clarify->PackFrom(num);

//Any 还可以为message
info::addressbook::AddressBookProto::Admin* admin_extra = new info::addressbook::AddressBookProto::Admin();
admin_extra->set_name("AdMin_Extra");
info::phone::PhoneNumberProto* admin_exphone = admin_extra->mutable_admin_phone();
admin_exphone->set_type(info::phone::WORK);
admin_exphone->set_number("888888813");
clarify = addressBookProto->add_clarify();
clarify->PackFrom(*admin_extra);
}

读取信息

读取信息规律:从最大封装开始取其成员,取值方法都是.xxx(); - map读取:将其装到STL的map中方便读取。

  • Any读取:通过Is<google::protobuf::StringValue>()等判断其初始类型,将Any UnpackTo该原始类型,再使用.value()函数获取其对应的C++简单类型,如果原始类型是message,则按照该message策略进行提取。

  • 无论singular、repeated,复杂类型也有has_xxx,简单类型只有显式声明optional才会产生;对于map、repeated any等,采用数组遍历或者size函数可以替代;

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
56
57
//AddressBookProto解析
void map_read(const info::addressbook::AddressBookProto& addressBookProto) {
google::protobuf::Map<google::protobuf::int32, info::addressbook::Family> fmap = addressBookProto.community();
std::map<int, info::addressbook::Family> smap(fmap.begin(), fmap.end());
cout << "community Parse:" << endl;
for (auto& pos : smap) {
cout << pos.first << " Family:" << endl; //以int32值显示而不是enum枚举,若需要类型要额外映射;
info::addressbook::Family sfamily = pos.second;
for (auto& ppos : sfamily.people()) {
cout << ppos.name() << ppos.id() << ppos.email() << endl;
if (ppos.phones().size() == 0)
continue;
for (auto& pppos : ppos.phones()) {
cout << pppos.type() << "\t" << pppos.number() << endl; //以int32值显示而不是enum枚举,若需要类型要额外映射;
}
}
cout << endl;
}

cout << "clarify Parse:" << endl;
google::protobuf::RepeatedPtrField<google::protobuf::Any> vp_clarify = addressBookProto.clarify();
for (google::protobuf::Any& clarify : vp_clarify) { //或者vp_clarify.Mutable(i)
if (clarify.Is<google::protobuf::StringValue>()) {
google::protobuf::StringValue clarify_str;
clarify.UnpackTo(&clarify_str);
string result = clarify_str.value();
cout << "string clarify:" << result << endl;
}
else if (clarify.Is<google::protobuf::Int32Value>()) {
google::protobuf::Int32Value clarify_int;
clarify.UnpackTo(&clarify_int);
int result = clarify_int.value();
cout << "int clarify:" << result << endl;
}
else if (clarify.Is<info::addressbook::AddressBookProto::Admin>()) {
info::addressbook::AddressBookProto::Admin clarify_admin;
clarify.UnpackTo(&clarify_admin);
cout << "messgae clarify:" << endl;
if(clarify_admin.has_name()) //简单类型,显式optional才会有has_xxx接口
cout << clarify_admin.name() << endl;
if (clarify_admin.has_admin_phone()) //无论singular、repeated,复杂类型也有has_xxx
cout << clarify_admin.admin_phone().type() << "\t" << clarify_admin.admin_phone().number() << endl;
}
cout << endl;
}

cout << "admin Parse:" << endl;

if (addressBookProto.has_admin()) {
info::addressbook::AddressBookProto::Admin admin = addressBookProto.admin();
info::phone::PhoneNumberProto admin_phone = admin.admin_phone();
if (admin.has_name())
cout << admin.name() << endl;
if (admin.has_admin_phone())
cout << admin_phone.type() << "\t" << admin_phone.number() << endl;
}
}

修改数据

基本上是使用mutable_获取要修改的对象指针,如果是修改message类型的Any,Unpack以后重新打包;

这里一个坑是修改应该使用原地操作,例如map必须使用auto&来获取对应的键值,而不是auto;同理,假设写成info::addressbook::Family family = pos.second,再进行family修改也是错误的,因为这也不是原地操作。

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
void map_modify(info::addressbook::AddressBookProto& addressBookProto) {
for (auto& [key,family]:*addressBookProto.mutable_community()) {
info::person::PersonProto* people0 = family.mutable_people(1);
people0->set_name("Modify_name");
people0->set_id(0);
people0->set_email("Modify_email");
for (auto& ppos : *people0->mutable_phones()) {
ppos.set_type(info::phone::MOBILE);
ppos.set_number("00000");
}
}

//Any为string
for (auto& pos : *addressBookProto.mutable_clarify()) {
if (pos.Is<google::protobuf::StringValue>()) {
google::protobuf::StringValue str;
str.set_value("Modify Any String");
pos.PackFrom(str);
}
else if (pos.Is<google::protobuf::Int32Value>()) {
google::protobuf::Int32Value num;
num.set_value(0);
pos.PackFrom(num);
}
else if (pos.Is<info::addressbook::AddressBookProto::Admin>()) {
info::addressbook::AddressBookProto::Admin clarify_admin;
pos.UnpackTo(&clarify_admin);
clarify_admin.set_name("Modify extra Admin");
info::phone::PhoneNumberProto* admin_phone = clarify_admin.mutable_admin_phone();
admin_phone->set_type(info::phone::MOBILE);
admin_phone->set_number("00000");
pos.PackFrom(clarify_admin);

}
}
}

wire format:数据序列化

提供了序列化到文件、字符串两种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//序列化方法
int Serialize(info::addressbook::AddressBookProto& addressBookProto) {
//填充完毕,法一:序列化到字符串再以二进制写入文件
string str_output;
addressBookProto.SerializeToString(&str_output);
ofstream output_StrFile("output_StrFile.pb", ios::binary);
if (!output_StrFile.is_open())
return -1;
output_StrFile.write(str_output.c_str(), str_output.size());
output_StrFile.close();

//法二:直接序列化到文件
ofstream output_file("Serialize_output.pb", ios::binary);
if (!output_file.is_open())
return -1;
addressBookProto.SerializeToOstream(&output_file);
output_file.close();
return 0;
}

wire format:数据反序列化

提供了从文件数据反序列化、经过字符串反序列化两种写法,其中字符串处理既可以采用分块读取字符串,也可以采用迭代器。

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
//反序列化文件/字符串方法:
int AntiSerialize(string serializePath) {
ifstream input_file(serializePath, ios::binary);
if (!input_file.is_open())
return -1;

#if way1
//法一:ParseFromIstream文件输入流直接反序列化成目标message
info::addressbook::AddressBookProto addressBookProto;
addressBookProto.ParseFromIstream(&input_file);
map_read(addressBookProto);
#endif


///法二:分块读取字符串,并对字符串反序列化
#if way2
#define BUFSIZE 32 //模拟分块读取,真正大文件应当增大buf
char buf[BUFSIZE];
string strBuf;
while (input_file.read(buf, BUFSIZE)) {
strBuf.append(buf, input_file.gcount()); //gcount()返回实际read的字节数
}
strBuf.append(buf, input_file.gcount()); //最后还要read掉末尾剩下、不足BUFSIZE的
#endif

#if way2_2
//法二之二:迭代器从文件内容构造string:适用于中小文件,GB级别的应该使用分块读取
string strBuf((istreambuf_iterator<char>(input_file)), istreambuf_iterator<char>());

info::addressbook::AddressBookProto addressBookProto2;
addressBookProto2.ParseFromString(strBuf);
printAddressProto(addressBookProto2);
#endif

input_file.close();
return 0;
}

CRC校验

为了验证多种方法结果是否一致,以免反序列化出现空格、制表符等空白符无法识别,这里提供一种思路,采用CRC校验文件数据,确保若干种方法的结果是一致无误的,上述几种方法均经过该验证,需要boost库支持,比较简单。

MSVC + boost库

boost库是C++社区享有盛誉的一个开源库,支持丰富的文本、字符串、容器、迭代器、图像、模板编程、并发编程等处理,结构语法精美,MSVC环境安装方法比较简单:从官方boost库地址获取,解压后执行bootstrap.bat处理(确保本地VS环境正常),生成b2.exe可执行文件,使用命令b2.exe toolset=msvc stage将使用msvc编译boost库到目录下的stage文件夹,MSVC只需要包含当前的下载目录即可使用boost库。

文件内容校验

计算每个文件的CRC冗余码,然后对比即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//计算文件crc
uint32_t calcuCRC32(const string& filePath) {
ifstream inputFile(filePath, ios::binary);
boost::crc_32_type crc;
char buf[4096];
while (inputFile.read(buf, 4096)) {
crc.process_bytes(buf, inputFile.gcount());
}
crc.process_bytes(buf, inputFile.gcount());
return crc.checksum();
}

//比较文件crc
bool crc_compare(const string& filePath1, const string& filePath2) {
return calcuCRC32(filePath1) == calcuCRC32(filePath2);
}