ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

一文带你学会ProtoBuf

一文带你学会ProtoBuf ProtoBuf教程一、为什么会有ProtoBuf1.1 序列化的概念序列化和反序列化是什么?什么情况下需要序列化如何实现序列化二、 ProtoBuf概述2.1 ProtoBuf是什么2.2 ProtoBuf的工作流程是什么样的2.3 ProtoBuf基础语法基本使用repeated限定修饰词枚举proto文件的导入包package)2.4 序列化与反序列化**.ph.h头文件序列化反序列化2.5 示例程序.proto文件进行编译序列化与反序列化的使用一、为什么会有ProtoBuf1.1 序列化的概念序列化和反序列化是什么?序列化把对象转换为字节序列的过程称为对象的序列化。反序列化把字节序列恢复为对象的过程 称为对象的反序列化。什么情况下需要序列化存储数据当你想把内存中的对象状态保存到⼀个⽂件中或者存到数据库中时。网络传输网络直接传输数据但是⽆法直接传输对象所以要在传输前序列化将各种数据转换为二进制序列传输完成后反序列化将二进制序列转换成对象。如何实现序列化为了实现序列化功能就随之而来产生了如xml、json、 protobuf等解决方法。XML 可读性强但标签冗余、解析慢JSON 简洁通用、跨语言友好却无类型校验、处理大数据效率一般Protobuf 凭借二进制格式兼具超高压缩率、极快解析速度与强类型约束是高性能跨语言通信的最优选择仅需预定义协议文件调试成本略高于前两者。二、 ProtoBuf概述2.1 ProtoBuf是什么ProtoBuf也叫Protocol Buffer是google 的一种数据交换的格式它独立于语言独立于平台。google 提供了多种语言的实现java、c#、c、go 和 python 等每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式比使用 xml 、json进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式可以用于诸如网络传输、配置文件、数据存储等诸多领域。简单来讲 ProtoBuf全称为 Protocol Buffer是让结构数据序列化的⽅法其具有以下特点语⾔⽆关、平台⽆关即 ProtoBuf ⽀持 Java、C、Python 等多种语⾔⽀持多个平台。⾼效即⽐ XML 更⼩、更快、更为简单。扩展性、兼容性好你可以更新数据结构⽽不影响和破坏原有的旧程序。2.2 ProtoBuf的工作流程是什么样的在学习protobuf之前我们需要搞清楚使用protbuf进行数据的序列化主要有哪几个步骤确定数据格式数据可简单可复杂比如// 要序列化的数据 // 第一种: 单一数据类型 int number; // 第二种: 复合数据类型 struct Person { int id; string name; string sex; int age; };创建一个新的文件, 文件名随意指定, 文件后缀为 .proto。根据protobuf的语法, 编辑.proto文件,目的是为了定义结构对象message及属性内容。使⽤ protoc 编译器编译 .proto ⽂件生成⼀系列接口代码存放在新生成头文件和源⽂件中使用 。源文件: xxx.pb.cc – xxx对应的名字和 .proto文件名相同头文件: xxx.pb.h – xxx对应的名字和 .proto文件名相同需要将生成的c文件添加到项目中, 实现对 .proto ⽂件中定义的字段进⾏设置和获取和对 message 对象进⾏序列化和反序列化。protobuf的工作流程如下图所示2.3 ProtoBuf基础语法基本使用假设我定义了这样一个结构体现在要基于这个结构体完成数据的序列化结构体原型如下struct Person { int id; string name; string sex; // man woman int age; };接下来我们需要新建一个文件给它起个名字后缀指定为 .proto在文件的第一行需要指定Protobuf的版本号有两个版本Protobuf 2 和 Protobuf 3此处我们使用的是版本3。syntaxproto3;接着需要定义一个消息体其格式如下message 名字 { // 类中的成员, 格式 数据类型 成员名字 1; 数据类型 成员名字 2; 数据类型 成员名字 3; ...... ...... }message后面的名字就是生成的类的名字自己指定一个合适的名字即可;等号后面的编号要从1开始每个成员都有一个唯一的编号不能重复一般连续编号即可;基于上面的语法上面结构体对应的.proto文件的内容可以写成这样// Person.proto syntax proto3; // 在该文件中对要序列化的结构体进行描述 message Person { int32 id 1; bytes name 2; bytes sex 3; int32 age 4; }下表展示了定义于消息体中的标量数据类型以及编译 .proto ⽂件之后自动生成的类中与之对应的字段类型。在这⾥展示了与 C 语⾔对应的类型。.proto TypeNotesC Typedoubledoublefloatfloatint32使⽤变⻓编码[1]。负数的编码效率较低⸺若字段可能为负值应使⽤ sint32 代替。int32int64使⽤变⻓编码[1]。负数的编码效率较低⸺若字段可能为负值应使⽤ sint64 代替。int64uint32使⽤变⻓编码[1]。uint32uint64使⽤变⻓编码[1]。uint64sint32使⽤变⻓编码[1]。符号整型。负值的编码效率⾼于常规的 int32 类型。int32sint64使⽤变⻓编码[1]。符号整型。负值的编码效率⾼于常规的 int64 类型。int64fixed32定⻓ 4 字节。若值常⼤于2^28 则会⽐ uint32 更高效uint32fixed64定⻓ 8 字节。若值常⼤于2^56 则会⽐ uint64 更⾼效。uint64sfixed32定⻓ 4 字节。int32sfixed64定⻓ 8字节。int64boolboolstring包含 UTF-8 和 ASCII 编码的字符串⻓度不能超过2^32 。stringbytes可包含任意的字节序列但⻓度不能超过 2^32 。string[1] 变⻓编码是指经过protobuf 编码后原本4字节或8字节的数可能会被变为其他字节数。.proto文件编辑好之后就可以使用protoc工具将其转换为C文件了。$ protoc-Ipath .proto文件--cpp_out输出路径(存储生成的c文件)在 protoc 命令中-I 参数后面可以跟随一个或多个路径用于告诉编译器在哪些路径下查找导入的文件或依赖的文件使用绝对路径或相对路径都是没问题的。如果有多个路径可以使用多个 -I 参数或在一个参数中使用冒号:分隔不同的路径。如果只有一个路径-I 参数可以省略。例如protoc -I path1 -I path2 或 protoc -I path1:path2 都表示告诉编译器在 path1 和 path2 路径下查找导入的文件或依赖的文件。我们可以在.proto文件所在目录执行protoc命令并生成到当前目录$ protoc ./Person.proto--cpp_out.# 或者使用 -I 参数$ protoc-I./ Person.proto--cpp_out.repeated限定修饰词在使用Protocol BuffersProtobuf中可以使用repeated关键字作为限定修饰符来表示一个字段可以有多个值即重复出现的字段。repeated关键字可以用于以下数据类型基本数据类型、枚举类型和自定义消息类型比如要序列化的数据中有一个数组结构如下// 要序列化的数据 struct Person { int id; string name[10]; string sex; int age; };Protobuf 的消息体就可以写成下面的样子:message Person { int32 id 1; repeated bytes name 2; bytes sex 3; int32 age 4; }使用repeated关键字定义的字段在Protobuf序列化和反序列化时会被当作一个集合或数组来处理。这个name可以作为一个动态数组来使用。枚举在 Protocol Buffers 中枚举类型用于定义一组特定的取值。下面定义一个枚举并将其添加到Person中// 要序列化的数据// 枚举enumColor{Red5,// 可以不给初始值, 默认为0Green,Yellow,Blue};// 要序列化的数据structPerson{intid;string name[10];string sex;intage;// 枚举类型Color color;};以下是如何在 Protobuf 中定义和使用枚举类型其语法如下enum 名字 { 元素名 0; // 枚举中第一个原素的值必须为0 元素名 数值; }枚举元素之间使用分号间隔 ;并且需要注意一点proto3 中的第一个枚举值必须为 0第一个元素以外的元素值可以随意指定。上面例子中的数据在.proto文件中可以写成如下格式:// 定义枚举类型 enum Color { Red 0; Green 3; // 第一个元素以外的元素值可以随意指定 Yellow 6; Blue 9; } // 在该文件中对要序列化的结构体进行描述 message Person { int32 id 1; repeated bytes name 2; bytes sex 3; int32 age 4; // 枚举类型 Color color 5; }proto文件的导入在 Protocol Buffers 中可以使用import语句在当前.proto中导入其它的.proto文件。这样就可以在一个.proto文件中引用并使用其它文件中定义的消息类型和枚举类型。语法格式如下import 要使用的proto文件的名字;假设现在我有一个proto文件Address.proto里边记录了地址信息syntax proto3; // 地址信息 message Address { bytes addr 1; bytes number 2; }我现需要在上面定义的Person.proto中添加这个人的地址信息因此就需要用到Address.proto中定义的消息体syntax proto3; // 使用另外一个proto文件中的数类型, 需要导入这个文件 import Address.proto; // 在该文件中对要序列化的结构体进行描述 // 定义枚举类型 enum Color { Red 0; Green 3; // 第一个元素以外的元素值可以随意指定 Yellow 6; Blue 9; } // 在该文件中对要序列化的结构体进行描述 message Person { int32 id 1; repeated bytes name 2; bytes sex 3; int32 age 4; // 枚举类型 Color color 5; // 添加地址信息, 使用的是外部proto文件中定义的数据类型 Address addr 6; }import语句中指定的文件路径可以是相对路径或绝对路径。如果文件在相同的目录中只需指定文件名即可。导入的文件将会在编译时与当前文件一起被编译。导入的文件也可以继续导入其他文件形成一个文件依赖的层次结构。包package)在 Protobuf 中可以使用package关键字来定义一个消息所属的包package。包是用于组织和命名消息类型的一种机制类似于命名空间的概念在一个.proto文件中可以通过在顶层使用package关键字来定义包syntax proto3; package mypackage; message MyMessage { // ... }在这个示例中我们使用package关键字将MyMessage消息类型定义在名为mypackage的包中。包名作为一个标识符来命名可以使用任何有效的标识符按惯例使用小写字母和下划线。使用包可以避免不同.proto文件中的消息类型名称冲突同时也可以更好地组织和管理大型项目中的消息定义。可以将消息类型的名称定义在特定的包中并使用限定名来引用这些类型。下面有两个proto文件分别给他们添加一个packageproto文件- Address.protosyntax proto3; // 添加命名空间 myAddress package myAddress; // 地址信息, 这个Address类属于命名空间: myAddress message Address { bytes addr 1; bytes number 2; }proto文件 - Person.protosyntax proto3; // 使用另外一个proto文件中的数类型, 需要导入这个文件 import Address.proto; // 指定命名空间 ErBing package ErBing; // 以下的类 Person 和枚举 Color 都属于命名空间 ErBing // 在该文件中对要序列化的结构体进行描述 // 定义枚举类型 enum Color { Red 0; Green 3; // 第一个元素以外的元素值可以随意指定 Yellow 6; Blue 9; } // 在该文件中对要序列化的结构体进行描述 message Person { int32 id 1; repeated bytes name 2; bytes sex 3; int32 age 4; // 枚举类型 Color color 5; // 添加地址信息, 使用的是外部proto文件中定义的数据类型 // 如果这个外边类型属于某个命名空间, 语法格式: // 命名空间的名字.类名 变量名编号; myAddress.Address addr 6; }2.4 序列化与反序列化**.ph.h头文件通过protoc 命令对.proto文件的转换得到的头文件中有一个类这个类的名字和 .proto文件中message关键字后边指定的名字相同.proto文件中message消息体的成员就是生成的类的私有成员。那么如何访问生成的类的私有成员呢 可以调用生成的类提供的公共成员函数这些函数有如下规律清空(初始化) 私有成员的值: clear_变量名()获取类私有成员的值: 变量名()给私有成员进行值的设置: set_变量名(参数)得到类私有成员的地址, 通过这块地址读/写当前私有成员变量的值: mutable_变量名()如果这个变量是数组类型:数组中元素的个数: 变量名_size()添加一块内存, 存储新的元素数据: add_变量名() 、add_变量名(参数)序列化序列化是指将数据结构或对象转换为可以在储存或传输中使用的二进制格式的过程。在计算机科学中序列化通常用于将内存中的对象持久化存储到磁盘上或者在分布式系统中进行数据传输和通信。Protobuf 中为我们提供了相关的用于数据序列化的 API如下所示// 头文件目录: google\protobuf\message_lite.h// --- 将序列化的数据 数据保存到内存中// 将类对象中的数据序列化为字符串, c 风格的字符串, 参数是一个传出参数boolSerializeToString(std::string*output)const;// 将类对象中的数据序列化为字符串, c 风格的字符串, 参数 data 是一个传出参数boolSerializeToArray(void*data,intsize)const;// ------ 写磁盘文件, 只需要调用这个函数, 数据自动被写入到磁盘文件中// -- 需要提供流对象/文件描述符关联一个磁盘文件// 将数据序列化写入到磁盘文件中, c 风格// ostream 子类 ofstream - 写文件boolSerializeToOstream(std::ostream*output)const;// 将数据序列化写入到磁盘文件中, c 风格boolSerializeToFileDescriptor(intfile_descriptor)const;反序列化反序列化是指将序列化后的二进制数据重新转换为原始的数据结构或对象的过程。通过反序列化我们可以将之前序列化的数据重新还原为其原始的形式以便进行数据的读取、操作和处理。Protobuf 中为我们提供了相关的用于数据反序列化的 API如下所示// 头文件目录: google\protobuf\message_lite.hboolParseFromString(conststd::stringdata);boolParseFromArray(constvoid*data,intsize);// istream - 子类 ifstream - 读操作// wo ri// w-写 o: ofstream , r-读 i: ifstreamboolParseFromIstream(std::istream*input);// 从流中读取数据再进行反序列化动作boolParseFromFileDescriptor(intfile_descriptor);2.5 示例程序.proto文件Address.protosyntax proto3; package myAddress; message Address { int32 num 1; bytes addr 2; }Person.protosyntax proto3; import Address.proto; package Erbing; enum Color { Red 0; Green 5; Yellow 6; Blue 9; } message Person { int32 id 1; repeated bytes name 2; bytes sex 3; int32 age 4; myAddress.Address addr 5; Color color 6; }进行编译编辑器会针对于每个 .proto ⽂件⽣成 .h 和 .cc ⽂件分别⽤来存放类的声明与类的实现。Address.ph.h部分代码如图所示序列化与反序列化的使用创建测试代码MyTest.hclassMyTest{public:voidtest();};创建测试代码MyTest.cpp#includeMyTest.h#includePerson.pb.h//using namespace myAddress;//using namespace Erbing;voidMyTest::test(){// 序列化Erbing::Person p;p.set_id(10);p.set_age(32);p.set_sex(man);p.add_name();p.set_name(0,路飞);p.add_name(艾斯);p.add_name(萨博);p.mutable_addr()-set_addr(北京市长安区天安门);p.mutable_addr()-set_num(1001);p.set_color(Erbing::Color::Blue);// 序列化对象 p, 最终得到一个字符串std::string output;p.SerializeToString(output);// 反序列化数据Erbing::Person pp;pp.ParseFromString(output);std::coutpp.id(), pp.sex(), pp.age()std::endl;std::coutpp.addr().addr(), pp.addr().num()std::endl;intsizepp.name_size();for(inti0;isize;i){std::coutpp.name(i)std::endl;}std::coutpp.color()std::endl;}main.cpp#includeMyTest.hintmain(intargc,char*argv[]){MyTest t;t.test();return0;}代码书写完成后编译 main.cpp⽣成可执⾏程序 TestProtoBufg main.cpp MyTest.cpp Person.pb.cc Address.pb.cc-oTestProtoBuf-stdc11-lprotobuf执行生成的可执行程序即可观察到测试结果
返回列表