Protobuff是一个与开发语言、平台无关序列化的工具,广泛应用于需要跨进程传输数据的场景。除此之外,PB还提供了强大的反射能力。可以利用反射的技巧,可以大量降低重复的代码。对于C++来说,语言层面是默认不支持反射的,PB的反射可以很好的补充cpp不支持反射的问题。
开局一张图:什么是反射?
计算机程序在运行时可以访问、检测和修改它本身状态或行为
反射可以干什么:
// 比如:根据对象的名称 去动态的创建出一个对象
int pb_reflect()
{
// "PersonInfo" 创建一个PersonInfo的对象
const google::protobuf::Descriptor * descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("PersonInfo");
const google::protobuf::Message * prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
google::protobuf::Message* instance = prototype->New();
// 反射相关接口
// 给PersonInfo对象里面的某个字段设置值
const google::protobuf::Reflection * reflecter = instance->GetReflection();
const google::protobuf::FieldDescriptor * field = descriptor->FindFieldByName("name");
reflecter->SetString(instance, field, "gagagaga") ;
// 打印下.
std::cout<GetString(*instance , field)<< std::endl ;
cout << "string_to_pb >>>> " << instance->DebugString() << endl;
return 0 ;
}
在pb的设计中,任何一个pb对象都是message类派生出来的对象。那么,对于任何一个pb对象,都可以拿到它的Descriptor 和 Reflection。也就是说,对于任何一个pb对象,都可以拿到这两个类,从而找到pb对象的描述信息和反射信息。
- Descriptor 的使用
Descriptor 中包含的message对象里面定义的字段,通过遍历可以依次拿到里面的field,包括field的对象类型,名称,是否是数组等各种信息。
int print_pb_message(const google::protobuf::Message & message)
{
const google::protobuf::Descriptor* pDescriptor = message.GetDescriptor();
int count = pDescriptor->field_count();
cout << "field count : " << count << " message : "
<< pDescriptor->name() << " file desc" << pDescriptor->file()<< endl;
for (int i = 0; i < count; i++)
{
const google::protobuf::FieldDescriptor * pFieldDesc = pDescriptor->field(i);
cout << "field index : " << i << ", field name :" << pFieldDesc->name()
<< ", field type : " << pFieldDesc->cpp_type() << ", is repeated : "
<< pFieldDesc->is_repeated() << ", number : " << pFieldDesc->number() << endl;
// option
pFieldDesc->options();
}
cout << message.DebugString() << endl;
return 0;
}
- Reflection 的使用
refection提供了动态获得message对象的值和修改它的能力。
int fill_pb_message(google::protobuf::Message & message)
{
const google::protobuf::Reflection* pReflection = message.GetReflection();
const google::protobuf::Descriptor* pDescriptor = message.GetDescriptor();
int count = pDescriptor->field_count();
for (int i = 0; i < count; i++)
{
const google::protobuf::FieldDescriptor * field = pDescriptor->field(i);
bool has_field = pReflection->HasField(message, field);
if (has_field)
{
if (field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_UINT32)
{
cout << pReflection->GetUInt32(message, field) << endl;
// 利用反射填充对象
int field_value = 666;
pReflection->SetUInt32(&message, field, field_value);
}
}
}
return 0;
}
/* proto 定义
message PersonInfo
{
uint32 id = 1;
uint32 age = 2;
uint32 passwd = 3;
string name = 4;
}
*/
int main()
{
PersonInfo info;
info.set_age(100);
cout <<"debug string : " << info.DebugString() << endl;
print_pb_message(info);
fill_pb_message(info);
cout <<"debug string : " << info.DebugString() << endl;
}
// 输出如下:
debug string : age: 100
field count : 4 message : PersonInfo file desc0x435c60
field index : 0, field name :id, field type : 3, is repeated : 0, number : 1
field index : 1, field name :age, field type : 3, is repeated : 0, number : 2
field index : 2, field name :passwd, field type : 3, is repeated : 0, number : 3
field index : 3, field name :name, field type : 9, is repeated : 0, number : 4
age: 100
100
debug string : age: 666
PB反射的几个妙用 :
- 根据字符串生成对象
- 利用反射Descriptor生成代码,减少冗余代码
- 利用自定义扩展生成相关代码
举个栗子展开一点点来整理:
- 例子1:根据字符串生成对象
根据输入的对象名称,比如"PersonInfo",去创建一个实际的PersonInfo对象。并且可以设置和修改PersonInfo对象中任何一个字段的值。
message PersonInfo
{
uint32 id = 1;
uint32 age = 2;
uint32 passwd = 3;
string name = 4;
}
int string_to_pb()
{
google::protobuf::Message* message = pb_create_by_name("PersonInfo");
std::queue args;
args.push("10001");
args.push("18");
args.push("123456");
args.push("haha");
int ret = pb_fill_message(args, message);
if (ret != 0) {
cout << "fill failed" << endl;
return ret;
}
cout << "string_to_pb >>>> " << message->DebugString() << endl;
return 0;
}
// 输出:
id: 10001
age: 18
passwd: 123456
name: "haha"
- 例子2:利用反射Descriptor生成代码,减少冗余代码
对于任何一个pb对象,把他装换为一个map。里面的key和value分别是字段的名称和它对应的数值。并且当pb新增字段或者修改字段的时候,这段函数不需要随着它来调整。
int PbToMap(const google::protobuf::Message &message,
std::map &out)
{
#define CASE_FIELD_TYPE(cpptype, method, valuetype) \
case google::protobuf::FieldDescriptor::CPPTYPE_##cpptype: { \
valuetype value = reflection->Get##method(message, field); \
std::ostringstream oss; \
oss << value; \
out[field->name()] = oss.str(); \
break; \
}
#define CASE_FIELD_TYPE_ENUM() \
case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { \
int value = reflection->GetEnum(message, field)->number(); \
std::ostringstream oss; \
oss << value; \
out[field->name()] = oss.str(); \
break; \
}
#define CASE_FIELD_TYPE_STRING() \
case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { \
std::string value = reflection->GetString(message, field); \
out[field->name()] = value; \
break; \
}
const google::protobuf::Descriptor *descriptor = message.GetDescriptor();
const google::protobuf::Reflection *reflection = message.GetReflection();
for (int i = 0; i < descriptor->field_count(); i++)
{
const google::protobuf::FieldDescriptor *field = descriptor->field(i);
bool has_field = reflection->HasField(message, field);
if (has_field)
{
if (field->is_repeated())
{
return -1; // 不支持转换repeated字段
}
const std::string &field_name = field->name();
switch (field->cpp_type())
{
CASE_FIELD_TYPE(INT32, Int32, int);
CASE_FIELD_TYPE(UINT32, UInt32, uint32_t);
CASE_FIELD_TYPE(FLOAT, Float, float);
CASE_FIELD_TYPE(DOUBLE, Double, double);
CASE_FIELD_TYPE(BOOL, Bool, bool);
CASE_FIELD_TYPE(INT64, Int64, int64_t);
CASE_FIELD_TYPE(UINT64, UInt64, uint64_t);
CASE_FIELD_TYPE_ENUM();
CASE_FIELD_TYPE_STRING();
default:
return -1; // 其他异常类型
}
}
}
return 0;
}
int main()
{
std::map pb_map;
PbToMap(info, pb_map);
}
// pb转换为map
// 输出
(gdb) p pb_map
$4 = std::map with 1 element = {["age"] = "666"}
- 利用自定义扩展生成相关代码
利用pb中的扩展字段,自动生成相关的代码,实现配置化的开发。当需要check的规则调整是,不需要改动代码。只需要修改proto的定义即可。
syntax = "proto3";
import "google/protobuf/descriptor.proto";
message PersonInfo
{
uint32 id = 1;
uint32 age = 2;
uint32 passwd = 3;
string name = 4;
}
message FieldRule{
uint32 length_min = 1; // 字段最小长度
uint32 id = 2; // 字段映射id
}
extend google.protobuf.FieldOptions{
FieldRule field_rule = 50000;
}
message Student{
string name =1 [(field_rule).length_min = 5, (field_rule).id = 1];
string email = 2 [(field_rule).length_min = 10, (field_rule).id = 2];
}
int allCheck(const google::protobuf::Message &oMessage){
const auto *poReflect = oMessage.GetReflection();
vector vecFD;
poReflect->ListFields(oMessage, &vecFD);
for (const auto &poFiled : vecFD) {
const auto &oFieldRule = poFiled->options().GetExtension(field_rule);
if (poFiled->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_STRING && !poFiled->is_repeated()) {
// 类型是string并且选项非重复的才会校验字段长度类型
const std::string strValue = poReflect->GetString(oMessage, poFiled);
const std::string strName = poFiled->name();
if (oFieldRule.length_min()) {
// 有才进行校验,没有则不进行校验
if (minLengthCheck(strValue, oFieldRule.length_min())) {
cout << "the length of " << strName << " is lower than " << oFieldRule.length_min()<
这里只整理了核心代码,全部的代码移步:PB REFLECT
-
PB的反射是什么实现的?
实现反射需要关键的两个点,第一,需要保存一个对象名称和对象实际定义的一个映射关系;第二,要知道一个对象的内存布局,对象中每个字段的内存偏移;对于pb而言,这些信息都存在于proto文件中。1、提供 .proto (范指 ProtoBuf Message 语法描述的元信息)
2、解析 .proto 构建 FileDescriptor、FieldDescriptor 等,即 .proto 对应的内存模型(对象)
3、创建一个实例,就将其存到相应的实例池中
4、将 Descriptor 和 instance 的映射维护到表中备查
5、通过 Descriptor 可查到相应的 instance,又由于了解 instance中字段类型(FieldDescriptor),所以知道字段的内存偏移,那么就可以访问或修改字段的值。任何一个对象最终都对应一段内存,有内存起始(start_addr)和结束地址, 而对象的每一个属性,都位于start_addr+$offset ,所以当对象和对应属性的offset已知的时候, 属性的内存地址也就是可以获取的。
小结:
PB除了被用于序列化和反序列化之外,这里总结了PB的一些技反射巧。利用pb的反射,可以减少写冗余代码的开发。但是,pb运行时的反射性能不是很好,官方也在文档中提到了这一点,要注意使用的场景。
关于pb的其他内容:
* PB插件开发指南
* protobuff的序列化和反序列化编码实现
参考:
https://cloud.tencent.com/developer/article/1753977
https://blog.csdn.net/JMW1407/article/details/107223287