目录
二、使用 ProtobufCodec 和 ProtobufDispatcher 的自定义协议
一、自定义应用层协议
自定义应用层协议是指在标准传输层协议(如TCP或UDP)之上定义的一套规则,用于描述应用程序如何在两个或多个端点之间交换数据。
- 数据格式
- 消息结构
- 传输规则
- 错误处理
- 会话管理
在muduo网络库中,使用 ProtobufDispatcher
和 ProtobufCodec
来实现不同消息类型的处理。
ProtobufCodec
的作用
ProtobufCodec
类负责将接收到的原始网络数据解析成Protocol Buffers消息,并调用适当的回调函数来处理这些消息。它的onMessage
方法是处理消息的核心,它从网络缓冲区中读取数据,解析出完整的Protocol Buffers消息,并调用messageCallback_
成员变量所指向的函数来进一步处理这些消息。
ProtobufDispatcher
的作用
ProtobufDispatcher
类负责根据消息的类型来调用不同的回调函数。它维护了一个映射表,其中键是消息类型,值是处理该类型消息的回调函数。当ProtobufCodec
解析出消息并调用messageCallback_
时,messageCallback_
会将消息传递给ProtobufDispatcher
,后者则根据消息类型来调用正确的回调函数。
二、使用 ProtobufCodec
和 ProtobufDispatcher
的自定义协议
在这个场景中,ProtobufCodec
和 ProtobufDispatcher
一起构成了一个自定义的应用层协议,它包括以下方面:
- 数据格式:使用Protocol Buffers作为序列化和反序列化数据的工具,Protocol Buffers是一种高效的二进制格式,支持多种编程语言。
- 消息结构:定义了不同类型的Protocol Buffers消息,每种消息类型都有其特定的字段和数据结构。
- 传输规则:
ProtobufCodec
负责在网络缓冲区中读取数据,解析出完整的Protocol Buffers消息,并调用适当的回调函数来处理这些消息。- 错误处理:
ProtobufCodec
中包含了错误处理逻辑,例如检查消息长度是否合理,以及如何处理解析失败的情况。- 会话管理:可以通过
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
方法会根据消息类型查找相应的回调函数。
小结
ProtobufCodec
和 ProtobufDispatcher
一起构成了一个自定义的应用层协议,它的工作流程如下:
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 的运行时库支持的,该库为所有消息类型提供了必要的基础功能。