通过源码分析如何使用muduo库实现自定义应用层协议

目录

一、自定义应用层协议 

ProtobufCodec 的作用

ProtobufDispatcher 的作用

二、使用 ProtobufCodec 和 ProtobufDispatcher 的自定义协议

三、如何实现(以消息队列客户端代码为例)

源码分析

小结

1 初始化 `ProtobufCodec` -

2 注册回调函数 -

3 处理消息 -


一、自定义应用层协议 

自定义应用层协议是指在标准传输层协议(如TCP或UDP)之上定义的一套规则,用于描述应用程序如何在两个或多个端点之间交换数据。

  • 数据格式
  • 消息结构
  • 传输规则
  • 错误处理
  • 会话管理

在muduo网络库中,使用 ProtobufDispatcherProtobufCodec 来实现不同消息类型的处理。

ProtobufCodec 的作用

ProtobufCodec 类负责将接收到的原始网络数据解析成Protocol Buffers消息,并调用适当的回调函数来处理这些消息。它的 onMessage 方法是处理消息的核心,它从网络缓冲区中读取数据,解析出完整的Protocol Buffers消息,并调用 messageCallback_ 成员变量所指向的函数来进一步处理这些消息。

ProtobufDispatcher 的作用

ProtobufDispatcher 类负责根据消息的类型来调用不同的回调函数。它维护了一个映射表,其中键是消息类型,值是处理该类型消息的回调函数。当 ProtobufCodec 解析出消息并调用 messageCallback_ 时,messageCallback_ 会将消息传递给 ProtobufDispatcher,后者则根据消息类型来调用正确的回调函数。

二、使用 ProtobufCodec 和 ProtobufDispatcher 的自定义协议

在这个场景中,ProtobufCodecProtobufDispatcher 一起构成了一个自定义的应用层协议,它包括以下方面:

  1. 数据格式:使用Protocol Buffers作为序列化和反序列化数据的工具,Protocol Buffers是一种高效的二进制格式,支持多种编程语言。
  2. 消息结构:定义了不同类型的Protocol Buffers消息,每种消息类型都有其特定的字段和数据结构。
  3. 传输规则ProtobufCodec 负责在网络缓冲区中读取数据,解析出完整的Protocol Buffers消息,并调用适当的回调函数来处理这些消息。
  4. 错误处理ProtobufCodec 中包含了错误处理逻辑,例如检查消息长度是否合理,以及如何处理解析失败的情况。
  5. 会话管理:可以通过 TcpConnectionPtr 参数来跟踪连接状态和管理会话。  

三、如何实现(以消息队列客户端代码为例)

代码示例

#ifndef __M_CONNECT_H__
#define __M_CONNECT_H__

#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher.h"
#include "muduo/base/Logging.h"
#include "muduo/base/Mutex.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpClient.h"
#include "muduo/base/CountDownLatch.h"
#include "muduo/net/EventLoopThread.h"
#include "mq_channel.hpp"
#include "mq_worker.hpp"

namespace bitmq {

// 定义Connection类
class Connection {
public:
    // 使用智能指针类型定义Connection的指针
    using ptr = std::shared_ptr<Connection>;

    // 构造函数,初始化Connection对象
    // 参数包括服务器的IP和端口,以及异步工作线程的指针
    Connection(const std::string &ip, int port, const AsynWorker::ptr &worker)
        : _latch(1), // 初始化CountDownLatch为1
          _client(worker->loopthread.startLoop(), muduo::net::InetAddress(ip, port), "client"), // 初始化TcpClient
          _dispatcher(std::bind(&Connection::onUnknownMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)), // 初始化消息分发器
          _codec(std::make_shared<ProtobufCodec>(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))), // 初始化protobuf编解码器
          _worker(worker), // 异步工作线程指针
          _channel_manager(std::make_shared<ChannelManager>()) // 初始化信道管理器
    {
        // 注册消息处理回调函数
        _dispatcher.registerMessageCallback<basicCommonResponse>(std::bind(&Connection::basicResponse, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        _dispatcher.registerMessageCallback<basicConsumeResponse>(std::bind(&Connection::consumeResponse, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        // 设置连接回调函数
        _client.setConnectionCallback(std::bind(&Connection::onConnection, this, std::placeholders::_1));
        // 设置消息回调函数
        _client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, _codec, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
        // 连接服务器
        _client.connect();
        // 等待连接建立
        _latch.wait();
    }

    // 打开信道的方法
    Channel::ptr openChannel() {
        Channel::ptr channel = _channel_manager->create(_conn, _codec);
        bool ret = channel->openChannel();
        if (ret == false) {
            DLOG("打开信道失败 !");
            return Channel::ptr();
        }
        return channel;
    }

    // 关闭信道的方法
    void closeChannel(const Channel::ptr &channel) {
        channel->closeChannel();
        _channel_manager->remove(channel->cid());
    }

private:
    // 处理基本响应消息的方法
    void basicResponse(const muduo::net::TcpConnectionPtr &conn, const basicCommonResponsePtr &message, muduo::Timestamp) {
        auto channel = _channel_manager->get(message->cid());
        if (channel.get() == nullptr) {
            DLOG("没有找到信道%s", message->cid().c_str());
            return;
        }
        channel->putBasicResponse(message);
    }

    // 处理消费响应消息的方法
    void consumeResponse(const muduo::net::TcpConnectionPtr &conn, const basicConsumeResponsePtr &message, muduo::Timestamp) {
        auto channel = _channel_manager->get(message->cid());
        if (channel.get() == nullptr) {
            DLOG("没有找到信道%s", message->cid().c_str());
            return;
        }
        // 将处理任务加入工作线程池
        _worker->pool.push([message, channel] {
            channel->consume(message);
        });
    }

    // 连接状态改变时的回调方法
    void onConnection(const muduo::net::TcpConnectionPtr &conn) {
        if (conn->connected()) {
            // 连接成功,计数器减1,唤醒等待线程
            _latch.countDown();
            _conn = conn;
        } else {
            // 连接关闭,重置连接指针
            _conn.reset();
        }
    }

    // 处理未知消息的方法
    void onUnknownMessage(const muduo::net::TcpConnectionPtr &, const MessagePtr &message, muduo::Timestamp) {
        DLOG("onUnknownMessage:%s ", message->GetTypeName().c_str());
        // 收到未知消息时关闭连接
        _conn->shutdown();
    }

private:
    muduo::CountDownLatch _latch; // 用于同步连接状态的计数器
    muduo::net::TcpConnectionPtr _conn; // TCP连接指针
    muduo::net::TcpClient _client; // 客户端对象
    ProtobufDispatcher _dispatcher; // 消息分发器对象
    ProtobufCodecPtr _codec; // protobuf协议处理器对象
    AsynWorker::ptr _worker; // 异步工作线程指针
    ChannelManager::ptr _channel_manager; // 信道管理器对象
};

} // namespace bitmq

#endif // __M_CONNECT_H__

源码分析

首先我们来看一下ProtobufCodec::onMessage

// 定义ProtobufCodec类的onMessage方法,该方法用于处理接收到的消息
void ProtobufCodec::onMessage(const TcpConnectionPtr& conn, // 连接指针
                              Buffer* buf, // 缓冲区指针
                              Timestamp receiveTime) // 消息接收时间戳
{
  // 当缓冲区中的可读字节数大于等于最小消息长度加上头部长度时,继续循环
  while (buf->readableBytes() >= kMinMessageLen + kHeaderLen)
  {
    // 窥探缓冲区中的前4个字节,获取消息长度
    const int32_t len = buf->peekInt32();
    // 如果消息长度大于最大消息长度或小于最小消息长度,则调用错误回调函数并跳出循环
    if (len > kMaxMessageLen || len < kMinMessageLen)
    {
      errorCallback_(conn, buf, receiveTime, kInvalidLength);
      break;
    }
    // 如果缓冲区中的可读字节数大于等于消息长度加上头部长度
    else if (buf->readableBytes() >= implicit_cast<size_t>(len + kHeaderLen))
    {
      ErrorCode errorCode = kNoError; // 初始化错误代码为无错误
      // 解析消息,并将解析结果赋值给message指针,同时检查错误代码
      MessagePtr message = parse(buf->peek()+kHeaderLen, len, &errorCode);
      // 如果没有错误且消息解析成功,则调用消息回调函数,并移除已处理的消息部分
      if (errorCode == kNoError && message)
      {
        messageCallback_(conn, message, receiveTime);
        buf->retrieve(kHeaderLen+len);
      }
      // 如果有错误,则调用错误回调函数并跳出循环
      else
      {
        errorCallback_(conn, buf, receiveTime, errorCode);
        break;
      }
    }
    // 如果缓冲区中的剩余字节不足以包含一个完整的消息,则跳出循环
    else
    {
      break;
    }
  }
}

可以看到如果消息解码成功则应该调用消息对应的回调函数,那么回调函数又是在哪设置的呢

我们来看一下ProtobufDispatcher::registerMessageCallback方法

// 定义一个模板函数,用于注册消息回调函数
// 参数为消息类型T的ProtobufMessageTCallback类型的回调函数
template<typename T>
void registerMessageCallback(const typename CallbackT<T>::ProtobufMessageTCallback& callback)
{
  // 创建一个新的CallbackT<T>实例,并将用户提供的回调函数作为参数传递给它
  // 使用std::shared_ptr来管理CallbackT<T>实例的生命周期
  std::shared_ptr<CallbackT<T> > pd(new CallbackT<T>(callback));
  
  // 将消息类型T的描述符作为键,将CallbackT<T>实例的智能指针作为值,存储到回调函数映射中
  // 这样可以方便地根据消息类型查找对应的回调函数
  callbacks_[T::descriptor()] = pd;
}

// 定义一个私有的类型别名,用于存储回调函数的映射关系
// 键为google::protobuf::Descriptor类型的指针,表示消息类型的描述符
// 值为std::shared_ptr<Callback>类型的智能指针,指向具体的回调函数对象
private:
  typedef std::map<const google::protobuf::Descriptor*, std::shared_ptr<Callback> > CallbackMap;

我们来看一下CallbackT<T>

// 定义一个类型别名ProtobufMessageTCallback,用于定义回调函数

// 回调函数的参数包括:TcpConnectionPtr(TCP连接指针),shared_ptr<T>(消息的智能指针),Timestamp(时间戳)

那我们的消息回调处理函数也该如此定义

ProtobufDispatcher::registerMessageCallback方法将对应的消息以键值对的方式存在hash表中,调用时通过消息对应的类型来映射回调处理函数。

接下来我们来看messageCallback又是怎么找到回调函数表的

 在ProtobufDispatcher中有一个onProtobufMessage方法

 函数内部首先尝试在callbacks_这个映射中查找与接收到的消息类型相对应的回调函数。callbacks_是一个映射,其键是消息的描述符(Descriptor),值是对应的回调接口指针。如果找到了匹配的回调函数(即it != callbacks_.end()),则调用该回调函数的onMessage方法来处理消息。如果没有找到匹配的回调函数,则调用defaultCallback_这个默认回调函数来处理消息。

而messageCallback_回调指针在ProtobufCodec对象初始化的时候就指定了

所以在初始化列表中需要指定

这样以来当 _codec 的 onMessage 方法接收到一条消息时,

它会调用 _dispatcher 的 onProtobufMessage 方法。

onProtobufMessage 方法会根据消息类型查找相应的回调函数。

小结

ProtobufCodecProtobufDispatcher 一起构成了一个自定义的应用层协议,它的工作流程如下:

1 初始化 `ProtobufCodec` -

`ProtobufCodec` 的 `messageCallback_` 成员变量被初始化为`ProtobufDispatcher::onProtobufMessage` 方法的地址。

2 注册回调函数 -

使用 `ProtobufDispatcher` 的 `registerMessageCallback` 方法来为每种消息类型注册一个回调函数。

3 处理消息 -

当 `_codec` 的 `onMessage` 方法接收到一条消息时,它会调用 `_dispatcher` 的 `onProtobufMessage` 方法。

- `onProtobufMessage` 方法会根据消息类型查找相应的回调函数。

- 如果找到的是 `basicCommonResponse` 类型的消息,`onProtobufMessage` 方法会调用 `basicResponse` 函数。

- 如果找到的是 `basicConsumeResponse` 类型的消息,`onProtobufMessage` 方法会调用 `consumeResponse` 函数。(其他类型的消息同理)

需要注意的是:消息类型一定要是message类型的

首先确保已经安装了 protoc

创建一个 .proto 文件来定义你的协议缓冲区消息结构。类似下图

打开终端(或命令提示符),并导航到包含 .proto 文件的目录。

运行以下命令来生成 C++ 源文件和头文件

protoc --cpp_out=. example.proto

命令执行后,你会在指定的目录中看到两个文件:example.pb.h 和 example.pb.cc

虽然这些消息类并非直接继承自一个特定的基类,但它们共享了一组通用的行为和接口,这使得它们在功能上类似一个继承体系中的子类。实际上,Protocol Buffers 的 C++ 库通过模板和宏机制来实现这些通用行为,而不是通过传统的面向对象继承。

因此,在概念上,你可以认为所有生成的消息类型都是 Message 接口的实现,但在实际的 C++ 代码中,并没有一个名为 Message 的基类。相反,这些消息类型是通过 Protocol Buffers 的运行时库支持的,该库为所有消息类型提供了必要的基础功能。

  • 33
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。 基于muduo实现的集群聊天服务器,通过mysql存储数据,通过nginx实现tcp负载均衡,通过redis实现集群内服务器间的消息订阅发布。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值