【protobuf】ProtoBuf——proto3语法详解、字段规则、消息类型的定义与使用、通讯录的写入和读取功能实现

文章目录

  • ProtoBuf
    • [5. proto3语法详解](#5. proto3语法详解)
      • [5.1 字段规则](#5.1 字段规则)
      • [5.2 消息类型的定义与使用](#5.2 消息类型的定义与使用)

ProtoBuf

5. proto3语法详解

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

(1)不再打印联系人的序列化结果,而是把通讯录序列化后写入文件。

(2)从文件中解析出通讯录,并予以打印。

(3)新增联系人属性,涵盖:姓名、年龄、电话信息、地址、其他联系方式、备注。

5.1 字段规则

消息的字段能够运用以下几种规则加以修饰:

singular :消息中能够包含该字段零次或者一次(不超过一次)。在 proto3 语法里,字段默认采用该规则。

我们在 Protobuf 定义的消息结构中,对于被标记为 singular 规则的字段,在一条消息里,这个字段可以不出现(即零次),也可以出现一次,但最多只能出现一次。

repeated :消息中能够包含该字段任意多次(包含零次),其中重复值的顺序会得以保留。

这意味着在我们定义的消息结构中,对于被标记为 "repeated" 规则的字段,在一条消息里,它可以出现零次、一次或者多次,没有数量上的限制。可以理解为定义了一个数组。

接下来我们就可以使用repeated字段在通讯录中添加我们的电话字段了:

cpp 复制代码
syntax = "proto3";
package contacts;

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

protoc编译文件:

bash 复制代码
protoc --cpp_out=. contacts.proto

查看contacts.pb.h文件,这就是自动生成的有关repeated的函数:

有关repeated类型的函数,下面以phone_numbers为例(自动生成的函数名和我们设置的字段名相关):

**  获取相关函数:**

int phone_numbers_size() const;:这个函数用于获取 phone_numbers 字段中元素的数量。

const std::string& phone_numbers(int index) const;:通过指定索引 index,获取 phone_numbers 中对应位置的元素,返回的是常量引用,意味着不能通过这个返回值修改元素。

**  清空相关函数:**

void clear_phone_numbers();:该函数用于清空 phone_numbers 字段中的所有元素。

**  修改相关函数:**

std::string* mutable_phone_numbers(int index);:通过索引获取 phone_numbers 中对应位置元素的指针,可通过这个指针修改对应位置的元素。

void set_phone_numbers(int index, const std::string& value);:通过指定索引和一个字符串常量值,设置 phone_numbers 中对应位置的元素。

void set_phone_numbers(int index, std::string&& value);:通过指定索引和一个右值引用的字符串,设置对应位置的元素。

void set_phone_numbers(int index, const char* value);:通过指定索引和一个字符指针,设置对应位置的元素。

void set_phone_numbers(int index, const char* value, size_t size);:通过指定索引、字符指针以及字符数量,设置对应位置的元素。

**  添加相关函数:**

std::string* add_phone_numbers();:用于向 phone_numbers 字段添加一个新元素,并返回指向新添加元素的指针,以便进行后续修改。

void add_phone_numbers(const std::string& value);:向 phone_numbers 字段添加一个字符串常量值。

void add_phone_numbers(std::string&& value);:向 phone_numbers 字段添加一个右值引用的字符串。

void add_phone_numbers(const char* value);:向 phone_numbers 字段添加一个通过字符指针表示的字符串。

void add_phone_numbers(const char* value, size_t size);:向 phone_numbers 字段添加一个通过字符指针和字符数量表示的字符串。

**  整体获取 / 修改相关函数:**

const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField < std::string> & phone_numbers() const;:获取整个 phone_numbers 字段的常量引用。

::PROTOBUF_NAMESPACE_ID::RepeatedPtrField < std::string> * mutable_phone_numbers();:获取整个 phone_numbers 字段的可修改指针。

5.2 消息类型的定义与使用

**  在单个 .proto 文件中可以定义多个消息体,且支持定义嵌套类型的消息(任意多层)。每个消息体的字段编号可以重复。**

单个 .proto 文件嵌套写法:

cpp 复制代码
syntax = "proto3";
package contacts;

// 定义联系⼈消息
message PeopleInfo {
    string name = 1; // 姓名
    int32 age = 2; // 年龄

    // 可以在消息字段中定义消息字段
    message Phone{ 
        string number = 1;   
    }
}

单个 .proto 文件非嵌套写法:

cpp 复制代码
syntax = "proto3";
package contacts;

message Phone{ 
    string number = 1;   
}

// 定义联系⼈消息
message PeopleInfo {
    string name = 1; // 姓名
    int32 age = 2; // 年龄
}

多个 .proto 文件导入头文件写法:

导入其它.proto文件:import path/xxx.proto,引入的文件声明了package,使用其类型时需要用 命名空间 . 消息类型 的格式。

cpp 复制代码
// 这个是a.proto文件
syntax = "proto3";
package A;

message Phone{ 
    string number = 1;   
}
cpp 复制代码
// 这个是contacts.proto文件
syntax = "proto3";
package contacts;

import "a.proto";

// 定义联系⼈消息
message PeopleInfo {
    string name = 1; // 姓名
    int32 age = 2; // 年龄

	// 引用包A,创建Phone类型消息字段,定义为phone
    A.Phone phone = 3; // 使用类外定义
}

我们的通讯录程序又得以再次的优化了,在 PeopleInfo 消息中:name:字符串类型,代表姓名。age:32 位整数类型,代表年龄。

**  嵌套的 Phone 消息,其中包含 number 字段,为字符串类型,代表电话号码。phone:Phone 类型的重复字段,代表电话信息。**

在 Contacts 消息中:contacts:PeopleInfo 类型的重复字段,代表通讯录中的联系人信息。

cpp 复制代码
syntax = "proto3";
package contacts;

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

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

编译再次生成我们所需要的 .h 和 .cc文件:

**  通讯录 2.0 的写入实现:**

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

// 新增联系⼈
void AddPeopleInfo(PeopleInfo *people_info_ptr)
{
    cout << "-------------新增联系⼈-------------" << endl;
    cout << "请输⼊联系⼈姓名: ";
    string name;
    getline(cin, name);
    people_info_ptr->set_name(name);
    cout << "请输⼊联系⼈年龄: ";
    int age;
    cin >> age;
    people_info_ptr->set_age(age);
    cin.ignore(256, '\n');
    for (int i = 1;; i++)
    {
        cout << "请输⼊联系⼈电话" << i << "(只输⼊回⻋完成电话新增): ";
        string number;
        getline(cin, number);
        if (number.empty())
        {
            break;
        }
        PeopleInfo_Phone *phone = people_info_ptr->add_phone();
        phone->set_number(number);
    }
    cout << "-----------添加联系⼈成功-----------" << endl;
}

int main(int argc, char *argv[])
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;
    if (argc != 2)
    {
        cerr << "Usage: " << argv[0] << " CONTACTS_FILE" << endl;
        return -1;
    }
    Contacts contacts;

    // 先读取已存在的 contacts
    fstream input(argv[1], ios::in | ios::binary);
    if (!input)
    {
        cout << argv[1] << ": File not found. Creating a new file." << endl;
    }
    else if (!contacts.ParseFromIstream(&input))
    {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }
    // 新增⼀个联系⼈
    AddPeopleInfo(contacts.add_contacts());
    // 向磁盘⽂件写⼊新的 contacts
    fstream output(argv[1], ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output))
    {
        cerr << "Failed to write contacts." << endl;
        input.close();
        output.close();
        return -1;
    }
    input.close();
    output.close();

    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

GOOGLE_PROTOBUF_VERIFY_VERSION 宏用于验证所链接的库版本与编译的头文件是否兼容。若检测到版本不匹配,程序会中止。每个 .pb.cc 文件启动时会自动调用此宏。在使用 C++ Protocol Buffer 库前执行该宏是良好做法。

在程序结束时可调用 ShutdownProtobufLibrary() 来删除 Protocol Buffer 库分配的所有全局对象。对多数程序而言这并非必要,因程序退出时操作系统会回收内存。但如果使用内存泄漏检查程序,或编写可被单个进程多次加载和卸载的库,可能需要强制 Protocol Buffers 清理所有内容。

**  Makefile:**

bash 复制代码
write:write.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY:clean
clean:
	rm -f write

接着就可以向我们指定的contacts.bin文件中写入信息了:

**  查看二进制文件:**

hexdump 是 Linux 中的二进制文件查看工具,能把二进制文件转换成 ASCII、八进制、十进制、十六进制格式来查看。其中 -C 选项意味着每个字节会以十六进制形式和对应的 ASCII 字符显示。

decode 是 ProtoBuf 中通过 protoc -h 命令可查看到的一个命令选项 --decode 。它的作用是从标准输入读取给定类型的二进制消息,并将其以文本格式输出到标准输出。而且,该消息的类型必须在 .proto 文件或导入的文件中有定义。

具体用法如下: protoc --decode=MESSAGE_TYPE MESSAGE_TYPE所在文件

bash 复制代码
protoc --decode=contacts.Contacts contacts.proto < contacts.bin

**  通讯录 2.0 的读取实现:**

cpp 复制代码
#include <iostream>
#include <fstream>
#include "contacts.pb.h"
using namespace std;
using namespace contacts;

//打印联系⼈列表
void PrintfContacts(const Contacts &contacts)
{
    for (int i = 0; i < contacts.contacts_size(); ++i)
    {
        const PeopleInfo &people = contacts.contacts(i);
        cout << "------------联系⼈" << i + 1 << "------------" << endl;
        cout << "姓名:" << people.name() << endl;
        cout << "年龄:" << people.age() << endl;
        int j = 1;
        for (const PeopleInfo_Phone &phone : people.phone())
        {
            cout << "电话" << j++ << ": " << phone.number() << endl;
        }
    }
}

int main(int argc, char *argv[])
{

    GOOGLE_PROTOBUF_VERIFY_VERSION;
    if (argc != 2)
    {
        cerr << "Usage: " << argv[0] << "CONTACTS_FILE" << endl;
        return -1;
    }
    // 以⼆进制⽅式读取 contacts
    Contacts contacts;
    fstream input(argv[1], ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input))
    {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }
    // 打印 contacts
    PrintfContacts(contacts);
    input.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

**  Makefile:**

bash 复制代码
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

我们可以读取到write写入文件的信息了:

相关推荐
ᅠᅠᅠ@3 分钟前
异常枚举;
开发语言·javascript·ecmascript
编程版小新9 分钟前
C++初阶:STL详解(四)——vector迭代器失效问题
开发语言·c++·迭代器·vector·迭代器失效
陈大爷(有低保)22 分钟前
UDP Socket聊天室(Java)
java·网络协议·udp
c4fx28 分钟前
Delphi5利用DLL实现窗体的重用
开发语言·delphi·dll
鸽芷咕1 小时前
【Python报错已解决】ModuleNotFoundError: No module named ‘paddle‘
开发语言·python·机器学习·bug·paddle
Jhxbdks1 小时前
C语言中的一些小知识(二)
c语言·开发语言·笔记
java6666688881 小时前
如何在Java中实现高效的对象映射:Dozer与MapStruct的比较与优化
java·开发语言
Violet永存1 小时前
源码分析:LinkedList
java·开发语言
代码雕刻家1 小时前
数据结构-3.1.栈的基本概念
c语言·开发语言·数据结构
Fan_web1 小时前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html