问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

快速上手Protobuf:从.proto文件创建到序列化反序列化实战

创作时间:
作者:
@小白创作中心

快速上手Protobuf:从.proto文件创建到序列化反序列化实战

引用
CSDN
1.
https://blog.csdn.net/Crocodile1006/article/details/141230472

Protocol Buffers(简称Protobuf)是一种语言中立、平台中立、可扩展的序列化结构数据格式,主要用于数据存储和通信协议。本文将详细介绍如何快速上手Protobuf,包括创建.proto文件、编译.proto文件以及序列化与反序列化的使用。

ProtoBuf简介

Protobuf是Google开发的一种数据序列化格式,用于结构化数据的序列化和反序列化。它具有以下特点:

  • 语言中立:支持多种编程语言,如C++、Java、Python等。
  • 平台中立:可以在不同操作系统和硬件平台上使用。
  • 可扩展性:可以轻松添加新的字段和消息类型。
  • 高效性:序列化后的数据体积小,传输速度快。

快速上手Protobuf

在上一节学习了Protobuf的基本概念后,现在我们将通过一个通讯录1.0版本的实现来小试牛刀。这个通讯录将实现以下功能:

  1. 联系人涵盖以下信息:姓名、年龄。
  2. 运用PB对一个联系人的信息进行序列化,并将所得结果予以打印。
  3. 针对序列化后的内容,使用PB执行反序列化操作,解析出联系人的信息并进行打印。

通过这个示例,我们将掌握使用Protobuf的基本要点,并亲身体验Protobuf的完整使用流程。

4.1 创建.proto文件

.proto文件是什么?

.proto文件是Protocol Buffers中用于定义数据结构的文件。它使用特定的语法来描述数据的格式和字段信息。通过在.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语法。

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 Type
Notes
C++ Type
double
null
double
float
null
float
int32
使用变成编码。负数的编码效率比较低(若字段出现负数的频率比较高,可以考虑使用sint32代替)
int32
int64
使用变长编码。负数的编码效率比较低(若字段出现负数的频率比较高,可以考虑使用sint64代替)
int64
uint32
使用变长编码
uint32
uint64
使用变长编码
uint64
sint32
使用变长编码。符号整型。负数编码效率高于常规int32
int32
sint64
使用变长编码。符号整型。负数编码效率高于常规int64
int64
fixed32
定长4字节。若值常大于2^28则会比uint32更高效
uint32
fixed64
定长8字节。若常值大于2^28则会比uint64更高效
uint64
sfixed32
定长4字节
int32
sfixed64
定长8字节
int64
bool
null
bool
string
包含UTF-8和ASCII编码的字符串,长度不能超过2^32
string
bytes
可以包含任意的字节序列但长度不能超过2^32
string

新增姓名、年龄字段:

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类:

再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是需要依赖通过编译生成的头文件和源文件来使用的。有了这种代码生成机制,开发人员再也不用费力地编写那些协议解析的代码了。

本文原文来自CSDN

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号