首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Protobuf】一、初始Protobuf && 快速上手

【Protobuf】一、初始Protobuf && 快速上手

作者头像
利刃大大
发布2025-05-22 13:18:20
发布2025-05-22 13:18:20
49000
代码可运行
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运
运行总次数:0
代码可运行

Ⅰ. 序列化的概念

1、什么是序列化

  • 序列化:将对象转换为字节序列的过程
  • 反序列化:将字节序列恢复为对象的过程

2、为什么需要序列化

下面举两个例子:

  1. 存储数据:当你想把的内存中的对象状态保存到⼀个⽂件中或者存到数据库中时,就需要进行序列化操作。
  2. ⽹络传输:⽹络可以直接传输数据,但是⽆法直接传输对象,所以要在传输前序列化,传输完成后反序列化成对象,例如我们之前学习过 socket 编程中发送与接收数据。

3、如何实现序列化

  • Json
  • XML
  • ProtoBuf

Ⅱ. 什么是ProtoBuf

从官方得到的解释是这样子的:

  • Protocol BuffersGoogle 的⼀种语⾔⽆关、平台⽆关、可扩展的序列化结构数据的⽅法,它可⽤于(数据)通信协议、数据存储等。
  • Protocol Buffers 类⽐于 XML,是⼀种灵活,⾼效,⾃动化机制的结构数据序列化⽅法,但是⽐ XML 更⼩、更快、更为简单。
  • 你可以定义数据的结构,然后使⽤特殊⽣成的源代码轻松的在各种数据流中使⽤各种语⾔进⾏编写和读取结构数据。你甚⾄可以更新数据结构,⽽不破坏由旧数据结构编译的已部署程序。

简单来讲, ProtoBuf 是让结构数据序列化的方法,其具有以下特点:

  • 语言无关、平台无关:即 ProtoBuf ⽀持 JavaC++Python 等多种语⾔,⽀持多个平台。
  • 高效:即⽐ XML 更⼩、更快、更为简单。
  • 扩展性、兼容性好:你可以更新数据结构,⽽不影响和破坏原有的旧程序。

Ⅲ. ProtoBuf的使用特点

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

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

快速上手前言

在快速上手中,会编写第一版本的通讯录 1.0。在通讯录 1.0 版本中,将实现:

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

通过通讯录 1.0,我们便能了解使⽤ ProtoBuf 初步要掌握的内容,以及体验到 ProtoBuf 的完整使用流程。

Ⅳ. 创建 .proto 文件

​ 下面先给出通讯录 1.0contacts.proto 文件的内容:

代码语言:javascript
代码运行次数:0
运行
复制
// 非注释的首行:声明proto的语法版本
syntax = "proto3";

// 指定对应的包名,防止冲突
package contacts;

// 定义联系人消息(下面的等号其实不是赋值,而是指定标记,实现protobuf的高效率特点)
message PeopleInfo
{
	string name = 1;
	int32 age = 2;
}

​ 下面我们来解释一下上述内容!

1、文件规范

  1. 创建 .proto ⽂件时,文件命名应该使用全小写字母命名,多个字⺟之间⽤ _ 连接。 例如:lower_snake_case.proto
  2. 书写 .proto ⽂件代码时,应使⽤ 两个空格的缩进,而不是一个 TAB 的缩进!
  3. 向⽂件添加注释,可使⽤ // 或者 /* ... */

2、指定proto3语法

Protocol Buffers 语言版本3,简称 proto3,是 .proto 文件最新的语法版本。proto3 简化了 Protocol Buffers 语言,既易于使用,又可以在更广泛的编程语言中使用。

​ 在 .proto 文件中,要 使用 syntax = "proto3"; 来指定文件语法为 proto3,并且 必须写在除去注释内容的第一行。 如果没有指定,编译器会使用 proto2 语法。内容如下:

代码语言:javascript
代码运行次数:0
运行
复制
// 非注释的首行:声明proto的语法版本
syntax = "proto3";

3、package声明符

package 声明 的作用是为了指定生成的代码的命名空间。这在 防止消息类型之间的命名冲突 是非常重要的。考虑到 Protocol Buffers 可以用于定义通信协议,而在这些协议中可能会定义相同的消息类型名称,因此必须有一种机制来确保不同的消息类型之间不会发生命名冲突。

通过指定 package,你可以为消息类型指定一个命名空间。这意味着不同的消息类型可以在不同的命名空间中定义相同的名称而不会发生冲突,因为它们在不同的包中。

​ 例如,假设有两个文件 A.protoB.proto,它们都定义了一个名为 Message 的消息类型。如果这两个文件没有使用 package 声明来指定命名空间,那么生成的代码可能会因为命名冲突而出现问题。但是,如果它们分别使用了不同的 package 声明,比如 package Apackage B,那么生成的代码会将它们放置在不同的命名空间中,从而避免了冲突。

​ 所以我们在通讯录 1.0 版本中可以指定以下命名空间:

代码语言:javascript
代码运行次数:0
运行
复制
// 指定对应的包名,防止冲突
package contacts;

4、定义消息

​ 消息(message): 表示要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。

​ 这⾥再提⼀下为什么要定义消息?在⽹络传输中,我们需要为传输双⽅定制协议。定制协议说⽩了就是定义结构体或者结构化数据,⽐如 tcpudp 报⽂就是结构化的。 ​ 再⽐如将数据持久化存储到数据库时,会将⼀系列元数据统⼀⽤对象组织起来,再进⾏存储。 ​ 所以 ProtoBuf 就是以 message 的⽅式来⽀持我们定制协议字段,后期帮助我们形成类和⽅法来使⽤。

​ 在通讯录 1.0 中我们就需要为“联系人”定义⼀个 message

代码语言:javascript
代码运行次数:0
运行
复制
message 消息类型名 {
	消息类型字段
}

​ 消息类型命名规范:使用驼峰命名法,首字母大写。如下所示:

代码语言:javascript
代码运行次数:0
运行
复制
// 定义联系人消息
message PeopleInfo {

}

​ 此外,一个 .proto 文件中是可以有多个 message 类的,并且 每个 message 类中的字段唯一编号是可以相同,因为这是互不影响的!

5、定义消息字段

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

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

​ 该表格展示了定义于消息体中的标量数据类型,以及编译 .proto 文件之后自动生成的类中与之对应的字段类型。在这里展示了与 C++ 语言对应的类型:(下面提到的变长编码,是指经过 protobuf 编码后,原本 4 字节或 8 字节的数可能会被变为其他字节数)

.proto 类型

强调内容

C++ 类型

double

double

float

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^56 则会比 uint64 更高效

uint64

sfixed32

定长 4 字节

int32

sfixed64

定长 8 字节

int64

bool

bool

string

包含 UTF-8 和 ASCII 编码的字符串,长度不能超过 2^32

string

bytes

可包含任意的字节序列但长度不能超过 2^32

string

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

代码语言:javascript
代码运行次数:0
运行
复制
// 定义联系人消息(下面的等号其实不是赋值,而是指定标记,实现protobuf的高效率特点)
message PeopleInfo
{
    string name = 1;
    int32 age = 2;
}

​ 在这里还要特别讲解⼀下字段唯一编号的范围:1 ~ 2^29-1其中 19000 ~ 19999 是不可用的。(19000 ~ 19999 不可用是因为:在 Protobuf 协议的实现中,对这些数进行了预留。如果非要在 .proto 文件中使用这些预留标识号,例如将 name 字段的编号设置为 19000,编译时就会报警)

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

Ⅴ. 编译 contacts.proto ⽂件,⽣成 C++ ⽂件

1、编译命令

​ 编译命令的格式如下所示:

代码语言:javascript
代码运行次数:0
运行
复制
protoc [--proto_path=IMPORT_PATH] --cpp_out=OUT_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 文件命令如下:

代码语言:javascript
代码运行次数:0
运行
复制
protoc --cpp_out=./contacts.proto

2、编译 .proto 文件后发生什么

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

​ 它们包含了以下内容 :

  • 编辑器会针对于每个 .proto ⽂件⽣成 .h.cc ⽂件,分别⽤来存放类的声明与类的实现。
  • 对于每个 message ,都会⽣成⼀个对应的消息类。
  • 在消息类中,编译器为每个字段提供了获取和设置⽅法,以及⼀下其他能够操作字段的⽅法,如 setget 函数等。

​ 下面是 contacts.pb.h 部分代码展示:

代码语言:javascript
代码运行次数:0
运行
复制
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);
};

上述的例子中:(contacts.pb.cc 中的代码就是对类声明方法的一些实现,在这里就不展开了)

  • 每个字段都有设置和获取的⽅法, getter 的名称与⼩写字段完全相同,setter ⽅法以 set_ 开头。
  • 每个字段都有一个 clear_ ⽅法,可以将字段重新设置回 empty 状态。

​ 到这里有同学可能就有疑惑了,那之前提到的序列化和反序列化方法在哪里呢?其实是 在消息类的父类 MessageLite 中,提供了读写消息实例的方法,包括序列化方法和反序列化方法,如下所示:

代码语言:javascript
代码运行次数:0
运行
复制
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 可以参⻅完整列表

3、序列化和反序列化的使用

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

  • 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。
  • 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <string>
#include "contacts.pb.h"

int main()
{
    std::string people_str;

    {
        // 对⼀个联系⼈的信息使⽤ PB 进行序列化,并将结果打印出来。
        contacts::PeopleInfo people;
        people.set_age(20);
        people.set_name("lirendada杨桐");
        if(!people.SerializeToString(&people_str))
        {
            std::cerr << "序列化联系人失败" << std::endl;
            return -1;
        }
        std::cout << "序列化联系人成功,结果为:" << people_str << std::endl;
    }

    std::cout << "----------------------------------------" << std::endl;

    {
        // 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
        contacts::PeopleInfo people;
        if(!people.ParseFromString(people_str))
        {
            std::cerr << "反序列化联系人失败" << std::endl;
            return -1;
        }
        std::cout << "反序列化联系人成功,结果为:" << std::endl 
            << "姓名:" << people.name() << std::endl
            << "年龄:" << people.age() << std::endl;
    }
    return 0;
}

​ 代码书写完成后,编译 main.cccontacts.pb.cc,生成可执行程序 main

代码语言:javascript
代码运行次数:0
运行
复制
g++ -o main main.cc contacts.pb.cc -std=c++11 -lprotobuf

注意事项:

  • -lprotobuf:必加,不然会有链接错误。
  • -std=c++11:必加,需要用到 c++11 的语法。

​ 最后看看运行结果:

代码语言:javascript
代码运行次数:0
运行
复制
// 运行结果:
序列化联系人成功,结果为:
lirendada杨桐
----------------------------------------
反序列化联系人成功,结果为:
姓名:lirendada杨桐
年龄:20

​ 由于 ProtoBuf 是把联系人对象序列化成了二进制序列,这里用 string 来作为接收二进制序列的容器。所以在终端打印的时候会有换行等一些乱码显示。

​ 所以相对于 xmlJSON 来说,因为 ProtoBuf 被编码成二进制,破解成本增大,ProtoBuf 编码是相对安全的

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-01-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ⅰ. 序列化的概念
    • 1、什么是序列化
    • 2、为什么需要序列化
    • 3、如何实现序列化
  • Ⅱ. 什么是ProtoBuf
  • Ⅲ. ProtoBuf的使用特点
  • 快速上手前言
  • Ⅳ. 创建 .proto 文件
    • 1、文件规范
    • 2、指定proto3语法
    • 3、package声明符
    • 4、定义消息
    • 5、定义消息字段
  • Ⅴ. 编译 contacts.proto ⽂件,⽣成 C++ ⽂件
    • 1、编译命令
    • 2、编译 .proto 文件后发生什么
    • 3、序列化和反序列化的使用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档