找回密码
 立即注册
查看: 169|回复: 0

用C++开发一个protobuf动态解析东西

[复制链接]
发表于 2024-7-15 18:11 | 显示全部楼层 |阅读模式
Protobuf动态解析东西

为什么需要这个东西

数据库中存储的protobuf序列化的内容,有时候查问题想直接解析查看内容。很多编码在网上很容易找到编解码东西,但protobuf没有找到编解码东西,可能这样的需求斗劲少吧,那就本身用C++实现一个。
需求描述

我们知道,要解析protobuf,需要有proto定义,所以我们的输入参数需要包含序列化的数据以及proto定义,如果proto中包含多个message,还需要指定解析到哪个message。所以一共是三个输入参数。
此外,为了便利使用,我们的东西不要求给出完整的proto定义,如果有嵌套的message没有定义,不应影响其他字段解析。
开发

搜索现成方案

网上搜索了一圈,找到的类似方案大多需要导入完整的proto文件:
  1. int DynamicParseFromPBFile(const std::string& file, const std::string& classname,
  2.             const std::string& pb_str) {
  3.     // ...
  4.     // 导入proto文件
  5.     ::google::protobuf::compiler::Importer importer(&sourceTree, NULL);
  6.     importer.Import(file);
  7.     // 找到要解析的message
  8.     auto descriptor = importer.pool()->FindMessageTypeByName(classname);
  9.     ::google::protobuf::DynamicMessageFactory factory;
  10.     auto message = factory.GetPrototype(descriptor);
  11.     // 动态创建message对象
  12.     auto msg = message->New();
  13.     msg->ParseFromString(pb_str);
  14.     // msg即为解析到的布局
  15. }
复制代码
这样可以实现动态解析,但仍不满足我们的需求——即使proto不完整,也但愿能解析。
举个例子:
  1. message MyMsg {
  2.     optional uint64 id = 1;
  3.     optional OtherMsg other = 2;
  4. }
复制代码
MyMsg中包含OtherMsg类型,但并没有给出OtherMsg的定义,所以无法正常解析。
AST在哪里

事实上,在解析proto文件时,必定需要先将其解析为抽象语法树(AST),在AST中,我们可以很容易改削proto的定义,例如将other字段删掉,或者将其类型改为bytes,这样就可以正常解析了。
那么,proto文件解析成的AST布局在哪里呢?只能从源码中寻找答案了。
一番查找后,终于看到了FindFileByName方式的这段代码:
protobuf/importer.cc at main · protocolbuffers/protobuf
  1. bool SourceTreeDescriptorDatabase::FindFileByName(const std::string& filename,
  2.                                                   FileDescriptorProto* output) {
  3.     // ...
  4.     io::Tokenizer tokenizer(input.get(), &file_error_collector);
  5.     Parser parser;
  6.     // Parse it.
  7.     output->set_name(filename);
  8.     return parser.Parse(&tokenizer, output) && !file_error_collector.had_errors();
  9. }
复制代码
从这段代码中可以看到,FileDescriptorProto就是我们要找的AST布局。那么这到底是个什么布局呢?
其实,FileDescriptorProto本身也是一个proto定义的message:
protobuf/descriptor.proto at bb96ec94af136216e4c3195166d1d80dd2bcf8a6 · protocolbuffers/protobuf
  1. message FileDescriptorProto {
  2.   optional string name = 1;     // file name, relative to root of source tree
  3.   optional string package = 2;  // e.g. ”foo”, ”foo.bar”, etc.
  4.   // All top-level definitions in this file.
  5.   repeated DescriptorProto message_type = 4;
  6.   repeated EnumDescriptorProto enum_type = 5;
  7.   repeated ServiceDescriptorProto service = 6;
  8.   repeated FieldDescriptorProto extension = 7;
  9.     // ...
  10. }
复制代码
从它的字段中可以看到,其代表的是整个proto文件,包罗文件中的所有message、enum等定义。
开始写代码

第一步,仿照上面的源码,将输入的proto定义解析为FileDescriptorProto对象:
  1. // proto输入
  2. istringstream ss(proto);
  3. istream* is = &ss;
  4. io::IstreamInputStream input(is);
  5. // 解析到FileDescriptorProto AST
  6. io::Tokenizer tokenizer(&input, nullptr);
  7. FileDescriptorProto output;
  8. compiler::Parser parser;
  9. if (!parser.Parse(&tokenizer, &output)) {
  10.   err_msg = ”parse proto failed”;
  11.   return -1;
  12. }
  13. output.set_name(”proto”);
  14. output.clear_source_code_info();
  15. printf(”MSG: proto parsed output: %s\n”, output.DebugString().c_str());
复制代码
第2步,措置FileDescriptorProto对象,将没有给定义的字段类型都改成bytes,保证proto可以正常解析:
  1. int ConvertUnknownType2Bytes(FileDescriptorProto& file_descriptor_proto) {
  2.   // 找出所有给出定义的message类型名
  3.     set<string> typename_set;
  4.   for (auto const& msgtype : file_descriptor_proto.message_type()) {
  5.     typename_set.insert(msgtype.name());
  6.         // message内嵌套定义的message也要包含在内
  7.     for (auto const& subtype : msgtype.nested_type()) {
  8.       typename_set.insert(subtype.name());
  9.     }
  10.   }
  11.     // 遍历所有field,查抄其类型是否存在定义
  12.   for (auto& msgtype : *file_descriptor_proto.mutable_message_type()) {
  13.     for (auto& field : *msgtype.mutable_field()) {
  14.       auto type_name = field.type_name();
  15.             // 基本类型的type_name是空的
  16.       if (!type_name.empty()) {
  17.         // 如果typename_set中找不到该类型名,则转为bytes类型
  18.         if (typename_set.find(type_name) == typename_set.end()) {
  19.           field.clear_type_name();
  20.           field.set_type(FieldDescriptorProto_Type_TYPE_BYTES);
  21.         }
  22.       }
  23.     }
  24.   }
  25.   return 0;
  26. }
复制代码
第3步,解析改削后的FileDescriptorProto对象,创建指定message类型对象。
  1. // 解析proto并查抄错误
  2. SimpleDescriptorDatabase db;
  3. db.Add(output);
  4. DescriptorPool pool(&db);
  5. auto descriptor = pool.FindMessageTypeByName(msg_type_name);
  6. if (descriptor == nullptr) {
  7.   // proto布局有错
  8.   err_msg = ”parse proto failed. FindMessageTypeByName result is null”;
  9.   return -1;
  10. }
  11. DynamicMessageFactory factory;
  12. auto message = factory.GetPrototype(descriptor);
  13. unique_ptr<Message> msg(message->New());
复制代码
第4步,将序列化的数据解析到msg中:
  1. msg->ParseFromString(serilized_pb);
  2. cout << ”proto msg: ” << msg->ShortDebugString().c_str() << endl;
复制代码
这样,我们就成功实现了动态解析,也成功将不成读的二进制数据serilized_pb以可读的形式打印出来了。
总结

我们为了实现动态解析不完整的proto,我们首先从源码中找到了将proto定义转化为AST——也就是FileDescriptorProto——的方式。接着,我们将AST对象进行改削,将不合法的proto改成合法的。最后,我们再操作改削后的FileDescriptorProto构造出需要的message对象,解析序列化的数据。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-12-4 16:44 , Processed in 0.101140 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表