1. 字段规则

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

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

下面我们更新 contacts.proto,并在 PeopleInfo 消息中新增 phone_numbers 字段,表示一个联系人有多个号码,可将其设置为 repeated。

写法如下:

syntax = "proto3";
package contacts;

// 定义联系人message
message PeopleInfo {
    string name = 1;
    int32 age = 2;
    repeated string phone_numbers = 3;
}

2. 消息类型的定义与使用

2.1 定义

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

我们更新 contacts.proto,我们可以将 phone_number 提取出来,单独成为一个消息。

嵌套写法:

syntax = "proto3";
package contacts;

// 定义联系人message
message PeopleInfo {
    string name = 1;
    int32 age = 2;
    message Phone {
        string number = 1;
    }
}

非嵌套写法:

syntax = "proto3";
package contacts;

message Phone {
    string number = 1;
}

// 定义联系人message
message PeopleInfo {
    string name = 1;
    int32 age = 2;
}

2.2 使用

  • 消息类型可以作为字段类型使用

contacts.proto 文件中:

syntax = "proto3";
package contacts;

// 定义联系人message
message PeopleInfo 
{
    string name = 1;
    int32 age = 2;

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

例如 Phone 消息定义在 phone.proto 文件中:

syntax = "proto3";
package phone;

message Phone 
{
	string number = 1;  
}

然后在 contacts.proto 文件中引入 PeopleInfo 的 Phone 消息:

syntax = "proto3";
package contacts;

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

// 定义联系人message
message PeopleInfo 
{
    string name = 1;
    int32 age = 2;

    // 引入的文件声明了 package,使用消息时,需要用 命名空间.消息类型 格式
    repeated phone.Phone phone = 3; 
}

注意:在 proto3 文件中可以导入 proto2 消息类型并使用它们,反之亦然。

3. 创建通讯录 2.0 版本

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

syntax = "proto3";
package contacts;

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

    message Phone {
        string number = 1;      // 电话号码
    }
    repeated Phone phone = 3;   // 电话
}

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

然后在终端进行一次编译:

protoc --cpp_out=. contacts.proto

如下图所示:

在这里插入图片描述

编译后生成的 contacts.pb.hcontacts.pb.cc 文件会将上一篇文章中生成的旧文件覆盖掉。

下面是 contacts.pb.h 更新的部分代码展示:

// 新增了 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_Phone 类
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 Contactsfinal : 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 contacts = 1;
	int contacts_size() const;
	void clear_contacts();
	::PeopleInfo* mutable_contacts(int index);
	::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo >*
		mutable_contacts();
	const ::PeopleInfo& contacts(int index) const;
	::PeopleInfo* add_contacts();
	const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::PeopleInfo >&
		contacts() const;
};

上述的例子中:

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

4. 通讯录 2.0 的写入实现

write.cc 代码如下:

#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;

// 新增联系人
void AddPeopleInfo(contacts::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;
        }
        contacts::PeopleInfo_Phone *phone = people->add_phone();
        phone->set_number(number);
    }

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

int main()
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    contacts::ContactBook contacts;

    // 读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if (!input)
    {
        cout << "contacts.bin not find, create new file!" << endl;
    }
    else if (!contacts.ParseFromIstream(&input))
    {
        cerr << "parse error!" << endl;
        input.close();
        return -1;
    }

    // 向通讯录中添加一个联系人
    AddPeopleInfo(contacts.add_contacts());

    // 将通讯录写入本地文件中
    fstream output("contacts.bin", 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;
}

其中 main 函数里面两个宏的作用如下:

GOOGLE_PROTOBUF_VERIFY_VERSION:验证没有链接到与编译的头文件不兼容的库版本。
如果检测到版本不匹配,程序将中止。注意,每个 .pb.cc 文件在启动时都会自动调用此宏。
在使用 C++ Protocol Buffer 库之前执行此宏是一种很好的做法,但不是绝对必要的。

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

然后编写 makefile

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

.PHONY:clean
clean:
	rm -f write	

然后在终端执行 make 命令,运行可执行文件,如下图所示:

在这里插入图片描述

然后可以使用 hexdump 命令来查看二进制文件:

hexdump -C contacts.bin

结果如下:

在这里插入图片描述

5. 通讯录 2.0 的读取实现

read.cc 代码如下:

#include <iostream>
#include <fstream>
#include "contacts.pb.h"

using namespace std;
using namespace contacts; // 把命名空间展开

// 打印联系人列表
void PrintContacts(ContactBook& 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;

        for (int j = 0; j < people.phone_size() ; j++) 
        {
            const PeopleInfo_Phone& phone = people.phone(j);
            cout << "联系人电话" << j+1 << ":" << phone.number() << endl;
        }
    }  
} 


int main() 
{
    GOOGLE_PROTOBUF_VERIFY_VERSION;

    ContactBook contacts;

    // 读取本地已存在的通讯录文件(以二进制方式读取)
    fstream input("contacts.bin", ios::in | ios::binary);
    if (!contacts.ParseFromIstream(&input)) 
    {
        cerr << "parse error!" << endl;
        input.close();
        return -1;
    }
    // 打印通讯录列表
    PrintContacts(contacts);

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

继续编写 makefile

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

执行 make 命令以后,运行 read 文件,结果如下:

在这里插入图片描述

6. decode 验证方法

我们可以用 protoc -h 命令来查看 ProtoBuf 为我们提供的所有命令 option。

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

命令如下:

protoc --decode=contacts.ContactBook contacts.proto < contacts.bin

结果如下:

在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐