所谓反射机制,就是能够在运行时知道任意类的所有属性和方法,能够调用任意对象的任意方法和属性。这种动态获取的信息以及动态调用对象方向的功能称为反射机制。
不像Jave等语言,C++本身没有反射机制,在使用C++版本Protobuf时,protobuf通过proto文件产生响应的message和service,protobuf可以通过proto文件提供反射机制,程序在运行时可以通过proto获取任意message和任意service的属性和方法,也可以在运行时调用message的属性和方法。
1.获取message和service的属性和方法
protobuf通过Descriptor获取任意message或service的属性和方法,Descriptor主要包括了一下几种类型:
| FileDescriptor | 获取Proto文件中的Descriptor和ServiceDescriptor |
| Descriptor | 获取类message属性和方法,包括FieldDescriptor和EnumDescriptor |
| FieldDescriptor | 获取message中各个字段的类型、标签、名称等 |
| EnumDescriptor | 获取Enum中的各个字段名称、值等 |
| ServiceDescriptor | 获取service中的MethodDescriptor |
| MethodDescriptor | 获取各个RPC中的request、response、名称等 |
也就是说,如果能够获取到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);
}
2. 调用message的属性和方法
首先获取message的对象:
const Message* prototype = MessageFactory::generated_factory()->GetPrototype(descriptor);
if(!prototype){
continue;
}
Message *message = prototype->New();
有了message对象,因为所有的message都是Message* 类型的,但不同的message对象的属相和方法是不一样的,所以并不能直接调用其对象和方法。
protobuf是通过Reflection来调用message的属性和方法的。message中的方法只有对各个属性的get和set,而调用message的属性其实也就是调用属性的get。调用message的某一个属性的get和set,就需要该属性的Descriptor,通过Reflection将相应的值写入到message对应的属性:
const FieldDescriptor * field_descriptor = message_descriptor->field(i);
if(field_descriptor->type() == FieldDescriptor::TYPE_INT32){
reflection->SetInt32(message, field_descriptor, 0);
}
3.Protobuf反射使用案例
这部分主要是重复上两节的内容,进行完整示例。以下是使用的test.proto文件
package T;
message Test
{
optional int32 id = 1;
}
3.1通过类型名字创建出类型对象
预编译好proto模式
//! 利用类型名字构造对象.
/*!
* @Param type_name 类型名字,比如 "Test.TestMessage".
* @Return 对象指针,new 出来的,使用者负责释放.
*/
#include <google/protobuf/descriptor.h>
#include <google/protobuf/message.h>
#include "cpp/test.pb.h" // 这是protoc给你生成的文件
int main()
{
// 先获得类型的Descriptor .
auto descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("T.Test");
if (nullptr == descriptor)
{
return 0 ;
}
// 利用Descriptor拿到类型注册的instance. 这个是不可修改的.
auto prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
if ( nullptr == descriptor)
{
return 0 ;
}
// 构造一个可用的消息.
auto message = prototype->New();
// 只有当我们预先编译了test消息并且正确链接才能这么干.
auto test = dynamic_cast<T::Test*>(message);
// 直接调用message的具体接口
// 其实这些接口是语法糖接口.所以并没有对应的反射机制来对应调用.
// 反射机制实现了的Set/Get XXX系列接口,是属于Reflection的接口,接收Message作为参数.
test->set_id(1);
std::cout<<test->Utf8DebugString()<<std::endl;
delete message ;
return 0 ;
}
直接解析proto文件模式
#include <iostream>
#include <google/protobuf/compiler/importer.h>
#include <google/protobuf/dynamic_message.h>
int main()
{
// 准备配置好文件系统
google::protobuf::compiler::DiskSourceTree sourceTree;
// 将当前路径映射为项目根目录 , project_root 仅仅是个名字,你可以你想要的合法名字.
sourceTree.MapPath("project_root","./");
// 配置动态编译器.
google::protobuf::compiler::Importer importer(&sourceTree, NULL);
// 动态编译proto源文件。 源文件在./source/proto/test.proto .
importer.Import("project_root/source_proto/test.proto");
// 现在可以从编译器中提取类型的描述信息.
auto descriptor1 = importer.pool()->FindMessageTypeByName("T.Test");
// 创建一个动态的消息工厂.
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 ;
return 0 ;
}
3.2 通过对象和对象的属性获取名字,修改对应属性
#include "cpp/test.pb.h"
#include <iostream>
int main()
{
// 拿到一个对象,不在乎怎么拿到,可以是通过反射拿到。
// 这里简单直接的创建一个.
T::Test p_test ;
// 拿到对象的描述包.
auto descriptor = p_test.GetDescriptor() ;
// 拿到对象的反射配置.
auto reflecter = p_test.GetReflection() ;
// 拿到属性的描述包.
auto field = descriptor->FindFieldByName("id");
// 设置属性的值.
reflecter->SetInt32(&p_test , field , 5 ) ;
// 获取属性的值.
std::cout<<reflecter->GetInt32(p_test , field)<< std::endl ;
return 0 ;
}
4. 处理Protobuf反射,通过协议字串动态生成协议
下面两部分是针对protobuf通讯协议的内容。下面这两个函数可以动态生成pb的message,其中ProtoMsg是pb package的名字
inline google::protobuf::Message* CreateMessage(const std::string& msg)
{
google::protobuf::Message* message = NULL;
const google::protobuf::Descriptor* descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("ProtoMsg." + msg);
if (descriptor)
{
const google::protobuf::Message* prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
if (prototype)
{
message = prototype->New();
}
}
return message;
}
inline void ReleaseMessage(google::protobuf::Message* pMsg)
{
if (NULL != pMsg)
{
pMsg->Clear();
delete pMsg;
}
}
5. 通过反射将配置中的值设置进pb字段
可以通过如下方法,自动创建message并对field赋值:
// mstrCurMsg 当前正在执行的协议
google::protobuf::Message* pMsg = Test::CreateMessage(mstrCurMsg);
const google::protobuf::Descriptor* pDescriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("ProtoMsg." + mstrCurMsg);
assert(NULL != pDescriptor);
// 这一个可以获取到反射类,然后可以将配置中值赋值进去
const google::protobuf::Reflection* pReflection = pMsg->GetReflection();
assert(NULL != pReflection);
for (int i = 0; i < pDescriptor->field_count(); ++i)
{
const google::protobuf::FieldDescriptor* pFieldDescriptor = pDescriptor->field(i);
assert(NULL != pFieldDescriptor);
const std::string& strFieldName = pFieldDescriptor->name();
const TestConfigModule::MsgEntry* pMsgEntry = pMsgStruct->GetMsgEntry(strFieldName);
assert(NULL != pMsgEntry);
// 读取字段类型,顺带可以做类型检查
assert(pMsgEntry->mnType == pFieldDescriptor->type());
// 设置值
switch (pMsgEntry->mnType)
{
case Test::TYPE_STRING:
pReflection->SetString(pMsg, pFieldDescriptor, pMsgEntry->mstrValue);
break;
// ...
default:
break;
}
}
std::string strData;
if (!pMsg->SerializeToString(&strData))
{
m_pLogModule->LogNormal("Test stop, cannot SerializeToString ", mstrCurMsg, __FUNCTION__, __LINE__);
return;
}
Test::ReleaseMessage(pMsg);
6. serialize_message
serialize_message遍历提取message中各个字段以及对应的值,序列化到string中。主要思路就是通过Descriptor得到每个字段的描述符:字段名、字段的cpp类型。通过Reflection的GetX接口获取对应的value。
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 field_count(); ++i) {
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()) {
#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(&wsize), sizeof(wsize));
serialized_string->append(field->name().c_str(), field->name().size());
wsize = sizeof(value);
serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
serialized_string->append(reinterpret_cast(&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();
//写入name占用字节数
serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
//写入name
serialized_string->append(field->name().c_str(), field->name().size());
wsize = sizeof(value);
//写入value占用字节数
serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
//写入value
serialized_string->append(reinterpret_cast(&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(&wsize), sizeof(wsize));
serialized_string->append(field->name().c_str(), field->name().size());
wsize = value.size();
serialized_string->append(reinterpret_cast(&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(&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(&wsize), sizeof(wsize));
serialized_string->append(value.c_str(), value.size());
break;
}
}
}
}
}
7. 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) {
const google::protobuf::Descriptor* descriptor = message->GetDescriptor();
const google::protobuf::Reflection* reflection = message->GetReflection();
std::map field_map;
for (int i = 0; i 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.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(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::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(value.c_str())));
std::cout name() (value.c_str())) enum_type()->FindValueByNumber(*(reinterpret_cast(value.c_str())));
reflection->SetEnum(message, field, enum_value_descriptor);
std::cout name() (value.c_str())) SetString(message, field, value);
std::cout name() MutableMessage(message, field);
parse_message(value, submessage);
break;
}
default: {
break;
}
}
}
}
只是基本介绍下,在遇到问题时候还是多求助官方文档的。
《Protobuf反射详解》有1条评论