当前位置: 首页 > article >正文

【ProtoBuf】文件编写及序列化

ProtoBuf文件编写及序列化

文章目录

  • ProtoBuf文件编写及序列化
  • 快速上手ProtoBuf
    • 创建.proto 文件
      • 指定Proto3语法
      • Package声明符
      • 定义消息(message)
      • 定义消息字段
      • 编译命令
  • 序列化与反序列化的使用
  • 小结


快速上手ProtoBuf

为了快速上手以及完整的使用ProtoBuf,我们将编写一个小项目,并根据PB学习程度对这个项目来逐渐改版,每一个版本对应PB的新知识点。在后续内容中,会使用简单的通讯录作为项目实现。

首先来确定第一版的通讯录要求:

  • 对一个联系人的信息使用PB进行序列化,并将结果打印出来。
  • 对序列化后的内容使用PB进行反序列化,解析出联系人信息并打印出来。
  • 联系人包含以下信息:姓名、年龄。

通过这个通讯录1.0版本,我们能快速了解并使用PB初步要掌握的内容,以及体验到PB的完整使用流程。

创建.proto 文件

文件规范

  • 创建 .proto ⽂件时,⽂件命名应该使⽤全⼩写字⺟命名,多个字⺟之间⽤ _ 连接。 例如:lower_snake_case.proto
  • 书写 .proto ⽂件代码时,应使⽤ 2 个空格的缩进。

我们为通讯录1.0新建文件:contacts.proto

注释

注释类型与C/C++中注释相同,使用 “//” 或者 “/* xxx*/”

指定Proto3语法

Protocol Buffers 语⾔版本3,简称 proto3,是 .proto ⽂件最新的语法版本。proto3 简化了 Protocol Buffers 语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤ Java,C++,Python等多种语⾔⽣成 protocol buffer 代码。

在 .proto ⽂件中,要使⽤ syntax = "proto3"; 来指定文件语法为 proto3,并且必须写在除去注释内容的第⼀行。 如果没有指定,编译器会默认使用proto2语法。在通讯录 1.0 的 contacts.proto 文件中,可以为文件指定 proto3 语法,内容如下:

syntax = "proto3";

Package声明符

package是一个可选的声明符,能表示.proto文件的命名空间(相当于C++中的namesapce),在项目中要有唯一性。

在通讯录1.0的contacts.proto文件中,我们可以声明命名空间如下:

syntax = "proto3";
package contacts;

定义消息(message)

消息(message): 要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。这⾥再提⼀下为什么要定义消息?

在网络传输中,我们需要为传输双⽅定制协议。定制协议说白了就是定义结构体或者结构化数据,
比如,tcp,udp 报文就是结构化的。再⽐如将数据持久化存储到数据库时,会将⼀系列元数据统一用对象组织起来,再进行存储

所以 ProtoBuf 就是以 message 的⽅式来⽀持我们定制协议字段,后期帮助我们形成类和⽅法来使⽤。在通讯录 1.0 中我们就需要为 联系⼈ 定义⼀个 message。

在.proto文件中定义一个消息类型的格式为:

message 消息类型名 // 建议使用驼峰命名法
{}

为contacts.proto新增联系人message内容如下:

syntax = "proto3"; // 需要指定为proto3,否则默认为proto2语法
package contacts; // package是命名空间

// 定义联系人message
message PeopleInfo
{}

定义消息字段

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

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

以下表格展⽰了定义于消息体中的标量数据类型,以及编译 .proto ⽂件之后⾃动⽣成的类中与之对应的
字段类型。在这⾥展⽰了与 C++ 语⾔对应的类型:

.proto TypeNotesC++ Type
doubledouble
floatfloat
int32使⽤变⻓编码[1]。负数的编码效率较低⸺若字段可能为负值,应使⽤ sint32 代替。int32
int64使⽤变⻓编码[1]。负数的编码效率较低⸺若字段可能为负值,应使⽤ sint64 代替。int64
uint32使⽤变⻓编码[1]uint32
uint64使⽤变⻓编码[1]uint64
sint32使⽤变⻓编码[1]。符号整型。负值的编码效率⾼于常规的 int32 类型int32
sint64使⽤变⻓编码[1]。符号整型。负值的编码效率⾼于常规的 int64 类型int64
fixed32定⻓ 4 字节。若值常⼤于2^28 则会⽐ uint32 更⾼效uint32
fixed64定⻓ 8 字节。若值常⼤于2^56 则会⽐ uint64 更⾼效uint64
sfixed32定长4字节int32
sfixed64定长8字节int64
boolbool
string包含 UTF-8 和 ASCII 编码的字符串,⻓度不能超过2^32string
bytes可能包含任意的字节序列但长度不能超过2^32string
  • 注意:【1】变长编码是指:经过protobuf编码后,原本4字节或8字节的数可能会被变为其他字节数。

更新contacts.proto, 新增姓名、年龄字段:

// 首行:语法指定行(去除注释外的第一行)
/*首行:语法指定行(去除注释外的第一行)*/
syntax = "proto3"; // 需要指定为proto3,否则默认为proto2语法
package contacts; // package是命名空间

// 定义联系人message
// 结构中,给每个成员都赋值,这是语法必须规定,作为字段编号, 用来标识字段
message PeopleInfo
{
    string name = 1; // 姓名
    int32 age = 2;   // 年龄
}

字段唯一编号是有一定范围的:

1 ~ 536,870,911(2^29 - 1),其中19000~19999不可用

19000~19999不可用是因为:在PB协议实现中,对这些数进行了预留。如果非要在.proto文件中使用这些预留标识号,例如将name字段的编号设置为19000,编译时机会报警。

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

编译命令

编译命令的格式为:

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.pb.h文件和contacts.pb.cc文件,分别用来存放类的声明和类的实现。

Contacts.pb.h部分demo展示:

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代码就是对类声明方法的一些实现,这里就不再展示了。

序列化方法

在消息类的⽗类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成员函数,因为序列化不会改变类对象的内容, 而是将序列化的结果保存到函数入参指定的地址中。

序列化与反序列化的使用

创建⼀个测试⽂件 main.cc,⽅法中我们实现:

  • 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。
  • 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
#include <iostream>
#include <string>
#include "contacts.pb.h"

int main()
{
    std::string msg;
    // 对一个联系人的信息使用 PB 进行序列化,并将结果答应出来
    contacts::PeopleInfo people;
    people.set_name("张三");
    people.set_age(20);
    if(!people.SerializeToString(&msg))
    {
        std::cerr << "序列化联系人失败!" << std::endl;
        return -1;
    }
    std::cout << "序列化成功,结果: " << msg << std::endl;
    // 对序列化后的内容使用 PB 进行反序列化,解析出联系人信息并打印出来
    contacts::PeopleInfo people_result;
    if(!people_result.ParseFromString(msg))
    {
        std::cerr << "反序列化联系人失败!" << std::endl;
        return -1;
    }
    std::cout << "反序列化成功! " << std::endl
              << "姓名: " << people_result.name() << std::endl
              << "年龄: " << people_result.age() << std::endl;

    return 0;
}

然后我们进行编译,因为使用了protobuf,而protobuf实际上使用了C++11的新特性:

  • -std=c++11: 必加,使用C++11语法。
  • -lprotobuf:必加,链接protubuf库。

在这里插入图片描述
在这里插入图片描述
由于 ProtoBuf 是把联系⼈对象序列化成了⼆进制序列,这⾥⽤ string 来作为接收⼆进制序列的容器。所以在终端打印的时候会有换⾏等⼀些乱码显⽰。所以相对于 xml 和 JSON 来说,因为被编码成⼆进制,破解成本增⼤,ProtoBuf 编码是相对安全的。

小结

在这里插入图片描述

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

总的来说ProtoBuf 是需要依赖通过编译⽣成的头文件和源文件来使用的。



http://www.kler.cn/a/550407.html

相关文章:

  • 事件传递和监控
  • 《Stable Diffusion绘画完全指南:从入门到精通的Prompt设计艺术》-配套代码示例
  • isp专业名词-sensor摄像头没有AEC 功能
  • 解决IDEA报错:java 找不到符号
  • Haskell语言的物联网
  • OlympicArena 论文简介
  • 开发一个音响控制板程序,需要从硬件架构设计、通信协议选择、核心功能实现三个层面进行系统化开发。以下是基于工业级开发流程的实施方案
  • 云平台结合DeepSeek的AI模型优化实践:技术突破与应用革新
  • 【leetcode】200.岛屿数量(DFS入门)
  • 科技云报到:科技普惠潮流渐起,“开源”将带我们走向何方?
  • HTTP协议 (爬虫)
  • docker批量pull/save/load/tag/push镜像shell脚本
  • 【Unity URP】PBR框架下的NPR 角色渲染 以《少女前线2:追放》为例
  • MongoDB索引介绍
  • Visual Studio Code使用ai大模型编成
  • 关于视频去水印的一点尝试
  • [250217] x-cmd 发布 v0.5.3:新增 DeepSeek AI 模型支持及飞书/钉钉群机器人 Webhook 管理
  • centos部署open-webui
  • OpenCV图像基本操作
  • github上文件过大无法推送问题