protobuf message定义_巧用 Protobuf 反射来优化代码,拒做 PB Boy
作者:iversonluo,騰訊 WXG 應(yīng)用開發(fā)工程師
有些后臺同學(xué)將自己稱為 SQL Boy,因?yàn)樨?fù)責(zé)的業(yè)務(wù)主要是對數(shù)據(jù)庫進(jìn)行增刪改查。經(jīng)常和 Proto 打交道的同學(xué),是不是也會叫自己 PB Boy? 因?yàn)榇蟛糠止ぷ饕彩菍?Proto 進(jìn)行 SET 和 GET。面對大量重復(fù)且丑陋的代碼,除了宏是否有更好的解決方法?本文結(jié)合 PB 反射給出了我在運(yùn)營系統(tǒng)開發(fā)工作中的一些代碼優(yōu)化實(shí)踐。一、背景
Protobuf(下文稱為 PB)是一種常見的數(shù)據(jù)序列化方式,常常用于后臺微服務(wù)之間傳遞數(shù)據(jù)。
筆者目前主要的工作都是和表單打交道,而表單一般涉及到大量的數(shù)據(jù)輸入,表單調(diào)用方一般將數(shù)據(jù)格式化為 JSON 后傳給 CGI,而 CGI 和后臺服務(wù)、后臺服務(wù)之前會用 PB 傳遞數(shù)據(jù)。
在寫代碼時(shí),經(jīng)常會遇到一些丑陋的、圈復(fù)雜度較高、較難維護(hù)的關(guān)于 PB 的使用代碼:
是否可以有方法解決上面的幾個(gè)問題呢?
答案是使用PB 反射。
二、PB 反射的使用
反射的一般定義如下:計(jì)算機(jī)程序在運(yùn)行時(shí)可以訪問、檢測和修改它本身狀態(tài)或行為。
protobuf 的類圖如下:
從上圖我們可以看出,Message 類繼承于 MessageLite 類,業(yè)務(wù)一般自定義的 Person 類繼承于 Message 類。
Descriptor 類和 Reflection 類都聚合于 Message,是弱依賴的關(guān)系。
所以一般使用 PB 反射的步驟如下:
1. 通過Message獲取單個(gè)字段的FieldDescriptor 2. 通過Message獲取其Reflection 3. 通過Reflection來操作FieldDescriptor,從而動(dòng)態(tài)獲取或修改單個(gè)字段獲取 Descript、Reflection 的函數(shù):
const google::protobuf::Reflection* pReflection = pMessage->GetReflection(); const google::protobuf::Descriptor* pDescriptor = pMessage->GetDescriptor();獲取 FieldDescriptor 的函數(shù):
const google::protobuf::FieldDescriptor * pFieldDesc = pDescriptor->FindFieldByName(id);下面分別介紹上面的三個(gè)類。
2.1 類 Descriptor 介紹
類 Descriptor 主要是對 Message 進(jìn)行描述,包括 message 的名字、所有字段的描述、原始 proto 文件內(nèi)容等,下面介紹該類中包含的函數(shù)。
首先是獲取自身信息的函數(shù):
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; // 根據(jù)定義順序索引獲取,即從0開始到最大定義的條目 const FieldDescriptor* FindFieldByNumber(int number) const; // 根據(jù)定義的message里面的順序值獲取(option string name=3,3即為number) const FieldDescriptor* FindFieldByName(const string& name) const; // 根據(jù)field name獲取 const FieldDescriptor* Descriptor::FindFieldByLowercaseName(const std::string & lowercase_name)const; // 根據(jù)小寫的field name獲取 const FieldDescriptor* Descriptor::FindFieldByCamelcaseName(const std::string & camelcase_name) const; // 根據(jù)駝峰的field name獲取其中FieldDescriptor* field(int index)和FieldDescriptor* FindFieldByNumber(int number)這個(gè)函數(shù)中index和number的含義是不一樣的,如下所示:
message Student{optional string name = 1;optional string gender = 2;optional string phone = 5; }其中字段phone,其index為 5,但是其number為 2。
同時(shí)還有一個(gè)我們在調(diào)試中經(jīng)常使用的函數(shù):
std::string Descriptor::DebugString(); // 將message轉(zhuǎn)化成人可以識別出的string信息2.2 類 FieldDescriptor 介紹
類 FieldDescriptor 的作用主要是對 Message 中單個(gè)字段進(jìn)行描述,包括字段名、字段屬性、原始的 field 字段等。
其獲取獲取自身信息的函數(shù):
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()函數(shù)是來獲取該字段是什么類型的,在 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 中還可以判斷字段是否是必填,還是選填或者重復(fù):
bool is_required() const; // 判斷字段是否是必填 bool is_optional() const; // 判斷字段是否是選填 bool is_repeated() const; // 判斷字段是否是重復(fù)值類 FieldDescriptor 中還可以獲取單個(gè)字段的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 中還有一個(gè)支持?jǐn)U展的函數(shù),函數(shù)如下:
// 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具體關(guān)于該函數(shù)的講解在 2.4 章。
2.3 類 Reflection 介紹
該類提供了動(dòng)態(tài)讀、寫 message 中單個(gè)字段能力。
讀單個(gè)字段的函數(shù)如下:
// 這里由于篇幅,省略了一部分代碼,后面的代碼部分也有省略,有需要的可以自行閱讀源碼。 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 // 讀取單個(gè)message字段寫單個(gè)字段的函數(shù)如下:
void SetInt32(Message * message, const FieldDescriptor * field, int32 value) constvoid SetString(Message * message, const FieldDescriptor * field, std::string value) const獲取重復(fù)字段的函數(shù)如下:
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寫重復(fù)字段的函數(shù)如下:
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..新增重復(fù)字段設(shè)計(jì)如下:
void AddInt32(Message * message, const FieldDescriptor * field, int32 value) constvoid AddString(Message * message, const FieldDescriptor * field, std::string value) const另外有一個(gè)較為重要的函數(shù),其可以批量獲取字段描述并將其放置到 vector 中:
void Reflection::ListFields(const Message & message, std::vector< const FieldDescriptor * > * output) const2.4 options 介紹
PB 允許在 proto 中自定義選項(xiàng)并使用選項(xiàng)。在定義 message 的字段時(shí),不僅可以定義字段內(nèi)容,還可以設(shè)置字段的屬性,比如校驗(yàn)規(guī)則,簡介等,結(jié)合反射,可以實(shí)現(xiàn)豐富豐富多彩的應(yīng)用。
下面來介紹下:
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; // 商戶組命名規(guī)范optional uint32 length_min = 50003 [default = 0]; // 字段最小長度optional uint32 length_max = 50004 [default = 1024]; // 字段最大長度optional string regex = 50005; // 該字段的正則表達(dá)式 }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 反射的進(jìn)階使用
第二章給出了 PB 反射,以及具體的使用細(xì)節(jié),在本章中,作者結(jié)合自己日常的代碼,給出 PB 反射一些使用場景。并且以開發(fā)一個(gè)表單系統(tǒng)為例,講一下 PB 反射在開發(fā)表單系統(tǒng)中的進(jìn)階使用。
3.1 獲取 PB 中所有非空字段
在業(yè)務(wù)中,經(jīng)常會需要獲取某個(gè) Message 中所有非空字段,形成一個(gè) 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; // 不支持轉(zhuǎn)換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 將字段校驗(yàn)規(guī)則放置在 Proto 中
后臺服務(wù)接收到前端傳來的字段后,會對字段進(jìn)行校驗(yàn),比如必填校驗(yàn),長度校驗(yàn),正則校驗(yàn),xss 校驗(yàn)等,這些規(guī)則我們常常會硬編碼在代碼中。但是隨著后臺字段的增加,校驗(yàn)規(guī)則代碼會變得越來越多,越來越難維護(hù)。如果我們把字段的定義和校驗(yàn)規(guī)則和定義放在一起,這樣是不是更好的維護(hù)?
示例 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]; }然后我們自己實(shí)現(xiàn) xss 校驗(yàn),必填校驗(yàn),長度校驗(yàn),選項(xiàng)校驗(yàn)等代碼。
示例校驗(yàn)最小長度代碼如下:
#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并且選項(xiàng)非重復(fù)的才會校驗(yàn)字段長度類型const std::string strValue = poReflect->GetString(oMessage, poFiled);const std::string strName = poFiled->name();if (oFieldRule.has_length_min()) {// 有才進(jìn)行校驗(yàn),沒有則不進(jìn)行校驗(yàn)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; }如上,如果需要校驗(yàn)最大長度,必填,xss 校驗(yàn),只需要使用工廠模式,擴(kuò)展代碼即可。
新增一個(gè)字段或者變更某個(gè)字段的校驗(yàn)規(guī)則,只需要修改 Proto,不需要修改代碼,從而防止因變更代碼導(dǎo)致錯(cuò)誤。
3.3 基于 PB 反射的前端頁面自動(dòng)生成方案
在我們常見的運(yùn)營系統(tǒng)中,經(jīng)常會涉及到各種各樣的表單頁面。在前后端交互方面,當(dāng)需要增加字段或者變更字段的校驗(yàn)規(guī)則時(shí),需要面臨如下問題:
- 前端:針對新字段編寫 html 代碼,同時(shí)需要修改前端頁面;
- 后臺:針對每個(gè)字段做接收,并進(jìn)行校驗(yàn)。
每增加或變更一個(gè)字段,我們都需要在前端和后臺進(jìn)行修改,工作量大,同時(shí)頻繁變更容易導(dǎo)致錯(cuò)誤。有什么方法可以解決這些問題嗎?答案是使用 PB 的反射能力。
通過獲取 Message 中每個(gè)字段的描述然后返回給前端,前端根據(jù)字段描述來展示頁面,并且對字段進(jìn)行校驗(yàn)。同時(shí)通過這種方式,前后端可以共享一份表單校驗(yàn)規(guī)則。
在使用上述方案之后,當(dāng)我們需要增加字段或者變更字段的校驗(yàn)規(guī)則時(shí),只需要在 Proto 中修改字段,大大節(jié)省了工作量,同時(shí)避免了因發(fā)布帶來的風(fēng)險(xiǎn)問題。
3.4 通用存儲系統(tǒng)
在運(yùn)營系統(tǒng)中,前端輸入字段,傳入到后臺,后臺校驗(yàn)字段之后,一般還需要把數(shù)據(jù)存儲到數(shù)據(jù)庫中。
對于某些運(yùn)營系統(tǒng)來說,其希望能夠快速接入一些數(shù)據(jù),傳統(tǒng)開發(fā)常常會面臨如下問題:
- 如何在不增加或變更表結(jié)構(gòu)的基礎(chǔ)上,如何快速接入數(shù)據(jù)?
- 如何零開發(fā)實(shí)現(xiàn)頻繁添加字段、新增渠道等需求?
- 如何兼容不同業(yè)務(wù)、不同數(shù)據(jù)協(xié)議(比如 PB 中的不同 message)?
答案是使用 PB 的反射,使得有結(jié)構(gòu)的數(shù)據(jù)轉(zhuǎn)換為非結(jié)構(gòu)的數(shù)據(jù),然后存儲到非關(guān)系型數(shù)據(jù)庫(在微信支付側(cè)一般存入到 table kv)中。
以 3.2 節(jié)中的 Proto 為例,舉例如下,學(xué)生類中定義了兩個(gè)字段,name 和 email 字段,原始信息為:
Student oStudent; oStudent.set_name("xiaowei"); oStudent.set_email("test@tencent.com");通過 PB 的反射,可以轉(zhuǎn)化為平鋪的結(jié)構(gòu):
[{"id":"1","value":"xiaowei"},{"id":"2","value":"test@tencent.com"}]轉(zhuǎn)化為平鋪結(jié)構(gòu)后,可以快速存入到數(shù)據(jù)庫中。如果現(xiàn)在學(xué)生信息里需要增加一個(gè)字段 address,則不需要修改表結(jié)構(gòu),從而完成存儲動(dòng)作。利用 PB 反射,可以完成有結(jié)構(gòu)數(shù)據(jù)和無結(jié)構(gòu)數(shù)據(jù)之間的轉(zhuǎn)換,達(dá)到存儲和業(yè)務(wù)解耦的特性。
四、總結(jié)
本文首先給出了 PB 的反射函數(shù),然后再結(jié)合自己平時(shí)負(fù)責(zé)的工作,給出了 PB 的進(jìn)階使用。通過對 PB 的進(jìn)階使用,可以大大提高開發(fā)和維護(hù)的效率,同時(shí)提升代碼的優(yōu)雅度。有需要更進(jìn)一步研究 PB 的,可以閱讀其源代碼,不得不說,通過閱讀優(yōu)秀代碼能夠極大的促進(jìn)編程能力。
需要注意的是 PB 反射需要依賴大量計(jì)算資源,在密集使用 PB 的場景下,需要注意 CPU 的使用情況。
加入我們
微信支付境外支付團(tuán)隊(duì)在不斷追求卓越的路上尋找同路人:
崗位詳情 | 騰訊招聘
更多干貨盡在騰訊技術(shù),官方微信交流群已建立,交流討論可加:Journeylife1900(備注騰訊技術(shù)) 。
總結(jié)
以上是生活随笔為你收集整理的protobuf message定义_巧用 Protobuf 反射来优化代码,拒做 PB Boy的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 设置为true有什么区别_海绵与珍珠棉有
- 下一篇: gtk 控件内存回收_咱们从头到尾说一次