引言:从收到一个 Message* 开始
在上一篇文章中,我们解决了 Codec(编解码) 的问题。现在,我们的网络服务器已经能够成功地从 TCP 字节流中切分出一个完整的网络包,并利用 Protobuf 的反射机制,将二进制数据还原成了一个 C++ 对象。
但是,这里有一个棘手的问题:由于 C++ 是强类型语言,底层网络库为了通用性,通常只返给我们一个 基类指针 (google::protobuf::Message*)。
当我们拿到这个基类指针时,我们不仅要问:它是谁? 还要问:我该把它交给谁处理?
- 如果是
LoginRequest,我需要调用onLogin()。 - 如果是
SensorData,我需要调用onSensorData()。
面对几十甚至上百种消息类型,我们该如何设计这个路由模块?
第一阶段:噩梦般的 Switch-Case 地狱
最直观(也是最糟糕)的做法,就是利用 if-else 或 switch 进行暴力判断。我们称之为"反面教材"。
cpp
// 反面教材:不可维护的代码
void OnMessage(google::protobuf::Message* msg) {
// 1. 获取消息的名字 (反射)
const std::string& type_name = msg->GetDescriptor()->full_name();
// 2. 暴力匹配
if (type_name == "app.LoginRequest") {
// 痛苦的转型
auto specific_msg = dynamic_cast<app::LoginRequest*>(msg);
OnLogin(specific_msg);
}
else if (type_name == "app.SensorData") {
auto specific_msg = dynamic_cast<app::SensorData*>(msg);
OnSensorData(specific_msg);
}
else if (type_name == "app.LogoutRequest") {
// ...
}
// ... 如果你有 100 种消息,这里就要写 100 个 else if
}
这种写法的致命缺陷:
- 违反开闭原则 (OCP): 每次新增一个业务消息,都要修改核心的
OnMessage函数,极易引入 Bug。 - 代码丑陋且低效: 随着业务增长,这个函数会变成几千行的"面条代码",字符串匹配的效率也会随着
else if的深度而降低。
我们需要一种更优雅的方案:Dispatcher(分发器)。
第二阶段:理解 Dispatcher ------ 邮政分拣中心的智慧
我们可以把 Dispatcher 想象成一个自动化的邮政分拣中心。
- 传送带 (TCP Connection): 源源不断地送来各种包裹(Message*)。
- 扫描仪 (RTTI/Descriptor): 瞬间识别包裹上的标签(Type Name)。
- 自动滑道 (Callback Map): 根据标签,直接把包裹推入对应的处理部门,而不需要人工一个个去问。
核心思想:
我们要把"判断逻辑"从代码中剥离出来,变成一张**"查找表" (Map)**。
- Key: 消息类型(Descriptor 或 名字)。
- Value: 处理这个消息的回调函数。
第三阶段:手写一个基础版 Dispatcher
让我们利用 std::map 来实现这个分拣中心。为了支持多态,我们的 Map 存储一个通用的函数指针。
1. 定义通用回调
首先,我们需要一个能接住所有消息的通用接口:
cpp
// 接收基类指针,无返回值
using ProtobufMessageCallback = std::function<void(google::protobuf::Message* msg)>;
2. 构建分发器类
cpp
class Dispatcher {
public:
// --- 注册 (写通讯录) ---
// 告诉分发器:看到 type_name,就调用 cb
void Register(const std::string& type_name, const ProtobufMessageCallback& cb) {
callbacks_[type_name] = cb;
}
// --- 分发 (查表办事) ---
// 收到消息后,自动派发
void Dispatch(google::protobuf::Message* msg) {
if (!msg) return;
// 1. 利用多态获取消息名字 (关键!)
const std::string& name = msg->GetDescriptor()->full_name();
// 2. 查表
auto it = callbacks_.find(name);
if (it != callbacks_.end()) {
// 3. 找到了,直接执行!
it->second(msg);
} else {
// 处理未知消息
std::cerr << "Unknown message type: " << name << std::endl;
}
}
private:
std::map<std::string, ProtobufMessageCallback> callbacks_;
};
3. 如何使用?
cpp
// 具体的业务函数
void onLogin(google::protobuf::Message* msg) {
// 注意:这里还需要手动转型,稍微有点不完美
auto real_msg = dynamic_cast<LoginRequest*>(msg);
std::cout << "User: " << real_msg->username() << std::endl;
}
int main() {
Dispatcher dispatcher;
// 注册:把 "LoginRequest" 关联到 onLogin 函数
dispatcher.Register("app.LoginRequest", onLogin);
// 网络层收到消息,直接 Dispatch,不需要 if-else
dispatcher.Dispatch(recv_msg);
}
第四阶段:终极进化 ------ 泛型分发器 (Templated Dispatcher)
上面的基础版虽然消灭了 if-else,但留下了一个遗憾:用户在回调函数里还需要自己写 dynamic_cast。 这不够优雅。
我们希望用户写代码时,直接接收子类指针,像这样:
cpp
void onLogin(LoginRequest* msg) { ... } // 不需要 Message* 和强转
这就需要利用 C++ 模板 来做一个自动转型的"中间层"。
完整的泛型实现
cpp
class ProtobufDispatcher {
public:
using MessagePtr = google::protobuf::Message*;
// 1. 泛型注册函数 (魔法所在)
// T 是具体的子类类型,如 LoginRequest
template <typename T>
void RegisterCallback(const std::function<void(T*)>& user_callback) {
// 我们获取 T 的描述符作为 Key
const google::protobuf::Descriptor* desc = T::descriptor();
// 我们把用户的特定回调 (LoginRequest*) 包装成通用回调 (Message*)
callbacks_[desc] = [user_callback](MessagePtr msg) {
// 这里自动进行安全的向下转型
T* specific_msg = dynamic_cast<T*>(msg);
if (specific_msg) {
user_callback(specific_msg); // 调用用户的函数
}
};
}
// 2. 分发函数
void OnProtobufMessage(MessagePtr msg) {
// 使用 Descriptor 指针查表,比字符串匹配更快
auto it = callbacks_.find(msg->GetDescriptor());
if (it != callbacks_.end()) {
it->second(msg); // 执行包装好的回调
}
}
private:
// Key 使用 Descriptor 指针,保证唯一且高效
using InternalCallback = std::function<void(MessagePtr)>;
std::map<const google::protobuf::Descriptor*, InternalCallback> callbacks_;
};
最终的用户体验
使用了这个 Dispatcher,你的业务代码将变得极其清爽:
cpp
// 业务函数 1
void HandleLogin(LoginRequest* msg) {
cout << "Login: " << msg->username() << endl;
}
// 业务函数 2
void HandleSensor(SensorData* msg) {
cout << "Sensor ID: " << msg->id() << ", Temp: " << msg->value() << endl;
}
int main() {
ProtobufDispatcher dispatcher;
// 注册:编译器自动推导类型,生成转型代码
dispatcher.RegisterCallback<LoginRequest>(HandleLogin);
dispatcher.RegisterCallback<SensorData>(HandleSensor);
// ... 网络层收到 msg ...
dispatcher.OnProtobufMessage(msg); // 完美分发!
}
核心原理总结
为什么这一套机制能行得通?因为它巧妙地结合了 Protobuf 的反射 和 C++ 的多态。
-
我是谁?(Identity):
即使我们手里只有
Message*基类指针,调用msg->GetDescriptor()时,利用 C++ 虚函数机制,会自动跳转到子类 (LoginRequest) 的实现,从而拿到正确的"身份证"(Descriptor)。 -
找谁办?(Lookup):
Dispatcher 利用
std::map建立了"身份证"到"办事员"的索引。 -
类型恢复 (Down-casting):
通过模板封装
dynamic_cast,我们在框架层解决了类型安全转换的问题,把干净、强类型的指针交给了业务层。
结语:
通过引入 Dispatcher 模式,我们将网络层的数据接收 与业务层的逻辑处理彻底解耦。无论你的系统中有多少种消息,核心的分发逻辑永远不需要修改。这,就是架构设计的魅力。