ProtoBuf 反射詳解

yingshin發表於2016-05-22

Protocol Buffer 簡稱 ProtoBuf,是用於結構化資料序列化的靈活、高效、自動的方法,又如 XML,不過它更小、更快、也更簡單。你可以定義自己的資料結構,然後使用程式碼生成器生成的程式碼來讀寫這個資料結構。你甚至可以在無需重新部署程式的情況下更新資料結構。

本文主要介紹 protobuf 裡的反射功能,使用的pb版本為2.6.1,同時為了簡潔,對 repeated/extension 欄位的處理方法沒有說明。

最初是起源於這樣一個問題:

給定一個pb物件,如何自動遍歷該物件的所有欄位?

即是否有一個通用的方法可以遍歷任意pb物件的所有欄位,而不用關心具體物件型別。

使用場景上有很多:

比如json格式字串的相互轉換,或者bigtable里根據pb物件的欄位自動寫列名和對應的value。

例如定義了pb messge型別Person如下:

能否將該物件自動轉化為json字串{"name":"yingshin","age":21},或者自動的寫hbase裡的多列:

key column-name column-value
uid name yingshin
uid age 21

如果設定了新的欄位,比如person.set_email("zhy198606@gmail.com"),則自動新增新的一列:

key column-name column-value
uid email zhy198606@gmail.com

答案就是 pb的反射功能

我們的目標是提供這樣兩個介面:

接下來逐步介紹下如何實現。

1. 反射相關介面

要介紹pb的反射功能,先看一個相關的UML示例圖:

pb-reflection

各個類以及介面說明:

1.1 Message

Person是自定義的pb型別,繼承自Message. MessageLite作為Message基類,更加輕量級一些。

通過Message的兩個介面GetDescriptor/GetReflection,可以獲取該型別對應的Descriptor/Reflection。

1.2 Descriptor

Descriptor是對message型別定義的描述,包括message的名字、所有欄位的描述、原始的proto檔案內容等。

本文中我們主要關注跟欄位描述相關的介面,例如:

  1. 獲取所有欄位的個數:int field_count() const
  2. 獲取單個欄位描述型別FieldDescriptor的介面有很多個,例如
1.3 FieldDescriptor

FieldDescriptor描述message中的單個欄位,例如欄位名,欄位屬性(optional/required/repeated)等。

對於proto定義裡的每種型別,都有一種對應的C++型別,例如:

獲取型別的label屬性:

獲取欄位的名稱:const string& name() const;

1.4 Reflection

Reflection主要提供了動態讀寫pb欄位的介面,對pb物件的自動讀寫主要通過該類完成。

對每種型別,Reflection都提供了一個單獨的介面用於讀寫欄位對應的值。

例如對於讀操作:

特殊的,對於列舉和巢狀的message:

對於寫操作也是類似的介面,例如SetInt32/SetInt64/SetEnum等。

2. 反射示例

示例主要是接收任意型別的message物件,遍歷解析其中的每個欄位、以及對應的值,按照自定義的格式儲存到一個string中。同時重新反序列化該string,讀取欄位以及value,填充到message物件中。例如:

其中Person是自定義的protobuf message型別,用於設定一些欄位驗證我們的程式。

單純的序列化/反序列化功能可以通過pb自帶的SerializeToString/ParseFromString介面完成。這裡主要是為了同時展示自動從pb物件裡提取field/value,自動根據field/value來還原pb物件這個功能。

其中Person定義是對example裡的addressbook.proto做了少許修改(修改的原因是本文沒有涉及pb裡陣列的處理)

3. 反射例項實現

3.1 serialize_message

serialize_message遍歷提取message中各個欄位以及對應的值,序列化到string中。主要思路就是通過Descriptor得到每個欄位的描述符:欄位名、欄位的cpp型別。通過Reflection的GetX介面獲取對應的value。

3.2 parse_message

parse_message通過讀取field/value,還原message物件。
主要思路跟serialize_message很像,通過Descriptor得到每個欄位的描述符FieldDescriptor,通過Reflection的SetX填充message。

相關文章