ProtoBuf入门基础

金歌  金牌会员 | 2024-8-18 00:27:01 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 578|帖子 578|积分 1734

如何入门Protobuf?
  

  • 认识 ProtoBuf,快速上手
  • 学习 proto3 语法
  • 实战 ProtoBuf
  • 总结,对比多种序列化协议
  Protobuf学习思维导图


一、序列化概念

序列化和反序列化



  • 序列化:把对象转换为字节序列的过程 称为对象的序列化。
  • 反序列化:把字节序列恢复为对象的过程 称为对象的反序列化。
什么情况下需要序列化



  • 存储数据:当你想把的内存中的对象状态保存到一个文件中大概存到数据库中时。
  • 网络传输:网络直接传输数据,但是无法直接传输对象,所以要在传输前序列化,传输完成后反序列化成对象。比方我们之前学习过 socket 编程中发送与吸收数据。
如何实现序列化

主流的三种序列化反序列化方案:


  • xml
  • json
  • protobuf
二、ProtoBuf 是什么

   官方解释
  Protocol buffers are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler. You define how you want your data to be structured once, then you can use special generated source code to easily write and read your structured data to and from a variety of data streams and using a variety of languages.
   翻译
  Protocol Buffers 是 Google 的一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通讯协议、数据存储等。
Protocol Buffers 类比于 XML,是一种灵活,高效,自动化机制的结构数据序列化方法,但是比 XML 更小、更快、更为简单。
你可以定义数据的结构,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言举行编写和读取结构数据。你甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。
简单来讲, ProtoBuf(全称为 Protocol Buffer)是让结构数据序列化的方法,其具有以下特点:


  • 语言无关、平台无关:即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台。
  • 高效:即比 XML 更小、更快、更为简单。
  • 扩展性、兼容性好:你可以更新数据结构,而不影响和破坏原有的旧程序。
三、ProtoBuf 的使用特点



  • 编写 .proto 文件,目的是为了定义结构对象(message)及其属性内容。
  • 使用 protoc 编译器编译 .proto 文件,生成一系列接口代码,存放在新生成头文件和源文件中。
  • 依靠生成的接口,将编译生成的头文件包罗进我们的代码中,实现对 .proto 文件中定义的字段举行设置和获取,和对 message 对象举行序列化和反序列化。
   总的来说:ProtoBuf 是需要依靠通过编译生成的头文件和源文件来使用的。生成的接口代码用于对二进制文件的读写。
  四、快速上手

   实现一个通讯录项目来快速上手ProtoBuf,我们会先编写第一版本的通讯录1.0,实现:
  

  • 对一个联系人的信息使用 PB 举行序列化,并将效果打印出来。
  • 对序列化后的内容使用 PB 举行反序列,解析出联系人信息并打印出来。
  • 联系人包罗以下信息: 姓名、年龄。
通过通讯录 1.0,我们便能相识使用 ProtoBuf 开端要掌握的内容,以及体验到 ProtoBuf 的完整使用流程。
步调1:创建 .proto 文件

文件规范



  • 创建 .proto 文件时,文件命名应该使用全小写字母命名,多个字母之间用 _ 毗连。 比方:lower_snake_case.proto 。
  • 书写 .proto 文件代码时,应使用 2 个空格的缩进。
我们为通讯录 1.0 新建文件: contacts.proto
添加注释

向文件添加注释,可使用 // 大概 /* ... */
指定 proto3 语法

Protocol Buffers 语言版本3,简称 proto3,是 .proto 文件最新的语法版本。proto3 简化了 Protocol Buffers 语言,既易于使用,又可以在更广泛的编程语言中使用。它允许你使用 Java,C++,Python 等多种语言生成 protocol buffer 代码。
在 .proto 文件中,要使用syntax = "proto3";
来指定文件语法为proto3,并且必须写在除去
注释内容的第一行。 假如没有指定,编译器会使用proto2语法。
   在通讯录 1.0 的 contacts.proto 文件中,可以为文件指定 proto3 语法
  1. syntax = "proto3";
复制代码
package 声明符

package 是一个可选的声明符,能表现 .proto 文件的命名空间,在项目中要有唯一性。它的作用是为了制止我们定义的消息出现冲突。
   在通讯录 1.0 的 contacts.proto 文件中,可以声明其命名空间为contacts
  1. syntax = "proto3";
  2. package contacts;
复制代码
定义消息(message)

   消息(message): 要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。
  为什么要定义消息?由于在网络传输中,我们需要为传输两边定制协议。定制协议说白了就是定义结构体大概结构化数据:


  • 好比,TCP,UDP报文就是结构化的
  • 好比,将数据持久化存储到数据库时,会将一系列元数据同一用对象组织起来,再举行存储。
    所以 ProtoBuf 就是以 message 的方式来支持我们定制协议字段,后期资助我们形成类和方法来使用。在通讯录 1.0 中我们就需要为 联系人 定义一个 message
   消息类型的格式
  1. message 消息类型名 {
  2. }
复制代码
消息类型命名规范:使用驼峰命名法,首字母大写。
定义消息字段

在 message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯一编号;


  • 字段名称命名规范:全小写字母,多个字母之间用_毗连。
  • 字段类型分为:标量数据类型 特殊类型(包罗枚举、其他消息类型等)
  • 字段唯一编号:用来标识字段,一旦开始使用就不可以或许再改变。
该表格展示了定义于消息体中的标量数据类型,以及编译 .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   更新 contacts.proto (通讯录 1.0),新增姓名、年龄字段:
  1. syntax = "proto3";
  2. package contacts;message PeopleInfo {  string name = 1;  int32 age = 2;}
复制代码
字段唯一编号的范围

范围:1 ~ 536,870,911 (2^29 - 1) ,此中 19000 ~ 19999 不可用。
19000 ~ 19999 不可用是由于:在 Protobuf 协议的实现中,对这些数举行了预留。假如非要在.proto文件中使用这些预留标识号,比方将 name 字段的编号设置为19000,编译时就会报错。
值得一提的是,范围为 1 ~ 15 的字段编号需要一个字节举行编码, 16 ~ 2047 内的数字需要两个字节举行编码。编码后的字节不仅只包罗了编号,还包罗了字段类型。所以 1 ~ 15 要用来标记出现非常频仍的字段,要为未来有可能添加的、频仍出现的字段预留一些出来。
步调2:编译 contacts.proto 文件,生成 C++ 文件

编译下令

   编译下令行格式
  1. protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto
复制代码
紧张的下令或参数说明protocProtocol Buffer 提供的下令行编译工具。--proto_path 或 -I指定被编译的 .proto 文件地点目录,可多次指定。如不指定该参数,则在当前目录举行搜索。当某个 .proto 文件 import 其他 .proto 文件时,或需要编译的 .proto 文件不在当前目录下,这时就要用 -I 来指定搜索目录。--cpp_out=OUT_DIR指编译后的文件为 C++ 文件,OUT_DIR 为编译后生成文件的目的路径。path/to/file.proto要编译的 .proto 文件。   通过protoc -h获取全部下令行参数
  全部下令或参数说明protocProtocol Buffer 提供的下令行编译工具。-IPATH, --proto_path=PATH指定搜索导入文件的目录。可以多次指定;目录将按次序搜索。假如未指定,则使用当前工作目录。假如在这些目录中找不到,则查抄 --descriptor_set_in 形貌符。--version表现版本信息并退出。-h, --help表现资助信息并退出。--encode=MESSAGE_TYPE从标准输入读取给定类型的文本格式消息,并以二进制格式写入标准输出。消息类型必须在 PROTO_FILES 或其导入中定义。--deterministic_output使用 --encode 时,确保映射字段按确定性次序排列。注意这种次序不是规范的,并且在差别版本或构建的 protoc 中可能会变革。--decode=MESSAGE_TYPE从标准输入读取给定类型的二进制消息,并以文本格式写入标准输出。消息类型必须在 PROTO_FILES 或其导入中定义。--decode_raw从标准输入读取任意协议消息,并以原始标签/值对的文本格式写入标准输出。使用此标记时不应指定 PROTO_FILES。--descriptor_set_in=FILES指定一个以分隔符分隔的文件列表,每个文件包罗一个 FileDescriptorSet(在 descriptor.proto 中定义的协议缓冲区)。提供的 PROTO_FILES 的 FileDescriptor 将从这些 FileDescriptorSets 中加载。假如 FileDescriptor 多次出现,则使用第一个。-oFILE, --descriptor_set_out=FILE写入一个包罗全部输入文件的 FileDescriptorSet(在 descriptor.proto 中定义的协议缓冲区)到 FILE。--include_imports使用 --descriptor_set_out 时,还包罗输入文件的全部依靠项,以使集合自包罗。--include_source_info使用 --descriptor_set_out 时,不要从 FileDescriptorProto 中剥离 SourceCodeInfo。这将导致形貌符显著增大,包罗源文件中每个声明的原始位置以及周围的注释信息。--dependency_out=FILE以 make 期望的格式写入依靠输出文件。这会将输入文件路径的传递集写入 FILE。--error_format=FORMAT设置打印错误的格式。格式可以是 ‘gcc’(默认)或 ‘msvs’(Microsoft Visual Studio 格式)。--fatal_warnings将告诫视为致命错误(类似于 gcc 中的 -Werr)。假如生成任何告诫,此标记将使 protoc 返回非零退出代码。--print_free_field_numbers打印给定 proto 文件中定义的消息的空闲字段号。组与父消息共享相同的字段编号空间。扩展范围被视为占用的字段编号。--plugin=EXECUTABLE指定要使用的插件可执行文件。通常,protoc 在 PATH 中搜索插件,但您可以使用此标记指定不在路径中的额外可执行文件。此外,EXECUTABLE 可以是 NAME=PATH 的情势,在这种情况下,纵然可执行文件的名称差别,也会将给定插件名称映射到给定可执行文件。--cpp_out=OUT_DIR生成 C++ 头文件和源文件。--csharp_out=OUT_DIR生成 C# 源文件。--java_out=OUT_DIR生成 Java 源文件。--kotlin_out=OUT_DIR生成 Kotlin 文件。--objc_out=OUT_DIR生成 Objective-C 头文件和源文件。--php_out=OUT_DIR生成 PHP 源文件。--pyi_out=OUT_DIR生成 Python pyi stub 文件。--python_out=OUT_DIR生成 Python 源文件。--ruby_out=OUT_DIR生成 Ruby 源文件。@<filename>从文件中读取选项和文件名。假如指定了相对文件路径,将在工作目录中搜索该文件。--proto_path 选项不会影响如何搜索此参数文件。文件的内容将在参数列表中的 @<filename> 位置睁开。请注意,文件的内容不会应用 shell 扩展(即,您不能使用引号、通配符、转义符、下令等)。每行对应一个参数,纵然它包罗空格。   编译 contacts.proto 文件
  1. protoc --cpp_out=. contacts.proto
复制代码
编译 contacts.proto 文件后会生成什么

编译 contacts.proto 文件后,会生成所选择语言的代码,我们选择的是C++,所以编译后生成了两个文件: contacts.pb.h contacts.pb.cc 。
对于编译生成的 C++ 代码,包罗了以下内容 :


  • 对于每个 message ,都会生成一个对应的消息类。
  • 在消息类中,编译器为每个字段提供了获取和设置方法,以及一下其他可以或许操作字段的方法。
  • 编辑器会针对于每个 .proto 文件生成.h 和 .cc 文件,分别用来存放类的声明与类的实现。
   contacts.pb.h 部门代码展示
  1. class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
  2. public:
  3.         using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
  4.         void CopyFrom(const PeopleInfo& from);
  5.         using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
  6.        
  7.         void MergeFrom( const PeopleInfo& from) {
  8.                 PeopleInfo::MergeImpl(*this, from);
  9.         }
  10.        
  11.         static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
  12.                 return "PeopleInfo";
  13.         }
  14.        
  15.         // string name = 1;
  16.         void clear_name();
  17.         const std::string& name() const;
  18.         template <typename ArgT0 = const std::string&, typename... ArgT>
  19.         void set_name(ArgT0&& arg0, ArgT... args);
  20.         std::string* mutable_name();
  21.         PROTOBUF_NODISCARD std::string* release_name();
  22.         void set_allocated_name(std::string* name);
  23.        
  24.         // int32 age = 2;
  25.         void clear_age();
  26.         int32_t age() const;
  27.         void set_age(int32_t value);
  28. };
复制代码


  • 每个字段都有设置和获取的方法, getter 的名称与小写字段完全相同,setter 方法以 set_ 开头。
  • 每个字段都有一个 clear_ 方法,可以将字段重新设置回 empty 状态。
contacts.pb.cc 中的代码就是对类声明方法的一些实现,在这里就不睁开了。
到这里有同学可能就有疑惑了,那之条件到的序列化和反序列化方法在那里呢?在消息类的父类 MessageLite 中,提供了读写消息实例的方法,包罗序列化方法和反序列化方法。
  1. class MessageLite {
  2. public:
  3.         //序列化:
  4.         bool SerializeToOstream(ostream* output) const; // 将序列化后数据写入文件流
  5.         bool SerializeToArray(void *data, int size) const;
  6.         bool SerializeToString(string* output) const;
  7.        
  8.         //反序列化:
  9.         bool ParseFromIstream(istream* input); // 从流中读取数据,再进行反序列化动作
  10.         bool ParseFromArray(const void* data, int size);
  11.         bool ParseFromString(const string& data);
  12. };
复制代码
  [!Attention] 注意:
  

  • 序列化的效果为二进制字节序列,而非文本格式。
  • 以上三种序列化的方法没有本质上的区别,只是序列化后输出的格式差别,可以供差别的应用场景使用。
  • 序列化的 API 函数均为const成员函数,由于序列化不会改变类对象的内容, 而是将序列化的效果保存到函数入参指定的地址中。
  • 详细 message API 可以拜见 完整列表。
  步调3:序列化与反序列化的使用

创建一个测试文件 main.cc,方法中我们实现:


  • 对一个联系人的信息使用 PB 举行序列化,并将效果打印出来。
  • 对序列化后的内容使用 PB 举行反序列,解析出联系人信息并打印出来。
   main.cc
  1. #include <iostream>
  2. #include "./contacts.pb.h"
  3. int main()
  4. {
  5.     std::string people_str;
  6.     {
  7.         contacts::PeopleInfo people;
  8.         people.set_name("张三");
  9.         people.set_age(12);
  10.         std::string out;
  11.         if (!people.SerializeToString(&people_str)) {
  12.             std::cout << "序列化联系人失败!" << std::endl;
  13.             return -1;
  14.         }
  15.         // 序列化成功
  16.         std::cout << "序列化成功!结果:\n" << people_str << std::endl;
  17.     }
  18.     {
  19.         contacts::PeopleInfo people;
  20.         if (!people.ParseFromString(people_str)) {
  21.             std::cout << "反序列化联系人失败!" << std::endl;
  22.             return -1;
  23.         }
  24.         // 序列化成功
  25.         std::cout << "反序列化成功!内容" << std::endl
  26.             << "姓名:" << people.name() << std::endl
  27.             << "年龄:" << people.age() << std::endl;
  28.     }
  29.     return 0;
  30. }
复制代码
代码书写完成后,编译 main.cc,生成可执行程序 TestProtoBuf :
  1. g++ main.cc contacts.pb.cc -o TestProtoBuf -1 std=c++11 -lprotobuf
复制代码


  • -lprotobuf:必加,否则会有链接错误。
  • -std=c++11:必加,使用C++11语法。
执行 TestProtoBuf ,可以瞥见 people 颠末序列化和反序列化后的效果:

由于 ProtoBuf 是把联系人对象序列化成了二进制序列,这里用 string 来作为吸收二进制序列的容器。所以在终端打印的时候会有换行等一些乱码表现。
所以相对于 xml 和 JSON 来说,由于被编码成二进制,破解本钱增大,ProtoBuf 编码是相对安全的。

   

  • 变长编码是指:颠末 protobuf 编码后,原本 4 字节或 8 字节的数值可能会被变为其他字节数。 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

金歌

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表