ProtoBuf Reflection Note
参考:官方文档 Descriptor
一种自动反射消息类型的 Google Protobuf 网络传输方案
Proto使用过程中,有这样几个问题:
对proto中已有的内容进行修改/删除非常不方便,例如将proto中的一个字段foo删除,需要:
在.proto里把foo删掉并重新编译,
在工程中删除所有的foo(),set_foo(),mutable_foo()等,否则编译会挂掉。
尤其当foo不仅仅是一个变量时,或者该proto同时被很多工程使用时,会非常麻烦。
拿到了一个pb对象,希望遍历该对象的所有字段(反序列化)。
例如定义一个pb message如下:
1 2 3 Person person; person.set_name("yingshin" ); person.set_age(21 );
希望将该对象自动转为json格式的字符串:
1 2 3 4 { "name" :"yingshin" , "age" :21 }
如果添加了新的字段,输出也能自动更新。
反射
protobuf提供了一种反射机制,能够动态地调用对象并获取信息。利用protobuf的反射机制,可以帮助优化以上两种场景。
相关的类:
Message
Message
pb对象
Descriptor
对 Message 进行描述,包括 message 的名字、所有字段的描述、原始 proto 文件内容等
FieldDescriptor
对 Message 中单个字段进行描述,包括字段名、字段属性、原始的 field 字段等
Reflection
提供了动态读和写 message 中单个字段能力
在实际应用时,我们拿到一个pb对象后,可以:
通过Descriptor获得其每个字段的name和type
通过Reflection中的GetX获得具体的字段
常规的流程是,收到数据包,构造一个pb对象,再反序列化
利用反射,我们可以使用Descriptor和Refelction来进行动态解析:
实例 - 获取Descriptor 和Reflection
对于proto文件:
1 2 3 4 5 package T;message Test { optional int32 id = 1 ; }
首先我们需要获取Descriptor,最常见的是调用message的GetDescriptor方法
1 2 3 4 5 6 7 8 9 10 T::Test test; auto descriptor = test.GetDescriptor() ;auto reflecter = test.GetReflection() ;auto field = descriptor->FindFieldByName("id" );reflecter->SetInt32(&test , field , 5 ) ; std ::cout <<reflecter->GetInt32(test , field)<< std ::endl ;
除了调用message的GetDescriptor方法之外,一般有两种方式来获取Descriptor:动态编译与静态编译获取
动态编译
使用protobuf的动态编译机制,在运行时对某个proto文件进行动态编译,从而得到其所有元数据(descriptor):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 DiskSourceTree sourceTree; sourceTree.MapPath("" , "./" ); Importer importer (&sourceTree, NULL ) ;importer.Import("foo.proto" ); const Descriptor *descriptor1 = importer.pool()->FindMessageTypeByName("Test.Foo" );google::protobuf::DynamicMessageFactory factory; auto proto1 = factory.GetPrototype(descriptor1);auto message1= proto1->New();auto reflection1 = message1->GetReflection();auto filed1 = descriptor1->FindFieldByName("id" );reflection1->SetInt32(message1,filed1,1 ); std ::cout << message1->DebugString();delete message1 ;
Note:我在尝试动态编译foo.proto时,如果foo.proto有import其他目录下的proto,会提示找不到文件,还没找到解决问题的方法。
静态编译
在proto生成的pb.h/cc的构造函数中,会调用静态类static void MessageFactory::InternalRegisterGeneratedFile将自己注册到DescriptorPool::generated_pool中,后续可以直接使用generated_pool拿到Descriptor。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 T::Test xxx; std ::string str;xxx.SerializeToString(str); auto descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("T.Test" );if (nullptr == descriptor) return 0 ;auto prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);if ( nullptr == descriptor) return 0 ;auto message = prototype->New();message->ParseFromString(str); const google::protobuf::Reflection* reflection = message.GetReflection();const google::protobuf::Descriptor* descriptor = message.GetDescriptor();delete message ;return 0 ;
一些工程中的应用实例
参考:
https://izualzhy.cn/protobuf-message-reflection
http://arganzheng.life/reflection-of-protobuf.html
https://cloud.tencent.com/developer/article/1753977
自动序列化及反序列化
serialize_message
serialize_message遍历提取message中各个字段以及对应的值,序列化到string中。 主要思路就是通过Descriptor得到每个字段的描述符:字段名、字段的cpp类型。 通过Reflection的GetX接口获取对应的value。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 void serialize_message (const google::protobuf::Message& message, std ::string * serialized_string) { 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) { assert(!field->is_repeated()); switch (field->cpp_type()) { #define CASE_FIELD_TYPE(cpptype, method, valuetype)\ case google::protobuf::FieldDescriptor::CPPTYPE_##cpptype:{\ valuetype value = reflection->Get##method(message, field);\ int wsize = field->name().size();\ serialized_string->append(reinterpret_cast <char *>(&wsize), sizeof (wsize));\ serialized_string->append(field->name().c_str(), field->name().size());\ wsize = sizeof (value);\ serialized_string->append(reinterpret_cast <char *>(&wsize), sizeof (wsize));\ serialized_string->append(reinterpret_cast <char *>(&value), sizeof (value));\ break ;\ } 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 ); #undef CASE_FIELD_TYPE case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { int value = reflection->GetEnum(message, field)->number(); int wsize = field->name().size(); serialized_string->append(reinterpret_cast <char *>(&wsize), sizeof (wsize)); serialized_string->append(field->name().c_str(), field->name().size()); wsize = sizeof (value); serialized_string->append(reinterpret_cast <char *>(&wsize), sizeof (wsize)); serialized_string->append(reinterpret_cast <char *>(&value), sizeof (value)); break ; } case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { std ::string value = reflection->GetString(message, field); int wsize = field->name().size(); serialized_string->append(reinterpret_cast <char *>(&wsize), sizeof (wsize)); serialized_string->append(field->name().c_str(), field->name().size()); wsize = value.size(); serialized_string->append(reinterpret_cast <char *>(&wsize), sizeof (wsize)); serialized_string->append(value.c_str(), value.size()); break ; } case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { std ::string value; int wsize = field->name().size(); serialized_string->append(reinterpret_cast <char *>(&wsize), sizeof (wsize)); serialized_string->append(field->name().c_str(), field->name().size()); const google::protobuf::Message& submessage = reflection->GetMessage(message, field); serialize_message(submessage, &value); wsize = value.size(); serialized_string->append(reinterpret_cast <char *>(&wsize), sizeof (wsize)); serialized_string->append(value.c_str(), value.size()); break ; } } } } }
parse_message
parse_message通过读取field/value,还原message对象。 主要思路跟serialize_message很像,通过Descriptor得到每个字段的描述符FieldDescriptor,通过Reflection的SetX填充message。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 void parse_message (const std ::string & serialized_string, google::protobuf::Message* message) { const google::protobuf::Descriptor* descriptor = message->GetDescriptor(); const google::protobuf::Reflection* reflection = message->GetReflection(); std ::map <std ::string , const google::protobuf::FieldDescriptor*> field_map; for (int i = 0 ; i < descriptor->field_count(); ++i) { const google::protobuf::FieldDescriptor* field = descriptor->field(i); field_map[field->name()] = field; } const google::protobuf::FieldDescriptor* field = NULL ; size_t pos = 0 ; while (pos < serialized_string.size()) { int name_size = *(reinterpret_cast <const int *>(serialized_string.substr(pos, sizeof (int )).c_str())); pos += sizeof (int ); std ::string name = serialized_string.substr(pos, name_size); pos += name_size; int value_size = *(reinterpret_cast <const int *>(serialized_string.substr(pos, sizeof (int )).c_str())); pos += sizeof (int ); std ::string value = serialized_string.substr(pos, value_size); pos += value_size; std ::map <std ::string , const google::protobuf::FieldDescriptor*>::iterator iter = field_map.find(name); if (iter == field_map.end()) { fprintf (stderr , "no field found.\n" ); continue ; } else { field = iter->second; } assert(!field->is_repeated()); switch (field->cpp_type()) { #define CASE_FIELD_TYPE(cpptype, method, valuetype)\ case google::protobuf::FieldDescriptor::CPPTYPE_##cpptype: {\ reflection->Set##method(\ message,\ field,\ *(reinterpret_cast <const valuetype*>(value.c_str())));\ std ::cout << field->name() << "\t" << *(reinterpret_cast <const valuetype*>(value.c_str())) << std ::endl ;\ break ;\ } 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 ); #undef CASE_FIELD_TYPE case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { const google::protobuf::EnumValueDescriptor* enum_value_descriptor = field->enum_type()->FindValueByNumber(*(reinterpret_cast <const int *>(value.c_str()))); reflection->SetEnum(message, field, enum_value_descriptor); std ::cout << field->name() << "\t" << *(reinterpret_cast <const int *>(value.c_str())) << std ::endl ; break ; } case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { reflection->SetString(message, field, value); std ::cout << field->name() << "\t" << value << std ::endl ; break ; } case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { google::protobuf::Message* submessage = reflection->MutableMessage(message, field); parse_message(value, submessage); break ; } default : { break ; } } } }
获取 PB 中所有非空字段
在业务中,经常会需要获取某个 Message 中所有非空字段,形成一个 map<string,string>,使用 PB 反射写法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include "pb_util.h" #include <sstream> namespace comm_tools {int PbToMap (const google::protobuf::Message &message, std ::map <std ::string , std ::string > &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 ; } 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 ; } }
通过上面的代码,如果需要在 proto 中增加字段,不再需要修改原来的代码。
repeated类型的数据比较特殊,可以单独搞一个List来存