protobuf message定义_巧用 Protobuf 反射来优化代码,拒做 PB Boy
作者:iversonluo,騰訊 WXG 應用開發工程師
有些后臺同學將自己稱為 SQL Boy,因為負責的業務主要是對數據庫進行增刪改查。經常和 Proto 打交道的同學,是不是也會叫自己 PB Boy? 因為大部分工作也是對 Proto 進行 SET 和 GET。面對大量重復且丑陋的代碼,除了宏是否有更好的解決方法?本文結合 PB 反射給出了我在運營系統開發工作中的一些代碼優化實踐。一、背景
Protobuf(下文稱為 PB)是一種常見的數據序列化方式,常常用于后臺微服務之間傳遞數據。
筆者目前主要的工作都是和表單打交道,而表單一般涉及到大量的數據輸入,表單調用方一般將數據格式化為 JSON 后傳給 CGI,而 CGI 和后臺服務、后臺服務之前會用 PB 傳遞數據。
在寫代碼時,經常會遇到一些丑陋的、圈復雜度較高、較難維護的關于 PB 的使用代碼:
是否可以有方法解決上面的幾個問題呢?
答案是使用PB 反射。
二、PB 反射的使用
反射的一般定義如下:計算機程序在運行時可以訪問、檢測和修改它本身狀態或行為。
protobuf 的類圖如下:
從上圖我們可以看出,Message 類繼承于 MessageLite 類,業務一般自定義的 Person 類繼承于 Message 類。
Descriptor 類和 Reflection 類都聚合于 Message,是弱依賴的關系。
所以一般使用 PB 反射的步驟如下:
1. 通過Message獲取單個字段的FieldDescriptor 2. 通過Message獲取其Reflection 3. 通過Reflection來操作FieldDescriptor,從而動態獲取或修改單個字段獲取 Descript、Reflection 的函數:
const google::protobuf::Reflection* pReflection = pMessage->GetReflection(); const google::protobuf::Descriptor* pDescriptor = pMessage->GetDescriptor();獲取 FieldDescriptor 的函數:
const google::protobuf::FieldDescriptor * pFieldDesc = pDescriptor->FindFieldByName(id);下面分別介紹上面的三個類。
2.1 類 Descriptor 介紹
類 Descriptor 主要是對 Message 進行描述,包括 message 的名字、所有字段的描述、原始 proto 文件內容等,下面介紹該類中包含的函數。
首先是獲取自身信息的函數:
const std::string & name() const; // 獲取message自身名字 int field_count() const; // 獲取該message中有多少字段 const FileDescriptor* file() const; // The .proto file in which this message type was defined. Never nullptr.在類 Descriptor 中,可以通過如下方法獲取類 FieldDescriptor:
const FieldDescriptor* field(int index) const; // 根據定義順序索引獲取,即從0開始到最大定義的條目 const FieldDescriptor* FindFieldByNumber(int number) const; // 根據定義的message里面的順序值獲取(option string name=3,3即為number) const FieldDescriptor* FindFieldByName(const string& name) const; // 根據field name獲取 const FieldDescriptor* Descriptor::FindFieldByLowercaseName(const std::string & lowercase_name)const; // 根據小寫的field name獲取 const FieldDescriptor* Descriptor::FindFieldByCamelcaseName(const std::string & camelcase_name) const; // 根據駝峰的field name獲取其中FieldDescriptor* field(int index)和FieldDescriptor* FindFieldByNumber(int number)這個函數中index和number的含義是不一樣的,如下所示:
message Student{optional string name = 1;optional string gender = 2;optional string phone = 5; }其中字段phone,其index為 5,但是其number為 2。
同時還有一個我們在調試中經常使用的函數:
std::string Descriptor::DebugString(); // 將message轉化成人可以識別出的string信息2.2 類 FieldDescriptor 介紹
類 FieldDescriptor 的作用主要是對 Message 中單個字段進行描述,包括字段名、字段屬性、原始的 field 字段等。
其獲取獲取自身信息的函數:
const std::string & name() const; // Name of this field within the message. const std::string & lowercase_name() const; // Same as name() except converted to lower-case. const std::string & camelcase_name() const; // Same as name() except converted to camel-case. CppType cpp_type() const; //C++ type of this field.其中cpp_type()函數是來獲取該字段是什么類型的,在 PB 中,類型的類目如下:
enum FieldDescriptor::Type {TYPE_DOUBLE = = 1,TYPE_FLOAT = = 2,TYPE_INT64 = = 3,TYPE_UINT64 = = 4,TYPE_INT32 = = 5,TYPE_FIXED64 = = 6,TYPE_FIXED32 = = 7,TYPE_BOOL = = 8,TYPE_STRING = = 9,TYPE_GROUP = = 10,TYPE_MESSAGE = = 11,TYPE_BYTES = = 12,TYPE_UINT32 = = 13,TYPE_ENUM = = 14,TYPE_SFIXED32 = = 15,TYPE_SFIXED64 = = 16,TYPE_SINT32 = = 17,TYPE_SINT64 = = 18,MAX_TYPE = = 18 }類 FieldDescriptor 中還可以判斷字段是否是必填,還是選填或者重復:
bool is_required() const; // 判斷字段是否是必填 bool is_optional() const; // 判斷字段是否是選填 bool is_repeated() const; // 判斷字段是否是重復值類 FieldDescriptor 中還可以獲取單個字段的index或者tag:
int number() const; // Declared tag number. int index() const; //Index of this field within the message's field array, or the file or extension scope's extensions array.類 FieldDescriptor 中還有一個支持擴展的函數,函數如下:
// Get the FieldOptions for this field. This includes things listed in // square brackets after the field definition. E.g., the field: // optional string text = 1 [ctype=CORD]; // has the "ctype" option set. Allowed options are defined by FieldOptions in // descriptor.proto, and any available extensions of that message. const FieldOptions & FieldDescriptor::options() const具體關于該函數的講解在 2.4 章。
2.3 類 Reflection 介紹
該類提供了動態讀、寫 message 中單個字段能力。
讀單個字段的函數如下:
// 這里由于篇幅,省略了一部分代碼,后面的代碼部分也有省略,有需要的可以自行閱讀源碼。 int32 GetInt32(const Message & message, const FieldDescriptor * field) conststd::string GetString(const Message & message, const FieldDescriptor * field) constconst Message & GetMessage(const Message & message, const FieldDescriptor * field, MessageFactory * factory = nullptr) const // 讀取單個message字段寫單個字段的函數如下:
void SetInt32(Message * message, const FieldDescriptor * field, int32 value) constvoid SetString(Message * message, const FieldDescriptor * field, std::string value) const獲取重復字段的函數如下:
int32 GetRepeatedInt32(const Message & message, const FieldDescriptor * field, int index) conststd::string GetRepeatedString(const Message & message, const FieldDescriptor * field, int index) constconst Message & GetRepeatedMessage(const Message & message, const FieldDescriptor * field, int index) const寫重復字段的函數如下:
void SetRepeatedInt32(Message * message, const FieldDescriptor * field, int index, int32 value) constvoid SetRepeatedString(Message * message, const FieldDescriptor * field, int index, std::string value) constvoid SetRepeatedEnumValue(Message * message, const FieldDescriptor * field, int index, int value) const // Set an enum field's value with an integer rather than EnumValueDescriptor. more..新增重復字段設計如下:
void AddInt32(Message * message, const FieldDescriptor * field, int32 value) constvoid AddString(Message * message, const FieldDescriptor * field, std::string value) const另外有一個較為重要的函數,其可以批量獲取字段描述并將其放置到 vector 中:
void Reflection::ListFields(const Message & message, std::vector< const FieldDescriptor * > * output) const2.4 options 介紹
PB 允許在 proto 中自定義選項并使用選項。在定義 message 的字段時,不僅可以定義字段內容,還可以設置字段的屬性,比如校驗規則,簡介等,結合反射,可以實現豐富豐富多彩的應用。
下面來介紹下:
import "google/protobuf/descriptor.proto";extend google.protobuf.FieldOptions {optional uint32 attr_id = 50000; //字段idoptional bool is_need_encrypt = 50001 [default = false]; // 字段是否加密,0代表不加密,1代表加密optional string naming_conventions1 = 50002; // 商戶組命名規范optional uint32 length_min = 50003 [default = 0]; // 字段最小長度optional uint32 length_max = 50004 [default = 1024]; // 字段最大長度optional string regex = 50005; // 該字段的正則表達式 }message SubMerchantInfo {// 商戶名稱optional string merchant_name = 1 [(attr_id) = 1,(is_encrypt) = 0,(naming_conventions1) = "company_name",(length_min) = 1,(length_max) = 80,(regex.field_rules) = "[a-zA-Z0-9]"];使用方法如下:
#include <google/protobuf/descriptor.h> #include <google/protobuf/message.h>std::string strRegex = FieldDescriptor->options().GetExtension(regex);uint32 dwLengthMinp = FieldDescriptor->options().GetExtension(length_min);bool bIsNeedEncrypt = FieldDescriptor->options().GetExtension(is_need_encrypt);三、PB 反射的進階使用
第二章給出了 PB 反射,以及具體的使用細節,在本章中,作者結合自己日常的代碼,給出 PB 反射一些使用場景。并且以開發一個表單系統為例,講一下 PB 反射在開發表單系統中的進階使用。
3.1 獲取 PB 中所有非空字段
在業務中,經常會需要獲取某個 Message 中所有非空字段,形成一個 map<string,string>,使用 PB 反射寫法如下:
#include "pb_util.h"#include <sstream>namespace comm_tools { int PbToMap(const google::protobuf::Message &message,std::map<std::string, std::string> &out) { #define CASE_FIELD_TYPE(cpptype, method, valuetype) case google::protobuf::FieldDescriptor::CPPTYPE_##cpptype: { valuetype value = reflection->Get##method(message, field); std::ostringstream oss; oss << value; out[field->name()] = oss.str(); break; }#define CASE_FIELD_TYPE_ENUM() case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: { int value = reflection->GetEnum(message, field)->number(); std::ostringstream oss; oss << value; out[field->name()] = oss.str(); break; }#define CASE_FIELD_TYPE_STRING() case google::protobuf::FieldDescriptor::CPPTYPE_STRING: { std::string value = reflection->GetString(message, field); out[field->name()] = value; break; }const google::protobuf::Descriptor *descriptor = message.GetDescriptor();const google::protobuf::Reflection *reflection = message.GetReflection();for (int i = 0; i < descriptor->field_count(); i++) {const google::protobuf::FieldDescriptor *field = descriptor->field(i);bool has_field = reflection->HasField(message, field);if (has_field) {if (field->is_repeated()) {return -1; // 不支持轉換repeated字段}const std::string &field_name = field->name();switch (field->cpp_type()) {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);CASE_FIELD_TYPE_ENUM();CASE_FIELD_TYPE_STRING();default:return -1; // 其他異常類型}}}return 0; } } // namespace comm_tools通過上面的代碼,如果需要在 proto 中增加字段,不再需要修改原來的代碼。
3.2 將字段校驗規則放置在 Proto 中
后臺服務接收到前端傳來的字段后,會對字段進行校驗,比如必填校驗,長度校驗,正則校驗,xss 校驗等,這些規則我們常常會硬編碼在代碼中。但是隨著后臺字段的增加,校驗規則代碼會變得越來越多,越來越難維護。如果我們把字段的定義和校驗規則和定義放在一起,這樣是不是更好的維護?
示例 proto 如下:
syntax = "proto2";package student;import "google/protobuf/descriptor.proto";message FieldRule{optional uint32 length_min = 1; // 字段最小長度optional uint32 id = 2; // 字段映射id }extend google.protobuf.FieldOptions{optional FieldRule field_rule = 50000; }message Student{optional string name =1 [(field_rule).length_min = 5, (field_rule).id = 1];optional string email = 2 [(field_rule).length_min = 10, (field_rule).id = 2]; }然后我們自己實現 xss 校驗,必填校驗,長度校驗,選項校驗等代碼。
示例校驗最小長度代碼如下:
#include <iostream> #include "student.pb.h" #include <google/protobuf/descriptor.h> #include <google/protobuf/message.h>using namespace std; using namespace student; using namespace google::protobuf;bool minLengthCheck(const std::string &strValue, const uint32_t &dwLenthMin) {return strValue.size() < dwLenthMin; }int allCheck(const google::protobuf::Message &oMessage){const auto *poReflect = oMessage.GetReflection();vector<const FieldDescriptor *> vecFD;poReflect->ListFields(oMessage, &vecFD);for (const auto &poFiled : vecFD) {const auto &oFieldRule = poFiled->options().GetExtension(student::field_rule);if (poFiled->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_STRING && !poFiled->is_repeated()) {// 類型是string并且選項非重復的才會校驗字段長度類型const std::string strValue = poReflect->GetString(oMessage, poFiled);const std::string strName = poFiled->name();if (oFieldRule.has_length_min()) {// 有才進行校驗,沒有則不進行校驗if (minLengthCheck(strValue, oFieldRule.length_min())) {cout << "the length of " << strName << " is lower than " << oFieldRule.length_min()<<endl;} else {cout << "check min lenth pass"<<endl;}}}}return 0; }int main() {Student oStudent1;oStudent1.set_name("xiao");Student oStudent2;oStudent2.set_name("xiaowei");allCheck(oStudent1);allCheck(oStudent2);return 0; }如上,如果需要校驗最大長度,必填,xss 校驗,只需要使用工廠模式,擴展代碼即可。
新增一個字段或者變更某個字段的校驗規則,只需要修改 Proto,不需要修改代碼,從而防止因變更代碼導致錯誤。
3.3 基于 PB 反射的前端頁面自動生成方案
在我們常見的運營系統中,經常會涉及到各種各樣的表單頁面。在前后端交互方面,當需要增加字段或者變更字段的校驗規則時,需要面臨如下問題:
- 前端:針對新字段編寫 html 代碼,同時需要修改前端頁面;
- 后臺:針對每個字段做接收,并進行校驗。
每增加或變更一個字段,我們都需要在前端和后臺進行修改,工作量大,同時頻繁變更容易導致錯誤。有什么方法可以解決這些問題嗎?答案是使用 PB 的反射能力。
通過獲取 Message 中每個字段的描述然后返回給前端,前端根據字段描述來展示頁面,并且對字段進行校驗。同時通過這種方式,前后端可以共享一份表單校驗規則。
在使用上述方案之后,當我們需要增加字段或者變更字段的校驗規則時,只需要在 Proto 中修改字段,大大節省了工作量,同時避免了因發布帶來的風險問題。
3.4 通用存儲系統
在運營系統中,前端輸入字段,傳入到后臺,后臺校驗字段之后,一般還需要把數據存儲到數據庫中。
對于某些運營系統來說,其希望能夠快速接入一些數據,傳統開發常常會面臨如下問題:
- 如何在不增加或變更表結構的基礎上,如何快速接入數據?
- 如何零開發實現頻繁添加字段、新增渠道等需求?
- 如何兼容不同業務、不同數據協議(比如 PB 中的不同 message)?
答案是使用 PB 的反射,使得有結構的數據轉換為非結構的數據,然后存儲到非關系型數據庫(在微信支付側一般存入到 table kv)中。
以 3.2 節中的 Proto 為例,舉例如下,學生類中定義了兩個字段,name 和 email 字段,原始信息為:
Student oStudent; oStudent.set_name("xiaowei"); oStudent.set_email("test@tencent.com");通過 PB 的反射,可以轉化為平鋪的結構:
[{"id":"1","value":"xiaowei"},{"id":"2","value":"test@tencent.com"}]轉化為平鋪結構后,可以快速存入到數據庫中。如果現在學生信息里需要增加一個字段 address,則不需要修改表結構,從而完成存儲動作。利用 PB 反射,可以完成有結構數據和無結構數據之間的轉換,達到存儲和業務解耦的特性。
四、總結
本文首先給出了 PB 的反射函數,然后再結合自己平時負責的工作,給出了 PB 的進階使用。通過對 PB 的進階使用,可以大大提高開發和維護的效率,同時提升代碼的優雅度。有需要更進一步研究 PB 的,可以閱讀其源代碼,不得不說,通過閱讀優秀代碼能夠極大的促進編程能力。
需要注意的是 PB 反射需要依賴大量計算資源,在密集使用 PB 的場景下,需要注意 CPU 的使用情況。
加入我們
微信支付境外支付團隊在不斷追求卓越的路上尋找同路人:
崗位詳情 | 騰訊招聘
更多干貨盡在騰訊技術,官方微信交流群已建立,交流討論可加:Journeylife1900(備注騰訊技術) 。
總結
以上是生活随笔為你收集整理的protobuf message定义_巧用 Protobuf 反射来优化代码,拒做 PB Boy的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 设置为true有什么区别_海绵与珍珠棉有
- 下一篇: 安卓期末作品小项目_北京部编版八年级上册