Protobuf的介绍及使用

Protobuf的介绍及使用 ProtoBuf是什么ProtoBuf全称 Protocol Buffer是数据结构序列化和反序列化框架它具有以下特点• 语言无关、平台无关即 ProtoBuf 支持 Java、C、Python 等多种语言支持多个平台。• 高效即比 XML 更小、更快、更为简单。• 扩展性、兼容性好你可以更新数据结构而不影响和破坏原有的旧程序。Protobuf 使用流程介绍1.编写 .proto 文件目的是为了定义结构对象message及属性内容。2.使用 protoc 编译器编译 .proto 文件生成一系列接口代码存放在新生成头文件和源文件中3.依赖生成的接口将编译生成的头文件包含进我们的代码中实现对 .proto 文件中定义的字段进行设置和获取和对 message 对象进行序列化和反序列化。我们以一个简单通讯录的实现来驱动对 Protobuf 的学习。在通讯录 demo 中我们将实现• 对一个联系人的信息使用 Protobuf 进行序列化并将结果打印出来• 对序列化后的内容使用 Protobuf 进行反序列解析出联系人信息并打印出来• 联系人包含以下信息: 姓名、年龄通过通讯录 demo我们能快速的了解 ProtoBuf 的使用流程。创建.proto文件.proto文件规范文件名应该使用全小写字母命名多个字母之间用_连接。例如lower_snake_case.proto书写.proto代码时应使用2个空格的缩进。我们为通讯录demo新建文件contacts.proto向文件添加注释可使用// 或者 /* … */指定proto3语法Protocol Buffers 语言版本 3简称 proto3是 .proto 文件最新的语法版本。proto3 简化了 Protocol Buffers 语言既易于使用又可以在更广泛的编程语言中使用。它允许你使用 JavaCPython 等多种语言生成 protocol buffer 代码。在 .proto 文件中要使用 syntax “proto3”; 来指定文件语法为 proto3并且必须写在除去注释内容的第一行。 如果没有指定编译器会使用 proto2 语法。**package 声明符 **package 是一个可选的声明符能表示 .proto 文件的命名空间在项目中要有唯一性。它的作用是为了避免我们定义的消息出现冲突。在通讯录 demo 的 contacts.proto 文件中可以声明其命名空间内容如下syntaxproto3; package contacts;定义消息消息message: 要定义的结构化对象我们可以给这个结构化对象中定义其对应的属性内容。在网络传输中我们需要为传输双方定制协议。定制协议说白了就是定义结构体或者结构化数据比如tcpudp 报文就是结构化的。再比如将数据持久化存储到数据库时会将一系列元数据统一用对象组织起来再进行存储。ProtoBuf 就是以 message 的方式来支持我们定制协议字段后期帮助我们形成类和方法来使用。在通讯录 demo 中我们就需要为 联系人 定义一个 message:.proto 文件中定义一个消息类型的格式为message 消息类型名 { } 消息类型命名规范使用驼峰命名法首字母大写。为 contacts.proto通讯录 demo新增联系人 message :message PeopleInfo { }定义消息字段在 message 中我们可以定义其属性字段字段定义格式为字段类型 字段名 字段唯一编号• 字段名称命名规范全小写字母多个字母之间用 _ 连接。• 字段类型分为标量数据类型 和 特殊类型包括枚举、其他消息类型等。• 字段唯一编号用来标识字段一旦开始使用就不能够再改变。该表格展示了定义于消息体中的标量数据类型以及编译 .proto 文件之后自动生成的类中与之对应的字段类型。在这里展示了与 C 语言对应的类型。变长编码是指经过 protobuf 编码后原本 4 字节或 8 字节的数可能会被变为其他字节数。更新 contacts.proto, 新增姓名、年龄字段message PeopleInfo { string name 1; int32 age 2; }注这里还要特别讲解一下字段唯一编号的范围 1 ~ 536,870,911 (2^29 - 1) 其中 19000 ~ 19999 不可用。 这些数不可用是因为在 Protobuf 协议的实现中对这些数进行了预留。如果非要在.proto 文件中使用这些预留标识号例如将 name 字段的编号设置为 19000编译时就会报警。值得一提的是范围为 1 ~ 15 的字段编号需要一个字节进行编码 16 ~ 2047 内的数字需要两个字节进行编码。编码后的字节不仅只包含了编号还包含了字段类型。所以 1 ~ 15 要用来标记出现非常频繁的字段要为将来有可能添加的、频繁出现的字段预留一些出来。编译 contacts.proto 文件编译命令行格式为protoc [--proto_pathIMPORT_PATH] --cpp_outDST_DIR path/to/file.proto protoc 是 Protocol Buffer 提供的命令行编译工具。 --proto_path 指定 被编译的.proto 文件所在目录可多次指定。可简写成 -I IMPORT_PATH 。如不指 定该参数则在当前目录进行搜索。当某个.proto 文件 import 其他 .proto 文件时 或需要编译的 .proto 文件不在当前目录下这时就要用-I 来指定搜索目录。 --cpp_out 指编译后的文件为 C 文件。 OUT_DIR 编译后生成文件的目标路径。 path/to/file.proto 要编译的.proto 文件。编译 contacts.proto 文件命令如下protoc --cpp_out. contacts.proto编译 contacts.proto 文件后会生成所选择语言的代码我们选择的是 C所以编译后生成了两个文件contacts.pb.h contacts.pb.cc。对于编译生成的 C 代码包含了以下内容 • 对于每个 message 都会生成一个对应的消息类 。• 在消息类中编译器为每个字段提供了获取和设置方法以及一下其他能够操作字段的方法 。• 编辑器会针对于每个 .proto 文件生成.h 和 .cc 文件分别用来存放类的声明与类的实现 。contacts.pb.h 部分代码展示 class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message { public: using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom; void CopyFrom(const PeopleInfo from); using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom; void MergeFrom( const PeopleInfo from) { PeopleInfo::MergeImpl(*this, from); } static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() { return PeopleInfo; } // string name 1; void clear_name(); const std::string name() const; template typename ArgT0 const std::string, typename... ArgT void set_name(ArgT0 arg0, ArgT... args); std::string* mutable_name(); PROTOBUF_NODISCARD std::string* release_name(); void set_allocated_name(std::string* name); // int32 age 2; void clear_age(); int32_t age() const; void set_age(int32_t value); };上述的例子中• 每个字段都有设置和获取的方法 getter 的名称与小写字段完全相同setter 方法以 set_ 开头。• 每个字段都有一个 clear_ 方法可以将字段重新设置回 empty 状态。contacts.pb.cc 中的代码就是对类声明方法的一些实现在这里就不展开了 。到这里有人可能就有疑惑了那之前提到的序列化和反序列化方法在哪里呢在消息类的父类 MessageLite 中提供了读写消息实例的方法包括序列化方法和反序列化方法。classMessageLite{public://序列化boolSerializeToOstream(ostream*output)const;// 将序列化后数据写入文件流boolSerializeToArray(void*data,intsize)const;boolSerializeToString(string*output)const;//反序列化boolParseFromIstream(istream*input);// 从流中读取数据再进行反序列化动作boolParseFromArray(constvoid*data,intsize);boolParseFromString(conststringdata);};注意• 序列化的结果为二进制字节序列而非文本格式。• 以上三种序列化的方法没有本质上的区别只是序列化后输出的格式不同可以供不同的应用场景使用• 序列化的 API 函数均为 const 成员函数因为序列化不会改变类对象的内容 而是将序列化的结果保存到函数入参指定的地址中 。序列化与反序列化的使用创建一个测试文件 info.cc方法中我们实现对一个联系人的信息使用 进行序列化并将序列化结果打印出来对序列化后的内容使进行反序列解析出联系人信息并打印出来#includeiostream#includecontacts.pb.h// 引入编译生成的头文件 using namespace std;intmain(){string people_str;// 序列化{// .proto 文件声明的 package通过 protoc 编译后会为编译生成的C代码声明同名的命名空间// 其范围是在.proto 文件中定义的内容contacts::PeopleInfo people;people.set_age(20);people.set_name(张珊);// 调用序列化方法将序列化后的二进制序列存入 string 中if(!people.SerializeToString(people_str)){cout序列化联系人失败.endl;}// 打印序列化结果cout序列化后的 people_str: people_str.size()endl;}// 反序列化{contacts::PeopleInfo people;// 调用反序列化方法读取 string 中存放的二进制序列并反序列化出对象if(!people.ParseFromString(people_str)){cout反序列化出联系人失败.endl;}// 打印结果coutParse age: people.age()endl;coutParse name: people.name()endl;}return0;}代码书写完成后编译 info.cc生成可执行程序 g info.cc contacts.pb.cc -o info -stdc11 -lprotobuf执行可执行程序可以看见 people 经过序列化和反序列化后的结果 :序列化后的 people_str: 10 Parse age: 20 Parse name: 张珊由于 ProtoBuf 是把联系人对象序列化成了二进制序列这里用 string 来作为接收二进制序列的容器。所以在终端打印的时候会有换行等一些乱码显示。另外相对于 xml 和 JSON 来说因为 PB 被编码成二进制破解成本增大ProtoBuf 编码是相对安全的。