ProtoBuf-反射原理与使用

前言

所谓反射机制,就是能够在运行时知道任意类的所有属性方法,能够调用任意对象的任意方法和属性。这种动态获取的信息以及动态调用对象方向的功能称为反射机制。
如果用一句话来总结反射实现的关键,可概括为获取系统元信息

元信息:即系统自描述信息,用于描述系统本身。举例来讲,即系统有哪些类?类中有哪些字段、哪些方法?字段属于什么类型、方法又有怎样的参数和返回值?…

不像Jave,python等语言,C++本身没有反射机制,但使用protobuf时,通过proto文件产生响应的messageservice,可以提供反射机制,运行时可以通过proto获取任意message和任意service的属性和方法,并调用、修改和设置。

相关应用场景

Protobuf是一种常见的数据序列化方式,常常用于后台微服务之间传递数据。
在处理ProtoBuf Message数据时,经常需要根据一个输入的字符串,读取Message中对应属性取值,并修改取值。;或者给定一个ProtoBuf对象,如何自动遍历该对象的所有字段,将其转换为json格式,或者在Bigtable中根据pb对象的字段自动写填写列名和对应的value。

在写代码时,经常会遇到一些丑陋的、圈复杂度较高、较难维护的关于 PB 的使用代码:

  1. 对字段的必填校验硬编码在代码中:如果需要变更校验规则,则需要修改代码
  2. 一个字段一个 if 校验,复杂度较高:对传进来的字段每个字段都进行多种规则校验,例如长度,XSS,正则校验等。
  3. 想要获取 PB 中所有的非空字段,形成一个 map<string,string>,需要大量的 if 判断和重复代码;
  4. 后台服务间传递数据,由于模块由不同的人开发,导致相同字段的命名不一样,从一个 PB 中挑选一部分内容到另外一个 PB 中,需要大量的 GET 和 SET 代码。

以上都痛点可以通过反射机制解决。


一、ProtoBuf 反射原理概述

  1. 通过Message获取单个字段的FieldDescriptor
  2. 通过Message获取其Reflection
  3. 通过Reflection来操作FieldDescriptor,从而动态获取或修改单个字段

1、获取message和service的属性和方法

protobuf通过Descriptor获取任意message或service的属性和方法,Descriptor主要包括了一下几种类型:

描述符方法
FileDescriptor获取Proto文件中的DescriptorServiceDescriptor
Descriptor获取类message属性和方法,包括FieldDescriptorEnumDescriptor
FieldDescriptor获取message中各个字段的类型、标签、名称
EnumDescriptor获取Enum中的各个字段名称、值等
ServiceDescriptor获取service中的MethodDescriptor
MethodDescriptor获取各个RPC中的request、response名称

在类 Descriptor 中 可以获取自身信息的函数

const std::string & name() const; // 获取message自身名字
int field_count() const; // 获取该message中有多少字段
const FileDescriptor* file() const; // The .proto file in which this message type was defined. Never nullptr.

在类 Descriptor 中,可以通过如下方法获取类 FieldDescriptor

const FieldDescriptor* field(int index) const; // 根据定义顺序索引获取,即从0开始到最大定义的条目
const FieldDescriptor* FindFieldByNumber(int number) const; // 根据定义的message里面的顺序值获取(option string name=3,3即为number)
const FieldDescriptor* FindFieldByName(const string& name) const; // 根据field name获取

也就是说,如果能够获取到proto文件的FileDescriptor,就能获取proto文件中的所有的内容。那么如何获取proto文件的FileDescriptor呢?protobuf提供多种方法。

1.1 使用protoc将proto文件生成.h和.cc文件

这种方法直接根据生成的类来获取响应的FileDescriptor,比如现在有test.proto文件,那么可以通过DescriptorPool::generated_pool()获取到其FileDescriptor

const FileDescriptor* fileDescriptor = DescriptorPool::generated_pool()->FindFileByName(file);

并且对于任意的message和service都可以根据其名称,通过DescriptorPool对应的Descriptor和ServiceDescriptor

1.2 只使用proto文件,不使用protoc进行编译

这种情况需要手动解析proto文件,再获取FileDescriptor。protobuf提供了响应的解析器compiler,通过compoiler可以方便的获取proto文件的FileDescriptor

const FileDescriptor* GetFileDescriptorFromProtoFile(const std::string &protoRoot, const std::string &protoFile){
    compiler::DeskSourceTree sourceTree;
    sourceTree.MapPath("", protoRoot);
    FileErrorCollector errorCollector;
    compiler::Importer importer(&sourceTree, &errorCollector);
    return importer.Import(protoFile);
}

1.3 非 .proto 文件 ,转换成.proto

  1. 可以从远程读取,如将数据与数据元信息一同进行 protobuf 编码并传输:
message Req {
  optional string proto_file = 1;
  optional string data = 2;
}
  1. 从 Json 或其它格式数据中转换而来

无论 .proto 文件来源于何处,我们都需要对其做进一步的处理和注册,将其解析成内存对象,并构建其与实例的映射,同时也要计算每个字段的内存偏移。可总结出如下步骤:

  1. 提供 .proto (范指 ProtoBuf Message 语法描述的元信息)
  2. 解析 .proto 构建 FileDescriptorFieldDescriptor 等,即 .proto 对应的内存模型(对象)
  3. 之后每创建一个实例,就将其存到实例工厂相应的实例池
  4. 将 Descriptor 和 instance 的映射维护到表中备查
  5. 通过 Descriptor 可查到相应的 instance,又由于了解 instance 中字段类型(FieldDescriptor),所以知道字段的内存偏移,那么就可以访问或修改字段的值

2、调用message的属性和方法

如下是通过字符串type_name调用message的属性和方法的流程图;
Person是自定义的pb类型,继承自Message. MessageLite作为Message基类,更加轻量级一些。
通过Descriptor能获得所需消息的Message* 指针。Message class 定义了 New() 虚函数,用来返回本对象的一份新实体,具体流程如下:

  1. 通过 DescriptorPool 的 FindMessageTypeByName 获得了元信息 Descriptor
  2. 根据 MessageFactory 实例工厂Descriptor获得了Message的默认实例Message*指针 default instance
  3. 通过new()构造一个可用消息对象

2.1根据type name反射自动创建实例

通过字符串"person" 创建新的 person message对象。线程安全

 // 先获得类型的Descriptor .
auto descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("Person");
//利用Descriptor拿到类型注册的instance. 这个是不可修改的.
auto prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
 // 构造一个可用的消息.
auto instance = prototype->New(); //创建新的 person message对象。

第一步我们通过 DescriptorPool 的 FindMessageTypeByName 获得了元信息 Descriptor。

DescriptorPool 为元信息池,对外提供了诸如 FindServiceByNameFindMessageTypeByName 等各类接口以便外部查询所需的Service或者Message元信息。当 DescriptorPool 不存在时需要查询的元信息时,将进一步到 DescriptorDatabase 中去查找。 DescriptorDatabase 可从 硬编码磁盘中查询对应名称的 .proto 文件内容,解析后返回查询需要的元信息。

不难看出,DescriptorPool 和 DescriptorDatabase 通过缓存机制提高了反射运行效率。DescriptorDatabase 从磁盘中读取 .proto 内容并解析成 Descriptor 并不常用,实际上我们在使用 protoc 生成 xxx.pb.cc 和 xxx.pb.h 文件时,其中不仅仅包含了读写数据的接口,还包含了 .proto 文件内容。阅读任意一个 xxx.pb.cc 的内容,你可以看到如下类似代码

static void AddDescriptorsImpl() {
  InitDefaults();

  // .proto 内容
  static const char descriptor[] GOOGLE_PROTOBUF_ATTRIBUTE_SECTION_VARIABLE(protodesc_cold) = {
      "\n\022single_int32.proto\"\035\n\010Example1\022\021\n\010int3"
      "2Val\030\232\005 \001(\005\" \n\010Example2\022\024\n\010int32Val\030\377\377\377\377"
      "\001 \003(\005b\006proto3"
  };

  // 注册 descriptor
  ::google::protobuf::DescriptorPool::InternalAddGeneratedFile(
      descriptor, 93);

  // 注册 instance
  ::google::protobuf::MessageFactory::InternalRegisterGeneratedFile(
    "single_int32.proto", &protobuf_RegisterTypes);
}

其中 descriptor 数组存储的便是 .proto 内容。这里当然不是简单的存储原始文本字符串,而是经过了 SerializeToString 序列化处理,而后将结果以 硬编码的形式保存在 xxx.pb.cc 中,真是充分利用了自己的高效编码能力。

硬编码的 .proto 元信息内容将以懒加载的方式(被调用时才触发)被 DescriptorDatabase 加载、解析,并 缓存到 DescriptorPool 中。

3、通过实例instance 的反射接口reflection对其pb字段读写

Reflection主要提供了动态读写 message对象字段的接口,对message对象的自动读写主要通过该类完成。

每种数据类型,Reflection都提供了一个单独的接口Get、Set)用于读写字段对应的值
例如对 int32、int64的读操作:输入参数为MessageFieldDescriptor*

  virtual int32  GetInt32 (const Message& message,const FieldDescriptor* field) const = 0;
  virtual int64  GetInt64 (const Message& message, const FieldDescriptor* field) const = 0;

特殊的,对于枚举和嵌套的message:

virtual const EnumValueDescriptor* GetEnum( const Message& message, const FieldDescriptor* field) const = 0;
virtual const Message& GetMessage(const Message& message,
                                    const FieldDescriptor* field,
                                    MessageFactory* factory = NULL) const = 0;

对于写操作也是类似的接口,例如SetInt32/SetInt64/SetEnum

写单个字段的函数如下:

void SetInt32(Message * message, const FieldDescriptor * field, int32 value) const
 
void SetString(Message * message, const FieldDescriptor * field, std::string value) const

获取重复字段的函数如下:

int32 GetRepeatedInt32(const Message & message, const FieldDescriptor * field, int index) const

std::string GetRepeatedString(const Message & message, const FieldDescriptor * field, int index) const

const Message & GetRepeatedMessage(const Message & message, const FieldDescriptor * field, int index) const

获取反射Reflection接口,并且值写入的代码如下:线程安全

auto reflecter = instance.GetReflection();//2.1通过字符串"Person"取得的实例instance
auto field = descriptor->FindFieldByName("name"); // Person这个Message 中有name字段
reflecter->SetString(&instance, field, "小明") ; //反射来设置name字段

反射机制

二、反射 例子

1、serialize_message

serialize_message遍历提取message中所有字段以及对应的值,序列化到string中。主要思路就是:

  1. Descriptor得到每个字段的描述符FieldDescriptor:字段名、字段的cpp类型。
  2. 通过ReflectionGetXXX接口获取对应的value

FieldDescriptor 介绍
类 FieldDescriptor 的作用主要是对 Message 中单个字段进行描述,包括字段名、字段属性、原始的 field 字段等。

其获取获取自身信息的函数:

const std::string & name() const; // Name of this field within the message.
const std::string & lowercase_name() const; // Same as name() except converted to lower-case.
const std::string & camelcase_name() const; // Same as name() except converted to camel-case.
CppType cpp_type() const; //C++ type of this field.

其中cpp_type()函数是来获取该字段什么类型的,在 protobuf 中,类型的类目如下:

enum FieldDescriptor::Type {
  TYPE_DOUBLE = = 1,
  TYPE_FLOAT = = 2,
  TYPE_INT64 = = 3,
  TYPE_UINT64 = = 4,
  TYPE_INT32 = = 5,
  TYPE_FIXED64 = = 6,
  TYPE_FIXED32 = = 7,
  TYPE_BOOL = = 8,
  TYPE_STRING = = 9,
  TYPE_GROUP = = 10,
  TYPE_MESSAGE = = 11,
  TYPE_BYTES = = 12,
  TYPE_UINT32 = = 13,
  TYPE_ENUM = = 14,
  TYPE_SFIXED32 = = 15,
  TYPE_SFIXED64 = = 16,
  TYPE_SINT32 = = 17,
  TYPE_SINT64 = = 18,
  MAX_TYPE = = 18
}

serialize_message遍历提取message中****所有字段以及对应的值代码如下:

void serialize_message(const google::protobuf::Message& message, std::string* serialized_string) {
    //获取描述Descriptor*和反射Reflection*
    const google::protobuf::Descriptor* descriptor = message.GetDescriptor();
    const google::protobuf::Reflection* reflection = message.GetReflection();
     
    //遍历消息的所有字段  
    for (int i = 0; i < descriptor->field_count(); ++i) {
        // 获得单个字段的描述FieldDescriptor*
        const google::protobuf::FieldDescriptor* field = descriptor->field(i);
        bool has_field = reflection->HasField(message, field);
        
        if (has_field) {
            //arrays not supported
            assert(!field->is_repeated());
            switch (field->cpp_type()) {
            //宏定义 CASE_FIELD_TYPE(cpptype, method, valuetype) 写case
#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 写所有简单数据类型的case
                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();
                    //写入name占用字节数
                    serialized_string->append(reinterpret_cast<char*>(&wsize), sizeof(wsize));
                    //写入name
                    serialized_string->append(field->name().c_str(), field->name().size());
                    wsize = sizeof(value);
                    //写入value占用字节数
                    serialized_string->append(reinterpret_cast<char*>(&wsize), sizeof(wsize));
                    //写入value
                    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;
                }
                //递归 序列化 嵌套Message
                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;
                }
            }
        }
    }
}

2、parse_message

parse_message通过读取field/value,还原message对象。
主要思路跟serialize_message很像,通过Descriptor得到每个字段的描述符FieldDescriptor,通过Reflection的SetX填充message。

void parse_message(const std::string& serialized_string, google::protobuf::Message* message) {
    // 通过传入的Message* 获得 Descriptor* 和Reflection* 
    const google::protobuf::Descriptor* descriptor = message->GetDescriptor();
    const google::protobuf::Reflection* reflection = message->GetReflection();
    // 字段名字 和FieldDescriptor*字段的 映射
    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;
        //通过name 从map中获取FieldDescriptor*
        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()) {
        // 利用宏定义 CASE_FIELD_TYPE 写所有简单数据类型的case
#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;
            }
            //递归解析 嵌套message字符串
            case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: {
                google::protobuf::Message* submessage = reflection->MutableMessage(message, field);
                parse_message(value, submessage);
                break;
            }
            default: {
                break;
            }
        }
    }
}
  • 11
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值