ProtoBuf-反射原理与使用

本文介绍了ProtoBuf的反射机制,这是一种在运行时获取和操作protobuf消息和服务属性的方法。通过反射,可以动态获取或修改message字段,解决代码维护难题。文章详细阐述了反射的原理,包括获取message和服务的属性和方法,以及如何调用message的属性和方法。文中还给出了反射的例子,如serialize_message和parse_message的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

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

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

不像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 元信息内容将以

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值