【protobuf】ProtoBuf——快速上手protobuf、创建.proto文件、编译.proto文件、序列化与反序列化的使用

ProtoBuf

在这里插入图片描述

  

4. 快速上手protobuf

  我们在上一节学习了 Protobuf 的基本概念,所以现在我们小试牛刀一下,我们将编写基于protobuf实现的通讯录。在这个通讯录1.0 版本中,会实现以下功能:

  (1)联系人涵盖以下信息:姓名、年龄。

  (2)运用 PB 对一个联系人的信息进行序列化,并将所得结果予以打印。

  (3)针对序列化后的内容,使用 PB 执行反序列化操作,解析出联系人的信息并进行打印。

  通过通讯录 1.0,我们能够知晓使用 ProtoBuf 初步需掌握的要点,同时亲身体验到 ProtoBuf 的完整使用流程。

  

4.1 创建.proto文件

  .proto文件是什么?

  .proto 文件是 Protocol Buffers(简称 Protobuf)中用于定义数据结构的文件。

  它使用特定的语法来描述数据的格式和字段信息。通过在 .proto 文件中定义 message ,可以明确数据包含的字段、字段的数据类型(如 int32 、 string 等)以及一些属性(如 required 、 optional 等)。

  简单来说:就是我们期望用于客户端和服务端之间进行传递的信息(.proto用结构体定义)。

  

  命名规范:

  创建 .proto 文件时,文件命名应该使用全小写字母命名,多个字母之间用 _ 连接。 例如:lower_snake_case.proto 。

  书写 .proto 文件代码时,应使用 2 个空格的缩进。

  添加注释:向文件添加注释,可使用 // 或者 / * … * / 优化文档。

  

  指定 proto3 语法:

  Protocol Buffers 语言版本 3,简称 proto3,乃是.proto 文件的最新语法版本。

  proto3 对 Protocol Buffers 语言予以了简化,不仅易用,而且能够在更为广泛的编程语言中得以运用。它支持您使用 Java、C++、Python 等众多语言生成 protocol buffer 代码。

  在.proto 文件里,需通过 syntax = "proto3"; 来指定文件语法为 proto3,并且此语句必须置于除去注释内容后的第一行。倘若未进行指定,编译器将会采用 proto2 语法。

  使用内容如下:

syntax = "proto3";

  

  package 声明符:

  package 是一个可选的声明符,用于表示 .proto 文件的命名空间。在项目中,package 应当具有唯一性,其主要作用在于避免我们所定义的消息产生冲突。

syntax = "proto3";
package contacts;

  

  定义消息(message):

  消息(message):它是要定义的结构化对象,我们能够为这个结构化对象设定其相应的属性内容。

  
  这里再谈一下为何要定义消息?

  在网络传输中,我们需要为传输的双方制定协议。所谓定制协议,直白地说就是定义结构体或者结构化数据,例如,tcp、udp 报文就是结构化的。

  再比如,在将数据持久化存储到数据库时,会把一系列元数据统一用对象组织起来,然后再进行存储。

  message内容如下:

syntax = "proto3";
package contacts;

// 定义联系⼈消息
message PeopleInfo {
	// 自定义信息
}

  

  定义消息字段:

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

  
  字段名称命名规范:采用全小写字母,多个字母之间用 _ 连接。例如:first_name 、 last_name 。

  字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)。标量数据类型如 int32 、 string 等。特殊类型中的枚举例如:

  字段唯一编号:用于标识字段,一旦开始使用就不能再更改。编号通常在 1 至 536870911 之间,其中 19000 ~ 19999 不可用,1 至 15 的编号占用较少字节进行编码。

  19000 ~ 19999 不可用的原因在于:在 Protobuf 协议的实现过程中,对这些数字进行了预留。倘若非要在 .proto 文件中使用这些预留的标识号,例如将 name 字段的编号设置为 19000,在编译时就会发出警报。

syntax = "proto3";
package contacts;

message PeopleInfo {
	string name = 1; 
	int32 age = 19000; 
}

在这里插入图片描述
  
  范围在 1 至 15 的字段编号只需一个字节编码,16 至 2047 内的数字则需两个字节编码。编码后的字节不仅包含编号,还涵盖字段类型。因此,1 至 15 的编号应用于标记出现非常频繁的字段,并且要为将来可能添加且频繁出现的字段预留一些

  

  以下表格展示了在消息体中定义的标量数据类型,以及编译 .proto 文件之后自动生成的类中与之对应的字段类型。此处展示的是与 C++ 语言对应的类型。

  变长编码是指:经过protobuf 编码后,原本4字节或8字节的数可能会被变为其他字节数。

.proto TypeNotesC++ Type
doublenulldouble
floatnullfloat
int32使用变成编码。负数的编码效率比较低(若字段出现负数的频率比较高,可以考虑使用sint32代替)int32
int64使用变长编码。负数的编码效率比较低(若字段出现负数的频率比较高,可以考虑使用sint64代替) int64
uint32使用变长编码uint32
uint64使用变长编码uint64
sint32使用变长编码。符号整型。负数编码效率高于常规int32int32
sint64使用变长编码。符号整型。负数编码效率高于常规int64int64
fixed32定长4字节。若值常大于2^28则会比uint32更高效uint32
fixed64定长8字节。若常值大于2^28则会比uint64更高效uint64
sfixed32定长4字节int32
sfixed64定长8字节int64
boolnullbool
string包含UTF-8和ASCII编码的字符串,长度不能超过2^32string
bytes可以包含任意的字节序列但长度不能超过2^32string

  新增姓名、年龄字段:

syntax = "proto3";
package contacts;

message PeopleInfo {
	string name = 1; 
	int32 age = 2; 
}

  
  这样就完成了我们的通讯录 1.0 版本的消息字段,如果后面的客户端和服务端要进行通信,就会基于我们的消息字段:

在这里插入图片描述

  

4.2 编译.proto文件

  编译命令:

protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_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

  编译成功,生成.pb.h和.pb.cc文件:

// 首行:语法指定行
syntax = "proto3";
package contacts; // 命名空间

// 定义联系⼈消息
message PeopleInfo {
    string name =1; // 姓名
    int32 age = 2; // 年龄
}

// 在fast_use下编译
//protoc --cpp_out=. contacts.proto

// 在fast_use当前目录下编译
//protoc -I fast_start/ --cpp_out=fast_start/ contacts.proto 

  
在这里插入图片描述
  

  编译 contacts.proto 文件后会生成什么:

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

  对于编译生成的 C++ 代码,包含了以下内容:

  (1)对于每个 message ,都会生成一个对应的消息类。

  (2)在消息类中,编译器为每个字段提供了获取和设置方法,以及一些其他能够操作字段的方法。

  编辑器会针对每个 .proto 文件生成 .h 和 .cc 文件,分别用于存放类的声明与类的实现。

  之后我们就可以在contacts.pb.h这个头文件中找到有关使用contacts类的序列化和反序列化函数了:
  
在这里插入图片描述
  

  变量名以name为例:

  void clear_name();:用于清除 name 字段的值,将其重置为默认或空状态。

  const std::string& name() const;:获取 name 字段当前的值,返回一个常量引用。

  template <typename ArgT0 = const std::string&, typename... ArgT>
  void set_name(ArgT0&& arg0, ArgT... args);:用于设置 name 字段的值,支持不同类型的参数传递。

  std::string* mutable_name();:获取 name 字段的可修改地址。

  PROTOBUF_NODISCARD std::string* release_name();:释放 name 字段的所有权并返回其指针。

  void set_allocated_name(std::string* name);:通过传入一个已分配的 std::string 指针来设置 name 字段的值。

  

  这些还是字段的处理方式,那序列化和反序列化方法在哪里?

  在消息类的父类 MessageLite 中,提供了读写消息实例的方法,包括序列化方法和反序列化方法。

  PeopleInfo消息字段继承自Message类:

在这里插入图片描述
  
  Message在我们的头文件中,且再继承自MessageLite类:

在这里插入图片描述

  
  再MessageLite类中就定义了一系列序列化和反序列化方法:

在这里插入图片描述
  
  序列化方法:

在这里插入图片描述

  
  反序列化方法:

在这里插入图片描述
  

  序列化的结果为二进制字节序列,而非文本格式。 所以ProtoBuf序列化后的结果的破译成本相比于JSON序列化后的结果的破译成本更高,更加安全。

  以上序列化的方法没有本质上的区别,只是序列化后输出的格式不同,可供不同的应用场景使用。

  序列化的 API 函数均为 const 成员函数,因为序列化不会改变类对象的内容,而是将序列化的结果保存到函数入参指定的地址中。

  完整Message API

  

4.3 序列化与反序列化的使用

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

  对一个联系人的信息使用 PB 进行序列化,并将结果打印出来。

  对序列化后的内容使用 PB 进行反序列化,解析出联系人信息并打印出来。

#include <iostream> 
#include "contacts.pb.h" // 引⼊编译⽣成的头⽂件
using namespace std; 

int main() 
{
    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 << endl;
    }

    {
        contacts::PeopleInfo people; 
        // 调⽤反序列化⽅法,读取string中存放的⼆进制序列,并反序列化出对象
        if (!people.ParseFromString(people_str)) 
        { 
            cout << "反序列化出联系⼈失败." << endl; 
        } 
        // 打印结果
        cout << "Parse age: " << people.age() << endl; 
        cout << "Parse name: " << people.name() << endl; 
    }

    return 0;
}

// 编译
//g++ -o TestPb main.cc contacts.pb.cc -std=c++11 -lprotobuf
g++ -o TestPb main.cc contacts.pb.cc -std=c++11 -lprotobuf

  编译的时候一定不要忘记链接protobuf库,不然会报连接错误。

在这里插入图片描述
  

  小结 ProtoBuf 使用流程:

在这里插入图片描述
  
  (1)编写 .proto 文件,其目的在于定义结构对象(message)及属性内容。

  (2)使用 protoc 编译器编译 .proto 文件,生成一系列接口代码,存放在新生成的头文件和源文件中。

  (3)依赖生成的接口,将编译生成的头文件包含进我们的代码中,以实现对 .proto 文件中定义的字段进行设置和获取,以及对 message 对象进行序列化和反序列化。

  总的来说:ProtoBuf 是需要依赖通过编译生成的头文件和源文件来使用的。有了这种代码生成机制,开发人员再也不用费力地编写那些协议解析的代码了。

            

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鳄鱼麻薯球

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值