C++ 网络编程中的 Protobuf 消息分发 (Dispatcher) 设计模式

引言:从收到一个 Message* 开始

在上一篇文章中,我们解决了 Codec(编解码) 的问题。现在,我们的网络服务器已经能够成功地从 TCP 字节流中切分出一个完整的网络包,并利用 Protobuf 的反射机制,将二进制数据还原成了一个 C++ 对象。

但是,这里有一个棘手的问题:由于 C++ 是强类型语言,底层网络库为了通用性,通常只返给我们一个 基类指针 (google::protobuf::Message*)。

当我们拿到这个基类指针时,我们不仅要问:它是谁? 还要问:我该把它交给谁处理?

  • 如果是 LoginRequest,我需要调用 onLogin()
  • 如果是 SensorData,我需要调用 onSensorData()

面对几十甚至上百种消息类型,我们该如何设计这个路由模块?


第一阶段:噩梦般的 Switch-Case 地狱

最直观(也是最糟糕)的做法,就是利用 if-elseswitch 进行暴力判断。我们称之为“反面教材”。

// 反面教材:不可维护的代码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}

这种写法的致命缺陷:

  1. 违反开闭原则 (OCP): 每次新增一个业务消息,都要修改核心的 OnMessage 函数,极易引入 Bug。
  2. 代码丑陋且低效: 随着业务增长,这个函数会变成几千行的“面条代码”,字符串匹配的效率也会随着 else if 的深度而降低。

我们需要一种更优雅的方案:Dispatcher(分发器)


第二阶段:理解 Dispatcher —— 邮政分拣中心的智慧

我们可以把 Dispatcher 想象成一个自动化的邮政分拣中心

  1. 传送带 (TCP Connection): 源源不断地送来各种包裹(Message*)。
  2. 扫描仪 (RTTI/Descriptor): 瞬间识别包裹上的标签(Type Name)。
  3. 自动滑道 (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++ 的多态

  1. 我是谁?(Identity):
    即使我们手里只有 Message* 基类指针,调用 msg->GetDescriptor() 时,利用 C++ 虚函数机制,会自动跳转到子类 (LoginRequest) 的实现,从而拿到正确的“身份证”(Descriptor)。
  2. 找谁办?(Lookup):
    Dispatcher 利用 std::map 建立了“身份证”到“办事员”的索引。
  3. 类型恢复 (Down-casting):
    通过模板封装 dynamic_cast,我们在框架层解决了类型安全转换的问题,把干净、强类型的指针交给了业务层。

结语:
通过引入 Dispatcher 模式,我们将网络层的数据接收与业务层的逻辑处理彻底解耦。无论你的系统中有多少种消息,核心的分发逻辑永远不需要修改。这,就是架构设计的魅力。

Read more

【算法通关指南:算法基础篇】二分算法:1.在排序树组中查找元素的第一个和最后一个位置 2.牛可乐和魔法封印

【算法通关指南:算法基础篇】二分算法:1.在排序树组中查找元素的第一个和最后一个位置 2.牛可乐和魔法封印

🔥小龙报:个人主页 🎬作者简介:C++研发,嵌入式,机器人方向学习者 ❄️个人专栏:《算法通关指南》 ✨ 永远相信美好的事情即将发生 文章目录 * 前言 * 一、二分算法 * 二、在排序树组中查找元素的第一个和最后一个位置 * 2.1题目 * 2.2 算法原理 * 2.3代码 * 三、牛可乐和魔法封印 * 3.1题目 * 3.2 算法原理 * 3.3代码 * 总结与每日励志 前言 本专栏聚焦算法题实战,系统讲解算法模块:以《c++编程》,《数据结构和算法》《基础算法》《算法实战》 等几个板块以题带点,讲解思路与代码实现,帮助大家快速提升代码能力ps:本章节题目分两部分,比较基础笔者只附上代码供大家参考,其他的笔者会附上自己的思考和讲解,希望和大家一起努力见证自己的算法成长 一、

By Ne0inhk
【算法】归并排序

【算法】归并排序

算法系列七:归并排序 一、归并排序的递归探寻 1.思路 2.搭建 2.1设计过掉不符情况(在最底层时) 2.2查验能实现基础排序(在最底层往上点时) 2.3跳转结果继续往上回搭 3.实质 4.实现 二、递归的调用栈 1.递归的执行过程 2.递归的函数栈帧 2.1递归函数的栈帧压弹 2.2合并有序数组函数的栈帧压弹 三、归并排序的复杂度 1.空间复杂度 2.时间复杂度 一、归并排序的递归探寻 1.思路 理想结果等于成分解断开子结果表达式的公式表示 快速排序: 整个数组有序 = 其中一个元素有序 + 其左断开数组有序 + 其右断开数组有序 归并排序: 整个数组有序 = 左断开数组有序 + 右断开数组有序

By Ne0inhk
【C语言】排序算法——快速排序详解(含多种变式)!!!

【C语言】排序算法——快速排序详解(含多种变式)!!!

【C语言】排序算法——快速排序详解(含多种变式)!!! * 前言 * 一 、快速排序(初阶) * 1. 视频演示 * 2. 算法思想 * 3. 实现思路 * (1)定key值 * (2)大小交换 * (3)循环 * (4)交换key * (5)分割区间 * (6)结束 * 4. 实现代码 * 二 、快速排序(中阶) * 1. 存在的问题 * 2. 优化(三数取中) * 3. 实现代码(中阶) * 三 、快速排序(高阶) * 1. 仍存在的问题 * 2. 优化(小区间优化) * 3. 实现代码(高阶)

By Ne0inhk
JVM 可达性分析算法

JVM 可达性分析算法

说实话,开源社区里面有很多人都在讲: 可达性算法中 JVM 会进行两次标记,第一次会标记所有对象,并找到继承实现了 finalize() 方法的对象,并查看该对象是否存在“自救”,这些内容都与《深入理解 Java 虚拟机》(后文简称为 ‘书’)中 3.2.4 生存还是死亡?这一小节存在出入,或者说几乎所有的博客都是通过阅读这一小节然后得到令人一知半解的回答,整个逻辑有点混乱,前后不搭,所有我打算总结一下 说明:虽然主要讲堆内存,但方法区也有 GC,只是条件更为苛刻(所有实例被回收、ClassLoader 被回收等)。 首先会先说明可达性分析算法的规则 其次会了解一下 finalize 方法,包括它的作用时机和作用次数 最后再来说一下 JVM 垃圾收集器根据 可达性分析算法 和 Java 对象机制怎么“两次标记”,怎么回收 Java

By Ne0inhk