认识 Protobuf 及其简单使用

news/2024/10/30 19:32:32/

文章目录

  • 一、序列化与反序列化
    • 1.1 序列化
    • 1.2 反序列化
    • 1.3 序列化与反序列化的使用场景
  • 二、初识 Protobuf
  • 三、Protobuf 的安装
  • 四、Protobuf 的使用案例
    • 4.1 创建并编写 .proto 文件的基本规范与语法
    • 4.2 编译 .proto 文件
    • 4.3 序列化与反序列化的使用
  • 五、总结 ProtoBuf 的使用特点


一、序列化与反序列化

序列化和反序列化是在计算机科学中常见的概念,用于将对象或数据结构转换为字节流的过程,以便在网络上传输或存储在磁盘上,并在需要时重新构建成原始对象。

1.1 序列化

序列化(Serialization)是指将对象或数据结构转换为字节流的过程。在序列化过程中,对象的状态被转换为一系列的字节,可以包括对象的属性值、方法等信息。这些字节可以被存储在磁盘上或通过网络传输。

1.2 反序列化

反序列化(Deserialization)是指将字节流转换回对象或数据结构的过程。在反序列化过程中,字节流被解析,并恢复为原始的对象或数据结构,使其可以被程序进一步使用。

1.3 序列化与反序列化的使用场景

序列化和反序列化在许多应用场景中都是很有用的。一些常见的应用包括:

  1. 对象持久化:通过序列化,可以将对象保存到磁盘上,以便在以后的时间点重新加载和使用。这在应用程序重启或数据传输的情况下非常有用。

  2. 远程通信:当不同的系统或应用程序需要进行通信时,可以通过序列化将对象转换为字节流并在网络上传输。接收方可以通过反序列化将字节流重新构建成对象并进行处理。

  3. 缓存和消息队列:序列化和反序列化可以在缓存系统或消息队列中使用,以便将对象存储在内存中或在不同的应用程序之间传递消息。

不同的编程语言和框架通常提供了序列化和反序列化的库或工具,用于简化这些过程。常见的序列化格式包括 JSON(JavaScript Object Notation)XML(eXtensible Markup Language)Protocol Buffers 等。选择适当的序列化格式取决于应用的需求和使用场景。

二、初识 Protobuf

  • Protobuf (Protocol Buffers) 是一种用于序列化结构化数据的语言无关、平台无关的开源数据交换格式。它由Google开发,并在许多不同的应用程序和系统中广泛使用。

  • Protobuf 的主要目标是提供一种高效、灵活和可扩展的方式来序列化结构化数据,使其可以在不同的平台和语言之间进行通信和存储。与其他序列化格式(如XML和JSON)相比,Protobuf具有更高的性能和更小的数据尺寸

  • 使用Protobuf,我们可以定义数据的结构和格式,然后使用 protoc 编译器生成与不同编程语言兼容的类或结构体。这些生成的类或结构体提供了一组方法,用于将数据序列化为 Protobuf 格式,以及从 Protobuf 格式反序列化回原始数据。

  • Protobuf 支持包括C++、Java、Python、Go、C#等多种编程语言,使得不同语言的应用程序能够通过序列化和反序列化Protobuf 消息来进行跨平台和跨语言的通信

  • 总结起来,Protobuf 是一种用于序列化结构化数据的高性能、可扩展和跨平台的数据交换格式,它提供了一种定义数据结构的方式,并生成与多种编程语言兼容的代码。

三、Protobuf 的安装

关于 ProtoBuf 的安装可以参考我的另一篇博客:Windows 和 Linux 环境下 ProtoBuf 的安装。

四、Protobuf 的使用案例

这里我们以通讯录中的联系人为例:

  • 对一个联系人的简单信息使用 Protobuf 进行序列化,并将结果保存到文件中。
  • 对序列化后的内容使用 Protobuf 进行反序列化,解析出联系⼈信息并打印出来。

4.1 创建并编写 .proto 文件的基本规范与语法

文件规范:

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

添加注释:

  • 向⽂件添加注释,可使用 // 或者 /* ... */

指定 proto3 语法:

  • Protocol Buffers 语⾔版本3,简称 proto3,是.proto文件最新的语法版本。proto3简化了 Protocol Buffers 语⾔,既易于使用,⼜可以在更⼴泛的编程语⾔中使⽤。即允许使用 Java、C++、Python等多种语言生成 Protocol Buffer 代码。
  • .proto文件中,要使用 syntax = "proto3"; 来指定文件语法为proto3,并且必须写在除去注释内容的第一行。如果没有指定,编译器会使用proto2语法。

package 声明符:

  • package是⼀个可选的声明符,能表示.proto文件的命名空间,在项目中要有唯⼀性。它的作用是为了避免我们定义的消息出现冲突。
  • 其语法为package 作用域名称;

定义消息(message):

  • 消息(message)即要定义的结构化对象,可以在这个结构化对象中定义其对应的属性内容。
  • 为什么要定义消息?
  • 在网络传输中,我们需要为传输双方定制协议。定制协议其实就是定义结构体或者结构化数据,比如TCPUDP 报文就是结构化的数据。
  • 另外将数据持久化存储到数据库时,也会将⼀系列元数据统⼀使用对象组织起来,再进行存储。

所以 Protobuf 就是以定义 message 的方式来支持我们定制协议字段,后期帮助我们形成类和方法来使用的。

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

  • 字段名称命名规范:全小写字⺟,多个字⺟之间⽤_连接。
  • 字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)。
  • 字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。

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

.proto TypeNotesC++ Type
double双精度浮点数。double
float单精度浮点数。float
int32使用变长编码。负数的编码效率较低,若字段可能为负值,应使用 sint32 代替。int32
int64使用变长编码。负数的编码效率较低,若字段可能为负值,应使用 sint64 代替。int64
uint32使用变长编码。uint32
uint64使用变长编码。uint64
sint32使用变长编码。符号整型,负值的编码效率⾼于常规的 int32 类型。int32
int64使用变长编码。符号整型,负值的编码效率⾼于常规的 int64 类型。int64
fixed32定长 4 字节。若值常大于 2^28 则会比 uint32 更高效。uint32
fixed64定长 8 字节。若值常大于 2^56 则会比 uint64 更高效。uint64
sfixed32定长 4 字节。int32
sfixed64定长 8 字节。int64
bool布尔类型bool
string包含 UTF-8 和 ASCII 编码的字符串,长度不能超过 2^32。string
bytes可包含任意的字节序列,但长度不能超过 2^32。string

值得⼀提的是,范围为 1 ~ 15 的字段编号需要⼀个字节进行编码,16 ~ 2047 内的数字需要两个字节进行编码。编码后的字节不仅只包含了编号,还包含了字段类型。所以 1 ~ 15 要用来标记出现非常频繁的字段,因此这些字段编号要为将来有可能添加的,频繁出现的字段预留⼀些出来。

创建并编写contacts.proto文件:

syntax = "proto3"; // 首行:语法指定
package contacts2; //指定作用域message PeopleInfo 
{string name = 1; //姓名int32 age = 2;   //年龄// repeated string phone_numbers = 3;// 嵌套定义messagemessage Phone {string number = 1;}repeated Phone phone = 3; //电话信息
}//通讯录message
message Contacts 
{repeated PeopleInfo contacts = 1; 
}

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 

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

  • 编译 contacts.proto 文件后,会生成所选择语⾔的代码,这里选择的是C++,所以编译后生成了两个文件: contacts.pb.hcontacts.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中的代码就是对类声明方法的实现,这里就不进行展示了。

到这里我们可能就有疑惑了,那之前提到的序列化和反序列化方法在哪⾥呢?其实通过不断的翻阅生成的源代码,可以发现在消息类Message的⽗类 MessageLite 中,里面提供了读写消息实例的方法,包括序列化和反序列化方法。

class MessageLite {
public:
//序列化:
bool SerializeToOstream(ostream* output) const; // 将序列化后数据写⼊⽂件流
bool SerializeToArray(void *data, int size) const;
bool SerializeToString(string* output) const;//反序列化:
bool ParseFromIstream(istream* input); // 从流中读取数据,再进⾏反序列化动作
bool ParseFromArray(const void* data, int size);
bool ParseFromString(const string& data);
};

注意:

  • 序列化的结果为⼆进制字节序列,而非文本格式。
  • 以上三种序列化的⽅法没有本质上的区别,只是序列化后输出的格式不同,可以供不同的应用场景使用。
  • 序列化的 API 函数均为 const 成员函数,因为序列化不会改变类对象的内容,而是将序列化的结果保存到函数入参指定的地址中。
  • 详细 message API 可以参考 Protobuf 官方文档。

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

分别创建write.ccread.cc源文件:

  • 其中write.cc负责将输入的联系人信息序列化,任何保存到文件中。
  • read.cc负责从文件中读取数据,然后将其反序列化为联系人对象,然后打印出来。

write.cc:

#include <iostream>
#include <fstream>
#include "contacts.pb.h"using namespace std;void AddPeopleInfo(contacts2::PeopleInfo *people)
{cout << "-------------新增联系⼈-------------" << endl;cout << "请输入联系人姓名:";string name;getline(cin, name);people->set_name(name);cout << "请输入联系人年龄:";int age;cin >> age;people->set_age(age);cin.ignore(256, '\n'); // 清除缓冲区中的回车等数据,直到遇到‘\n’;// 如果清除个256字符还没遇到‘\n’,也会停下来;// 缓冲区内容不足256,也没有遇到‘\n’,也会停止for (int i = 0;; ++i){cout << "请输⼊联系⼈电话" << i + 1 << "(只输⼊回⻋完成电话新增): ";string number;getline(cin, number);if (number.empty()){break;}contacts2::PeopleInfo_Phone *phone = people->add_phone();phone->set_number(number);}cout << "-----------添加联系⼈成功-----------" << endl;
}main()
{contacts2::Contacts contacts;// 先读取本地已经存在的通讯录文件fstream input("contacts.bin", ios::in | ios::binary);if (!input){cout << "contacts.bin not found, create a new file!" << std::endl;}else if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();return -1;}// 向通讯录中添加联系人AddPeopleInfo(contacts.add_contacts());// 将通讯录写入本地文件fstream output("contacts.bin", ios::out | ios::trunc | ios::binary); // 覆盖写入,前面已经读取通讯录内容if (!contacts.SerializeToOstream(&output)){cerr << "write error!" << endl;input.close();output.close();return -1;}cout << "write sucess!" << endl;input.close();output.close();return 0;
}

read.cc:

#include <iostream>
#include <fstream>
#include "contacts.pb.h"using namespace std;void PrintContacts(contacts2::Contacts &contacts)
{for (int i = 0; i < contacts.contacts_size(); ++i){cout << "--------------------联系人" << i + 1 << "--------------------"<<endl;const ::contacts2::PeopleInfo &people = contacts.contacts(i);cout << "联系人姓名:" << people.name() << endl;cout << "联系人年龄:" << people.age() << endl;for (int j = 0; j < people.phone_size(); ++j){const ::contacts2::PeopleInfo_Phone &phone = people.phone(j);cout << "联系人电话" << j + 1 << ": "<< phone.number() << endl;}}
}int main()
{contacts2::Contacts contacts;// 先读取本地已经存在的通讯录文件fstream input("contacts.bin", ios::in | ios::binary);if (!contacts.ParseFromIstream(&input)){cerr << "parse error!" << endl;input.close();return -1;}// 打印通讯录列表PrintContacts(contacts);return 0;
}

注意,最后在使用g++编译源代码的时候,需要加上lprotobuf-std=c++11。若不加前者,会链接报错;后者使用C++11语法进行编译。

首先执行write,输入联系人信息,可见将数据序列化后保存到文件Contacts.bin文件中:


由于 Protobuf 是把联系人对象序列化成了二进制序列,因此直接查看文件就是一堆乱码。可以执行read文件,读取后进行反序列化并打印出来:

五、总结 ProtoBuf 的使用特点

Protobuf 的使用方法可以用以下图片进行概括:

其使用步骤可描述如下:

  1. 首先编写.proto 文件,目的就是结构对象(message)及其属性内容。
  2. 使用 protoc 编译器编译 .proto 文件,生成一系列的相关的接口代码,存放在新的头文件和源文件中。
  3. 依赖生成的接口,将编译生成的头文件包含进我们自己的代码中,实现对 .proto 文件中定义的字段进行设置和获取,以及对 message /对象进行序列化和反序列化。

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


http://www.ppmy.cn/news/84169.html

相关文章

Jmeter组件:Random CSV Data Set Config(随机读取文件数据)

一、Jmeter组件&#xff1a;Random CSV Data Set Config(随机读取文件数据) 功能&#xff1a;该组件可以随机读取CSV文件中的每一行的数据 二、下载插件&#xff1a;(jmeter-plugins-random-csv-data-set-xx.jar),并放到lib/ext目录下&#xff0c;重启jmeter 也可以在Jmeter…

远程服务和web服务和前端,三方通过socket和websocket进行双向通信传输数据

1. 什么是socket? 在计算机通信领域&#xff0c;socket 被翻译为“套接字”&#xff0c;它是计算机之间进行通信的一种约定或一种方式。通过 socket 这种约定&#xff0c;一台计算机可以接收其他计算机的数据&#xff0c;也可以向其他计算机发送数据。 2. 什么是websocket?…

android aidl及binder基础知识总结

1、什么是binder binder是android framework提供的&#xff0c;用于跨进程方法调用的机制&#xff0c;具有安全高效等特点。 我们知道&#xff0c;在 Android 系统中&#xff0c;每个应用程序都运行在一个独立的进程中&#xff0c;各个进程之间需要进行数据交换和调用&#x…

C++第三章:字符串、向量和数组

字符串、向量和数组 一、命名空间的using声明每个名字独立using声明头文件不应包含using声明 二、标准库类型string2.1 定义和初始化string对象直接初始化和拷贝初始化 2.2 string对象上的操作读写string对象读取未知数量的string对象使用getline读取一整行string的empty和size…

python绘制带置信区间的折线图

本文目录 一、数据准备二、添加置信区间三、完整代码四、运行结果五、python绘图往期系列文章目录 在统计学和数据分析领域中&#xff0c;我们常常需要比较两个或多个样本数据之间的差异。而带置信区间的折线图则是一种直观且常用的展示数据差异的方式。在这篇文章中&#xff0…

开箱即用的ChatGPT替代模型,还可训练自己数据

一、普遍关注是什么&#xff1f; OpenAI 是第一个在该领域取得重大进展的公司&#xff0c;并且使围绕其服务构建抽象变得更加容易。然而&#xff0c;便利性带来了集中化、通过中介的成本、数据隐私和版权问题。 而数据主权和治理是这些新的LLM服务提供商如何处理商业秘密或敏…

【发电厂用JDHF-1010 合闸(分闸)监测继电器(220V/110V) JOSEF约瑟】

■JDHF-1000合闸(分闸)监测继电器主要用于各种保护和自动控制装置中&#xff0c;作为断路器操作运行状态的监测继电器。■交直流两用■监测继电器具有高内阻特性&#xff0c;可适应各种框架式断路器的合分回路。■快速导轨安装结构&#xff0c;适合各种导轨安装。■螺钉压接式端…

外购设备PDA

专业扫描引擎&#xff0c;扫尽千军万码 工业级专业扫描引擎&#xff0c;数据采集精准、快速、安全&#xff1b; 同时增加摄像头扫描&#xff0c;自带绿点定位&#xff0c;实现快速对准&#xff1b; 可识别破损&#xff0c;沾染灰渍等条码提高工作效率。 一、产品特点 216GB/…