1. 程式人生 > >Google protocol buffer 的反射機制和應用

Google protocol buffer 的反射機制和應用

何謂反射?

我在工作中大量使用了 google protocal buffer, 為了方便描述, 下文簡稱為 pb. pb 的作用和基本使用方法在這裡就不再陳述, 相關的文章網上很多.  這裡主要介紹 pb 的反射機制. 什麼是反射機制呢? 該機制能在執行時, 對於任意一個類, 都能知道這個類的所有屬性和方法; 對於任意一個物件, 都能呼叫它的任意一個方法和屬性; 簡言之, 反射機制使程式設計師可以動態獲取物件資訊以及動態呼叫物件的方法. 下面用兩個例子來解釋什麼是反射. 1. 動態獲取物件資訊 對於一個 c++ 類 Player , 如果標準提供一個函式 Show(Player), 可以取到 Player 的所有成員函式和變數的名字. 我們就說 Show 函式可以動態獲取物件的資訊. 2. 動態呼叫物件的方法. Player 類裡有一個函式 AddMoney(int a), playerA 是一個Player 物件.  如果標準提供一個函式 Call(playerA, "AddMoney", 100), 能達到 playerA.AddMoney(100) 相同的效果. 我們就說 Call 函式可以動態呼叫物件的方法. 當然, c++ 沒有 Show 和 Call 這兩個函式, 所以c++不具備反射機制. 提供了 Show 和 Call, 或是能達到同樣功能的函式的系統, 就實現了反射機制.

pb 的反射

知道了什麼是反射, 我們來看看 pb 是如何實現的. 首先認識幾個關鍵類. google::protobuf::Message 這是使用者自定義訊息的基類. 系統會為給每個使用者自定義訊息例項化一個預設物件, 物件用 Message* 儲存. Message 類定義了 New() 虛擬函式, 可以返回本物件的一份新的例項, 型別和本物件的真實型別相同. 拿到了 Message*, 不用知道它的具體型別, 就可以建立和它一樣型別的物件. 通過 New() 函式建立的物件, 需要使用者自己釋放. New() 函式是執行緒安全的. google::protobuf::MessageFactory 管理所有自動例項化的 Message 物件. 它自身也會被自動例項化, 通過靜態函式 generated_factory() 能拿到該物件指標. 通過成員函式 
GetPrototype() 能拿到自動例項化的 Message*; GetPrototype() 的引數是一個 Descriptor*; MessageFactory 是執行緒安全的. google::protobuf::Descriptor
每一個使用者自定義訊息, 都對應一個 Descriptor 物件. 該物件描述了訊息的格式. 系統會為每個訊息的 Descriptor 例項化一個預設物件. google::protobuf::DescriptorPool 管理所有自動例項化的 Descriptot 物件. 它自身也會自動被例項化, 通過靜態函式 generated_pool() 能拿到該物件指標. 通過成員函式 
FindMessageTypeByName() 能拿到對應的 Descriptor*. DescriptorPool 是執行緒安全的 google::protobuf::FieldDescriptor 一個 Message 裡有若干域, 每個域用一個 FieldDescriptor 來描述它的資訊. 可以通過 Descriptor 的成員函式 FindFieldByName() 來取得對應的 FieldDescriptor*;  google::protobuf::Reflection 這個類提供了一系列介面, 用來操作 Message 的域. 這些介面一般需要兩個引數 Message* 和 FieldDescriptor*; 可以通過 Message 的成員函式 GetReflection(), 來拿到對應的 Reflection*; 通過以上類, 我們就可以根據訊息的名字, 得到訊息的例項了, 並且可以動態的讀寫訊息的資料. 這些類一起實現了 pb 的反射機制. 下面我們看看能用反射做些什麼. 

基於訊息名的網路傳輸

在網路中傳輸pb格式的訊息時, 我們通常會這麼定義資料流: messageid | size | data messageid是一個整數, size是後面data的長度, data是系列化的pb message; 訊息的接收和傳送方會維護一個 messageid -- pb message 的對應表. 根據message id辨認是那種 pb message. 這種方法的缺點在於, 大家必須使用一致的對應表, 否則就無法解析訊息. 其實, 我們可以在訊息裡用 pb message name 替代 messageid. 通過 strMsgName 獲得 message, 只需要簡單的幾步 const google::protobuf::Descriptor* descriptor              = google::protobuf::DescriptorPool::generated_pool()             ->FindMessageTypeByName(strMsgName); const google::protobuf::Message* prototype              = google::protobuf::MessageFactory::generated_factory()             ->GetPrototype( descriptor ); google::protobuf::Message *pMsg = prototype->New(); 這種方法的有點事可以省略對應表. 缺點是傳輸的資料長度略長, 訊息解析的效率略低.

自描述的訊息

pb 定義了一個 proto 格式的訊息 FileDescriptorSet, 這個訊息可以用來描述任何使用者定義的訊息. pb 在解釋使用者訊息是, 首先轉化成 FileDescriptorSet, 在解釋這個 FileDescriptorSet. 我們可以在 pb 原始碼包的  descriptor.proto 中看到這個檔案. 所以, pb 的訊息可以是自描述的. 我們可以定義一個這樣的訊息 message SelfDescMsg {     required     FileDescriptorSet proto_files    = 1;             required     string type_name    = 2;             required     string data               = 3;                  } ptoto_files 是訊息的描述, type_name是訊息的名字(proto檔案裡的訊息名), 之所以要指定訊息的名字, 是因為一個proto檔案裡可能有多個 message. data 是訊息系列化後的資料. 用protoc生成程式碼是引數可以這麼寫: protoc --cpp_out=. --descriptor_set_out=Card.desc Card.proto Card.desc 就是 Card.proto 對應的 FileDescriptorSet; 像這樣傳送資料: 假設我們 Card.proto 裡有一個 message 叫 CardHero hero, 我們往 hero 寫入了資料. 傳送 SelfDescMsg 時, 我們用 Card.desc 裡的資料填充 proto_files. 用 "CardHero" 填充 type_name. 用 hero 系列化後的字串填充 data. 最後, 將 SelfDescMsg  系列化後傳送. SelfDescMsg  sdMse; fstream desc("Card.desc", ios::in | ios::binary); sdMse. mutable_proto_files()->ParseFromIstream(&desc); sdMse.set_type_name((hero.GetDescriptor())->full_name()); sdMse.clear_data(); hero.SerializeToString(sdMse.mutable_data()); 像這樣解析資料: 解析這些資料時, 不需要知道 Card.proto. 首先 反序列化 SelfDescMsg , 通過 proto_files 得到 FileDescriptorSet, 通過 type_name 得到訊息的型別名, 利用反射得到訊息例項, 然後反系列化訊息. 最後通過 message 的 reflection 介面操作 message 的各個欄位. SelfDescMsg sdMse; sdMse.ParseFromString(&input)); SimpleDescriptorDatabase db; for(int i=0;i<sdMse.proto_files().file_size();i++) {    db.Add(sdmessage.proto_files().file(i));  } DescriptorPool pool(&db); const Descriptor *descriptor = pool.FindMessageTypeByName(sdMse.type_name()); DynamicMessageFactory factory(&pool); Message *msg = factory.GetPrototype(descriptor)->New(); msg->ParseFromString(sdMse._data()); 自描述的訊息使用起來有一定難度, 效率也打了不少折扣, 它提供了一種不瞭解訊息格式就可以使用訊息的方式.

動態讀寫訊息資料

在我做的專案中, 許多資料是儲存在 pb message 裡的. 系統執行時, 我們想要知道 pb message 裡某些內容的資訊, 還想要修改某些類容. 比如有這樣一個訊息: message CardBase {     optional    int64        id        = 1;             repeated   int32        values = 2; } CardBase base; 在執行時, 如何知道 base.id 的值, 又如何修改它 ? 用靜態程式碼很容易做到這點, 但是 c 語言並不是解釋型語言, 無法再執行時執行靜態程式碼. 為了實現這樣的需求, 我寫了幾個函式.  使用這樣函式可以動態讀寫 pb message 裡的全部或部分資料. 這個 message 可以巢狀其它 message, 裡面的欄位也支援 repeated 型別. 為了減少複雜度, 可修改的資料型別只支援這幾種 : int32 int64, string. 這些需要兩個引數來定位訊息裡的域, 第一個引數是 message 例項, 第二個引數是域的路徑 path, path 是一個字串, 它的格式類似這樣:  player.baseMgr.army(2).foodcosttime army(2) 表示repeated域的第二個元素. 定位某個域後, 就可以讀寫這個域了. 程式碼很簡單, 我會把它們放到 github 上.