前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >【ProtoBuf】proto3语法(一)

【ProtoBuf】proto3语法(一)

作者头像
用户11029129
发布2025-02-26 08:24:41
发布2025-02-26 08:24:41
4700
代码可运行
举报
文章被收录于专栏:编程学习编程学习
运行总次数:0
代码可运行

【ProtoBuf】proto3语法(一)

proto3语法解析

在语法详解部分。这个部分会对通讯录进⾏多次升级,使⽤ 2.x表⽰升级的版本,最终将会升级如下内容:

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

字段规则

消息的字段可以⽤下⾯⼏种规则来修饰:

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

更新 contacts.proto , PeopleInfo 消息中新增 phone_numbers 字段,表⽰⼀个联系⼈有多个号码,可将其设置为 repeated,写法如下:

代码语言:javascript
代码运行次数:0
复制
syntax = "proto3";
package contacts;

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

消息类型的定义与使用

在单个 .proto ⽂件中可以定义多个消息体,且⽀持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复(不同作用域)。更新 contacts.proto,我们可以将 phone_number 提取出来,单独成为⼀个消息:

代码语言:javascript
代码运行次数:0
复制
syntax = "proto3";
package contacts2;

message Phone
{
    string number  = 1;
}

// 定义联系人message
message PeopleInfo
{
    string name = 1;    // 姓名
    int32 age = 2;      // 年龄
    // repeated string phone_numbers = 3; // 手机号码
    repeated Phone phone = 3;
}
  • 消息类型可作为字段类型使用:
代码语言:javascript
代码运行次数:0
复制
message PeopleInfo
{
    string name = 1;    // 姓名
    int32 age = 2;      // 年龄
    // repeated string phone_numbers = 3; // 手机号码

    message Phone
    {
        string number  = 1;
    }
    repeated Phone phone = 3;
}
  • 可以导入其他 .proto 文件的消息类型并使用:

Phone消息定义在phone.proto中:

代码语言:javascript
代码运行次数:0
复制
syntax = "proto3";
package phone;

message Phone
{
    string number  = 1;
}

如果想要在contacts.proto文件中使用Phone message类型字段,则:

代码语言:javascript
代码运行次数:0
复制
syntax = "proto3";
package contacts2;

import "Phone.proto";// 使用import 将 Phone.proto 文件导入进来

// 定义联系人message
message PeopleInfo
{
    string name = 1;    // 姓名
    int32 age = 2;      // 年龄
    repeated phone.Phone payphone = 3;
}
  • 注意:在proto3文件中可以导入proto2消息类型并使用它们,反之亦然。

我们使用嵌套式的作为演示,PeopleInfo里的字段已经差不多了,这样每个人的信息结构也就出来了,随后我们应该定义一个message 作为通讯录,通讯录里是一个数组,数组每一个元素都是一个PeopleInfo:

代码语言:javascript
代码运行次数:0
复制
syntax = "proto3";
package contacts2;

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

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

我们来看一下生成的cpp代码中有什么:

当然,我们的关注点不在这里,我们进入到一个类当中(PeopleInfo_Phone类中,这里生成的代码有报错语法提示,是因为插件原因,不用在意,实际上没有任何问题):

  • clear_number():对字段进行清空
  • number():省略了get前缀,实际是get方法,获取number字段
  • set_number():设置number字段

以上几种方法是基本message常用方法,而在PeopleInfo中因为声明了Phone字段为 repeated 类型,所以会多几个不同的方法:

这样,带有repeated声明的成员处理方法我们也就清楚了。

编写通讯录demo

新增/读取联系人并进行序列化

对通讯录序列化有三点:

  • 读取本地已存在的联系人文件(不存在则创建该文件)
代码语言:javascript
代码运行次数:0
复制
#define FILE_SAVE "contacts.bin"// 文件保存的文件名

int main()
{
    contacts2::Contacts contacts; // 定义通讯录方法
    // 1. 读取本地已存在的联系人文件
    fstream input(FILE_SAVE, ios::in | ios::binary);
    if(!input)
    {
        cout << "contacts.bin not found, create new file!" << endl;
    }
    else if(contacts.ParseFromIstream(&input))
    {
        cerr << "parse error!" << endl;
        input.close();
        return -1;
    }
    return 0;
}

contacts2为contacts.pb.h中的命名空间,由于PB序列化之后是二进制文件,所以从文件中读取数据使用ios::binary,读取成功之后,通过contacts.pb.h中提供的解析二进制序列方法,将通讯录数据序列化为二进制文件,并保存在 contacts.bin 文件中。

  • 向通讯录中添加一个联系人
代码语言:javascript
代码运行次数:0
复制
// 联系人信息类型为PeopleInfo类型
void AddPeopleInfo(contacts2::PeopleInfo* people)
{
    cout << "---------------新增联系人---------------" << endl;

    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people->set_name(name);// 调用set方法设置联系人名称
    cout << "请输入联系人年龄: ";

    int age;
    cin >> age;
    people->set_age(age);// 调用set方法设置联系人年龄
    cin.ignore(256, '\n');// 防止\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;
}

int main()
{
	// ...
	
	// 2. 向通讯录中添加一个联系人
    AddPeopleInfo(contacts.add_contacts());
	return 0;
}

使用 cin.ignore(256, '\n') 接口,目的是为了不让’\n’影响到接下来的输入。首先来解释第二个参数,第二个参数的含义是遇到 ‘\n’ 则会停止清空缓冲区(前面的内容全部清除,包括\n)。第一个参数的含义是如果清除了256个字符后还没有遇到 ‘\n’,则也会停下来不再清楚缓冲区。

之后就进行循环输入电话号码,如果想要停止输入(输入为空),则按两次回车即可终止。而由于电话信息是一个数组,所以一个电话信息在存储之前需要先添加一个数组元素,即调用add_phone()函数,然后再调用set函数将新输入的电话号码保存到数组中。

  • 将通讯录写入到本地文件中
代码语言:javascript
代码运行次数:0
复制
int main()
{
	// 3. 将通讯录写入到本地文件中
    fstream output(FILE_SAVE, ios::out | ios::trunc | ios::binary);
    if(!contacts.SerializeToOstream(&output))
    {
        cerr << "write error!" << endl;
        input.close();
        output.close();
        return -1;
    }
    cout << "write success!" << endl;
    input.close();
    output.close();
	return 0;
}

最后将通讯录写入到本地文件中,写入之前我们先进行序列化,依旧使用PB提供的序列化函数来进行序列化:

编写makefile对通讯录项目做编译:

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

.PHONY:clean
clean:
	rm -f write 
代码语言:javascript
代码运行次数:0
复制
#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

#define FILE_SAVE "contacts.bin"// 文件保存的文件名

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');

    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;
}

int main()
{
    contacts2::Contacts contacts; // 定义通讯录方法
    // 1. 读取本地已存在的联系人文件
    fstream input(FILE_SAVE, ios::in | ios::binary);
    if(!input)
    {
        cout << "contacts.bin not found, create new file!" << endl;
    }
    else if(contacts.ParseFromIstream(&input))
    {
        cerr << "parse error!" << endl;
        input.close();
        return -1;
    }

    // 2. 向通讯录中添加一个联系人
    AddPeopleInfo(contacts.add_contacts());
    // 3. 将通讯录写入到本地文件中
    fstream output(FILE_SAVE, ios::out | ios::trunc | ios::binary);
    if(!contacts.SerializeToOstream(&output))
    {
        cerr << "write error!" << endl;
        input.close();
        output.close();
        return -1;
    }
    cout << "write success!" << endl;
    input.close();
    output.close();
    // google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

接下来就使用make来编译文件,然后运行程序:

我们发现生成了这个文件,并且记录着我们之前输入的数据,不过这里格式不对,因为是序列化为了二进制文件,而我们不能直接读取二进制文件,我们可以通过 hexdump 指令来查看二进制序列:

  • hexdump :用来查看二进制序列的命令,有不同的选项可以将二进制序列以十六进制形式进行输出。
  • C选项: 以标准格式显示包含地址和 ASCII 值的内容。每一行显示 16 字节的十六进制数字以及对应的 ASCII 字符。
  • n <数值>选项: 仅输出前 <数值> 字节。
  • v选项: 显示所有字节,包括重复的字节。

当然,如果你观看别人用PB写的C++程序,可能会看到这样一句宏定义在main函数开头:

代码语言:javascript
代码运行次数:0
复制
GOOGLE_PROTOBUF_VERIFY_VERSION;
  • GOOGLE_PROTOBUF_VERIFY_VERSION : 验证没有意外链接到与编译的头⽂件不兼容的库版本。如果检测到版本不匹配,程序将中⽌。注意,每个 .pb.cc ⽂件在启动时都会⾃动调⽤此宏。在使⽤ C++ Protocol Buffer 库之前执⾏此宏是⼀种很好的做法,但不是绝对必要的。

用人话来说就是可能使用PB版本不匹配,而版本不匹配是会报错的,所以在启动程序之前添加这个宏可以用来检测使用的PB版本是否一致。

而我们在结束时注释了一个函数:

代码语言:javascript
代码运行次数:0
复制
google::protobuf::ShutdownProtobufLibrary();
  • google::protobuf::ShutdownProtobufLibrary() : 在程序结束时调⽤这个接口,是为了删除 Protocol Buffer 库分配的所有全局对象。对于⼤多数程序来说这是不必要的,因为该过程⽆论如何都要退出,并且操作系统将负责回收其所有内存。但是,如果你使⽤了内存泄漏检查程序,该程序需要释放每个最后对象,或者你正在编写可以由单个进程多次加载和卸载的库,那么你可能希望强制使⽤ Protocol Buffers 来清理所有内容。

读取二进制数据并进行反序列化

我们新创建一个read.cc文件用来从contacts.bin文件中进行读取二进制数据并进行反序列化解析,将makefile文件更新:

代码语言:javascript
代码运行次数:0
复制
all:write read
write:write.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf
read:read.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY:clean
clean:
	rm -f write read

接下来对本地通讯录的操作有两步:

  • 读取本地已经存在的通讯录文件
代码语言:javascript
代码运行次数:0
复制
#define FILE_READ "contacts.bin"// 文件保存的文件名

int main()
{
	contacts2::Contacts contacts; // 定义通讯录对象
    // 1. 读取本地已存在的通讯录文件
    fstream input(FILE_READ, ios::in | ios::binary);
    if(!contacts.ParseFromIstream(&input))
    {
        cerr << "parse error!" << endl;
        input.close();
        return -1;
    }
	return 0;
}

来解析通讯录中的消息,与write.cc开始相同

  • 打印通讯录列表
代码语言:javascript
代码运行次数:0
复制
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;
        }
        cout << "-----------------通讯录尾-----------------" << endl;
    }
}

int main()
{
	// ...
	// 2. 打印通讯录列表
    PrintContacts(contacts);
	return 0;
}

由于通讯录message中只有一个PeopleInfo字段并且是数组类型,所以我们直接使用contacts()来获取数组中的元素值,即每一个联系人的基本信息。然后打印出联系人的姓名和年龄信息。

而每个人的手机号信息都是一个数组,所以我们需要对每一个联系人的手机号数组进行遍历,将所有手机号给打印出来,因为phone也是一个数组,所以可以直接调用 people.phone() 来获取手机号数组。

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 【ProtoBuf】proto3语法(一)
  • proto3语法解析
    • 字段规则
    • 消息类型的定义与使用
    • 编写通讯录demo
      • 新增/读取联系人并进行序列化
      • 读取二进制数据并进行反序列化
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档