需求背景
在接收到 protobuf 數據之后,如何自動創建具體的 Protobuf Message 對象,再做反序列化。“自動”的意思主要有兩個方面:(1)當程序中新增一個 protobuf Message 類型時,這部分代碼不需要修改,不需要自己去注冊消息類型,不需要重啟進程,只需要提供protobuf文件;(2)當protobuf Message修改后,這部分代碼不需要修改,不需要自己去注冊消息類型,不需要重啟進程只需要提供修改后protobuf文件。
技術介紹
Protobuf的入門可以參考Google Protocol Buffer 的在線幫助 網頁 或者IBM developerwor上的文章《Google Protocol Buffer 的使用和原理》。
protobuf的動態解析在google protobuf buffer官網並沒有什么介紹。通過google出的一些參考文檔可以知道,其實,Google Protobuf 本身具有很強的反射(reflection)功能,可以根據 type name 創建具體類型的 Message 對象,我們直接利用即可,應該就可以滿足上面的需求。
實現可以參考淘寶的文章《玩轉Protocol Buffers 》,里面對protobuf的動態解析的原理做了詳細的介紹,在此我介紹一下Protobuf class diagram。
大家通常關心和使用的是圖的左半部分:MessageLite、Message、Generated Message Types (Person, AddressBook) 等,而較少注意到圖的右半部分:Descriptor, DescriptorPool, MessageFactory。
上圖中,其關鍵作用的是 Descriptor class,每個具體 Message Type 對應一個 Descriptor 對象。盡管我們沒有直接調用它的函數,但是Descriptor在“根據 type name 創建具體類型的 Message 對象”中扮演了重要的角色,起了橋梁作用。上圖的紅色箭頭描述了根據 type name 創建具體 Message 對象的過程。
實現
先直接上代碼,這個代碼來自於《玩轉Protocol Buffers 》:
#include <iostream> #include <google/protobuf/descriptor.h> #include <google/protobuf/descriptor.pb.h> #include <google/protobuf/dynamic_message.h> #include <google/protobuf/compiler/importer.h>
using namespace std; using namespace google::protobuf; using namespace google::protobuf::compiler;
int main(int argc,const char *argv[]) { DiskSourceTree sourceTree; //look up .proto file in current directory sourceTree.MapPath("","./"); Importer importer(&sourceTree, NULL); //runtime compile foo.proto importer.Import("foo.proto");
const Descriptor *descriptor = importer.pool()-> FindMessageTypeByName("Pair"); cout << descriptor->DebugString();
// build a dynamic message by "Pair" proto DynamicMessageFactory factory; const Message *message = factory.GetPrototype(descriptor); // create a real instance of "Pair" Message *pair = message->New();
// write the "Pair" instance by reflection const Reflection *reflection = pair->GetReflection();
const FieldDescriptor *field = NULL; field = descriptor->FindFieldByName("key"); reflection->SetString(pair, field,"my key"); field = descriptor->FindFieldByName("value"); reflection->SetUInt32(pair, field, 1111);
cout << pair->DebugString(); delete pair; return0; } |
那我們就來看看上面的代碼
1)把本地地址映射為虛擬地址
DiskSourceTree sourceTree;
//look up .proto file in current directory
sourceTree.MapPath("","./");
2)構造DescriptorPool
Importer importer(&sourceTree, NULL);
//runtime compile foo.proto
importer.Import("foo.proto");
3)獲取Descriptor
const Descriptor *descriptor = importer.pool()->FindMessageTypeByName("Pair");
4)通過Descriptor獲取Message
const Message *message = factory.GetPrototype(descriptor);
5)根據類型信息使用DynamicMessage new出這個類型的一個空對象
Message *pair = message->New();
6)通過Message的reflection操作message的各個字段
const Reflection *reflection = pair->GetReflection();
const FieldDescriptor *field = NULL;
field = descriptor->FindFieldByName("key");
reflection->SetString(pair, field,"my key");
field = descriptor->FindFieldByName("value");
reflection->SetUInt32(pair, field, 1111);
直接copy上面代碼看起來我們上面的需求就滿足了,只是唯一的缺點就是每次來個包加載一次配置文件,當時覺得性能應該和讀取磁盤的性能差不多,但是經過測試性能極差,一個進程每秒盡可以處理1000多個包,經過分析性能瓶頸不在磁盤,而在頻繁調用malloc和free上。
看來我們得重新考慮實現,初步的實現想法:只有protobuf描述文件更新時再重新加載,沒有更新來包只需要使用加載好的解析就可以。這個方案看起來挺好的,性能應該不錯,經過測試,性能確實可以,每秒可以處理3萬左右的包,但是實現中遇到了困難。要更新原來的Message,必須更新Importer和Factory,那么要更新這些東西,就涉及到了資源的釋放。經過研究這些資源的釋放順序特別重要,下面就介紹一下protobuf相關資源釋放策略。
動態的Message是我們用DynamicMessageFactory構造出來的,因此銷毀Message必須用同一個DynamicMessageFactory。 動態更新.proto文件時,我們銷毀老的並使用新的DynamicMessageFactory,在銷毀DynamicMessageFactory之前,必須先刪除所有經過它構造的Message。
原理:DynamicMessageFactory里面包含DynamicMessage的共享信息,析構DynamicMessage時需要用到。生存期必須保持Descriptor>DynamicMessageFactory>DynamicMessage。
釋放順序必須是:釋放所有DynamicMessage,釋放DynamicMessageFactory,釋放Importer。
總結
資源釋放前,必須要了解資源的構造原理,通過構造原理反推釋放順序,這樣就少走彎路、甚至不走。
參考文獻
Google Protocol Buffer 的在線幫助 網頁
一種自動反射消息類型的 Google Protobuf 網絡傳輸方案
《Google Protocol Buffer 的使用和原理》