【C++】protobuf序列化与反序列化
介绍protobuf是一种将结构数据进行序列化的方法。protobuf序列化是将数据序列化为二进制比其他序列化方式如JSON更加高效。protobuf序列化规则非常简单它序列化只存储字段编号、数据类型、字段值。(字段编号 数据类型) 字段值protobuf使用protobuf的使用流程分为3步1编写.proto文件.proto是协议文件用来定义要传输的信息字段。如下编写contacts.protosyntax proto3;// 版本使用proto3版本package contacts;// 包名C命名空间message People {// message消息的定义string name 1;// 字段11是字段编int32 age 2;// 字段22是字段编号}// 字段编号用于唯一识别字段传输数据时根据编号识别字段以上是基本的proto语法具体的其他语法后面会详细介绍。2用protoc编译生成C代码当proto文件编写好后下面需要用protobuf提供的编译器编译proto文件使其生成C代码。编译格式如下protoc [--proto_pathIMPORT_PATH] --cpp_outOUT_DIR path/to/file.protoprotoc// 是protobuf提供的命令行编译工具--proto_path// 指定被编译的.proto问件所在木录可多次指定。可简写成 -I。若不指定改参数则在当前目录进行搜索--cpp_out// 指编译后的文件为 C 文件OUT_DIR// 编译后生成文件的目标路径path/to/file.proto// 要编译的.proto文件编译 contacts.proto 文件命令如下protoc --cpp_out. contacts.proto3引入编译的C文件编译 contacts.proto 后会生成所选择语言的代码我们选择的是C所以编译后生成了两个文件contacts.pb.h 和 contacts.pb.cc。其中每个 message都会生成一个对应的消息类。在消息类中编译器为每个字段提供了获取和设置方法以及能够操作字段的方法。其中序列化和反序列化方法也包含在其中。序列化与反序列化相关的接口如下class MessageLite {public:// 序列化// 将序列化后数据写入文件流bool SerializeToOstream(ostream* output) const;// 把消息序列化成二进制写入指定的内存缓冲区数组bool SerializeToArray(void *data, int size) const;// 把消息序列化成二进制存入 C 字符串stringbool SerializeToString(string* output) const;// 反序列化与上面序列化接口对应bool ParseFromIstream(istream* input);// 从流中读取数据再反序列化bool ParseFromArray(const void* data, int size);bool ParseFromString(const string data);};编译C代码时需要链接protubuf的库且至少是C11语法。例如g main.cc contacts.pb.cc -o TestProtoBuf -stdc11 -lprotobufproto语法详解proto语法这里主要围绕proto3展开。1字段规则message消息的字段可以用 singular 和 repeated 两个字段规则修饰。singular表示消息中可以包含该字段零次或⼀次不超过⼀次。proto3语法中字段默认使用该规则。repeated表示消息中可以包含该字段任意多次可以将其看作一个数组。syntax proto3;package contacts;message People {string name 1;int32 age 2;repeated string phone_numbers 3;// phone_numbers可以看作一个数组}2消息类型的定义与使用在单个.proto文件中可以定义多个消息体且支持定义嵌套类型的消息任意多层。每个消息体中的字段编号可以重复。除此外还可以使用 import 导入其他的proto文件// 嵌套写法syntax proto3;package contacts;message Peoplefo {string name 1;int32 age 2;message Phone {string number 1;}}// 非嵌套写法syntax proto3;package contacts;message Phone {string number 1;}message Peoplefo {string name 1;int32 age 2;}嵌套信息在信息的作用域内访问时需要使用域的解引用操作符::非嵌套信息在全局中可直接访问。后面样例会说明。contacts.proto文件syntax proto3;package contacts;message Peoplefo {string name 1;int32 age 2;message Phone {string number 1;}repeated Phone phone 3;}phone.proto文件syntax proto3;package phone;message Phone {string number 1;}contacts.proto中的Peoplefo 使用 Phone 消息syntax proto3;package contacts;import phone.proto;message Peoplefo {string name 1;int32 age 2;// 引入的文件声明了package使用消息时需要用 “命名空间.消息类型” 格式repeated phone.Phone phone 3;}3enum类型enum表示枚举类型且枚举必须定义在 message内部或文件顶层枚举值与编号默认情况下是相同的格式如下enum 枚举名 {枚举项 编号;...}proto3语法有强制要求第一个枚举项的编号必须 0枚举编号必须是整数、非负、唯一同级作用域下的枚举不能包含相同枚举值名称。enum PhoneType {MP 0;TEL 1;}enum PhoneTypeCopy {MP 0;// 编译后报错MP 已经定义}4Any类型Any是万能容器可以包裹任意一个其他消息对象实现动态类型嵌套类似 C 里的 “基类指针或泛型容器”。Any类型是在安装protobuf时就已经定义好的在include目录下的goole里的protobuf中可以看到。syntax proto3;import google/protobuf/any.proto;// 引入 any.proto 文件// 使用Any存储任意消息message AnyMessage {google.protobuf.Any data 1;}5oneof类型oneof是protobuf中定义互斥字段的关键字核心作用是在一组字段里最多只能同时设置一个字段设置其中一个会自动清空同组内的其他所有字段。oneof定义的规则如下oneof后面跟组名用于代码访问大括号内定义多个同级别互斥字段字段编号不能重复和普通 message 规则一致同一时间只能赋值一个字段给 oneof 组内任意一个字段赋值会自动清空组内其他字段。oneof 内部不能定义 repeated 字段。同一个 oneof 组里的所有字段共享同一块内存空间。syntax proto3;message UserInfo {string name 1;// 定义 oneof 组contact 是组名自定义oneof contact {string phone 2;// 字段1string email 3;// 字段2int32 wechat 4;// 字段3}}oneof内的字段可看成信息作用域中的字段只不过该字段被oneof进行了修饰限制使用时跟上面name字段的基本运用方法还是一样的。6保留字段reservedreserved保留字段是为了防止误用老的字段、防止协议兼容出问题的一种方式。来看以下情况message User {int32 id 1;string name 2;string phone 3;// 后来不用了想删掉}这里若直接删掉 phone 3未来别人可能会复用 3 这个编号给新字段string email 3; 这里就会出现严重问题旧数据里的 3 是 phone新代码里的 3 是 email导致数据错乱。reserved把废弃的字段名 字段编号保护起来不让别人用message User {int32 id 1;string name 2;reserved 34;// 保留 3 这个编号不许用reserved phone;// 保留 phone 这个名字不许用}这样再写编号3、4或者名称phone编译器编译时会直接报错。7选项optionoption选项是 protobuf 的核心配置机制用来定义 protobuf 协议行为规则简单说它不定义数据结构本身只控制数据结构如何被处理和使用。该选项分为文件级、消息级、字段级等文件级是在整个 proto 文件中设置消息级是在message中进行设置字段级是在字段中进行设置。文件级syntax proto3;option cc_generic_services true;.....消息级message TestMsg {option no_standard_descriptor true;int32 id 1;}字段级string old_name 1 [deprecated true];对于C而言常用的有 optimize_for 和 allow_alias 两个选项。optimize_for该选项为文件选项可以设置 protoc 编译器的优化级别分别为 SPEED、CODE_SIZE、LITE_RUNTIME。受该选项影响设置不同的优化级别编译 .proto 文件后生成的代码内容不同。SPEEDprotoc编译器将生成的代码是高度优化的代码运行效率高序列化/反序列化速度最快但是由此生成的代码编译后会占用更多的空间代码体积大功能全面。SPEED是默认选项。CODE_SIZEproto编译器将生成最少的类与 SPEED 相反它的代码运行效率较低但会占用更少的空间精简代码。这种方式适合资源受限的设备。LITE_RUNTIME生成的代码执行效率高同时生成代码编译后的所占用的空间也是非常少。这是以牺牲 protobuf 提供的反射功能为代价的 其次编译连接时必须链接protobuf-lite不能链接完整 protobuf。allow_alias该选项为枚举选项用来定义别名它允许同一个枚举值对应多个不同的名字也就是取别名。别名值相同名字不同与 C 里别名引用是完全等价的。enum Test {A 1;B 1;// 报错值重复}enum Test {option allow_alias true;// 开启别名A 1;B 1;// 合法A 和 B 是别名}protobuf实战——实现一个通讯录protobuf通讯录