[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 和業務直接解析對比耗時
我們看到,反射還是比較耗時的,但耗時階段其實是在構建反射第一次的時候。后續解析pb_path
對應的數據時,耗時和直接業務解析是一致的。
當數據量很大,filed_name字段很多的時候,初始化可以另外啟動一個線程去初始化。初始化完畢后,在去做特征反射
4.2 每次反射解析ParseFromString
的耗時
大家可以添加我的wx一起交流
我是dandyhuang_,碼字不易,有不清楚的可以加w一起交流。