利用C++開發(fā)一個protobuf動態(tài)解析工具
為什么需要這個工具
數(shù)據(jù)庫中存儲的protobuf序列化的內(nèi)容,有時候查問題想直接解析查看內(nèi)容。很多編碼在網(wǎng)上很容易找到編解碼工具,但protobuf沒有找到編解碼工具,可能這樣的需求比較少吧,那就自己用C++實現(xiàn)一個。
需求描述
我們知道,要解析protobuf,需要有proto定義,所以我們的輸入?yún)?shù)需要包含序列化的數(shù)據(jù)以及proto定義,如果proto中包含多個message,還需要指定解析到哪個message。所以一共是三個輸入?yún)?shù)。
此外,為了方便使用,我們的工具不要求給出完整的proto定義,如果有嵌套的message沒有定義,不應影響其他字段解析。
開發(fā)
搜索現(xiàn)成方案
網(wǎng)上搜索了一圈,找到的類似方案大多需要導入完整的proto文件:
int DynamicParseFromPBFile(const std::string& file, const std::string& classname, const std::string& pb_str) { // ... // 導入proto文件 ::google::protobuf::compiler::Importer importer(&sourceTree, NULL); importer.Import(file); // 找到要解析的message auto descriptor = importer.pool()->FindMessageTypeByName(classname); ::google::protobuf::DynamicMessageFactory factory; auto message = factory.GetPrototype(descriptor); // 動態(tài)創(chuàng)建message對象 auto msg = message->New(); msg->ParseFromString(pb_str); // msg即為解析到的結(jié)構(gòu) }
這樣可以實現(xiàn)動態(tài)解析,但仍不滿足我們的需求——即使proto不完整,也希望能解析。
舉個例子:
message MyMsg { optional uint64 id = 1; optional OtherMsg other = 2; }
MyMsg中包含OtherMsg類型,但并沒有給出OtherMsg的定義,所以無法正常解析。
AST在哪里
事實上,在解析proto文件時,肯定需要先將其解析為抽象語法樹(AST),在AST中,我們可以很容易修改proto的定義,例如將other字段刪掉,或者將其類型改為bytes,這樣就可以正常解析了。
那么,proto文件解析成的AST結(jié)構(gòu)在哪里呢?只能從源碼中尋找答案了。
一番查找后,終于看到了FindFileByName方法的這段代碼:
bool SourceTreeDescriptorDatabase::FindFileByName(const std::string& filename, FileDescriptorProto* output) { // ... io::Tokenizer tokenizer(input.get(), &file_error_collector); Parser parser; // Parse it. output->set_name(filename); return parser.Parse(&tokenizer, output) && !file_error_collector.had_errors(); }
從這段代碼中可以看到,F(xiàn)ileDescriptorProto就是我們要找的AST結(jié)構(gòu)。那么這到底是個什么結(jié)構(gòu)呢?
其實,F(xiàn)ileDescriptorProto本身也是一個proto定義的message:
message FileDescriptorProto { optional string name = 1; // file name, relative to root of source tree optional string package = 2; // e.g. "foo", "foo.bar", etc. // All top-level definitions in this file. repeated DescriptorProto message_type = 4; repeated EnumDescriptorProto enum_type = 5; repeated ServiceDescriptorProto service = 6; repeated FieldDescriptorProto extension = 7; // ... }
從它的字段中可以看到,其代表的是整個proto文件,包括文件中的所有message、enum等定義。
開始寫代碼
第一步
仿照上面的源碼,將輸入的proto定義解析為FileDescriptorProto對象:
// proto輸入 istringstream ss(proto); istream* is = &ss; io::IstreamInputStream input(is); // 解析到FileDescriptorProto AST io::Tokenizer tokenizer(&input, nullptr); FileDescriptorProto output; compiler::Parser parser; if (!parser.Parse(&tokenizer, &output)) { err_msg = "parse proto failed"; return -1; } output.set_name("proto"); output.clear_source_code_info(); printf("MSG: proto parsed output: %s\n", output.DebugString().c_str());
第2步
處理FileDescriptorProto對象,將沒有給定義的字段類型都改成bytes,保證proto可以正常解析:
int ConvertUnknownType2Bytes(FileDescriptorProto& file_descriptor_proto) { // 找出所有給出定義的message類型名 set<string> typename_set; for (auto const& msgtype : file_descriptor_proto.message_type()) { typename_set.insert(msgtype.name()); // message內(nèi)嵌套定義的message也要包含在內(nèi) for (auto const& subtype : msgtype.nested_type()) { typename_set.insert(subtype.name()); } } // 遍歷所有field,檢查其類型是否存在定義 for (auto& msgtype : *file_descriptor_proto.mutable_message_type()) { for (auto& field : *msgtype.mutable_field()) { auto type_name = field.type_name(); // 基本類型的type_name是空的 if (!type_name.empty()) { // 如果typename_set中找不到該類型名,則轉(zhuǎn)為bytes類型 if (typename_set.find(type_name) == typename_set.end()) { field.clear_type_name(); field.set_type(FieldDescriptorProto_Type_TYPE_BYTES); } } } } return 0; }
第3步
解析修改后的FileDescriptorProto對象,創(chuàng)建指定message類型對象。
// 解析proto并檢查錯誤 SimpleDescriptorDatabase db; db.Add(output); DescriptorPool pool(&db); auto descriptor = pool.FindMessageTypeByName(msg_type_name); if (descriptor == nullptr) { // proto結(jié)構(gòu)有錯 err_msg = "parse proto failed. FindMessageTypeByName result is null"; return -1; } DynamicMessageFactory factory; auto message = factory.GetPrototype(descriptor); unique_ptr<Message> msg(message->New());
第4步
將序列化的數(shù)據(jù)解析到msg中:
msg->ParseFromString(serilized_pb); cout << "proto msg: " << msg->ShortDebugString().c_str() << endl;
這樣,我們就成功實現(xiàn)了動態(tài)解析,也成功將不可讀的二進制數(shù)據(jù)serilized_pb以可讀的形式打印出來了。
總結(jié)
我們?yōu)榱藢崿F(xiàn)動態(tài)解析不完整的proto,我們首先從源碼中找到了將proto定義轉(zhuǎn)化為AST——也就是FileDescriptorProto——的方法。
接著,我們將AST對象進行修改,將不合法的proto改成合法的。
最后,我們再利用修改后的FileDescriptorProto構(gòu)造出需要的message對象,解析序列化的數(shù)據(jù)。
到此這篇關(guān)于利用C++開發(fā)一個protobuf動態(tài)解析工具的文章就介紹到這了,更多相關(guān)C++ protobuf動態(tài)解析內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Qt多線程實現(xiàn)網(wǎng)絡發(fā)送文件功能
這篇文章主要為大家詳細介紹了Qt多線程實現(xiàn)網(wǎng)絡發(fā)送文件功能,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-08-08C++內(nèi)存管理之簡易內(nèi)存池的實現(xiàn)
大家好,本篇文章主要講的是C++內(nèi)存管理之簡易內(nèi)存池的實現(xiàn),感興趣的同學趕快來看一看吧,對你有幫助的話記得收藏一下2021-12-12C++的try塊與異常處理及調(diào)試技術(shù)實例解析
這篇文章主要介紹了C++的try塊與異常處理及調(diào)試技術(shù)實例解析,有助于讀者加深對try塊調(diào)試技術(shù)的認識,需要的朋友可以參考下2014-07-07C++類模板實戰(zhàn)之vector容器的實現(xiàn)
本文我們將做一個類模板實戰(zhàn)-手寫精簡版vector容器。讓我們自己封裝一個數(shù)組類,可以適應基本數(shù)據(jù)類型和自定義數(shù)據(jù)類型,感興趣的可以了解一下2022-07-07C語言結(jié)構(gòu)體中內(nèi)存對齊的問題理解
內(nèi)存對齊”應該是編譯器的“管轄范圍”。編譯器為程序中的每個“數(shù)據(jù)單元”安排在適當?shù)奈恢蒙?。但是C語言的一個特點就是太靈活,太強大,它允許你干預“內(nèi)存對齊”。如果你想了解更加底層的秘密,“內(nèi)存對齊”對你就不應該再模糊了2022-02-02