用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沒有定義愕把,不應(yīng)影響其他字段解析。

開發(fā)

搜索現(xiàn)成方案

網(wǎng)上搜索了一圈森爽,找到的類似方案大多需要導(dǎo)入完整的proto文件:

int DynamicParseFromPBFile(const std::string& file, const std::string& classname, 
      const std::string& pb_str) {
  // ...
  // 導(dǎo)入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ù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末栅屏,一起剝皮案震驚了整個濱河市飘千,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌栈雳,老刑警劉巖护奈,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異哥纫,居然都是意外死亡霉旗,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門蛀骇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來厌秒,“玉大人,你說我怎么就攤上這事擅憔⊥疑粒” “怎么了?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵雕欺,是天一觀的道長岛马。 經(jīng)常有香客問我,道長屠列,這世上最難降的妖魔是什么啦逆? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮笛洛,結(jié)果婚禮上夏志,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好沟蔑,可當(dāng)我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布湿诊。 她就那樣靜靜地躺著,像睡著了一般瘦材。 火紅的嫁衣襯著肌膚如雪厅须。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天食棕,我揣著相機與錄音朗和,去河邊找鬼。 笑死簿晓,一個胖子當(dāng)著我的面吹牛眶拉,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播憔儿,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼忆植,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了谒臼?” 一聲冷哼從身側(cè)響起朝刊,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎屋休,沒想到半個月后坞古,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡劫樟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年痪枫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叠艳。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡奶陈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出附较,到底是詐尸還是另有隱情吃粒,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布拒课,位于F島的核電站徐勃,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏早像。R本人自食惡果不足惜僻肖,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卢鹦。 院中可真熱鬧臀脏,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至搀玖,卻和暖如春余境,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背灌诅。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工葛超, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人延塑。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像答渔,于是被迫代替她去往敵國和親关带。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,960評論 2 355

推薦閱讀更多精彩內(nèi)容