1. 信道管理
同样的,客户端也有信道,其功能与服务端几乎一致,或者说不管是客户端的 channel 还是服务端的 channel 都是为了用户提供具体服务而存在的,只不过服务端是为客户端的对应请求提供服务,而客户端的接口服务是为了用户具体需要服务,也可以理解是用户通过客户端 channel 的接口调用来向服务端发送对应请求,获取请求的服务。
- 信道信息:
- 信道 ID
- 信道关联的网络通信连接对象
- protobuf 协议处理对象
- 信道关联的消费者
- 请求对应的响应信息队列(这里队列使用<请求 ID,响应>hash 表,以便于查找指定的响应)
- 互斥锁&条件变量(大部分的请求都是阻塞操作,发送请求后需要等到响应才能继续,但是 muduo 库的通信是异步的,因此需要我们自己在收到响应后,通过判断是否是等待的指定响应来进行同步)
- 信道操作:
- 提供创建信道操作
- 提供删除信道操作
- 提供声明交换机操作(强断言-有则 OK,没有则创建)
- 提供删除交换机
- 提供创建队列操作(强断言-有则 OK,没有则创建)
- 提供删除队列操作
- 提供交换机-队列绑定操作
- 提供交换机-队列解除绑定操作
- 提供添加订阅操作
- 提供取消订阅操作
- 提供发布消息操作
- 提供确认消息操作
- 信道管理:
- 创建信道
- 查询信道
- 删除信道
cpp
#ifndef __M_CHANNEL_H__
#define __M_CHANNEL_H__
#include "muduo/net/TcpConnection.h"
#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher.h"
#include "../mqcommon/logger.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/msg.pb.h"
#include "../mqcommon/proto.pb.h"
#include "consumer.hpp"
#include <iostream>
#include <mutex>
#include <condition_variable>
#include <unordered_map>
namespace rabbitmq
{
typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
using ProtobufCodecPtr = std::shared_ptr<ProtobufCodec>;
using basicConsumeResponsePtr = std::shared_ptr<basicConsumeResponse>;
using basicCommonResponsePtr = std::shared_ptr<basicCommonResponse>;
class Channel
{
public:
using ptr = std::shared_ptr<Channel>;
Channel(const muduo::net::TcpConnectionPtr& conn, const ProtobufCodecPtr& codec)
:_cid(UUIDHelper::uuid()), _conn(conn), _codec(codec) {}
~Channel() { basicCancel(); }
std::string cid() { return _cid; }
bool openChannel()
{
//构造一个打开信道的请求对象,
std::string rid = UUIDHelper::uuid();
openChannelRequest req;
req.set_rid(rid);
req.set_cid(_cid);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
basicCommonResponsePtr resp = waitResponse(rid);
//返回
return resp->ok();
}
void closeChannel()
{
//构造一个关闭信道的请求对象,
std::string rid = UUIDHelper::uuid();
closeChannelRequest req;
req.set_rid(rid);
req.set_cid(_cid);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
waitResponse(rid);
}
bool declareExchange(
const std::string &name,
ExchangeType type,
bool durable,
bool auto_delete,
google::protobuf::Map<std::string, std::string> &args)
{
//构造一个声明交换机的请求对象,
std::string rid = UUIDHelper::uuid();
declareExchangeRequest req;
req.set_rid(rid);
req.set_cid(_cid);
req.set_exchange_name(name);
req.set_exchange_type(type);
req.set_durable(durable);
req.set_auto_delete(auto_delete);
req.mutable_args()->swap(args);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
basicCommonResponsePtr resp = waitResponse(rid);
//返回
return resp->ok();
}
void deleteExchange(const std::string& name)
{
//构造一个删除交换机的请求对象,
std::string rid = UUIDHelper::uuid();
deleteExchangeRequest req;
req.set_rid(rid);
req.set_cid(_cid);
req.set_exchange_name(name);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
waitResponse(rid);
}
bool declareQueue(
const std::string &qname,
bool qdurable,
bool qexclusive,
bool qauto_delete,
google::protobuf::Map<std::string, std::string> &qargs)
{
//构造一个声明队列的请求对象,
std::string rid = UUIDHelper::uuid();
declareQueueRequest req;
req.set_rid(rid);
req.set_cid(_cid);
req.set_queue_name(qname);
req.set_durable(qdurable);
req.set_exclusive(qexclusive);
req.set_auto_delete(qauto_delete);
req.mutable_args()->swap(qargs);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
basicCommonResponsePtr resp = waitResponse(rid);
//返回
return resp->ok();
}
void deleteQueue(const std::string& qname)
{
//构造一个删除队列的请求对象,
std::string rid = UUIDHelper::uuid();
deleteQueueRequest req;
req.set_rid(rid);
req.set_cid(_cid);
req.set_queue_name(qname);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
waitResponse(rid);
}
bool queueBind(
const std::string &ename,
const std::string &qname,
const std::string &key)
{
//构造一个队列绑定的请求对象,
std::string rid = UUIDHelper::uuid();
queueBindRequest req;
req.set_rid(rid);
req.set_cid(_cid);
req.set_exchange_name(ename);
req.set_queue_name(qname);
req.set_binding_key(key);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
basicCommonResponsePtr resp = waitResponse(rid);
//返回
return resp->ok();
}
void queueUnBind(const std::string &ename, const std::string &qname)
{
//构造一个队列解绑的请求对象,
std::string rid = UUIDHelper::uuid();
queueUnBindRequest req;
req.set_rid(rid);
req.set_cid(_cid);
req.set_exchange_name(ename);
req.set_queue_name(qname);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
waitResponse(rid);
}
void basicPublish(
const std::string &ename,
const BasicProperties *bp,
const std::string &body)
{
//构造一个消息发布的请求对象,
std::string rid = UUIDHelper::uuid();
basicPublishRequest req;
req.set_rid(rid);
req.set_cid(_cid);
req.set_exchange_name(ename);
req.set_body(body);
if(bp != nullptr)
{
req.mutable_properties()->set_id(bp->id());
req.mutable_properties()->set_delivery_mode(bp->delivery_mode());
req.mutable_properties()->set_routing_key(bp->routing_key());
}
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
waitResponse(rid);
}
void basicAck(const std::string &msgid)
{
if (_consumer.get() == nullptr)
{
DLOG("消息确认时,找不到消费者信息!");
return ;
}
//构造一个消息确认的请求对象,
std::string rid = UUIDHelper::uuid();
basicAckRequest req;
req.set_rid(rid);
req.set_cid(_cid);
req.set_queue_name(_consumer->_qname);
req.set_message_id(msgid);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
waitResponse(rid);
}
void basicCancel()
{
if (_consumer.get() == nullptr)
{
return ;
}
//构造一个取消消息订阅的请求对象,
std::string rid = UUIDHelper::uuid();
basicCancelRequest req;
req.set_rid(rid);
req.set_cid(_cid);
req.set_queue_name(_consumer->_qname);
req.set_consumer_tag(_consumer->_tag);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
waitResponse(rid);
_consumer.reset();
}
bool basicConsume(
const std::string &consumer_tag,
const std::string &queue_name,
bool auto_ack,
const ConsumerCallback &cb)
{
if (_consumer.get() != nullptr)
{
DLOG("当前信道已订阅其他队列消息!");
return false;
}
//构造一个消息订阅的请求对象,
std::string rid = UUIDHelper::uuid();
basicConsumeRequest req;
req.set_rid(rid);
req.set_cid(_cid);
req.set_queue_name(queue_name);
req.set_consumer_tag(consumer_tag);
req.set_auto_ack(auto_ack);
//然后向服务器发送请求
_codec->send(_conn, req);
//等待服务器的响应
basicCommonResponsePtr resp = waitResponse(rid);
if(resp->ok() == false)
{
DLOG("添加订阅失败!");
return false;
}
_consumer = std::make_shared<Consumer>(consumer_tag, queue_name, auto_ack, cb);
return true;
}
//连接收到基础响应后,向hash_map中添加响应
void putBasicResponse(const basicCommonResponsePtr& resp)
{
std::unique_lock<std::mutex> lock(_mutex);
_basic_resp.insert(std::make_pair(resp->cid(), resp));
_cv.notify_all();
}
//连接收到消息推送后,需要通过信道找到对应的消费者对象,通过回调函数进行消息处理
void consume(const basicConsumeResponsePtr& resp)
{
if (_consumer.get() == nullptr)
{
DLOG("消息处理时,未找到订阅者信息!");
return;
}
if (_consumer->_tag != resp->consumer_tag())
{
DLOG("收到的推送消息中的消费者标识,与当前信道消费者标识不一致!");
return ;
}
_consumer->_callback(resp->consumer_tag(), resp->mutable_properties(), resp->body());
}
private:
basicCommonResponsePtr waitResponse(const std::string &rid)
{
std::unique_lock<std::mutex> lock(_mutex);
_cv.wait(lock, [&rid, this](){
return _basic_resp.find(rid) != _basic_resp.end();
});
//等价于while(condition()) _cv.wait();
basicCommonResponsePtr basic_resp = _basic_resp[rid];
_basic_resp.erase(rid);
return basic_resp;
}
private:
std::string _cid;
muduo::net::TcpConnectionPtr _conn;
ProtobufCodecPtr _codec;
Consumer::ptr _consumer;
std::mutex _mutex;
std::condition_variable _cv;
std::unordered_map<std::string, basicCommonResponsePtr> _basic_resp;
};
}
#endif
关键点分析
- 信道标识(_cid)
每个信道在创建时都会生成一个唯一的UUID作为信道ID,用于在请求和响应中标识信道。
- 网络通信
依赖muduo库的TcpConnection进行网络通信,使用ProtobufCodec进行消息的编解码。每个操作都会构造对应的Protobuf请求对象,通过_codec发送到服务器,然后等待服务器的响应。
- 同步请求-响应机制
每个请求都会生成一个唯一的请求ID(rid),发送请求后,调用waitResponse方法阻塞等待直到收到对应的响应。响应通过putBasicResponse方法存入哈希表,并由条件变量通知等待的线程。
- 消费者管理
每个信道同一时间只能有一个消费者(_consumer),消费者包含消费者标签、队列名称、自动确认标志和回调函数。当收到服务器的推送消息(basicConsumeResponse)时,会调用消费者的回调函数处理消息。
- 线程安全
使用互斥锁(_mutex)和条件变量(_cv)保护响应哈希表(_basic_resp)的并发访问,确保多线程环境下等待和通知的正确性。
- 资源管理
在析构函数中会自动调用basicCancel取消订阅,释放资源。消费者对象使用智能指针管理生命周期。
代码结构
每个AMQP操作遵循相同的模式:
- 生成请求ID(rid)。
- 构造对应的Protobuf请求对象,设置rid和cid(信道ID)等参数。
- 通过_codec发送请求。
- 调用waitResponse等待服务器的响应。
- 根据响应返回操作结果(成功或失败)。
异常处理
代码中通过DLOG输出日志,但在错误处理方面相对简单,例如在basicAck和basicCancel中,如果找不到消费者,只是打印日志并返回,没有抛出异常。在basicConsume中,如果已经存在消费者,则返回false。
流程总结
下面,我们对这个客户端Channel类的使用流程进行简要说明:
- 首先,客户端需要建立与服务器的TCP连接,并创建ProtobufCodec用于编解码。
- 然后,创建一个Channel对象,并调用openChannel来打开信道。
- 之后,就可以通过Channel对象调用declareExchange、declareQueue等方法,这些方法会阻塞直到收到服务器响应。
- 如果要做消费者,调用basicConsume,并传递一个回调函数。当有消息时,服务器会推送,然后回调函数会被调用。
- 在回调函数中,处理消息,然后可能需要调用basicAck来确认消息(如果不是自动确认的话)。
- 最后,在不再需要时,可以调用closeChannel关闭信道,或者由析构函数自动清理。
2. 封装对外管理的类
cpp
class ChannelManager
{
public:
using ptr = std::shared_ptr<ChannelManager>;
ChannelManager(){}
Channel::ptr create(const muduo::net::TcpConnectionPtr &conn, const ProtobufCodecPtr &codec)
{
std::unique_lock<std::mutex> lock(_mutex);
auto channel = std::make_shared<Channel>(conn, codec);
_channels.insert(std::make_pair(channel->cid(), channel));
return channel;
}
void remove(const std::string &cid)
{
std::unique_lock<std::mutex> lock(_mutex);
_channels.erase(cid);
}
Channel::ptr get(const std::string &cid)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _channels.find(cid);
if (it == _channels.end())
{
return Channel::ptr();
}
return it->second;
}
private:
std::mutex _mutex;
std::unordered_map<std::string, Channel::ptr> _channels;
};
这段代码是一个简单的信道管理类(ChannelManager),用于管理多个Channel对象。其功能类似于一个容器,提供了创建、删除和查找信道的功能,并且是线程安全的。
主要功能:
- 创建信道(create):创建一个新的Channel对象,并将其添加到管理器中。
- 删除信道(remove):根据信道ID从管理器中移除对应的Channel对象。
- 查找信道(get):根据信道ID获取对应的Channel对象。
细节分析:
- 线程安全:使用互斥锁(std::mutex)保护对内部unordered_map的并发访问,所有公共方法都加锁。
- 智能指针管理:使用shared_ptr管理Channel对象,便于自动管理生命周期。
- 映射关系:使用unordered_map存储信道ID到Channel对象的映射,便于快速查找。
3. 异步工作线程实现
客户端这边存在两个异步工作线程,
- 一个是 muduo 库中客户端连接的异步循环线程 EventLoopThread,
- 一个是当收到消息后进行异步处理的工作线程池。
这两项都不是以连接为单元进行创建的,而是创建后,可以用以多个连接中,因此单独进行封装。
cpp
#ifndef __M_WORKER_H__
#define __M_WORKER_H__
#include "muduo/net/EventLoopThread.h"
#include "../mqcommon/logger.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/threadpool.hpp"
namespace rabbitmq
{
class AsyncWorker
{
public:
using ptr = std::shared_ptr<AsyncWorker>;
muduo::net::EventLoopThread _loopthread;
threadpool _pool;
};
}
#endif
两个异步线程组件详解
- muduo::net::EventLoopThread _loopthread
功能描述:
这是基于 muduo 网络库的事件循环线程,专门处理网络I/O事件。
工作内容:
- 监听 TCP 连接的读写事件
- 处理连接建立和断开
- 接收服务器发送的数据
- 发送客户端请求到服务器
- 管理定时器(如心跳、超时)
- threadpool _pool
功能描述:
这是一个通用的工作线程池,专门处理业务逻辑计算。
工作内容:
- 处理服务器推送的消息
- 执行消息确认操作
- 处理用户回调函数
- 其他耗时的业务计算