认识 Protobuf 及其简单使用

news2024/12/25 9:34:25

文章目录

  • 一、序列化与反序列化
    • 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;

    // 嵌套定义message
    message 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.coloradmin.cn/o/568297.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

spring boot日志

日志介绍日志的使用日志级别日志持久化更简单的输入日志lombok的运行原理 日志介绍 日志的作用&#xff1a; 1&#xff1a;发现问题&#xff1b; 2&#xff1a;定位问题&#xff1b; 3&#xff1a;记录用户的行为&#xff1a;看哪些是方法用户&#xff1b;还能拿到用户的ip&am…

【云原生|探索 Kubernetes 系列 4】理解现代云原生时代的引擎

文章目录 系列文章目录&#x1f479; 关于作者一、前言|回顾二、静态和动态视图三、爆火的容器编排工具 Kubernetes 的诞生四、Kubernetes 要解决的问题是什么&#xff1f;五、理解 Kubernetes 全局架构图Master&#xff08;控制节点&#xff09;Node&#xff08;计算节点&…

源码分析:springboot如何确定当前应用程序类型

文章目录 一、介绍二、源码分析三、测试 一、介绍 大多数java后端开发的朋友们想必都是通过创建springboot项目&#xff0c;然后通过编写Controller进行接口开发的&#xff0c;该接口底层是由非响应式的servlet提供支持的&#xff0c;其接口内部逻辑为阻塞式的。但也有一部分朋…

leetcode 837. New 21 Game(新21点)

起始点数为0&#xff0c;当手上点数 < k 时抽取新的卡片&#xff0c; 每次抽取的点数范围在 1 ~ maxPts. 每次收取是独立的&#xff0c;每个点数概率相同。 当手上点数 > k 时游戏结束。 返回手上点数 < n 的概率。 思路&#xff1a; 先看特殊情况&#xff0c; k …

JQuery实现小项目

博主简介&#xff1a;想进大厂的打工人博主主页&#xff1a;xyk:所属专栏: JavaEE初阶 目录 文章目录 一、JQuery是什么 二、JQuery项目 2.1 猜数字 2.2 表白墙 2.3 聚合搜索 2.4 计算器 一、JQuery是什么 jQuery是一个快速、简洁的JavaScript框架&#xff0c;是继Prototype之…

MQTT(1):MQTT协议介绍

随着 5G 时代的来临&#xff0c;万物物联的伟大构想正在成为现实。联网的物联网设备在 2018 年已经达到了 70 亿&#xff0c;在未来两年&#xff0c;仅智能水电气表就将超过10亿 海量的设备接入和设备管理对网络带宽、通信协议以及平台服务架构都带来了很大挑战。对于物联网协议…

pandas 遇到Key Error错误的一个小问题

最近刚刚接触Python&#xff0c;安装了Anaconda&#xff0c; 编程小白一个&#xff0c;照着教程准备做一个中考成绩录取分数线分析的案例&#xff0c; 使用read_excel()读入数据后&#xff0c; import pandas as pd data pd.read_excel(rC:\2021-2022深圳中考录取分数线(1).xl…

SpringBoot配置文件 | 多环境配置 | 读取配置的4种方式

文章目录 一、写配置文件的位置读取的优先级&#xff1a;1.文件位置&#xff1a;2.文件名和文件后缀&#xff1a;3.配置文件中的profile-specific文件&#xff1a;4.命令行参数 二、多环境配置1. properties&#xff1a;2. yaml 三、yaml配置文件yaml、properties、xml对比&…

【软考-中级】系统集成项目管理工程师 【13合同管理】

持续更新。。。。。。。。。。。。。。。 【第十三章】合同管理 2 分 考点 1考点 2考点 3考点4:成本补偿合同考点5:工料合同考点6:合同类型的选择考点 7考点 8:合同管理包括考点9考点 10考点 11考点 12考点 13考点 14考点 15历年真题2022 年 05 月2021 年 11 月2021 年 05 月 考…

Redis底层原理深入学习

一、基本类型及底层实现 1.String 1&#xff09;使用场景&#xff1a;简单字符串存储、分布式锁、计数器、全局唯一ID 2&#xff09;数据结构&#xff1a;C语言中String用char[]表示&#xff0c;源码中用SDS封装char[]&#xff0c;这是Redis存储的最小单元&#xff0c;一个SD…

安全中级3-nginx反向代理负载均衡的webshell

目录 一、负载均衡 1.nginx的负载均衡 2.nginx 支持的几种策略&#xff1a; 二、负载均衡下的webshell连接&#xff08;负载均衡下的wenbshell环境下载地址&#xff09; 1.内部网络的结构 2.场景描述 3.利用我们的中国蚁剑连接我们的代理服务器nginx 三、webshell遇到的…

电脑待机或者睡眠后TeamViewer就无法连接了

电脑待机或者睡眠后TeamViewer就无法连接了 设置睡眠状态下不关闭网卡驱动 公司的笔记本&#xff0c;安装了teamviewer&#xff0c;离开时把teamviewer打开&#xff0c;回家后连接时提示伙伴未在机器上运行&#xff0c;此时电脑处于黑屏、待机、睡眠状态 其实电脑睡眠后会关掉网…

创新管理工具:低代码平台在学校管理中的应用实践

随着信息技术的不断发展&#xff0c;学校管理也随之发生了变革。传统的学校管理方式往往是依靠人工操作&#xff0c;存在信息不透明、效率低下等问题&#xff0c;而数字化管理的出现&#xff0c;可以帮助学校提高管理效率、降低管理成本、提升数据统计和分析能力。而低代码技术…

SWAT模型教程

详情点击链接&#xff1a;SWAT模型教程详情点击链接&#xff1a;SWAT模型&#xff08;建模方法、实例应用、高级进阶&#xff09; 一&#xff1a;基于网络资源的SWAT模型快速建模​ 二&#xff1a;基于遥感产品的SWAT模型率定与验证​ 三&#xff1a;基于水文响应单元&#xff…

相机光圈和快门

相机光圈和快门 光圈光圈结构光圈值由来光圈范围光圈作用控制画面明暗控制画面景深和锐度 自动光圈 Auto IrisDC-IRIS原理及问题P-IRIS工作原理 快门快门简介快门速度与曝光快门速度与运动安全快门速度高速快门和慢速快门B门和T门 参考文献 光圈 光圈结构 光圈&#xff08;Ap…

记一次 Android 源码编译刷机过程

0 背景 为了能在开发设备上运行 adb root 命令得到 root 权限&#xff0c;获得更加强大的调试能力、开发体验&#xff0c;方便以后阅读源码时 Debug 跟踪、进行定制化开发&#xff0c;需要编译 Android 源码 userdebug 版本并刷入手机当中。 1 准备 1.1 硬盘 首先 Android 源…

新星计划 Electron+vue2 桌面应用 2 项目编写

练手的项目&#xff0c;需求简单&#xff0c;打算做平面设计社交类的软件。 一、需求 练手用&#xff0c;简单处理&#xff0c;写个简单的记事本&#xff0c;本地保存txt&#xff0c;能导出为其他格式的文件。 获取用户的网卡地址用于数据加密&#xff0c;本地保存加密后的文…

想变身“科技型”企业?掌汇云数字化服务平台为工业升级加分

40万亿元&#xff01;占GDP比重达到33.2%&#xff0c;这就是国新办公布的2022年数据&#xff0c;中国工业可以说是当之无愧的支柱产业。 中国工业规模大、覆盖面广&#xff0c;企业员工众多&#xff0c;项目遍及海内外。但由于科技欠发达、信息不流通等因素&#xff0c;近些年…

IDEA中 lombok不生效解决方法

目录 前言: springboot启动的时候报错, 说没有lombok编译器 第一步: 检查插件lombok是否存在 第二步: 查看springboot 官方推荐的lombok版本 -> 2.1 第一步 找到这个parent ctrl点进去 -> 2.2 在点红框位置 进去 -> 2.3 ctrlf 搜索一下 lombok.version 复制这个…

[论文评析]mixup: B EYOND E MPIRICAL R ISK M INIMIZATION, ICLR 2018,

mixup: B EYOND E MPIRICAL R ISK M INIMIZATION 介绍MixupMixup的提出动机Mixup与常规数据增广方法的区别References 介绍 采用ERM训练的模型往往存在泛化能力差的情形-可能是在简单的记忆样本, 对于噪声干扰的鲁棒性很差. 这篇论文提出了一种新的数据增广方法-Mixup, 这里主…