何谓反射?
我在工作中大量使用了 google protocal buffer, 为了方便描述, 下文简称为 pb. pb 的作用和基本使用方法在这里就不再陈述, 相关的文章网上很多. 这里主要介绍 pb 的反射机制.
什么是反射机制呢? 该机制能在运行时, 对于任意一个类, 都能知道这个类的所有属性和方法; 对于任意一个对象, 都能调用它的任意一个方法和属性; 简言之, 反射机制使程序员可以动态获取对象信息以及动态调用对象的方法. 下面用两个例子来解释什么是反射.
1. 动态获取对象信息
对于一个 c++ 类 Player , 如果标准提供一个函数 Show(Player), 可以取到 Player 的所有成员函数和变量的名字. 我们就说 Show 函数可以动态获取对象的信息.
2. 动态调用对象的方法.
Player 类里有一个函数 AddMoney(int a), playerA 是一个Player 对象. 如果标准提供一个函数 Call(playerA, "AddMoney", 100), 能达到 playerA.AddMoney(100) 相同的效果. 我们就说 Call 函数可以动态调用对象的方法.
当然, c++ 没有 Show 和 Call 这两个函数, 所以c++不具备反射机制. 提供了 Show 和 Call, 或是能达到同样功能的函数的系统, 就实现了反射机制.
pb 的反射
知道了什么是反射, 我们来看看 pb 是如何实现的. 首先认识几个关键类.
google::protobuf::Message
这是用户自定义消息的基类. 系统会为给每个用户自定义消息实例化一个默认对象, 对象用 Message* 保存.
Message 类定义了 New() 虚函数, 可以返回本对象的一份新的实例, 类型和本对象的真实类型相同. 拿到了 Message*, 不用知道它的具体类型, 就可以创建和它一样类型的对象. 通过 New() 函数创建的对象, 需要用户自己释放. New() 函数是线程安全的.
google::protobuf::MessageFactory
管理所有自动实例化的 Message 对象. 它自身也会被自动实例化, 通过静态函数
generated_factory() 能拿到该对象指针. 通过成员函数
GetPrototype() 能拿到自动实例化的 Message*; GetPrototype() 的参数是一个 Descriptor*; MessageFactory 是线程安全的.
google::protobuf::Descriptor
每一个用户自定义消息, 都对应一个 Descriptor 对象. 该对象描述了消息的格式. 系统会为每个消息的 Descriptor 实例化一个默认对象.
google::protobuf::DescriptorPool
管理所有自动实例化的 Descriptot 对象. 它自身也会自动被实例化, 通过静态函数
generated_pool() 能拿到该对象指针. 通过成员函数
FindMessageTypeByName() 能拿到对应的 Descriptor*. DescriptorPool 是线程安全的
google::protobuf::FieldDescriptor
一个 Message 里有若干域, 每个域用一个 FieldDescriptor 来描述它的信息. 可以通过 Descriptor 的成员函数
FindFieldByName() 来取得对应的 FieldDescriptor*;
google::protobuf::Reflection
这个类提供了一系列接口, 用来操作 Message 的域. 这些接口一般需要两个参数 Message
* 和 FieldDescriptor*; 可以通过 Message 的成员函数
GetReflection(), 来拿到对应的
Reflection*;
通过以上类, 我们就可以根据消息的名字, 得到消息的实例了, 并且可以动态的读写消息的数据. 这些类一起实现了 pb 的反射机制. 下面我们看看能用反射做些什么.
基于消息名的网络传输
在网络中传输pb格式的消息时, 我们通常会这么定义数据流:
messageid | size | data
messageid是一个整数, size是后面data的长度, data是系列化的pb message;
消息的接收和发送方会维护一个 messageid -- pb message 的对应表. 根据message id辨认是那种 pb message.
这种方法的缺点在于, 大家必须使用一致的对应表, 否则就无法解析消息.
其实, 我们可以在消息里用 pb message name 替代 messageid.
通过 strMsgName 获得 message, 只需要简单的几步
const google::protobuf::Descriptor* descriptor
= google::protobuf::DescriptorPool::generated_pool()
->FindMessageTypeByName(strMsgName);
const google::protobuf::Message* prototype
= google::protobuf::MessageFactory::generated_factory()
->GetPrototype( descriptor );
google::protobuf::Message *pMsg = prototype->New();
这种方法的有点事可以省略对应表.
缺点是传输的数据长度略长, 消息解析的效率略低.
自描述的消息
pb 定义了一个 proto 格式的消息
FileDescriptorSet, 这个消息可以用来描述任何用户定义的消息. pb 在解释用户消息是, 首先转化成 FileDescriptorSet, 在解释这个 FileDescriptorSet.
我们可以在 pb 源码包的
descriptor.proto 中看到这个文件.
所以, pb 的消息可以是自描述的. 我们可以定义一个这样的消息
message SelfDescMsg {
required FileDescriptorSet proto_files = 1;
required string type_name = 2;
required string data = 3;
}
ptoto_files 是消息的描述, type_name是消息的名字(proto文件里的消息名), 之所以要指定消息的名字, 是因为一个proto文件里可能有多个 message. data 是消息系列化后的数据.
用protoc生成代码是参数可以这么写:
protoc --cpp_out=. --descriptor_set_out=Card.desc Card.proto
Card.desc 就是 Card.proto 对应的 FileDescriptorSet;
像这样发送数据:
假设我们 Card.proto 里有一个 message 叫
CardHero hero, 我们往 hero 写入了数据.
发送 SelfDescMsg 时, 我们用 Card.desc 里的数据填充 proto_files.
用 "CardHero" 填充 type_name. 用 hero 系列化后的字符串填充 data. 最后, 将
SelfDescMsg 系列化后发送.
SelfDescMsg sdMse;
fstream desc("Card.desc", ios::in | ios::binary);
sdMse. mutable_proto_files()->ParseFromIstream(&desc);
sdMse.set_type_name((hero.GetDescriptor())->full_name());
sdMse.clear_data();
hero.SerializeToString(sdMse.mutable_data());
像这样解析数据:
解析这些数据时, 不需要知道 Card.proto. 首先 反序列化 SelfDescMsg
, 通过 proto_files 得到 FileDescriptorSet, 通过 type_name 得到消息的类型名, 利用反射得到消息实例, 然后反系列化消息. 最后通过 message 的 reflection 接口操作 message 的各个字段.
SelfDescMsg sdMse;
sdMse.ParseFromString(&input));
SimpleDescriptorDatabase db;
for(int i=0;i<sdMse.proto_files().file_size();i++)
{ db.Add(sdmessage.proto_files().file(i)); }
DescriptorPool pool(&db);
const Descriptor *descriptor = pool.FindMessageTypeByName(sdMse.type_name());
DynamicMessageFactory factory(&pool);
Message *msg = factory.GetPrototype(descriptor)->New();
msg->ParseFromString(sdMse._data());
自描述的消息使用起来有一定难度, 效率也打了不少折扣, 它提供了一种不了解消息格式就可以使用消息的方式.
动态读写消息数据
在我做的项目中, 许多数据是保存在 pb message 里的. 系统运行时, 我们想要知道 pb message 里某些内容的信息, 还想要修改某些类容.
比如有这样一个消息:
message CardBase {
optional int64 id = 1;
repeated
int32 values = 2;
}
CardBase base;
在运行时, 如何知道 base.id 的值, 又如何修改它 ? 用静态代码很容易做到这点, 但是 c 语言并不是解释型语言, 无法再运行时执行静态代码. 为了实现这样的需求, 我写了几个函数.
使用这样函数可以动态读写 pb message 里的全部或部分数据. 这个 message 可以嵌套其它 message, 里面的字段也支持 repeated 类型. 为了减少复杂度, 可修改的数据类型只支持这几种 : int32 int64, string.
这些需要两个参数来定位消息里的域, 第一个参数是 message 实例, 第二个参数是域的路径 path, path 是一个字符串, 它的格式类似这样:
player.baseMgr.army(2).foodcosttime
army(2) 表示repeated域的第二个元素.
定位某个域后, 就可以读写这个域了.
代码很简单, 我会把它们放到 github 上.