Protocol Buffer 簡稱 ProtoBuf,是用於結構化資料序列化的靈活、高效、自動的方法,又如 XML,不過它更小、更快、也更簡單。你可以定義自己的資料結構,然後使用程式碼生成器生成的程式碼來讀寫這個資料結構。你甚至可以在無需重新部署程式的情況下更新資料結構。
本文主要介紹 protobuf 裡的反射功能,使用的pb版本為2.6.1,同時為了簡潔,對 repeated/extension 欄位的處理方法沒有說明。
最初是起源於這樣一個問題:
給定一個pb物件,如何自動遍歷該物件的所有欄位?
即是否有一個通用的方法可以遍歷任意pb物件的所有欄位,而不用關心具體物件型別。
使用場景上有很多:
比如json格式字串的相互轉換,或者bigtable里根據pb物件的欄位自動寫列名和對應的value。
例如定義了pb messge型別Person如下:
1 2 3 |
Person person; person.set_name("yingshin"); person.set_age(21); |
能否將該物件自動轉化為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 | zhy198606@gmail.com |
答案就是 pb的反射功能。
我們的目標是提供這樣兩個介面:
1 2 3 4 5 |
//從給定的message物件序列化為固定格式的字串 void serialize_message(const google::protobuf::Message& message, std::string* serialized_string); //從給定的字串按照固定格式還原為原message物件 void parse_message(const std::string& serialized_string, google::protobuf::Message* message); |
接下來逐步介紹下如何實現。
1. 反射相關介面
要介紹pb的反射功能,先看一個相關的UML示例圖:
各個類以及介面說明:
1.1 Message
Person是自定義的pb型別,繼承自Message. MessageLite作為Message基類,更加輕量級一些。
通過Message的兩個介面GetDescriptor/GetReflection
,可以獲取該型別對應的Descriptor/Reflection。
1.2 Descriptor
Descriptor是對message型別定義的描述,包括message的名字、所有欄位的描述、原始的proto檔案內容等。
本文中我們主要關注跟欄位描述相關的介面,例如:
- 獲取所有欄位的個數:
int field_count() const
- 獲取單個欄位描述型別
FieldDescriptor
的介面有很多個,例如
1 2 3 |
const FieldDescriptor* field(int index) const;//根據定義順序索引獲取 const FieldDescriptor* FindFieldByNumber(int number) const;//根據tag值獲取 const FieldDescriptor* FindFieldByName(const string& name) const;//根據field name獲取 |
1.3 FieldDescriptor
FieldDescriptor描述message中的單個欄位,例如欄位名,欄位屬性(optional/required/repeated)等。
對於proto定義裡的每種型別,都有一種對應的C++型別,例如:
1 2 3 |
enum CppType { CPPTYPE_INT32 = 1, //TYPE_INT32, TYPE_SINT32, TYPE_SFIXED32 } |
獲取型別的label屬性:
1 2 3 4 5 6 7 |
enum Label { LABEL_OPTIONAL = 1, //optional LABEL_REQUIRED = 2, //required LABEL_REPEATED = 3, //repeated MAX_LABEL = 3, //Constant useful for defining lookup tables indexed by Label. } |
獲取欄位的名稱:const string& name() const;
。
1.4 Reflection
Reflection主要提供了動態讀寫pb欄位的介面,對pb物件的自動讀寫主要通過該類完成。
對每種型別,Reflection都提供了一個單獨的介面用於讀寫欄位對應的值。
例如對於讀操作:
1 2 3 4 |
virtual int32 GetInt32 (const Message& message, const FieldDescriptor* field) const = 0; virtual int64 GetInt64 (const Message& message, const FieldDescriptor* field) const = 0; |
特殊的,對於列舉和巢狀的message:
1 2 3 4 5 6 |
virtual const EnumValueDescriptor* GetEnum( const Message& message, const FieldDescriptor* field) const = 0; // See MutableMessage() for the meaning of the "factory" parameter. virtual const Message& GetMessage(const Message& message, const FieldDescriptor* field, MessageFactory* factory = NULL) const = 0; |
對於寫操作也是類似的介面,例如SetInt32/SetInt64/SetEnum
等。
2. 反射示例
示例主要是接收任意型別的message物件,遍歷解析其中的每個欄位、以及對應的值,按照自定義的格式儲存到一個string中。同時重新反序列化該string,讀取欄位以及value,填充到message物件中。例如:
其中Person是自定義的protobuf message型別,用於設定一些欄位驗證我們的程式。
單純的序列化/反序列化功能可以通過pb自帶的SerializeToString/ParseFromString介面完成。這裡主要是為了同時展示自動從pb物件裡提取field/value,自動根據field/value來還原pb物件這個功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
int main() { std::string serialized_string; { tutorial::Person person; person.set_name("yingshin"); person.set_id(123456789); person.set_email("zhy198606@gmail.com"); ::tutorial::Person_PhoneNumber* phone = person.mutable_phone(); phone->set_type(tutorial::Person::WORK); phone->set_number("13266666666"); serialize_message(person, &serialized_string); } { tutorial::Person person; parse_message(serialized_string, &person); } return 0; } |
其中Person定義是對example裡的addressbook.proto做了少許修改(修改的原因是本文沒有涉及pb裡陣列的處理)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package tutorial; message Person { required string name = 1; required int32 id = 2; // Unique ID number for this person. optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } optional PhoneNumber phone = 4; } |
3. 反射例項實現
3.1 serialize_message
serialize_message遍歷提取message中各個欄位以及對應的值,序列化到string中。主要思路就是通過Descriptor得到每個欄位的描述符:欄位名、欄位的cpp型別。通過Reflection的GetX介面獲取對應的value。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
void serialize_message(const google::protobuf::Message& message, std::string* serialized_string) { const google::protobuf::Descriptor* descriptor = message.GetDescriptor(); const google::protobuf::Reflection* reflection = message.GetReflection(); for (int i = 0; i field_count(); ++i) { const google::protobuf::FieldDescriptor* field = descriptor->field(i); bool has_field = reflection->HasField(message, field); if (has_field) { //arrays not supported assert(!field->is_repeated()); switch (field->cpp_type()) { #define CASE_FIELD_TYPE(cpptype, method, valuetype) case google::protobuf::FieldDescriptor::CPPTYPE_##cpptype:{ valuetype value = reflection->Get##method(message, field); int wsize = field->name().size(); serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize)); serialized_string->append(field->name().c_str(), field->name().size()); wsize = sizeof(value); serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize)); serialized_string->append(reinterpret_cast(&value), sizeof(value)); break; } CASE_FIELD_TYPE(INT32, Int32, int); CASE_FIELD_TYPE(UINT32, UInt32, uint32_t); CASE_FIELD_TYPE(FLOAT, Float, float); CASE_FIELD_TYPE(DOUBLE, Double, double); CASE_FIELD_TYPE(BOOL, Bool, bool); CASE_FIELD_TYPE(INT64, Int64, int64_t); CASE_FIELD_TYPE(UINT64, UInt64, uint64_t); #undef CASE_FIELD_TYPE case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { int value = reflection->GetEnum(message, field)->number(); int wsize = field->name().size(); //寫入name佔用位元組數 serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize)); //寫入name serialized_string->append(field->name().c_str(), field->name().size()); wsize = sizeof(value); //寫入value佔用位元組數 serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize)); //寫入value serialized_string->append(reinterpret_cast(&value), sizeof(value)); break; } case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { std::string value = reflection->GetString(message, field); int wsize = field->name().size(); serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize)); serialized_string->append(field->name().c_str(), field->name().size()); wsize = value.size(); serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize)); serialized_string->append(value.c_str(), value.size()); break; } case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: { std::string value; int wsize = field->name().size(); serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize)); serialized_string->append(field->name().c_str(), field->name().size()); const google::protobuf::Message& submessage = reflection->GetMessage(message, field); serialize_message(submessage, &value); wsize = value.size(); serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize)); serialized_string->append(value.c_str(), value.size()); break; } } } } } |
3.2 parse_message
parse_message通過讀取field/value,還原message物件。
主要思路跟serialize_message很像,通過Descriptor得到每個欄位的描述符FieldDescriptor,通過Reflection的SetX填充message。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
void parse_message(const std::string& serialized_string, google::protobuf::Message* message) { const google::protobuf::Descriptor* descriptor = message->GetDescriptor(); const google::protobuf::Reflection* reflection = message->GetReflection(); std::map field_map; for (int i = 0; i field_count(); ++i) { const google::protobuf::FieldDescriptor* field = descriptor->field(i); field_map[field->name()] = field; } const google::protobuf::FieldDescriptor* field = NULL; size_t pos = 0; while (pos (serialized_string.substr(pos, sizeof(int)).c_str())); pos += sizeof(int); std::string name = serialized_string.substr(pos, name_size); pos += name_size; int value_size = *(reinterpret_cast(serialized_string.substr(pos, sizeof(int)).c_str())); pos += sizeof(int); std::string value = serialized_string.substr(pos, value_size); pos += value_size; std::map::iterator iter = field_map.find(name); if (iter == field_map.end()) { fprintf(stderr, "no field found.n"); continue; } else { field = iter->second; } assert(!field->is_repeated()); switch (field->cpp_type()) { #define CASE_FIELD_TYPE(cpptype, method, valuetype) case google::protobuf::FieldDescriptor::CPPTYPE_##cpptype: { reflection->Set##method( message, field, *(reinterpret_cast(value.c_str()))); std::cout name() (value.c_str())) enum_type()->FindValueByNumber(*(reinterpret_cast(value.c_str()))); reflection->SetEnum(message, field, enum_value_descriptor); std::cout name() (value.c_str())) SetString(message, field, value); std::cout name() MutableMessage(message, field); parse_message(value, submessage); break; } default: { break; } } } } |