C++ protobuf反射特征工程正確姿勢

[toc]

因部門每次加特征,都需要修改protobuf,添加對應protobuf獲取的代碼。重復性開發是真滴多。因此重構獲取特征的版本,通過反射+配置動態獲取。每次只需升級pb,就可以獲取到對應的特征。

1.1 Message

Message 類繼承于 MessageLite 類,業務一般自定義的 refactor_reqs 類繼承于Message 類。是自定義的pb類型,繼承自Message. MessageLite作為Message基類,更加輕量級一些。

一般使用通過Message的兩個接口GetDescriptor/GetReflection,可以獲取該類型對應的Descriptor/Reflection。

因為我們的特征都是包含在一個大的Message里頭,所以使用FindMessageTypeByName獲取Descriptor

const google::protobuf::Reflection* pReflection = pMessage->GetReflection();
const google::protobuf::Descriptor* pDescriptor = pMessage->GetDescriptor();
const ::google::protobuf::Descriptor* pDescriptor =
      google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName(msg_name);

1.2 Descriptor

Descriptor是對message類型定義的描述,包括message的名字、所有字段的描述、原始的proto文件內容等。

在類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獲取

1.2 FieldDescriptor

FieldDescriptor描述message中的單個字段,例如字段名,字段屬性(optional/required/repeated)等。
對于proto定義里的每種類型,都有一種對應的C++類型

const std::string & name() const; // Name of this field within the message.
CppType cpp_type() const; //C++ type of this field.
bool is_required() const; // 判斷字段是否是必填
bool is_optional() const; // 判斷字段是否是選填
bool is_repeated() const; // 判斷字段是否是重復值
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.

1.2 Reflection

Reflection主要提供了動態讀寫pb字段的接口,對pb對象的自動讀寫主要通過該類完成

讀操作和嵌套的message:

 
  virtual int32  GetInt32 (const Message& message,
                           const FieldDescriptor* field) const = 0;
  virtual int64  GetInt64 (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等。

 void SetInt32(Message * message, const FieldDescriptor * field, int32 value) const

讀repeated類型字段:

int32 GetRepeatedInt32(const Message & message, const FieldDescriptor * field, int index) const
std::string GetRepeatedString(const Message & message, const FieldDescriptor * field, int index) const
const Message & GetRepeatedMessage(const Message & message, const FieldDescriptor * field, int index) const

寫repeated類型字段:

void SetRepeatedInt32(Message * message, const FieldDescriptor * field, int index, int32 value) const
void SetRepeatedString(Message * message, const FieldDescriptor * field, int index, std::string value) const
void 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) const
void AddString(Message * message, const FieldDescriptor * field, std::string value) const

2.1 特征工程如何使用

有了上面的知識,我們如何使用到自己的工程中呢。

首先我們定義一個proto文件test_refactor.proto

syntax = "proto3";
package test.refactor;

option cc_generic_services = true;

message item_info {    // item 信息 
    int32 source               = 1;
    repeated int32 newsTypes   = 2;
    string name = 3;
};

message user_info {    // 用戶信息
    int32 type           = 1;
    repeated int32 sex   = 2;
    string imei = 3;
};

message item_req {
    item_info item = 1;
    user_info user = 2;
};

message refactor_reqs {
    item_req req = 1;
}
  • 業務場景是所有的特征都包括在message的refactor_reqs中,利用這個message我們可以獲取到對應的Descriptor
const ::google::protobuf::Descriptor* descriptor =
      google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("test.refactor.refactor_reqs");
  • 在獲取對應field name獲取對應需要獲取的FieldDescriptor,如獲取item信息的數據,寫為req.item

field_descriptor = descriptor->FindFieldByName("item");

  • 最終每次獲取的時候,我們獲取的數據都是填充到test::refactor::refactor_reqs refactor_reqs中。

最終可以得到如下:

3.1 初始化獲取FiledDescriptor信息

std::vector<const ::google::protobuf::FieldDescriptor*> GenerateDescriptorSegments(
    const std::string& msg_name, const std::string& pb_path) {
  std::vector<const ::google::protobuf::FieldDescriptor*> descriptor_segments;
  const ::google::protobuf::Descriptor* descriptor =
      google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName(msg_name);
  if (descriptor == nullptr) {
    LOG(ERROR) << "get descriptor failed";
  }

  std::vector<std::string> segments;
  boost::split(segments, pb_path, boost::is_any_of("."));
  if (segments.empty()) {
    LOG(ERROR) << "parse pb_path segment empty:" << pb_path;
  }

  // 校驗解析數據
  const ::google::protobuf::FieldDescriptor* field_descriptor = NULL;
  for (const auto& segment : segments) {
    if (descriptor == nullptr) {
      LOG(ERROR) << "segment:" << segment << ", descriptor null";
      break;
    }
    // // 根據field name獲取
    field_descriptor = descriptor->FindFieldByName(segment);
    if (field_descriptor == nullptr) {
      LOG(ERROR) << "find segment:" << segment << ", descriptor null";
      break;
    }
    // repeate字段暫不支持
    if (field_descriptor->is_repeated()) {
      LOG(ERROR) << " is repeated";
      break;
    }
    descriptor_segments.emplace_back(field_descriptor);
    LOG(INFO) << "cpp_type:" << field_descriptor->cpp_type();
    if (field_descriptor->cpp_type() == ::google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE) {
      descriptor = field_descriptor->message_type();
    } else {
      descriptor = nullptr;
    }
  }

  if (field_descriptor == nullptr) {
    // descriptor_segments.clear();
    LOG(ERROR) << "field descriptor null";
  }
  return std::move(descriptor_segments);
}

  • msg_name我們傳入test.refactor.refactor_reqs,
  • pb_path解析對應的req.item數據
  • 最終我們可以獲取到每個filed對應的FieldDescriptor

3.2 實時獲取對應的特征數據

bool ParseFromString(::google::protobuf::Message* last_message,
                 std::vector<const ::google::protobuf::FieldDescriptor*> desc_seg,
                 const std::string& data) {
  auto t1 = butil::gettimeofday_us();
  for (auto& seg : desc_seg) {
    // 處理每一個字段
    auto reflection = last_message->GetReflection();
    // const google::protobuf::Message& submessage = reflection->GetMessage(message, field);
    last_message = reflection->MutableMessage(last_message, seg);
    if (!last_message) {
      LOG(ERROR) << "get message failed, param:";
      break;
    }
  }
  if (!last_message) {
    LOG(ERROR) << "get message failed, key:";
    return false;
  }
  auto suc = last_message->ParseFromString(data);
  LOG(INFO) << "parse suc:" << suc << " feature:" << last_message->Utf8DebugString();
  return suc;
}

  • 將獲取到的FieldDescriptor,通過GetReflection逐步初始化。獲取到最終數據需要解析的message
  • 最后調用msg->ParseFromString實例化得到最終想要的特征數據

3.3 代碼驗證

void main() {
  // 構造item特征
  test::refactor::item_info reqs_item;
  reqs_item.set_source(2);
  reqs_item.add_newstypes(3);
  reqs_item.add_newstypes(4);
  reqs_item.set_name("dandyhuang");

  // 構造用戶特征
  test::refactor::user_info reqs_user;
  reqs_user.set_imei("dsfdsderw");
  reqs_user.add_sex(3);
  reqs_user.add_sex(4);
  reqs_user.set_type(6666);
    // 從redis獲取的item和user特征
  std::string item_data_str = reqs_item.SerializeAsString();
  std::string user_data_str = reqs_user.SerializeAsString();
    
  // 初始化對應需要獲取的數據
  auto item_des_seg = GenerateDescriptorSegments("test.refactor.refactor_reqs", "req.item");
  auto user_des_seg = GenerateDescriptorSegments("test.refactor.refactor_reqs", "req.user");
  
  auto t1 = butil::gettimeofday_us();
  // 大proto,獲取里頭的特征數據
  test::refactor::refactor_reqs refactor_reqs;
  // 解析對應數據
  ParseFromString(&refactor_reqs, item_des_seg, item_data_str);
  LOG(INFO) << "refactor_reqs item:" << refactor_reqs.Utf8DebugString()
            << "name:" << refactor_reqs.req().item().name();
  // 解析對應數據
  ParseFromString(&refactor_reqs, user_des_seg, user_data_str);
  LOG(INFO) << "refactor_reqs user+item:" << refactor_reqs.Utf8DebugString()
            << "imei:" << refactor_reqs.req().user().imei();
  auto t2 = butil::gettimeofday_us();

  // 業務直接解析
  test::refactor::refactor_reqs origin_reqs;
  origin_reqs.mutable_req()->mutable_item()->ParseFromString(item_data_str);
  VLOG(INFO) << "origin_reqs item:" << origin_reqs.Utf8DebugString();
  origin_reqs.mutable_req()->mutable_user()->ParseFromString(user_data_str);
  VLOG(INFO) << "origin_reqs user+item:" << origin_reqs.Utf8DebugString();
  auto t3 = butil::gettimeofday_us();
  VLOG_APP(INFO) << "parse  cost1: " << t2 - t1 << " cost2:" << t3 - t2;
}

4.1 和業務直接解析對比耗時

image-20221110151113654.png

我們看到,反射還是比較耗時的,但耗時階段其實是在構建反射第一次的時候。后續解析pb_path對應的數據時,耗時和直接業務解析是一致的。

當數據量很大,filed_name字段很多的時候,初始化可以另外啟動一個線程去初始化。初始化完畢后,在去做特征反射

4.2 每次反射解析ParseFromString的耗時

refactor_parse.png

大家可以添加我的wx一起交流

我是dandyhuang_,碼字不易,有不清楚的可以加w一起交流。

reference

protobuf反射詳解

巧用 Protobuf 反射來優化代碼,拒做 PB Boy

google protobuf 反射機制學習筆記

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容