首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【protobuf】二、proto3语法详解①

【protobuf】二、proto3语法详解①

作者头像
利刃大大
发布2025-05-22 13:36:17
发布2025-05-22 13:36:17
4240
举报
文章被收录于专栏:csdn文章搬运csdn文章搬运

前言

​ 在语法详解部分,依旧使用 项目推进 的方式完成教学。这个部分会对通讯录进行多次升级,使用 2.x 表示升级的版本,最终将会升级如下内容:

  • 不再打印联系人的序列化结果,而是将通讯录序列化后并写入文件中。
  • 从文件中将通讯录解析出来,并进行打印。
  • 新增联系人属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注。

Ⅰ. 字段规则

消息的字段可以用下面几种规则来修饰:

  • singular:消息中可以包含该字段 零次或一次。 在 proto3 语法中,字段默认使用该规则
  • repeated:消息中可以包含该字段 任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组

​ 下面我们更新之前写的 contacts.proto 文件,在 PeopleInfo 消息类中新增 Phone 类型的字段并且将其设置为 repeated 规则,表示一个联系人有多个号码,写法如下:

代码语言:javascript
复制
syntax = "proto3";
package contacts1;

// 定义电话号码信息
message Phone {
    string number = 1; // 不同信息类的字段编号是可以相同的
}

message PeopleInfo {
    string name = 1;
    int32 age = 2;

    repeated Phone phone = 3; // 设置为repeated规则,表示可以有多个电话号码
}

Ⅱ. 消息类型的定义和使用

1、定义

​ 在 单个 .proto 文件中可以定义多个消息体,且 支持定义任意多层的「嵌套」类型消息体

​ 并且每个消息体中的字段编号可以重复。

代码语言:javascript
复制
syntax = "proto3";
package contacts1;

// -------------------------- 1. 嵌套写法 -------------------------
message PeopleInfo {
    string name = 1;
    int32 age = 2;

    message Phone {
        string number = 1; // 不同信息类的字段编号是可以相同的
    }
}

 // -------------------------- 2. ⾮嵌套写法 -------------------------
message Phone {
    string number = 1; // 不同信息类的字段编号是可以相同的
}

message PeopleInfo {
    string name = 1;
    int32 age = 2;
}

2、使用

1️⃣消息类型可作为字段类型使⽤
代码语言:javascript
复制
syntax = "proto3";
package contacts1;

// -------------------------- 1. 嵌套写法 -------------------------
message PeopleInfo {
    string name = 1;
    int32 age = 2;

    message Phone {
        string number = 1;
    }
    repeated Phone phone = 3; // 在PeopleInfo需要注意编号
}

// -------------------------- 2. ⾮嵌套写法 -------------------------
message Phone {
    string number = 1; 
}
message PeopleInfo {
    string name = 1;
    int32 age = 2;
    
    repeated Phone phone = 3; // 在PeopleInfo需要注意编号
}
2️⃣可导入其他 .proto 文件的消息并使用 – import

​ 通常情况下,使用 import 关键字在一个 .proto 文件中引入另一个 .proto 文件,这样可以将消息类型分割为多个文件,使代码更加模块化和可维护。

​ 下面是注意事项:

  • 若要导入的消息在其文件中 声明了命名空间,则在使用该消息时候需要使用 命名空间.消息类型 格式进行定义使用。
  • proto3 文件中可以导入 proto2 消息类型并使用它们,反之亦然。

​ 假设现在 Phone 消息类是定义在别的 proto 文件中,如下面的 phone.proto 文件:

代码语言:javascript
复制
syntax = "proto3";
package phone;

message Phone {
    string number = 1; 
}

​ 然后我们在 contacts.proto 中的 PeopleInfo 使用 Phone 消息类:

代码语言:javascript
复制
syntax = "proto3";
package contacts1;

import "phone.proto"; // 引入Phone文件

message PeopleInfo {
    string name = 1;
    int32 age = 2;
    
    // 若引入的⽂件声明了package,则在使⽤该消息类时,需要⽤ “命名空间.消息类型” 格式定义
    repeated phone.Phone phone = 3; 
}

3、创建通讯录 2.0 版本的 .proto 文件

​ 下面我们先来完成「前言」中提到的前两个要求,也就是结合文件操作实现通讯录的读写、序列化和反序列化操作,其中我们定义第三个要求中的姓名、年龄、电话号码字段,其它的字段得等我们后面讲「特殊类型」才能进行定义!

​ 所以通讯录 2.x 的需求是向文件中写入通讯录列表,以上我们只是定义了一个联系人的消息,并不能存放通讯录列表,所以还需要再完善一下 contacts.proto

代码语言:javascript
复制
syntax = "proto3";
package contacts2;

// 联系人
message PeopleInfo {
    string name = 1; // 姓名
    int32 age = 2;	 // 年龄
    
    message Phone {
        string number = 1; // 电话号码
    }
    repeated Phone phone = 3; 
}

// 通讯录
message Contacts {
    repeated PeopleInfo people = 1;
}

​ 通过我们前面学的编译语法进行编译:

代码语言:javascript
复制
protoc --cpp_out=. contacts.proto

​ 编译之后就能得到 contacts.pb.h 以及 contacts.pb.cc 文件,我们打开 contacts.pb.h 文件可以看到三个很眼熟的类:

​ 没错,它们就是我们在 proto 中的消息体,protobuf 会自动将它们转化为对应语言的类!

​ 然后我们再观察一下三个类内一些常用的接口:

代码语言:javascript
复制
// 新增了 PeopleInfo_Phone 类
class PeopleInfo_Phone final : public ::PROTOBUF_NAMESPACE_ID::Message 
{
public:
	using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
    void CopyFrom(const PeopleInfo_Phone& from);
    using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
    void MergeFrom( const PeopleInfo_Phone& from) {
    	PeopleInfo_Phone::MergeImpl(*this, from);
    }
    static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
    	return "PeopleInfo.Phone";
    }

	// string number = 1;
    void clear_number();
    const std::string& number() const;
    template <typename ArgT0 = const std::string&, typename... ArgT>
    void set_number(ArgT0&& arg0, ArgT... args);
    std::string* mutable_number();
    PROTOBUF_NODISCARD std::string* release_number();
    void set_allocated_number(std::string* number);
};

// 更新了 PeopleInfo 类
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";
	}

	typedef PeopleInfo_Phone Phone;
	// repeated .PeopleInfo.Phone phone = 3;
	int phone_size() const;
	void clear_phone();
	::PeopleInfo_Phone* mutable_phone(int index);
	::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo_Phone >*
		mutable_phone();
	const ::PeopleInfo_Phone& phone(int index) const;
	::PeopleInfo_Phone* add_phone();
	const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo_Phone >&
		phone() const;
	};

// 新增了 Contacts 类
class Contacts final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
	using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
	void CopyFrom(const Contacts& from);
	using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
	void MergeFrom( const Contacts& from) {
		Contacts::MergeImpl(*this, from);
	}
	static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
		return "Contacts";
	}

	// repeated .PeopleInfo people = 1;
	int people_size() const;
	void clear_people();
	::PeopleInfo* mutable_people(int index);
	::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo >* mutable_people();
	const ::PeopleInfo& people(int index) const;
	::PeopleInfo* add_people();
	const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo >& people() const;

从上述的例子中可以看到:

  • 每个字段都有一个 clear_ 方法,可以将字段重新设置回 empty 状态。
  • 每个字段都有设置和获取的方法, 获取方法的方法名称与小写字段名称完全相同。但如果是消息类型的字段,其设置方法为 mutable_ 方法,返回值为消息类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。
  • 对于使用 repeated 修饰的字段,也就是数组类型protobuf 为我们提供了 add_ 方法来新增一个值,并且提供了 _size 方法来查看数组存放元素的个数

4、通讯录 2.0 版本的读写实现 – 第一种验证方式

下面我们通过实现以下需求,来学习对这些头文件中函数的使用:

  1. 不再打印联系人的序列化结果,而是将通讯录序列化后并写入文件中
  2. 从文件中将通讯录解析出来,并进行打印

​ 首先是 write.cc 文件,来实现第一个需求,首先就是要读取已有的通讯录,然后再添加联系人,最后写入文件中,这用到 fstream 对象来操作比较方便,细节都在下面代码注释中

代码语言:javascript
复制
// write.cc
#include "contacts.pb.h"
#include <iostream>
#include <fstream>
using namespace std;

void addPeople(::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'); // 去除缓冲区中的回车(而上面的getline则不需要,因为getline会清除缓冲区的回车)

    for(int i = 1; ; i++)
    {
        cout << "请输入联系人电话" << i << "(只输入回车则退出):";
        string number;
        getline(cin, number);
        if(number.empty())
            break;

        auto phone = people->add_phone();
        phone->set_number(number);
    }

    cout << "-----------添加联系⼈成功-----------" << endl;
}

int main()
{
    contacts2::Contacts contacts;

    // 1. 读取本地已存在的通讯录
    fstream input("./contacts.bin", ios::in | ios::binary); // protobuf是二进制操作
    if(!input.is_open())
        cout << "contacts.bin not found, create new file!" << endl;
    else if(!contacts.ParseFromIstream(&input))
    {
        // 这个操作建议学起来!
        // 直接通过ParseFromIstream()获取文件流进行反序列化,还能判断是否成功,一步到位!
        cerr << "parse contacts.bin error!" << endl;
        input.close();
        return -1;
    }   
    input.close();

    // 2. 向通讯录中添加一个联系人
    addPeople(contacts.add_people());

    // 3. 将通讯录写入本地文件中
    fstream output("./contacts.bin", ios::out | ios::binary | ios::trunc); // 记得要覆盖写
    if(!contacts.SerializeToOstream(&output))
    {
        // 上面的操作,这里同理!
        cerr << "write contacts.bin error!" << endl;
        output.close();
        return -1;
    }
    cout << "write success" << endl;
    output.close();
    return 0;
}


// makefile文件:
write:write.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
	rm -f write

​ 这里介绍一个 hexdump 工具,可以将 .bin 文件中的二进制数据转化为十六进制和 ASCII 码方便我们观察,命令如下所示:

代码语言:javascript
复制
hexdump -C 二进制文件

​ 然后就是 read.cc 文件,负责获取通讯录序列化之后打印出各个联系人的信息:

代码语言:javascript
复制
// read.cc
#include "contacts.pb.h"
#include <iostream>
#include <fstream>
using namespace std;

void printContacts(contacts2::Contacts& contacts)
{
    for(int i = 0; i < contacts.people_size(); ++i)
    {
        cout << "------------联系⼈" << i + 1 << "------------" << endl;

        cout << "名称:" << contacts.people(i).name() << endl;
        cout << "年龄:" << contacts.people(i).age() << endl;
        
        for(int j = 0 ; j < contacts.people(i).phone_size(); ++j)
            cout << "手机号码" << j + 1 << ":" << contacts.people(i).phone(j).number() << endl;

        cout << "------------------------------" << endl;
    }
}

int main()
{
    contacts2::Contacts contacts;

    // 1. 读取本地已存在的通讯录(默认存在,故不需要判断是否存在)
    fstream input("./contacts.bin", ios::in | ios::binary); // protobuf是二进制操作
    if(!contacts.ParseFromIstream(&input))
    {
        // 这个操作建议学起来!
        // 直接通过ParseFromIstream()获取文件流进行反序列化,还能判断是否成功,一步到位!
        cerr << "parse contacts.bin error!" << endl;
        input.close();
        return -1;
    }   
    input.close();

    // 2. 打印通讯录列表
    printContacts(contacts);
    return 0;
}

5、decode选项 – 第二种验证方式

​ 其实我们可以不用像上面那么麻烦专门写个程序查看当前序列化的内容,可以直接使用 protoc -h 命令来查看 ProtoBuf 为我们提供的所有命令选项。

​ 其中 ProtoBuf 提供一个 命令选项 --decode ,表示从标准输入中读取给定类型的二进制消息,并将其以文本格式写入标准输出。 注意消息类型必须在 .proto 文件或导入的文件中定义!

​ 非常方便是吧!

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • Ⅰ. 字段规则
  • Ⅱ. 消息类型的定义和使用
    • 1、定义
    • 2、使用
      • 1️⃣消息类型可作为字段类型使⽤
      • 2️⃣可导入其他 .proto 文件的消息并使用 – import
    • 3、创建通讯录 2.0 版本的 .proto 文件
    • 4、通讯录 2.0 版本的读写实现 – 第一种验证方式
    • 5、decode选项 – 第二种验证方式
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档