Protobuf:复杂类型接口
本博客基于proto3
语法,讲解protobuf
中的复杂类型。
package
在.proto
文件中,支持导入其它.proto
文件的内容,例如:
test.proto
:
cpp
syntax = "proto3";
package test_pkg;
import "other.proto";
message Person {
int32 id =1;
string name = 2;
other_pkg.Address addr = 3;
}
other.proto
:
cpp
syntax="proto3";
package other_pkg;
message Address {
string country = 1;
string city = 2;
}
在test.proto
文件中,使用了other.proto
的内容。想要实现多个文件的效果,需要通过improt
导入文件:
cpp
import 文件名.proto
随后在导入的文件中,使用包名.变量名
的形式使用变量,比如other_pkg.Address
。
字段规则
如果想在protobuf
中实现一个数组,它使用了一种字段规则的修饰方式来实现数组。
消息字段类型可以用以下规则修饰:
singular
:消息中该字段可以出现0
次或1
次,字段默认使用该规则repeated
:消息中该字段可以出现任意次数,包括0
次
简单来说,对于singular
变量只能存储一个值,如果输入新的值,后来的值会把原先的值覆盖。而对于repeated
变量,其可以存储多个值,所以值都会被保留,其实也就是数组。
示例:
cpp
syntax = "proto3";
package test_pkg;
message Person {
int32 id = 1;
string name = 2;
repeated string phone = 3;
}
此处的phone
字段被设置为了repeated
,可以理解为一个string
的数组,在pb.h
文件中,该字段包含以下接口:
cpp
// repeated string phone = 3;
int phone_size() const;
private:
int _internal_phone_size() const;
public:
void clear_phone();
const std::string& phone(int index) const;
std::string* mutable_phone(int index);
void set_phone(int index, const std::string& value);
void set_phone(int index, std::string&& value);
void set_phone(int index, const char* value);
void set_phone(int index, const char* value, size_t size);
std::string* add_phone();
void add_phone(const std::string& value);
void add_phone(std::string&& value);
void add_phone(const char* value);
void add_phone(const char* value, size_t size);
可以看到,相比于一般的string
类型,被repeated
修饰后,多出了很多下标操作,这里挑几个接口出来讲解:
int phone_size() const
:获取当前数组的长度。const std::string& phone(int index) const
:通过下标获取值的操作,获取该变量的const
引用。std::string* mutable_phone(int index)
:获取指定下标元素的指针,可以通过指针修改该元素。
cpp
void set_phone(int index, const std::string& value);
void set_phone(int index, std::string&& value);
void set_phone(int index, const char* value);
void set_phone(int index, const char* value, size_t size);
这四个接口用于设置指定下标的值,四种函数重载效果是一样的,只是传入字符串的形式不同。
std::string* add_phone()
:数组末尾增加一个空元素,并返回指向该元素的指针,可以通过指针修改该元素。
cpp
void add_phone(const std::string& value);
void add_phone(std::string&& value);
void add_phone(const char* value);
void add_phone(const char* value, size_t size);
这四个接口直接插入一个指定元素到数组末尾,重载是为了兼容不同形式的字符串。
被repeat
修饰的变量接口总结:
接口 | 功能 |
---|---|
xxx_size() |
获取数组的长度 |
xxx(index) |
获取指定下标的元素,不可修改 |
muteble_xxx(index) |
获取指定下标元素的指针,可修改 |
set_xxx(index, value) |
设置指定下标的元素为value |
add_xxx() |
尾插一个元素到数组,返回指向该元素的指针,可修改 |
add_xxx(value) |
尾插value 到数组 |
复杂类型
enum
enum
是枚举类型,其列举多个常量,方便后续使用。
定义一个枚举,表示有哪些人物种类:
cpp
enum Type{
STU = 0;
TEACHER = 1;
OTHER = 2;
}
此处枚举了三个常量,分别表示学生,老师,其它。
枚举类型有以下要求:
0
值必须存在,且要作为第一个元素- 在语言中,定义枚举变量后,如果指定变量值,第一个元素
0
是默认值 - 枚举的常量值在
32
位整数内,但是负数无效
示例:
cpp
syntax = "proto3";
package test_pkg;
enum Type{
STU = 0;
TEACHER = 1;
OTHER = 2;
}
message Person {
int32 id = 1;
string name = 2;
repeated string phone = 3;
Type type = 4;
}
编译后,在.pb.h
文件可以找到以下枚举类型:
cpp
enum Type : int {
STU = 0,
TEACHER = 1,
OTHER = 2,
Type_INT_MIN_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::min(),
Type_INT_MAX_SENTINEL_DO_NOT_USE_ = std::numeric_limits<int32_t>::max()
};
此处使用了C++11
的强枚举类型语法,enum Type : int
指定底层类型为int
。除去在.proto
文件中指定的三个成员,还增加了两个其它成员,最后两个成员用于做边界值判断,限制成员的值只在int32
范围内。
紧接着的是部分枚举类型的专有接口:
cpp
bool Type_IsValid(int value);
constexpr Type Type_MIN = STU;
constexpr Type Type_MAX = OTHER;
constexpr int Type_ARRAYSIZE = Type_MAX + 1;
const ::PROTOBUF_NAMESPACE_ID::EnumDescriptor* Type_descriptor();
template<typename T>
inline const std::string& Type_Name(T enum_t_value)
inline bool Type_Parse(
::PROTOBUF_NAMESPACE_ID::ConstStringParam name, Type* value);
Type_IsValid
:判断整数是否是enum
内部的成员之一Type_Name
:输入枚举值,返回枚举值对应的字符串名称Type_parse
:输入字符串和枚举值,判断两者是否匹配
再往后,在Person
类中,定义了枚举的通用接口:
cpp
// .test_pkg.Type type = 4;
void clear_type();
::test_pkg::Type type() const;
void set_type(::test_pkg::Type value);
可以看到,枚举类型的get
和set
接口还是比较简单的。
enum
接口总结:
接口 | 功能 |
---|---|
xxx_IsValid() |
判断整数是否是enum 内部的成员之一 |
xxx_Name(value) |
输入枚举值,返回枚举值对应的字符串名称 |
xxx_Parse(string, value) |
输入字符串和枚举值,判断两者是否匹配 |
xxx() |
获取当前枚举变量的值 |
set_xxx(value) |
设置枚举的值 |
Any
假设现在要给Person
类定义一个info
字段,用于存储该人的具体信息。但是由于Person
可以是学生,可以是老师,这两种人存储的info
是不同的,如何让一个字段存储不同的值,成为一个泛型?
此处Any
类型就排上用场了,Any
可以存储任何其它类型的message
,相当于一个泛型。
cpp
syntax = "proto3";
package test_pkg;
import "google/protobuf/any.proto";
message test{
google.protobuf.Any info = 1;
}
Any
的本质是一个message
,写在google/protobuf/any.proto
中,使用Any
类型需要import
导包。在类内使用类型时,也需要指定包名google.protobuf.Any
。
引入泛型后,当前Person
类如下:
cpp
syntax = "proto3";
package test_pkg;
import "google/protobuf/any.proto";
enum Type{
STU = 0;
TEACHER = 1;
OTHER = 2;
}
message stuInfo {
string className = 1; // 班级
int32 score = 2; // 成绩
}
message teacherInfo {
string subject = 1; // 科目
int32 jobAge = 2; // 工龄
}
message Person {
int32 id = 1;
string name = 2;
repeated string phone = 3;
Type type = 4;
google.protobuf.Any info = 5;
}
Person
的info
字段用于存储具体信息,对于学生来说,要存储班级和成绩。而对于老师来说,要存储所教的科目和工龄。此时将这些信息分别定义在stuInfo
和teacherInfo
中,在Person
内引入一个Any
字段info
,这个info
就可以存储任意其它的message
,自然也包括stuInfo
和teacherInfo
。
编译后,在.pb.h
中Any
的接口如下:
cpp
// .google.protobuf.Any info = 5;
bool has_info() const;
private:
bool _internal_has_info() const;
public:
void clear_info();
const ::PROTOBUF_NAMESPACE_ID::Any& info() const;
PROTOBUF_NODISCARD ::PROTOBUF_NAMESPACE_ID::Any* release_info();
::PROTOBUF_NAMESPACE_ID::Any* mutable_info();
void set_allocated_info(::PROTOBUF_NAMESPACE_ID::Any* info);
has_info()
:检查该字段是否为空clear_info()
:清空该字段的值info()
:获取该字段的值release_info()
:获取字段内的值后,清空字段内的值mutable_info()
:返回指向元素的指针,可通过指针修改元素
另外的,还有几个重要的接口,在google/protobuf/any.pb.h
中:
cpp
bool PackFrom(const ::PROTOBUF_NAMESPACE_ID::Message& message);
bool UnpackTo(::PROTOBUF_NAMESPACE_ID::Message* message);
template<typename T>
bool Is();
这三个接口是Any
最核心的接口:
PackFrom
:将一个message
类型转化为Any
类型UnpackTo
:将一个Any
类型转化回message
Is
:传入模板参数,判断当前Any
存储的类型是否和模板参数一样
这个用法有点复杂,示例:
cpp
#include <iostream>
#include "test.pb.h"
using namespace std;
using namespace test_pkg;
int main()
{
test_pkg::Person ps;
ps.set_id(123456);
ps.set_name("张三");
// repeat字段,可以设置多个值
ps.add_phone("123456");
ps.add_phone("654321");
ps.set_type(Type::STU); // 学生类型
stuInfo stuinfo;
stuinfo.set_classname("六年级一班");
stuinfo.set_score(95);
google::protobuf::Any any_message;
if (any_message.PackFrom(stuinfo))
*ps.mutable_info() = any_message;
stuInfo getStuinfo;
if (ps.info().Is<stuInfo>())
ps.info().UnpackTo(&getStuinfo);
return 0;
}
首先初始化了一个Person
,随后初始化了该对象的初始信息,确定该对象是一个学生。所以他的info
要存储一个stuInfo
类型,用于表示详细信息。
cpp
stuInfo stuinfo;
stuinfo.set_classname("六年级一班");
stuinfo.set_score(95);
这段代码初始化了一个stuinfo
对象。
随后将这个stuInfo
对象设置到ps.info
成员中:
cpp
google::protobuf::Any any_message;
if (any_message.PackFrom(stuinfo))
*ps.mutable_info() = any_message;
设置Any
成员时,首先要定义一个Any
对象,然后通过PackFrom
函数,把stuInfo
对象转化为Any
对象。如果PackFrom
返回true
,说明转化成功,此时any_message
内部就已经是stuInfo
的内容了。最后通过*ps.mutable_info() = any_message
设置info
字段为any_message
对象。
如果后续想要把Any
对象变回stuInfo
对象:
cpp
stuInfo getStuinfo;
if (ps.info().Is<stuInfo>())
ps.info().UnpackTo(&getStuinfo);
定义一个getStuinfo
接收返回值。随后执行ps.info().Is<stuInfo>()
,判断当前的info
内存储的变量类型是不是stuInfo
,如果是,那么执行ps.info().UnpackTo(&getStuinfo)
,将info
的内容解析到getStuinfo
中。
不论用Any
存储或提取任何一个message
类型,都是这一套逻辑。
Any
接口总结:
接口 | 功能 |
---|---|
has_xxx() |
检查该字段是否为空 |
clear_xxx() |
清空该字段的值 |
xxx() |
获取该字段的值 |
release_xxx() |
获取字段内的值后,清空字段内的值 |
mutable_xxx() |
返回指向元素的指针,可通过指针修改元素 |
PackFrom(message) |
将一个message 类型转化为Any 类型 |
UnpackTo(&message) |
将一个Any 类型转化回message |
Is<messageType>() |
传入模板参数,判断当前Any 存储的类型是否和模板参数一样 |
oneof
有时候,message
中的字段在多个中选择一个,就可以使用oneof
类型。
示例:
cpp
message Person {
int32 id = 1;
string name = 2;
oneof contact {
int32 qq = 3;
int32 tel = 4;
}
}
此处的Person
,增加了一个联系方式contact
,用户可以在qq
和tel
中二选一。
在oneof
内部的字段编号,不能与外部的message
冲突,比如qq
和tel
的字段编号就不可以是1
和2
。
另外的,在oneof
内部,不允许使用repeated
修饰字段,比如这样:
cpp
message Person {
int32 id = 1;
string name = 2;
oneof contact {
repeated int32 qq = 3; // err
repeated int32 tel = 4; // err
}
}
如果设置了repeated
,该修饰会失效,变量依然只能保存一个值。
对以下protoc
代码编译:
cpp
message Person {
int32 id = 1;
string name = 2;
oneof contact {
int32 qq = 3;
int32 tel = 4;
}
}
结果:
cpp
// int32 qq = 3;
bool has_qq() const;
private:
bool _internal_has_qq() const;
public:
void clear_qq();
int32_t qq() const;
void set_qq(int32_t value);
public:
// int32 tel = 4;
bool has_tel() const;
private:
bool _internal_has_tel() const;
public:
void clear_tel();
int32_t tel() const;
void set_tel(int32_t value);
void clear_contact();
看起来这个oneof
好像没有生效?此处就好像是单独定义了qq
和tel
两个字段,也没有什么oneof
的相关方法。
其实这个地方,qq
和tel
确实是直接当作两个字段处理的,但是两个字段有点像C语言
中的联合体,最后一次设置的是那个变量,那么存储的就是哪一个类型。
示例:
cpp
test_pkg::Person ps;
ps.set_id(123456);
ps.set_name("张三");
ps.set_qq(111111);
ps.set_tel(222222);
if (ps.has_qq())
cout << "qq: " << ps.qq() << endl;
if (ps.has_tel())
cout << "tel: " << ps.tel() << endl;
定义了一个Person
变量后,先后设置了qq(111111)
和tel(222222)
,随后通过has_xxx
方法分别检测两个值,随后输出。
输出结果:
cpp
tel: 222222
最后只输出了tel
,因为后来的tel
覆盖了qq
,两者是二选一的关系。
当然,oneof
类型也不是完全没有接口,比如这个接口:
cpp
void clear_contact();
在需要清除oneof
内部的值的时候,如果不确定具体是哪一个类型,就无法确定是caler_qq
还是clear_tel
,如果一个个通过has
判断还是有点麻烦的,就可以通过clear_contact
一键清除。
所有的oneof
的字段都被保存在一个枚举中:
cpp
enum ContactCase {
kQq = 3,
kTel = 4,
CONTACT_NOT_SET = 0,
};
ContactCase contact_case() const;
此处的kQq
和kTel
分别就是之前设置的qq
和tel
,而CONTACT_NOT_SET
表示未设置参数。
其实在判断当前oneof
存储的是哪一个字段时,不用一个个进行has_xxx
的判断。通过contact_case
函数,会返回一个枚举类型,只需要判断枚举类型是哪一个变量即可:
cpp
switch (ps.contact_case())
{
case Person::ContactCase::kQq:
// 处理qq字段
break;
case Person::ContactCase::kTel:
// 处理tel字段
break;
case Person::ContactCase::CONTACT_NOT_SET:
// 没有字段被设置
break;
}
oneof
接口总结:
接口 | 功能 |
---|---|
clear_xxx() |
不论字段存储的是什么,清空该字段的值 |
xxx_case() |
返回当前存储的类型的枚举值 |
map
map
用于创建一个键值对的映射关系,语法:
cpp
map<key_tyep, value_type> map_name = N;
其实语法和C++
的std::map
是一样的。
map
有以下注意点:
key_type
:可以是处理float
和bytes
之外的任意标量类型value_type
:可以是任意类型map
中的元素是无序的
示例:
cpp
message score {
map<string, int32> chinese = 1;
map<string, int32> english = 2;
map<string, int32> math = 3;
}
这是一个记录学生成绩的表格,三个成员分别代表不同科目的成绩。
编译结果以chinese
为例:
cpp
// map<string, int32> chinese = 1;
int chinese_size() const;
private:
int _internal_chinese_size() const;
public:
void clear_chinese();
private:
const ::PROTOBUF_NAMESPACE_ID::Map< std::string, int32_t >&
_internal_chinese() const;
::PROTOBUF_NAMESPACE_ID::Map< std::string, int32_t >*
_internal_mutable_chinese();
public:
const ::PROTOBUF_NAMESPACE_ID::Map< std::string, int32_t >&
chinese() const;
::PROTOBUF_NAMESPACE_ID::Map< std::string, int32_t >*
mutable_chinese();
chinese_size()
:获取map
内部元素的个数clear_chinese()
:清空map
的元素chinese()
:获取map
的const
引用mutable_chinese()
:获取map
的指针,可通过指针修改map
此处map
的类型不是std::map
,而是protobuf
自己封装的::PROTOBUF_NAMESPACE_ID::Map
,但是其用法和std::map
没有很大区别。
::PROTOBUF_NAMESPACE_ID::Map
也重载了operator[]
,拿到变量后,可以直接通过[]
进行访问。
示例:
cpp
test_pkg::Score sc;
auto chinese_mp = *sc.mutable_chinese();
auto english_mp = *sc.mutable_english();
auto math_mp = *sc.mutable_math();
chinese_mp["张三"] = 66;
chinese_mp["李四"] = 88;
chinese_mp["王五"] = 98;
english_mp["张三"] = 97;
english_mp["李四"] = 77;
english_mp["王五"] = 82;
math_mp["张三"] = 46;
math_mp["李四"] = 25;
math_mp["王五"] = 79;
cout << "张三-chinese: " << chinese_mp["张三"] << endl;
cout << "张三-english: " << english_mp["张三"] << endl;
cout << "张三-math: " << math_mp["张三"] << endl;
通过auto chinese_mp = *sc.mutable_chinese()
,获取到map
,由于mutable_chinese
返回的是一个指针,所以还要进行解引用。
随后通过chinese_mp[]
设置与获取元素了。
输出结果:
cpp
张三-chinese: 66
张三-english: 97
张三-math: 46
map
接口总结:
接口 | 功能 |
---|---|
xxx_size() |
获取map 内部元素的个数 |
clear_xxx() |
清空map 的元素 |
xxx() |
获取map 的const 引用 |
mutable_xxx() |
获取map 的指针,可通过指针修改map |
map.operator[] |
直接通过[] 获取与设置元素 |