C++ 网络编程中的 Protobuf 消息分发 (Dispatcher) 设计模式
引言:从收到一个 Message* 开始
在上一篇文章中,我们解决了 Codec(编解码) 的问题。现在,我们的网络服务器已经能够成功地从 TCP 字节流中切分出一个完整的网络包,并利用 Protobuf 的反射机制,将二进制数据还原成了一个 C++ 对象。
但是,这里有一个棘手的问题:由于 C++ 是强类型语言,底层网络库为了通用性,通常只返给我们一个 基类指针 (google::protobuf::Message*)。
当我们拿到这个基类指针时,我们不仅要问:它是谁? 还要问:我该把它交给谁处理?
- 如果是
LoginRequest,我需要调用onLogin()。 - 如果是
SensorData,我需要调用onSensorData()。
面对几十甚至上百种消息类型,我们该如何设计这个路由模块?
第一阶段:噩梦般的 Switch-Case 地狱
最直观(也是最糟糕)的做法,就是利用 if-else 或 switch 进行暴力判断。我们称之为“反面教材”。
// 反面教材:不可维护的代码voidOnMessage(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);}elseif(type_name =="app.SensorData"){auto specific_msg =dynamic_cast<app::SensorData*>(msg);OnSensorData(specific_msg);}elseif(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. 定义通用回调
首先,我们需要一个能接住所有消息的通用接口:
// 接收基类指针,无返回值using ProtobufMessageCallback = std::function<void(google::protobuf::Message* msg)>;2. 构建分发器类
classDispatcher{public:// --- 注册 (写通讯录) ---// 告诉分发器:看到 type_name,就调用 cbvoidRegister(const std::string& type_name,const ProtobufMessageCallback& cb){ callbacks_[type_name]= cb;}// --- 分发 (查表办事) ---// 收到消息后,自动派发voidDispatch(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. 如何使用?
// 具体的业务函数voidonLogin(google::protobuf::Message* msg){// 注意:这里还需要手动转型,稍微有点不完美auto real_msg =dynamic_cast<LoginRequest*>(msg); std::cout <<"User: "<< real_msg->username()<< std::endl;}intmain(){ Dispatcher dispatcher;// 注册:把 "LoginRequest" 关联到 onLogin 函数 dispatcher.Register("app.LoginRequest", onLogin);// 网络层收到消息,直接 Dispatch,不需要 if-else dispatcher.Dispatch(recv_msg);}第四阶段:终极进化 —— 泛型分发器 (Templated Dispatcher)
上面的基础版虽然消灭了 if-else,但留下了一个遗憾:用户在回调函数里还需要自己写 dynamic_cast。 这不够优雅。
我们希望用户写代码时,直接接收子类指针,像这样:
voidonLogin(LoginRequest* msg){...}// 不需要 Message* 和强转这就需要利用 C++ 模板 来做一个自动转型的“中间层”。
完整的泛型实现
classProtobufDispatcher{public:using MessagePtr = google::protobuf::Message*;// 1. 泛型注册函数 (魔法所在)// T 是具体的子类类型,如 LoginRequesttemplate<typenameT>voidRegisterCallback(const std::function<void(T*)>& user_callback){// 我们获取 T 的描述符作为 Keyconst 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. 分发函数voidOnProtobufMessage(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,你的业务代码将变得极其清爽:
// 业务函数 1voidHandleLogin(LoginRequest* msg){ cout <<"Login: "<< msg->username()<< endl;}// 业务函数 2voidHandleSensor(SensorData* msg){ cout <<"Sensor ID: "<< msg->id()<<", Temp: "<< msg->value()<< endl;}intmain(){ 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 模式,我们将网络层的数据接收与业务层的逻辑处理彻底解耦。无论你的系统中有多少种消息,核心的分发逻辑永远不需要修改。这,就是架构设计的魅力。