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

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

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

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

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

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

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


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

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

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
}

这种写法的致命缺陷:

  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. 定义通用回调

首先,我们需要一个能接住所有消息的通用接口:

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++ 的多态

  1. 我是谁?(Identity):

    即使我们手里只有 Message* 基类指针,调用 msg->GetDescriptor() 时,利用 C++ 虚函数机制,会自动跳转到子类 (LoginRequest) 的实现,从而拿到正确的"身份证"(Descriptor)。

  2. 找谁办?(Lookup):

    Dispatcher 利用 std::map 建立了"身份证"到"办事员"的索引。

  3. 类型恢复 (Down-casting):

    通过模板封装 dynamic_cast,我们在框架层解决了类型安全转换的问题,把干净、强类型的指针交给了业务层。

结语:

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

相关推荐
Tandy12356_2 小时前
手写TCP/IP协议——IP层输出处理
c语言·网络·c++·tcp/ip·计算机网络
luoganttcc2 小时前
tcp 三次 握手
网络·网络协议·tcp/ip
博语小屋2 小时前
实现简单日志
linux·服务器·数据库·c++
ZouZou老师8 小时前
C++设计模式之装饰器模式:以家具生产为例
c++·设计模式·装饰器模式
ZouZou老师8 小时前
C++设计模式之桥接模式:以家具生产为例
c++·设计模式·桥接模式
呱呱巨基9 小时前
Linux 进程概念
linux·c++·笔记·学习
土星云SaturnCloud9 小时前
不止是替代:从机械风扇的可靠性困局,看服务器散热技术新范式
服务器·网络·人工智能·ai
liulilittle9 小时前
C++ 浮点数封装。
linux·服务器·开发语言·前端·网络·数据库·c++
ZouZou老师9 小时前
C++设计模式之组合模式:以家具生产为例
c++·设计模式·组合模式