一,公共模块
1.1 目录结构
如下图:

1.2 日志打印
cpp
#ifndef __M_LOG_H__
#define __M_LOG_H__
#include <ctime>
#include <iostream>
// 封装一个日志宏,通过这个宏进行日志打印 ,包括时间、文件名、行号
#define DEBUG_LEVEL 0
#define INFO_LEVEL 1
#define ERROR_LEVEL 2
#define LOG_LEVEL DEBUG_LEVEL // 暂时设置成0
#define LOG(lev_str, level, format, ...) \
{ \
if (level >= LOG_LEVEL) \
{ \
time_t t = time(nullptr); \
struct tm *ptm = localtime(&t); \
char time_str[32]; \
strftime(time_str, 31, "%H:%M:%S", ptm); \
printf("[%s][%s][%s:%d]\t" format "\n", lev_str, time_str, __FILE__, __LINE__, ##__VA_ARGS__); \
} \
}
#define DLOG(format, ...) LOG("DEBUG", DEBUG_LEVEL, format, ##__VA_ARGS__)
#define ILOG(format, ...) LOG("INFO", INFO_LEVEL, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG("ERROR", ERROR_LEVEL, format, ##__VA_ARGS__)
#endif
// int main()
// {
// DLOG("111111");
// ILOG("222222");
// ELOG("222222");
// return 0;
// }
1.3 sqlite 基础操作
这个我们之前已经实现好了,直接复制过来即可
cpp
//mq/mqcommon/helper.hpp
#ifndef __M_HELPER_H__
#define __M_HELPER_H__
#include <iostream>
#include <string>
#include <vector>
#include <sqlite3.h>
#include "log.hpp"
namespace my_mq
{
class SqliteHelper
{
public:
typedef int (*SqliteCallback)(void *, int, char **, char **);
SqliteHelper(const std::string &dbfile)
: _dbfile(dbfile), _handler(nullptr)
{
}
bool open(int save_leve = SQLITE_OPEN_FULLMUTEX) // 打开数据库文件(串行化)
{
int ret = sqlite3_open_v2(_dbfile.c_str(), &_handler, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | save_leve, nullptr); // 可读可写打开,不存在则创建,默认等级为串行化
if (ret != SQLITE_OK)
{
ELOG("创建或打开SQLite数据库文件失败: %s", sqlite3_errmsg(_handler));
return false;
}
return true;
}
bool exec(const std::string &sql, SqliteCallback cb = nullptr, void *arg = nullptr) // arg参数表示查询结果保存位置的指针,作为参数传递给回调函数
{
int ret = sqlite3_exec(_handler, sql.c_str(), cb, arg, nullptr);
if (ret != SQLITE_OK)
{
ELOG("%s \n执行语句失败: %s", sql.c_str(), sqlite3_errmsg(_handler))
return false;
}
return true;
}
void close() // 关闭数据库
{
if (_handler)
sqlite3_close_v2(_handler);
}
private:
std::string _dbfile;
sqlite3 *_handler;
};
}
#endif
1.4 字符串操作
也放在helper.hpp 的 my_mq 命名空间里
cpp
class StrHelper
{
public:
static size_t split(const std::string &str, const std::string &sep, std::vector<std::string> &result) //字符串分割函数
{
// 分割字符串,比如 game.news.#.pop
// 1,从0开始查找指定字符的位置,找到之后进行分割
// 2,从上次查找的位置继续向后指定字符
size_t pos, idx = 0;
while (idx < str.size())
{
pos = str.find(sep, idx);
if (pos == std::string::npos) // 遍历到最后了,没有找到
{
result.push_back(str.substr(idx)); // 截取到末尾
break;
}
if (pos == idx) // 防止连续多个点点的情况,代表两个分隔符之间没有数据
{
idx += sep.size(); // 将查找的位置往后偏移
continue;
}
result.push_back(str.substr(idx, pos - idx));
idx = pos + sep.size();
}
return result.size();
}
};
// int main()
// {
// std::string str = "...game...news.#.pop...";
// std::vector<std::string> array;
// int n = split(str, ".", array);
// for (auto e : array)
// std::cout << e << std::endl;
// return 0;
// }
1.5 UUID 生成器
UUID(UniversallyUniqueIdentifier),也叫通用唯一识别码,通常由32位16进制数字字符组成。
UUID的标准型式包含32个16进制数字字符,以连字号分为五段,形式为8-4-4-4-12的32个字符,比如: 550e8400-e29b-41d4-a716-446655440000。
我们在这里采用生成8个随机数字,加上8字节序号,共16字节数组生成32位16进制字符的组合形式来确保全局唯一的同时能够根据序号来分辨数据:
- 生成8个0-255之前的随机数
- 取出一个8字节的序号
- 通过以上数据组成16字节的数据,转换为16进制字符,共32个
也是放在 helper.hpp 的命名空间里
cpp
#include <random>
#include <sstream>
#include <iostream>
#include <iomanip>
#include <atomic>
class UUIDHelper
{
public:
static std::string uuid()
{
std::random_device rd; // 这个类型用来生成随机数,可直接()调用,生成一个机器随机数,但是效率较低
std::mt19937_64 gernator(rd()); // 这个类型是生成伪随机数,将机器随机数作为种子去生成伪随机数(梅森旋转算法)
std::uniform_int_distribution<int> distribution(0, 255); // 上面生成的随机数太大,这个类型用来限制数据区间,生成区间之内的数字 0-255
std::stringstream ss;
for (int i = 0; i < 8; i++)
{
ss << std::setw(2) << std::setfill('0') << std::hex << distribution(gernator);
// 转换成16进制字符,setw是设置位宽,setfill是填充,是因为如果生成的只有一个数,那么后面的0会被忽略导致最终生成的数的长度达不到要求
if (i == 3 || i == 5 || i == 7)
ss << "-";
}
// 上面是前16个字符
static std::atomic<size_t> seq(1); // 定义一个原子类型整数初始化为1
size_t num = seq.fetch_add(1); // 每次进行+1操作,size_t无符号整型同时也是8字节的,刚好可作为序号处理
for (int i = 7; i >= 0; i--)
{
ss << std::setw(2) << std::setfill('0') << std::hex << ((num >> (i * 8)) & 0xff); // num一次右移一个字节也就是8位,然后保留最低字节数据然后把其他的去掉
if (i == 6)
ss << "-";
}
return ss.str();
}
};
//int main()
//{
// std::cout << uuid() << std::endl;
// return 0;
//}
1.6 文件操作
主要提供下面几个操作
- 文件是否存在
- 获取文件大小
- 读写文件
- 文件创建/删除
- 目录创建/删除
也是放在helper.hpp 的命名空间里
cpp
class FileHelper
{
public:
FileHelper(const std::string &filename)
: _filename(filename)
{
}
bool exists() // 判断文件是否存在
{
struct stat st;
// stat接口,获取一个文件的属性,放到一个结构体里面,常用来判断文件是否存在
return (stat(_filename.c_str(), &st) == 0); // 如果为0代表文件存在,否则返回-1
}
size_t size() // 获取文件大小
{
struct stat st;
int ret = stat(_filename.c_str(), &st);
if (ret < 0)
return 0;
return st.st_size;
}
bool read(char *body, size_t offset, size_t len) // 从哪里开始读,读多少
{
// 1,打开文件
std::ifstream ifs(_filename, std::ios::binary | std::ios::in);
// 是对文件进行二进制的操作,因为默认是以文本形式的,而文本读取会进行特殊处理比如 \n \r 回车等等,会导致原数据有差别
if (ifs.is_open() == false)
{
ELOG("%s 打开文件失败!", _filename.c_str());
return false;
}
// 2,跳转文件读写位置
ifs.seekg(offset, std::ios::beg);
// 3,读取文件数据
ifs.read(body, len);
if (ifs.good() == false) // read的返回值不是true或者false,要用这个来判断
{
ELOG("%s 文件读取数据失败!", _filename.c_str());
ifs.close();
return false;
}
// 4,关闭文件
ifs.close();
return true;
}
bool read(std::string &body) // 读所有数据放到body里去
{
// 获取文件大小,并调整body空间
size_t fsize = this->size(); // 获取大小
body.resize(fsize); // 跳转body大小
return read(&body[0], 0, fsize);
// 不能直接body.c_str(),因为它返回const char*,不能修改指定空间的,所以访问第一个字符地址相当于访问到这个空间的首地址
}
bool write(const char *body, size_t offset, size_t len) // 从哪里写入,覆盖
{
// 1,打开文件
std::fstream fs(_filename, std::ios::binary | std::ios::in | std::ios::out);
// 用fstream,ifstream是输入,但是我们现在是往里面写数据是输出,ofstream是纯输出,因为我们还有文件跳转步骤,这个需要读权限,ofstram也不能用,fstram就是读写权限全加上
if (fs.is_open() == false)
{
ELOG("%s 打开文件失败!", _filename.c_str());
return false;
}
// 2,跳转到文件指定位置
fs.seekg(offset, std::ios::beg);
// 3,写入数据
fs.write(body, len);
if (fs.good() == false) // read的返回值不是true或者false,要用这个来判断
{
ELOG("%s 文件写入数据失败!", _filename.c_str());
fs.close();
return false;
}
// 4,关闭文件
fs.close();
return true;
}
bool write(const std::string &body) // 写数据
{
// 如果文件原本长度是100,我们写入50个数字,那么文件长度依旧是50,后面50个数字不变
return write(body.c_str(), 0, body.size());
}
static std::string parentDirectory(const std::string &filename) // 获取文件父级目录
{
// 比如aaa/bbb/test.cpp,就是从后往前读,获取第一个/
size_t pos = filename.find_last_of("/");
if (pos == std::string::npos)
{
return "./";
}
std::string path = filename.substr(0, pos);
return path;
}
bool rename(const std::string &name) // 修改文件名称
{
// 直接调用系统接口
return ::rename(_filename.c_str(), name.c_str()) == 0;
}
static bool createFile(const std::string &filename) // 创建文件
{
std::fstream ofs(filename, std::ios::binary | std::ios::out);
if (ofs.is_open() == false)
{
ELOG("%s 打开文件失败!", filename.c_str());
return false;
}
ofs.close();
return true;
}
static bool removeFile(const std::string &filename) // 删除文件
{
return ::remove(filename.c_str()) == 0;
}
static bool createDirectory(const std::string &path)
{
// 麻烦点就在创建多级路径时,需要从第一个父级目录开始创建
size_t pos, idx = 0;
while (idx < path.size())
{
pos = path.find("/", idx); // 初始从0位置找斜杠
if (pos == std::string::npos) // 没找到,单级目录
{
// 最后一次创建时不截取,直接以完整路径创建最终文件
return (mkdir(path.c_str(), 0775) == 0); // 直接调用
}
std::string subpath = path.substr(0, pos);
int ret = mkdir(subpath.c_str(), 0775);
if (ret != 0 && errno != EEXIST)
{
ELOG("创建⽬录 %s 失败 : %s ", subpath.c_str(), strerror(errno));
return false;
}
idx = pos + 1;
}
return true;
}
static bool removeDirectory(const std::string &path) // 删除目录
{
// 也有系统接口可以删除,但是需要是空目录,在删除之前得先把目录下文件全部删除才行
// 所以直接调用system,这个库函数是直接执行一条指令
std::string cmd = "rm -rf " + path;
return (system(cmd.c_str()) != -1);
}
private:
std::string _filename; // 文件名
};
int main()
// {
// // 1,测试
// FileHelper h1("./log.cpp");
// DLOG("是否存在:%d", h1.exists());
// DLOG("文件大小:%ld", h1.size());
// // 2,多级目录创建测试
// FileHelper h2("./aaa/bbb/test.txt");
// if (h2.exists() == false)
// {
// std::string path = FileHelper::parentDirectory("./aaa/bbb/ccc/test.txt");
// if (FileHelper(path).exists() == false)
// {
// FileHelper::createDirectory(path);
// }
// FileHelper::createFile("./aaa/bbb/test.txt");
// }
// // 3,读写测试
// std::string b1;
// h1.read(b1);
// h2.write(b1);
// // 4,局部读写测试
// char str[16] = {0};
// h1.read(str, 8, 11);
// DLOG("[%s]", str);
// h2.write("12345678901", 8, 11);
// // 5,测试修改文件名和删除
// h2.rename("./aaa/bbb/test.cpp");
// FileHelper::removeFile("./aaa/bbb/test.cpp");
// FileHelper::removeDirectory("./aaa");
// return 0;
// }
二,虚拟机实现
2.1 消息类型proto
我们的消息是要存到文件里去的,而一旦涉及到持久化存储,就要序列化和反序列化,我们在准备工作里已经介绍过了 protobuf 的作用:【项目实战】基于protobuf的发布订阅式消息队列(1)------ 准备工作-CSDN博客
我们的消息包括如下两个:
①消息本身要素:
- 消息ID
- 消息投递模式:持久化/非持久化模式
- 消息的 routing_key
②消息额外存储所需元素:
- 消息的存储位置
- 消息的长度
- 消息是都有效:这里使用字符的0/1,因为bool类型在持久化时占用长度不懂(true/false),会导致修改文件内容后导致消息长度与原来不一致
因为客户端与服务端都会用到交换机的以些相关信息,比如交换机类型,还有就是消息的持久化模式,因此我们将交换机类型的枚举,与消息投递模式的枚举也顺便同时定义到proto文件中
①交换机类型
- DIRECT
- FANOUT
- TOPIC
②消息投递模式
- UNDURABLE:直接设置成1即可
- DURABLE:设为2即可
message.proto 写在 mqcommon 目录里:
bash
syntax = "proto3";
package my_mq;
//广播模式
//proto规则:枚举的第一个值必须是0,并且名字不能一样
enum ExchangeType
{
UNKNOWTYPE = 0;
DIRECT = 1; //直接交换
FANOUT = 2; //广播交换
TOPIC = 3; //主题交换
};
//消息投递模式
enum DeliveryMode
{
UNKNOWMODE = 0;
UNDURABLE = 1;
DURABLE = 2;
};
//消息属性
message BasicProperties
{
string id = 1; //消息ID
DeliveryMode delivery_mode = 2; //与routing_key做匹配
string routing_key = 3; //持久化模式
};
//消息本体
message Message
{
//消息有效载荷
message Payload
{
BasicProperties properties = 1; //消息属性
string body = 2; //有效数据载荷
string valid = 3; //是否有效
};
Payload payload = 1; //有效载持久化字段
uint32 offset = 2; //用于记录消息在持久化文件中的位置和长度
uint32 length = 3; //便于在加载时可以在指定位置读取指定长度的数据
};
然后使用命令生成 .cc 和 .h 即可:
bash
protoc --cpp_out=./ mq_msg.proto
2.2 交换机实现
客户端发消息是把消息发到这个交换机里面去,然后由交换机决定该发送到哪个队列里去,需要实现三个东西:
①交换机数据类
- 交换机名称
- 交换机类型
- 是否持久化标志
- 是否自动删除标志
- 其他参数
②交换机数据持久化类(数据持久化的sqlite3数据库中)
- 创建/删除交换机数据表
- 新增交换机数据
- 移除交换机数据
- 查询所有交换机数据
- 查询指定交换机数据(根据名称)
③交换机数据管理类
- 声明交换机,并添加管理(存在则啥也不做,不存在则创建)
- 删除交换机
- 获取指定交换机
- 销毁所有交换机数据(用于测试)
首先是交换机类,mq_exchange.hpp 定义在 mqserver目录下:
cpp
#ifndef __M_EXCHANGE_H__
#define __M_EXCHANGE_H__
#include "../mqcommon/helper.hpp"
#include "../mqcommon/message.pb.h"
#include <google/protobuf/map.h>
#include <unordered_map>
#include <mutex>
#include <memory>
namespace my_mq
{
//-----交换机类-----//
struct Exchange
{
using ptr = std::shared_ptr<Exchange>; // 使用只能指针方便后续管理new出来的对象
std::string name; // 交换机名称
ExchangeType type; // 交换机类型
bool durable; // 交换机持久化标志
bool auto_delete; // 是否自动删除标志
std::unordered_map<std::string, std::string> args; // 其他参数
// 由于protobuf里的map和unordered_map不是同一个,所以直接用它里面的
Exchange() {}
Exchange(const std::string &ename,
ExchangeType etype,
bool edurable,
bool eauto_delete,
std::unordered_map<std::string, std::string> &eargs)
: name(ename), type(etype), durable(edurable), auto_delete(eauto_delete), args(eargs)
{
// 存储数据库的时候,args存储键值对会组织一个格式字符串进行存储:key=val&key=val....
}
// 序列化:将args中的内容进行序列化后,返回一个字符串
std::string getArgs()
{
std::string result;
for (auto start = args.begin(); start != args.end(); ++start)
{
result += start->first + "=" + start->second + "&";
}
return result;
}
// 反序列化:内部解析str_args字符串,将内容存储到成员中
void setArgs(const std::string &str_args)
{
// key=val&key=val&,分别去掉&和=,分离出每个key和value
std::vector<std::string> sub_args;
StrHelper::split(str_args, "&", sub_args); // 第一个是要分割的字符串,第二个是分隔符,分割完后存储到第三个数组里去
for (auto &str : sub_args)
{
size_t pos = str.find("="); // 遍历数组,再次进行分割,分出key和value
std::string key = str.substr(0, pos);
std::string val = str.substr(pos + 1); // 跳过等号
args[key] = val;
}
}
};
}
#endif
下面是数据化持久类,定义在命名空间里,交换机类后面:
cpp
//-----交换机持久化管理类-----
using ExchangeMap = std::unordered_map<std::string, Exchange::ptr>;
class ExchangeMapper
{
public: // 构造函数传入数据库文件名称来初始化
ExchangeMapper(const std::string &dbfile) : _sql_helper(dbfile)
{
// 连接数据库的时候如果数据库文件不存在他会自己创建,所以我们只需要创建文件所在的路径即可
std::string path = FileHelper::parentDirectory(dbfile); // 获取父级路径
FileHelper::createDirectory(path); // 创建路径
assert(_sql_helper.open()); // 打开文件,如果不存在会自动创建后再打开
createTable(); // 创建表
}
void createTable() // 创建数据库表
{
#define CREATE_TABLE "create table if not exists exchange_table(\
name varchar(32) primary key, \
type int, \
durable int, \
auto_delete int, \
args varchar(128));"
bool ret = _sql_helper.exec(CREATE_TABLE, nullptr, nullptr);
// 语句执行接口,后面两个是回调函数和参数,一般在查询的时候才传入
if (ret == false)
{
DLOG("创建交换机数据库表失败!");
abort(); // 直接异常退出程序
}
}
void removeTable() // 删除数据库表
{
#define DROP_TABLE "drop table if exists exchange_table;"
bool ret = _sql_helper.exec(DROP_TABLE, nullptr, nullptr);
if (ret == false)
{
DLOG("删除交换机数据库表失败!");
abort(); // 直接异常退出程序
}
}
bool insert(Exchange::ptr &exp) // 插入新的交换机
{
// C写法
// #define INSERT_SQL "insert into exchange_table values('%s', %d, %d, %d, '%s')"
// std::string args_str = exp->setArgs();
// char sql_str[4096] = {0};
// sprintf(sql_str, INSERT_SQL, exp->name, exp->type, exp->durable, exp->auto_delete, args_str.c_str());
// C++写法
std::stringstream ss;
ss << "insert into exchange_table values(";
ss << "'" << exp->name << "', ";
ss << exp->type << ", ";
ss << exp->durable << ", ";
ss << exp->auto_delete << ", ";
ss << "'" << exp->getArgs() << "');";
return _sql_helper.exec(ss.str(), nullptr, nullptr);
}
void remove(const std::string &name) // 给上名称,进行交换机删除
{
std::stringstream ss;
ss << "delete from exchange_table where name=";
ss << "'" << name << "';";
_sql_helper.exec(ss.str(), nullptr, nullptr);
}
ExchangeMap recovery() // 用于虚拟机重启后恢复历史数据
{
ExchangeMap result;
std::string sql = "select name, type, durable, auto_delete, args from exchange_table;";
_sql_helper.exec(sql, selectCallback, &result); // 调用回调函数读取数据库数据并存到result中后返回
return result;
}
private:
// 需要是静态,因为成员函数默认有个this指针不好搞
static int selectCallback(void *arg, int numcol, char **row, char **fields)
{
ExchangeMap *result = (ExchangeMap *)arg;
auto exp = std::make_shared<Exchange>();
exp->name = row[0];
exp->type = (my_mq::ExchangeType)std::stoi(row[1]);
// type是ExchangeType(其实也是整型),row[1]是字符串,需要转整型后再强转一次,后面同理
exp->durable = (bool)std::stoi(row[2]);
exp->auto_delete = (bool)std::stoi(row[3]);
if (row[4])
exp->setArgs(row[4]);
result->insert(std::make_pair(exp->name, exp)); // 插入交换机名称和交换机对象
return 0;
}
private:
SqliteHelper _sql_helper; // 因为主要针对数据库,所以只有数据库的操作句柄
};
下面是交换机数据内存管理类,位置同上
cpp
//-----交换机数据内存管理类-----
class ExchangeManager
{
public:
using ptr = std::shared_ptr<ExchangeManager>;
ExchangeManager(const std::string &dbfile) : _mapper(dbfile) // 需要传入数据库文件名称
{
_exchanges = _mapper.recovery(); // 每次中期构造对象时都恢复历史数据
}
// 声明交换机(新增交换机)
bool declareExchange(const std::string &name,
ExchangeType type, bool durable, bool auto_delete,
std::unordered_map<std::string, std::string> &args)
{
// 由于需要对公共数据库文件修改所以需要加锁
std::unique_lock<std::mutex> lock(_mutex);
auto it = _exchanges.find(name);
if (it != _exchanges.end()) // 如果交换机已经存在,不需要重复新增直接返回
return true;
// 构建交换机对象
auto exp = std::make_shared<Exchange>(name, type, durable, auto_delete, args);
if (durable == true) // 需要持久化存储
{
if (_mapper.insert(exp) == false)
return false;
}
_exchanges.insert(std::make_pair(name, exp));
return true;
}
void deleteExchange(const std::string &name) // 删除交换机
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _exchanges.find(name);
if (it == _exchanges.end()) // 如果不存在不需要删除
return;
if (it->second->durable == true) // 如果需要删除的数据是持久化过的,所以需要将数据库中的也进行删除
_mapper.remove(name);
_exchanges.erase(name);
}
Exchange::ptr selectExchange(const std::string &name) // 获取指定交换机对象
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _exchanges.find(name);
if (it == _exchanges.end()) // 没找到则返回一个空的智能指针
return Exchange::ptr();
return it->second;
}
// 判断交换机是否存在
bool exists(const std::string &name)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _exchanges.find(name);
if (it == _exchanges.end())
return false;
return true;
}
// 清理所有交换机数据
void clear()
{
std::unique_lock<std::mutex> lock(_mutex);
_mapper.removeTable(); // 直接把表删了
_exchanges.clear();
}
size_t size() // 获取交换机数量
{
std::unique_lock<std::mutex> lock(_mutex);
return _exchanges.size();
}
private:
std::mutex _mutex; // 需要加锁,下面的ExchangeMap可以通过交换机名称快速找到交换机对象,但是该过程存在线程安全问题
ExchangeMapper _mapper;
ExchangeMap _exchanges; // 交换机数据管理
};
测试:
cpp
#include "../mqserver/mq_exchange.hpp"
#include <gtest/gtest.h>
my_mq::ExchangeManager::ptr emp;
class ExchangeTest : public testing::Environment
{
public:
virtual void SetUp() override
{
emp = std::make_shared<my_mq::ExchangeManager>("./data/meta.db");
}
virtual void TearDown() override
{
// emp->clear();
// std::cout << "111111" << std::endl;
}
};
// TEST(exchange_test, insert_test) // 测试新增
// {
// std::unordered_map<std::string, std::string> map = {{"k1", "v1"}, {"k2", "v2"}};
// emp->declareExchange("exchange1", my_mq::ExchangeType::DIRECT, true, false, map);
// emp->declareExchange("exchange2", my_mq::ExchangeType::DIRECT, true, false, map);
// emp->declareExchange("exchange3", my_mq::ExchangeType::DIRECT, true, false, map);
// emp->declareExchange("exchange4", my_mq::ExchangeType::DIRECT, true, false, map);
// ASSERT_EQ(emp->size(), 4);
// }
TEST(exchange_test, select_test) // 测试查询
{
ASSERT_EQ(emp->exists("exchange1"), true);
ASSERT_EQ(emp->exists("exchange2"), true);
ASSERT_EQ(emp->exists("exchange3"), false);
ASSERT_EQ(emp->exists("exchange4"), true);
my_mq::Exchange::ptr exp = emp->selectExchange("exchange2");
ASSERT_NE(exp.get(), nullptr);
ASSERT_EQ(exp->name, "exchange2");
ASSERT_EQ(exp->durable, true);
ASSERT_EQ(exp->auto_delete, false);
ASSERT_EQ(exp->type, my_mq::ExchangeType::DIRECT);
ASSERT_EQ(exp->getArgs(), std::string("k1=v1&k2=v2&"));
}
TEST(exchange_test, remove_test) // 测试删除数据
{
emp->deleteExchange("exchange2");
my_mq::Exchange::ptr exp = emp->selectExchange("exchange2");
ASSERT_EQ(exp.get(), nullptr);
ASSERT_EQ(emp->exists("exchange2"), false);
}
int main(int argc, char *argv[])
{
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new ExchangeTest);
RUN_ALL_TESTS();
return 0;
}
2.3 队列数据管理
当前队列数据的管理,本质上是队列描述信息的管理,描述当前服务器上有哪些队列,需要实现下面三个东西:
①队列描述数据类
- 队列名称
- 是否持久化标志
②队列数据持久化类
- 创建/删除队列数据表
- 新增/移除队列数据
- 查询所有队列数据
③定义队列数据管理类
- 创建队列,并添加管理
- 删除对了
- 获取指定队列
- 获取所有队列
- 判断指定队列是否存在
- 获取队列数量
- 销毁所有队列
mqserver目录下的 mq_queue.hpp,队列的这三个类逻辑和交换机很像,可以考虑直接复制:
cpp
#ifndef __M_QUEUE_H__
#define __M_QUEUE_H__
#include "../mqcommon/log.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/message.pb.h"
#include <unordered_map>
#include <mutex>
#include <memory>
namespace my_mq
{
//-----队列描述类-----//
struct MsgQueue
{
// 队列数据管理和交换机数据管理非常类似,可以考虑复制粘贴
using ptr = std::shared_ptr<MsgQueue>;
std::string name;
bool durable; // 持久化
bool exclusive; // 是否独占
bool auto_delete; // 是否自动删除
std::unordered_map<std::string, std::string> args;
MsgQueue() {}
MsgQueue(const std::string &qname,
bool qdurable,
bool qexclusive,
bool qauto_delete,
std::unordered_map<std::string, std::string> &qargs)
: name(qname), durable(qdurable), exclusive(qexclusive),
auto_delete(qauto_delete), args(qargs) {}
// args也要持久化,所以也需要序列化和反序列化,可以直接把交换机的拿过来
std::string getArgs()
{
std::string result;
for (auto start = args.begin(); start != args.end(); ++start)
{
result += start->first + "=" + start->second + "&";
}
return result;
}
void setArgs(const std::string &str_args)
{
std::vector<std::string> sub_args;
StrHelper::split(str_args, "&", sub_args);
for (auto &str : sub_args)
{
size_t pos = str.find("=");
std::string key = str.substr(0, pos);
std::string val = str.substr(pos + 1);
args[key] = val;
}
}
};
//-----队列数据持久化类-----//
using QueueMap = std::unordered_map<std::string, MsgQueue::ptr>;
class MsgQueueMapper
{
public:
MsgQueueMapper(const std::string &dbfile) : _sql_helper(dbfile)
{
// 和交换机基本一样
std::string path = FileHelper::parentDirectory(dbfile);
FileHelper::createDirectory(path);
assert(_sql_helper.open());
createTable();
}
void createTable()
{
std::stringstream sql;
sql << "create table if not exists queue_table(";
sql << "name varchar(32) primary key, ";
sql << "durable int, ";
sql << "exclusive int, ";
sql << "auto_delete int, ";
sql << "args varchar(128));";
bool ret = _sql_helper.exec(sql.str(), nullptr, nullptr);
if (ret == false)
{
DLOG("创建队列数据库表失败!");
abort(); // 直接异常退出程序
}
}
void removeTable()
{
std::string sql = "drop table if exists queue_table;";
bool ret = _sql_helper.exec(sql, nullptr, nullptr);
if (ret == false)
{
DLOG("删除队列数据库表失败!");
abort(); // 直接异常退出程序
}
}
bool insert(MsgQueue::ptr &queue)
{
// insert into queue_table values('queue1', true, false, false, "k1=v1&k2=v2&");
std::stringstream sql;
sql << "insert into queue_table values(";
sql << "'" << queue->name << "', ";
sql << queue->durable << ", ";
sql << queue->exclusive << ", ";
sql << queue->auto_delete << ", ";
sql << "'" << queue->getArgs() << "');";
return _sql_helper.exec(sql.str(), nullptr, nullptr);
}
void remove(const std::string &name) // 删除单个队列,逻辑和交换机一致
{
// delete from queue_table where name='queue1';
std::stringstream ss;
ss << "delete from queue_table where name=";
ss << "'" << name << "';";
_sql_helper.exec(ss.str(), nullptr, nullptr);
}
QueueMap recovery() // 获取所有队列信息,用于恢复数据
{
QueueMap result;
std::string sql = "select name, durable, exclusive, auto_delete, args from queue_table;";
_sql_helper.exec(sql, selectCallback, &result);
return result;
}
private:
// 这个逻辑也和交换机一模一样,直接CV
static int selectCallback(void *arg, int numcol, char **row, char **fields)
{
QueueMap *result = (QueueMap *)arg;
MsgQueue::ptr mqp = std::make_shared<MsgQueue>();
mqp->name = row[0];
mqp->durable = (bool)std::stoi(row[1]);
mqp->exclusive = (bool)std::stoi(row[2]);
mqp->auto_delete = (bool)std::stoi(row[3]);
if (row[4])
mqp->setArgs(row[4]);
result->insert(std::make_pair(mqp->name, mqp));
return 0;
}
private:
SqliteHelper _sql_helper; // 持久化,所以只需要数据库操作句柄
};
//-----队列数据管理类-----//
class MsgQueueManager
{
public:
using ptr = std::shared_ptr<MsgQueueManager>;
MsgQueueManager(const std::string &dbfile) : _mapper(dbfile)
{
_msg_queues = _mapper.recovery();
}
// 声明队列(新增队列)
bool declareQueue(const std::string &qname,
bool qdurable, bool qexclusive, bool qauto_delete,
std::unordered_map<std::string, std::string> &qargs)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _msg_queues.find(qname);
if (it != _msg_queues.end())
return true;
MsgQueue::ptr mqp = std::make_shared<MsgQueue>();
mqp->name = qname;
mqp->durable = qdurable;
mqp->exclusive = qexclusive;
mqp->auto_delete = qauto_delete;
mqp->args = qargs;
if (qdurable == true)
{
if (_mapper.insert(mqp) == false)
return false;
}
_msg_queues.insert(std::make_pair(qname, mqp));
return true;
}
void deleteQueue(const std::string &name) // 删除队列
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _msg_queues.find(name);
if (it == _msg_queues.end())
return;
if (it->second->durable == true)
_mapper.remove(name);
_msg_queues.erase(name);
}
MsgQueue::ptr selectQueue(const std::string &name) // 获取指定队列对象
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _msg_queues.find(name);
if (it == _msg_queues.end())
return MsgQueue::ptr();
return it->second;
}
bool exists(const std::string &name) // 判断队列是否存在
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _msg_queues.find(name);
if (it == _msg_queues.end())
return false;
return true;
}
size_t size()
{
std::unique_lock<std::mutex> lock(_mutex);
return _msg_queues.size();
}
void clear()
{
_mapper.removeTable();
_msg_queues.clear();
}
// 获取所有
QueueMap allQueues()
{
std::unique_lock<std::mutex> lock(_mutex);
return _msg_queues;
}
private:
std::mutex _mutex;
MsgQueueMapper _mapper;
QueueMap _msg_queues;
};
}
#endif
测试:
cpp
#include "../mqserver/mq_queue.hpp"
#include <gtest/gtest.h>
my_mq::MsgQueueManager::ptr mqmp;
class QueueTest : public testing::Environment
{
public:
virtual void SetUp() override
{
mqmp = std::make_shared<my_mq::MsgQueueManager>("./data/meta.db");
}
virtual void TearDown() override
{
// mqmp->clear();
}
};
// TEST(queue_test, insert_test)
// {
// std::unordered_map<std::string, std::string> map = {{"k1", "v1"}};
// mqmp->declareQueue("queue1", true, false, false, map);
// mqmp->declareQueue("queue2", true, false, false, map);
// mqmp->declareQueue("queue3", true, false, false, map);
// mqmp->declareQueue("queue4", true, false, false, map);
// ASSERT_EQ(mqmp->size(), 4);
// }
TEST(queue_test, select_test)
{
ASSERT_EQ(mqmp->exists("queue1"), true);
ASSERT_EQ(mqmp->exists("queue2"), false);
ASSERT_EQ(mqmp->exists("queue3"), true);
ASSERT_EQ(mqmp->exists("queue4"), true);
my_mq::MsgQueue::ptr mqp = mqmp->selectQueue("queue1");
ASSERT_NE(mqp.get(), nullptr);
ASSERT_EQ(mqp->name, "queue1");
ASSERT_EQ(mqp->durable, true);
ASSERT_EQ(mqp->exclusive, false);
ASSERT_EQ(mqp->auto_delete, false);
ASSERT_EQ(mqp->getArgs(), std::string("k1=v1&"));
}
TEST(queue_test, remove_test)
{
mqmp->deleteQueue("queue3");
ASSERT_EQ(mqmp->exists("queue3"), false);
}
int main(int argc, char *argv[])
{
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new QueueTest);
RUN_ALL_TESTS();
return 0;
}
2.4 绑定数据管理
绑定信息本质上就一个交换机关联了哪些队列的描述,需要实现下面三个东西:
①定义绑定信息类
- 交换机名称
- 队列名称
- binding_key(匹配规则-决定了哪些数据能被交换机放⼊队列)
②定义绑定信息数据持久化类(数据持久化的sqlite3数据库中)
- 创建/删除绑定信息数据表
- 新增绑定信息数据
- 移除指定绑定信息数据
- 移除指定交换机相关绑定信息数据:移除交换机的时候会被调用
- 移除指定队列相关绑定信息数据:移除队列的时候会被调用
- 查询所有绑定信息数据:用于重启服务器时进行历史数据恢复
③定义绑定信息数据管理类
- 创建绑定信息,并添加管理(存在则OK,不存在则创建)
- 解除指定的绑定信息
- 删除指定队列的所有绑定信息
- 删除交换机相关的所有绑定信息
- 获取交换机相关的所有绑定信息:交换机收到消息后,需要分发给自己关联的队列
- 判断指定绑定信息是否存在:测试用
- 获取当前绑定信息数量:测试用
- 销毁所有绑定信息数据:测试用
mqserver目录下的mq_binding.hpp
绑定信息类
cpp
#ifndef __M_BINDING_H__
#define __M_BINDING_H__
#include "../mqcommon/log.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/message.pb.h"
#include <unordered_map>
#include <mutex>
#include <memory>
namespace my_mq
{
//------绑定信息类-----//
struct Binding
{
using ptr = std::shared_ptr<Binding>;
std::string exchange_name;
std::string msgqueue_name;
std::string binding_key;
Binding() {}
Binding(const std::string &ename, const std::string &qname, const std::string &key) : exchange_name(ename), msgqueue_name(qname), binding_key(key) {}
};
}
#endif
数据持久化类:
cpp
//-----绑定数据持久化类-----//
// 队列与绑定信息是一一对应的,但是一个交换机可能会有多个队列的绑定信息
// 先定义一个队列名,建立绑定信息的映射关系,这个是为了方便通过队列名查找绑定信息
using MsgQueueBindingMap = std::unordered_map<std::string, Binding::ptr>; // key是队列名,value是队列的绑定关系
// 然后定义一个交换机名称与队列绑定信息的映射关系,这个map中包含了所有的绑定信息,并且以交换机为单元进行了区分
using BindingMap = std::unordered_map<std::string, MsgQueueBindingMap>; // key是交换机名,value是与该交换机的绑定队列的信息,可能有多个绑定队列信息
// 如果采用下面两个结构,则删除交换机相关绑定信息的时候,还要删除对应队列中的映射,否则对象得不到释放
// std::unordered_map<std::string, Binding::ptr> MsgQueueBindingMap
// std::unordered_map<std::string, Binding::ptr> BindingMap
class BindingMapper
{
public:
BindingMapper(const std::string &dbfile) : _sql_helper(dbfile)
{
std::string path = FileHelper::parentDirectory(dbfile);
FileHelper::createDirectory(path);
_sql_helper.open();
createTable();
}
void createTable() // 创建表
{
// create table if not exists binding_table(exchange_name varchar(32), msgqueue_name, binding_key)
std::stringstream sql;
sql << "create table if not exists binding_table(";
sql << "exchange_name varchar(32), ";
sql << "msgqueue_name varchar(32), ";
sql << "binding_key varchar(128));";
bool ret = _sql_helper.exec(sql.str(), nullptr, nullptr);
if (ret == false)
{
DLOG("创建绑定数据库表失败!");
abort();
}
}
void removeTable()
{
std::string sql = "drop table if exists binding_table;";
bool ret = _sql_helper.exec(sql, nullptr, nullptr);
if (ret == false)
{
DLOG("删除队列数据库表失败!");
abort();
}
}
bool insert(Binding::ptr &binding)
{
// insert into binding_table values('exchange1', 'msgqueue1', 'news.music.#');
std::stringstream sql;
sql << "insert into binding_table values(";
sql << "'" << binding->exchange_name << "', ";
sql << "'" << binding->msgqueue_name << "', ";
sql << "'" << binding->binding_key << "');";
return _sql_helper.exec(sql.str(), nullptr, nullptr);
}
void remove(const std::string &ename, const std::string &qname) // 移除单个
{
// delete from binding_table where exchange_name='交换机名字' and msgqueue_name='队列名字';
std::stringstream sql;
sql << "delete from binding_table where ";
sql << "exchange_name='" << ename << "' and ";
sql << "msgqueue_name='" << qname << "';";
_sql_helper.exec(sql.str(), nullptr, nullptr);
}
void removeExchangeBindings(const std::string &ename) // 移除交换机绑定
{
// delete from binding_table where exchange_name='';
std::stringstream sql;
sql << "delete from binding_table where ";
sql << "exchange_name='" << ename << "';";
_sql_helper.exec(sql.str(), nullptr, nullptr);
}
void removeMsgQueueBindings(const std::string &qname) // 移除队列绑定
{
std::stringstream sql;
sql << "delete from binding_table where ";
sql << "msgqueue_name='" << qname << "';";
_sql_helper.exec(sql.str(), nullptr, nullptr);
}
BindingMap recovery() // 恢复数据,返回所有绑定信息
{
BindingMap result;
// select exchange_name, msgqueue_name, binding_key from binding_table;
std::string sql = "select exchange_name, msgqueue_name, binding_key from binding_table;";
_sql_helper.exec(sql, selectCallback, &result);
return result;
}
private:
static int selectCallback(void *arg, int numcol, char **row, char **fields)
{
// 要获取三个东西:交换机名称、队列名称、binding_key
BindingMap *result = (BindingMap *)arg; // 把查询到的结果存到这里面去,这个是交换机和队列(队列可能是多个)的键值对
Binding::ptr bp = std::make_shared<Binding>(row[0], row[1], row[2]);
// 当读取到这三个东西之后不能直接放到result里面,result是BindingMap类型,key是string,value是另一个键值对,而我们读取到的是三个
// 所以得先把 row[1] 和 row[2] 先搞成一个键值对,再把这个键值对和row[1]搞成另一个键值对才行
MsgQueueBindingMap &qmap = (*result)[bp->exchange_name];
// 解引用result,获得BindingMap,这个key是交换机名,value是队列和绑定信息的键值对
// bp->exchange_name是交换机名,然后[]获取该交换机名称对应的[队列名, bind]键值对,也就是前面的MsgQueueBindingMap
// 如果我们交换机没有对应的[队列名, bind]键值对,那么可能会出错
// 所以这里我们用引用(不存在则创建),如果后面没有[队列名, bind],那么可以先把[交换机名, [队列名, bing]] 这个给建立好,value为空,然后后面再插入即可
// 而且使用引用还有个好处,就是如果交换机相关的绑定信息已经存在,则不能直接创建队列映射进行添加,这样会覆盖历史数据
// 因此像这样得先获取交换机对应的[队列名, bind],直接往这个里边添加数据即可就不会覆盖了,而是如果有就啥也不做,没有就添加,不会覆盖
qmap.insert(std::make_pair(bp->msgqueue_name, bp));
return 0;
}
private:
SqliteHelper _sql_helper;
};
绑定信息数据管理类:
cpp
//-----绑定信息数据管理类-----//
class BindingManager
{
public:
using ptr = std::shared_ptr<BindingManager>;
BindingManager(const std::string &dbfile) : _mapper(dbfile)
{
_bindings = _mapper.recovery(); // 恢复历史数据
}
bool bind(const std::string &ename, const std::string &qname, const std::string &key, bool durable) // 绑定
{
// 加锁,先构建[队列名, bind],再构建[交换机名, [队列名, bind]]
std::unique_lock<std::mutex> lock(_mutex);
auto it = _bindings.find(ename); // 先查找交换机名
if (it != _bindings.end() && it->second.find(qname) != it->second.end()) // 找到了交换机,并且该交换机对应的这个绑定信息已经存在,则啥也不做了直接返回
return true;
Binding::ptr bp = std::make_shared<Binding>(ename, qname, key);
// 绑定信息([队列名, bind])是否需要持久化取决于什么? 交换机数据和队列数据都是持久化的。
if (durable)
{
if (_mapper.insert(bp) == false)
return false;
}
auto &qbmap = _bindings[ename]; // 获取交换机绑定映射关系的value,也就死[队列名, bind],并且使用引用,原因和前面那个一致
qbmap.insert(std::make_pair(qname, bp)); // 直接通过引用插入对应的[队列名, bind]
return true;
}
void unBind(const std::string &ename, const std::string &qname) // 解绑
{
std::unique_lock<std::mutex> lock(_mutex);
auto eit = _bindings.find(ename); // 查找交换机的value
if (eit == _bindings.end()) // 没找到,说明该[交换机名, [队列名, bind]] 不存在,无需删除直接返回
return;
auto qit = eit->second.find(qname); // 查找队列的value(bind)
if (qit == eit->second.end()) // 说明该[队列名, bing] 不存在,无需删除直接返回
return;
_mapper.remove(ename, qname); //
_bindings[ename].erase(qname); // 删除交换机的 [队列名, bind]
}
void removeExchangeBindings(const std::string &ename) // 移除一个交换机绑定信息
{
std::unique_lock<std::mutex> lock(_mutex);
_mapper.removeExchangeBindings(ename); // 把数据库里的干掉
_bindings.erase(ename); // 把内存里的干掉
}
void removeMsgQueueBindings(const std::string &qname) // 移除一个队列相关绑定信息
{
std::unique_lock<std::mutex> lock(_mutex);
_mapper.removeMsgQueueBindings(qname); // 把数据库里的干掉
for (auto start = _bindings.begin(); start != _bindings.end(); ++start) // 把内存里的干掉
{
// 遍历每个交换机的的每个绑定信息,删除队列名相同的 [队列名, bind] 绑定关系
start->second.erase(qname);
}
}
MsgQueueBindingMap getExchangeBindings(const std::string &ename) // 获取交换机绑定信息
{
std::unique_lock<std::mutex> lock(_mutex);
auto eit = _bindings.find(ename);
if (eit == _bindings.end())
return MsgQueueBindingMap(); // 没有就返回空对象
return eit->second;
}
Binding::ptr getBinding(const std::string &ename, const std::string &qname) // 获取指定绑定信息:测试
{
std::unique_lock<std::mutex> lock(_mutex);
auto eit = _bindings.find(ename);
if (eit == _bindings.end()) // 如果这个交换机本身没有绑定信息,则直接返回
return Binding::ptr(); // 返回空
auto qit = eit->second.find(qname); // 交换机有的话再去找队列
if (qit == eit->second.end()) // 如果队列没有直接返回空
return Binding::ptr();
return qit->second; // 找到了,返回
}
bool exists(const std::string &ename, const std::string &qname) // 判断是否存在:测试
{
// 判断存在的逻辑和上面一模一样
std::unique_lock<std::mutex> lock(_mutex);
auto eit = _bindings.find(ename);
if (eit == _bindings.end())
return false;
auto qit = eit->second.find(qname);
if (qit == eit->second.end())
return false;
return true;
}
size_t size() // 获取数量:测试
{
size_t total_size = 0;
std::unique_lock<std::mutex> lock(_mutex);
for (auto start = _bindings.begin(); start != _bindings.end(); ++start)
{
// 遍历每个交换机的绑定信息,依此统计每个绑定信息数量
total_size += start->second.size();
}
return total_size;
}
void clear() // 测试
{
std::unique_lock<std::mutex> lock(_mutex);
_mapper.removeTable();
_bindings.clear();
}
private:
std::mutex _mutex;
BindingMapper _mapper; // 持久化数据管理
BindingMap _bindings; // 交换机和[队列名, bind]映射关系
};
测试
cpp
#include "../mqserver/mq_binding.hpp"
#include <gtest/gtest.h>
my_mq::BindingManager::ptr bmp;
class QueueTest : public testing::Environment
{
public:
virtual void SetUp() override
{
bmp = std::make_shared<my_mq::BindingManager>("./data/meta.db");
}
virtual void TearDown() override
{
// bmp->clear();
}
};
// TEST(queue_test, insert_test)
// {
// bmp->bind("exchange1", "queue1", "news.music.#", true);
// bmp->bind("exchange1", "queue2", "news.sport.#", true);
// bmp->bind("exchange1", "queue3", "news.gossip.#", true);
// bmp->bind("exchange2", "queue1", "news.music.pop", true);
// bmp->bind("exchange2", "queue2", "news.sport.football", true);
// bmp->bind("exchange2", "queue3", "news.gossip.#", true);
// ASSERT_EQ(bmp->size(), 6);
// }
// TEST(queue_test, recovery_test)
// {
// ASSERT_EQ(bmp->exists("exchange1", "queue1"), true);
// ASSERT_EQ(bmp->exists("exchange1", "queue2"), true);
// ASSERT_EQ(bmp->exists("exchange1", "queue3"), true);
// ASSERT_EQ(bmp->exists("exchange2", "queue1"), true);
// ASSERT_EQ(bmp->exists("exchange2", "queue2"), true);
// ASSERT_EQ(bmp->exists("exchange2", "queue3"), true);
// my_mq::Binding::ptr bp = bmp->getBinding("exchange1", "queue1");
// ASSERT_NE(bp.get(), nullptr);
// ASSERT_EQ(bp->exchange_name, std::string("exchange1"));
// ASSERT_EQ(bp->msgqueue_name, std::string("queue1"));
// ASSERT_EQ(bp->binding_key, std::string("news.music.#"));
// }
// TEST(queue_test, select_exchange_test)
// {
// my_mq::MsgQueueBindingMap mqbm = bmp->getExchangeBindings("exchange1");
// ASSERT_EQ(mqbm.size(), 3);
// ASSERT_NE(mqbm.find("queue1"), mqbm.end());
// ASSERT_NE(mqbm.find("queue2"), mqbm.end());
// ASSERT_NE(mqbm.find("queue3"), mqbm.end());
// }
TEST(queue_test, remove_queue_test)
{
bmp->removeMsgQueueBindings("queue1");
ASSERT_EQ(bmp->exists("exchange1", "queue1"), false);
ASSERT_EQ(bmp->exists("exchange2", "queue1"), false);
}
TEST(queue_test, remove_exchange_test)
{
bmp->removeExchangeBindings("exchange1");
ASSERT_EQ(bmp->exists("exchange1", "queue1"), false);
ASSERT_EQ(bmp->exists("exchange1", "queue2"), false);
ASSERT_EQ(bmp->exists("exchange1", "queue3"), false);
}
TEST(queue_test, remove_single_test)
{
ASSERT_EQ(bmp->exists("exchange2", "queue2"), true);
bmp->unBind("exchange2", "queue2");
ASSERT_EQ(bmp->exists("exchange2", "queue2"), false);
ASSERT_EQ(bmp->exists("exchange2", "queue3"), true);
}
int main(int argc, char *argv[])
{
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new QueueTest);
RUN_ALL_TESTS();
return 0;
}
2.5 *消息管理
2.5.1 消息持久化管理
#消息的管理
①消息的要素:
网络传输的消息要素:
- 消息属性:消息id,routing_key,投递模式
- 消息的实际内容:有点类似于Tcp等网络协议的报头和有效载荷的关系
服务器上的消息管理所需的额外要素:最主要就是持久化管理:
- 消息有效标志:因为每一条消息都有可能要进行持久化存储,等到推送给客户端就会删除掉,然而每次删除一条数据就需要重写一次文件,效率太低,所以需要一个有效标志,如果设置了有效标志位,每次只需要将该标志位设置为无效即可,这个标志位也需要随着消息的持久化内容一起进行持久化
- 消息的实际存储位置(相对于文件起始位置的偏移量):当要删除某个消息时,需要重写覆盖这条消息在文件中的对应位置,但这时候就需要能够找到这条消息,引出下一个
- 消息的长度:当恢复历史消息,并且读取或修改消息内容时,需要解决粘包问题
- 垃圾回收机制:当无效的消息超过一定比例,一次性删除,这样能够大幅度提高效率
创建消息类型的proto文件,并使用protobuf命令生成相对应的代码文件(可以跳转2.1):
- 属性:消息ID,路由主题,持久化模式标志
- 消息内容有效标志(持久化需要)
- 持久化位置(内存中)
- 持久化消息长度(内存中)
②消息的持久化管理:以队列为单元进行消息的持久化管理:
- 当进行消息文件垃圾回收时,需要重新加载所有有效消息并生成新的数据文件,但是这样之后消息的存储位置会发生变化,这时候需要重新更新内存中的数据,但是这样就需要将所有队列数据进行加锁,频繁的锁冲突,效率太低 ,所以我们每个队列都搞一个独立的数据文件,每次只需要对操作的队列进行加锁即可
- 需要特别说明的是,我们消息的存储并没有使用数据库 ,因为消息长度通常不定,且有些消息可能会非常庞大,因此并不适合存储在数据库中,并且我们存储消息只是为了备份,几乎没用到查询功能,因此我们的处理方式是直接将消息存储在文件中进行管理,而内存中管理的消息只需要记录好自己在文件中的所在位置和长度即可。
- 总结:每个队列都要自己的数据文件,不使用数据库直接对文件进行操作
存储在文件中的数据必须要有格式的要求,如下图,这样可以解决粘包问题:
向外提供的操作:
消息文件的创建和删除
消息的新增与持久化 / 删除消息的持久化:只是将标志位置为无效
历史数据恢复 / 垃圾回收:放一块去了,因为两个操作的目的都是获取有效消息,垃圾回收只是多了一步就是把旧数据全删了
③关于垃圾回收原因:每次删除没有真正删除,只是更改标志位
规则:当文件中有效消息超过2000提奥,且其中有效消息比例低于50%时
策略一:先加载文件中所有有效消息,删除源文件,再生成新的数据文件,最后写入(有风险,比如删除源文件后生成新数据文件失败)
策略二:加载有效消息,写入一个临时文件,如果写入成功,再删除源文件,在将临时文件文件名进行更改即可
后续处理:返回所有的有效消息,每条消息中都记录当前新的存储位置,最后更新内存中数据的内容
④需要管理的数据队列名
根据队列名生成数据文件名称:队列名.mqd
临时文件名称:队列名.mqd.tmp
mqserver/message.hpp
cpp
#ifndef __M_MSG_H__
#define __M_MSG_H__
#include "../mqcommon/log.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/message.pb.h"
#include <unordered_map>
#include <mutex>
#include <memory>
#include <list>
namespace my_mq
{
#define DATAFILE_SUBFIX ".mqd"
#define TMPFILE_SUBFIX ".mqd.tmp"
using MessagePtr = std::shared_ptr<my_mq::Message>;
//-----消息持久化管理类-----//
class MessageMapper
{
public:
MessageMapper(std::string &basedir, const std::string &qname) : _qname(qname)
{
if (basedir.back() != '/') // 第一个参数是要存的目录,第二个是文件名
basedir.push_back('/');
_datafile = basedir + qname + DATAFILE_SUBFIX;
_tmpfile = basedir + qname + TMPFILE_SUBFIX;
if (FileHelper(basedir).exists() == false) // 如果目录不存在则先创建目录
assert(FileHelper::createDirectory(basedir));
createMsgFile();
}
bool createMsgFile() // 创建文件(单纯创建文件,不做其它操作)
{
if (FileHelper(_datafile).exists() == true) // 如果文件已经存在则直接返回
return true;
bool ret = FileHelper::createFile(_datafile);
if (ret == false)
{
DLOG("创建队列数据文件 %s 失败!", _datafile.c_str());
return false;
}
return true;
}
void removeMsgFile() // 删除文件
{
FileHelper::removeFile(_datafile);
FileHelper::removeFile(_tmpfile);
}
bool insert(MessagePtr &msg) // 新增消息
{
return insert(_datafile, msg);
}
bool remove(MessagePtr &msg) // 删除消息
{
// 主要步骤就是把内存中的该消息标志位改一下,然后将其原封不动的覆盖掉文件里对应的字段
// 1. 将msg中的有效标志位修改为字符0
msg->mutable_payload()->set_valid("0");
// 2. 对msg进行序列化
std::string body = msg->payload().SerializeAsString();
if (body.size() != msg->length())
{
DLOG("不能修改文件中的数据信息,因为新生成的数据与原数据长度不一致!");
return false;
}
// 3. 将序列化后的消息,写入到数据在文件中的指定位置(覆盖原有的数据)
FileHelper helper(_datafile);
bool ret = helper.write(body.c_str(), msg->offset(), body.size());
if (ret == false)
{
DLOG("向队列数据文件写入数据失败!");
return false;
}
return true;
}
std::list<MessagePtr> gc() // 垃圾回收,返回一个链表,里面都是有效消息
{
// 1,先加载文件中所有的有效数据
bool ret;
std::list<MessagePtr> result;
if (load(result) == false)
{
DLOG("加载有效数据失败!\n");
return result;
}
// DLOG("垃圾回收,得到有效消息数量:%d", result.size());
// 2. 将有效数据,进行序列化存储到临时文件中
FileHelper::createFile(_tmpfile); // 创建临时文件
for (auto &msg : result)
{
DLOG("向临时文件写入数据: %s", msg->payload().body().c_str());
ret = insert(_tmpfile, msg);
if (ret == false)
{
DLOG("向临时文件写入消息数据失败!!");
return result;
}
}
// DLOG("垃圾回收后,向临时文件写入数据完毕,临时文件大小: %ld", FileHelper(_tmpfile).size());
// 3. 删除源文件
ret = FileHelper::removeFile(_datafile);
if (ret == false)
{
DLOG("删除源文件失败!");
return result;
}
// 4. 修改临时文件名为源文件名称
ret = FileHelper(_tmpfile).rename(_datafile);
if (ret == false)
{
DLOG("修改临时文件名称失败!");
return result;
}
// 5. 返回新的有效数据
return result;
}
private:
bool load(std::list<MessagePtr> &result) // 加载出文件中所有的有效数据,将数据传给result
{
// 存储格式为:4字节|数据|4字节|数据......
FileHelper data_file_helper(_datafile);
size_t offset = 0, msg_size;
size_t fsize = data_file_helper.size(); // 获取文件大小
// DLOG("准备开始加载持久化数据,当前文件大小: %ld", data_file_helper.size());
bool ret;
while (offset < fsize)
{
ret = data_file_helper.read((char *)&msg_size, offset, sizeof(size_t)); // 先读取四字节长度,从文件的offset开始读,读size_t大小,然后存到msg_size里去
if (ret == false)
{
DLOG("读取消息长度失败!");
return false;
}
offset += sizeof(size_t); // 更新偏移量,跳过前面四字节
std::string msg_body(msg_size, '\0');
ret = data_file_helper.read(&msg_body[0], offset, msg_size); // 从文件的offset位置开始读,读msg_size大小,然后从msg_body[0]位置开始存
if (ret == false)
{
DLOG("读取消息数据失败!");
return false;
}
offset += msg_size; // 更新偏移量,跳过有效载荷部分
// 读取出来后开始将读出来的字符串进行反序列化后构建对象
MessagePtr msgp = std::make_shared<Message>();
msgp->mutable_payload()->ParseFromString(msg_body);
// DLOG("加载到有效数据:%s", msgp->payload().body().c_str());
if (msgp->payload().valid() == "0") // 跳过无效消息
{
DLOG("加载到无效消息:%s", msgp->payload().body().c_str());
continue;
}
// 有效消息则保存起来
result.push_back(msgp);
}
return true;
}
bool insert(const std::string &filename, MessagePtr &msg) // 新增数据都是添加在文件末尾的
{
// 1. 进行消息的序列化,获取到格式化后的消息
std::string body = msg->payload().SerializeAsString();
// 2. 获取文件长度
FileHelper helper(filename);
size_t fsize = helper.size();
size_t msg_size = body.size();
// 3,先写入4字节数据长度,再写入指定长度数据
bool ret = helper.write((char *)&msg_size, fsize, sizeof(size_t));
if (ret == false)
{
DLOG("向队列数据文件写入数据长度失败!");
return false;
}
// 3. 将数据写入文件的指定位置
ret = helper.write(body.c_str(), fsize + sizeof(size_t), body.size());
if (ret == false)
{
DLOG("向队列数据文件写入数据失败!");
return false;
}
// 4. 更新msg中的实际存储信息
msg->set_offset(fsize + sizeof(size_t));
msg->set_length(body.size());
return true;
}
private:
std::string _qname;
std::string _datafile;
std::string _tmpfile;
};
}
#endif
2.5.2 消息队列管理
#队列消息管理:
需要提供的接口:
- 构造对象时:创建。打开队列数据文件,恢复队列历史消息数据
- 新增 / 确认 / 删除消息
- 垃圾回收:如上
- 获取队首消息:用于推送给客户端
- 删除队列所有消息
- 获取待推送 / 待确认 / 持久化消息数量:用于测试
需要管理的数据:
- 持久化管理句柄
- 队列名称
- 待推送消息链表:以头插尾删实现队列功能,支持随机访问
- 持久化待确认消息hashmap:因为垃圾回收后需要更新消息数据(实际存储位置),而这就需要每次都去遍历一次链表找到对应更新的消息节点去更新,效率太低,所以需要一种能够快速查找的结构去找到要更新的消息
- 待确认消息的hashmap:一条消息被推送给客户端后,不立即删除,等待客户端发回确认后再进行删除
- 持久化文件中有效消息数量
- 持久化文件中所有消息数量:可以计算出比例,用来进行垃圾回收
cpp
//-----消息队列管理类-----//
// 每个队列都有一个消息数据的管理结构(减少锁的冲突提高效率),然后向外提供一个总体的消息管理类
class QueueMessage
{
public:
using ptr = std::shared_ptr<QueueMessage>;
// 构造传入消息文件存放路径和文件名称,用于构造_mapper
QueueMessage(std::string &basedir, const std::string &qname)
: _mapper(basedir, qname), _qname(qname), _valid_count(0), _total_count(0)
{
}
bool recovery() // 恢复历史消息
{
std::unique_lock<std::mutex> lock(_mutex);
_msgs = _mapper.gc(); // 获取有效消息链表
for (auto &msg : _msgs) // 逐个建立映射关系
_durable_msgs.insert(std::make_pair(msg->payload().properties().id(), msg));
_valid_count = _total_count = _msgs.size(); // 更新数量
return true;
}
bool insert(const BasicProperties *bp, const std::string &body, bool queue_is_durable) // 传入消息属性,内容,以及是否自动删除
{
// 1. 构造消息对象
MessagePtr msg = std::make_shared<Message>();
msg->mutable_payload()->set_body(body);
if (bp != nullptr) // 客户端设置了属性,按照该属性填充
{
DeliveryMode mode = queue_is_durable ? bp->delivery_mode() : DeliveryMode::UNDURABLE;
msg->mutable_payload()->mutable_properties()->set_id(bp->id());
msg->mutable_payload()->mutable_properties()->set_delivery_mode(mode);
msg->mutable_payload()->mutable_properties()->set_routing_key(bp->routing_key());
}
else // 客户端单纯的发消息,没有设置属性,需要自动生成uuid
{
DeliveryMode mode = queue_is_durable ? DeliveryMode::DURABLE : DeliveryMode::UNDURABLE;
msg->mutable_payload()->mutable_properties()->set_id(UUIDHelper::uuid());
msg->mutable_payload()->mutable_properties()->set_delivery_mode(mode);
msg->mutable_payload()->mutable_properties()->set_routing_key("");
}
std::unique_lock<std::mutex> lock(_mutex);
// 2. 判断消息是否需要持久化
if (msg->payload().properties().delivery_mode() == DeliveryMode::DURABLE)
{
msg->mutable_payload()->set_valid("1"); // 表示在持久化存储中表示数据有效
// 3. 进行持久化存储
if (_mapper.insert(msg) == false)
{
DLOG("持久化存储消息:%s 失败了!", body.c_str());
return false;
}
_valid_count += 1; // 持久化信息中的数据量+1
_total_count += 1;
_durable_msgs.insert(std::make_pair(msg->payload().properties().id(), msg)); // 持久化哈希也添加一个
}
// 4. 添加到内存的list里去
_msgs.push_back(msg);
return true;
}
MessagePtr front() // 获取队首消息,从_msgs 中取出数据,
{
std::unique_lock<std::mutex> lock(_mutex);
if (_msgs.size() == 0) // 队列没有消息返回空
return MessagePtr();
// 由于是取出,不是复制,取出后需要 pop_front
MessagePtr msg = _msgs.front();
_msgs.pop_front();
// 然后添加进待确认的hash表中,收到消息确认后进行删除
_waitack_msgs.insert(std::make_pair(msg->payload().properties().id(), msg));
return msg;
}
bool remove(const std::string &msg_id) // 删除消息,每次删除消息后,判断是否需要垃圾回收
{
std::unique_lock<std::mutex> lock(_mutex);
// 1. 先从待确认队列中查找消息
auto it = _waitack_msgs.find(msg_id);
if (it == _waitack_msgs.end())
{
DLOG("没有找到要删除的消息:%s!", msg_id.c_str());
return true;
}
// 2. 根据消息的持久化模式,决定是否删除持久化信息
if (it->second->payload().properties().delivery_mode() == DeliveryMode::DURABLE)
{
// 3. 删除持久化信息
_mapper.remove(it->second);
_durable_msgs.erase(msg_id);
_valid_count -= 1;
gc(); // 内部判断是否需要垃圾回收,需要的话则回收一下
}
// 4. 删除内存中的信息
_waitack_msgs.erase(msg_id);
DLOG("确认消息后,删除消息的管理成功:%s", it->second->payload().body().c_str());
return true;
}
size_t getable_count() // 可获取消息数量(可推送消息数量)
{
std::unique_lock<std::mutex> lock(_mutex);
return _msgs.size();
}
size_t total_count() // 总体消息数量
{
std::unique_lock<std::mutex> lock(_mutex);
return _total_count;
}
size_t durable_count() // 持久化消息数量
{
std::unique_lock<std::mutex> lock(_mutex);
return _durable_msgs.size();
}
size_t waitack_count() // 等待确认消息数量
{
std::unique_lock<std::mutex> lock(_mutex);
return _waitack_msgs.size();
}
void clear()
{
std::unique_lock<std::mutex> lock(_mutex);
_mapper.removeMsgFile(); // 直接删除文件
_msgs.clear(); // 然后清空内存里的所有数据
_durable_msgs.clear();
_waitack_msgs.clear();
_valid_count = 0;
_total_count = 0;
}
private:
bool GCCheck()
{
// 持久化的消息总量大于2000, 且其中有效比例低于50%则需要持久化
if (_total_count > 2000 && (_valid_count * 10 / _total_count < 5)) // 乘10是为了去掉浮点数运算
return true;
return false;
}
void gc()
{
// 1. 进行垃圾回收,获取到垃圾回收后,有效的消息信息链表
if (GCCheck() == false)
return;
std::list<MessagePtr> msgs = _mapper.gc(); // 垃圾回收后返回有效消息列表
for (auto &msg : msgs) // 遍历有效消息列表
{
auto it = _durable_msgs.find(msg->payload().properties().id());
if (it == _durable_msgs.end()) // 垃圾回收后返回过来的有效消息中,在有效消息链表里有,但是在内存里却没有
{
DLOG("垃圾回收后,有一条持久化消息,在内存中没有进行管理!");
_msgs.push_back(msg); // 重新添加到推送链表的末尾
_durable_msgs.insert(std::make_pair(msg->payload().properties().id(), msg)); // 然后再次添加映射
continue;
// 几乎不会发生这种情况,但是万一发生了也能有应对措施
}
// 2. 更新每一条消息的实际存储位置
it->second->set_offset(msg->offset());
it->second->set_length(msg->length());
}
// 3. 更新当前的有效消息数量和总体持久化消息数量
_valid_count = _total_count = msgs.size();
}
private:
std::mutex _mutex;
std::string _qname; // 队列名
size_t _valid_count; // 有效消息数量
size_t _total_count; // 总体消息数量
MessageMapper _mapper; // 持久化句柄
std::list<MessagePtr> _msgs; // 待推送消息
std::unordered_map<std::string, MessagePtr> _durable_msgs; // 持久化消息hash
std::unordered_map<std::string, MessagePtr> _waitack_msgs; // 待确认消息hash
};
2.5.3 消息总体管理类
上面队列是管理消息的,这个类是管理每个队列的
①管理的成员:队列名称 与 每个队列的消息管理句柄 的映射哈希表
②对外提供的接口
- 初始化队列的消息管理句柄:创建队列时调用
- 销毁队列的消息管理句柄:删除队列时调用
队列的各项消息操作
- 向队列新增消息
- 获取队列队首消息
- 对队列进行消息确认
- 获取队列消息数量:可获取消息数量、持久化消息数量、待确认消息数量、总体持久化的新消息数量
- 恢复队列历史消息
cpp
//-----消息总体管理类(向外提供接口)-----//
class MessageManager
{
public:
using ptr = std::shared_ptr<MessageManager>;
// 传入队列文件存储目录
MessageManager(const std::string &basedir) : _basedir(basedir) {}
// 在构造对象或者清理对象时,以构造对象为例,可能正在同时进行恢复历史消息的操作,因为这个操作时间可能比较长
// 如果把构造对象过程和恢复历史数据过程都用一个锁来保护,那么会造成锁冲突或效率问题,而每个队列对象里都有各自的锁,无需再次加锁
void initQueueMessage(const std::string &qname) // 初始队列消息
{
// 初始化的解决操作是,判断,构建对象等操作用一个锁保护,但是恢复历史消息单独移出去
// 就是先在内存里构建对象,再恢复历史数据
QueueMessage::ptr qmp;
{
// 恢复历史消息的过程比较慢,所以单独把恢复历史消息的操作移动出去,等构造玩再恢复历史数据
std::unique_lock<std::mutex> lock(_mutex);
auto it = _queue_msgs.find(qname);
if (it != _queue_msgs.end()) // 该队列操作句柄已存在无需重复初始化
return;
qmp = std::make_shared<QueueMessage>(_basedir, qname); // 构建队列对象
_queue_msgs.insert(std::make_pair(qname, qmp)); // 新增[队列名, 管理句柄]的映射关系
}
qmp->recovery();
}
void destroyQueueMessage(const std::string &qname) // 销毁队列消息
{
// 先把内存数据删除完后再去删外部数据,解决上面的问题
QueueMessage::ptr qmp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _queue_msgs.find(qname);
if (it == _queue_msgs.end()) // 没找到,无需删除
return;
qmp = it->second;
_queue_msgs.erase(it);
}
qmp->clear();
}
// 向指定队列新增消息
bool insert(const std::string &qname, BasicProperties *bp, const std::string &body, bool queue_is_durable)
{
// 后面几个接口的操作和上面一样,内存和持久化的操作都分开搞,就可以减少锁冲突带来的效率问题
QueueMessage::ptr qmp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _queue_msgs.find(qname);
if (it == _queue_msgs.end())
{
DLOG("向队列%s新增消息失败:没有找到消息管理句柄!", qname.c_str());
return false;
}
qmp = it->second;
}
return qmp->insert(bp, body, queue_is_durable); // 向指定队列新增消息
}
MessagePtr front(const std::string &qname) // 获取队首消息
{
QueueMessage::ptr qmp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _queue_msgs.find(qname);
if (it == _queue_msgs.end())
{
DLOG("获取队列%s队首消息失败:没有找到消息管理句柄!", qname.c_str());
return MessagePtr();
}
qmp = it->second;
}
return qmp->front();
}
void ack(const std::string &qname, const std::string &msg_id) // 对哪个队列的哪个消息进行确认
{
QueueMessage::ptr qmp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _queue_msgs.find(qname);
if (it == _queue_msgs.end())
{
DLOG("确认队列%s消息%s失败:没有找到消息管理句柄!", qname.c_str(), msg_id.c_str());
return;
}
qmp = it->second;
}
qmp->remove(msg_id);
return;
}
size_t getable_count(const std::string &qname)
{
QueueMessage::ptr qmp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _queue_msgs.find(qname);
if (it == _queue_msgs.end())
{
DLOG("获取队列%s待推送消息数量失败:没有找到消息管理句柄!", qname.c_str());
return 0;
}
qmp = it->second;
}
return qmp->getable_count();
}
size_t total_count(const std::string &qname)
{
QueueMessage::ptr qmp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _queue_msgs.find(qname);
if (it == _queue_msgs.end())
{
DLOG("获取队列%s总持久化消息数量失败:没有找到消息管理句柄!", qname.c_str());
return 0;
}
qmp = it->second;
}
return qmp->total_count();
}
size_t durable_count(const std::string &qname)
{
QueueMessage::ptr qmp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _queue_msgs.find(qname);
if (it == _queue_msgs.end())
{
DLOG("获取队列%s有效持久化消息数量失败:没有找到消息管理句柄!", qname.c_str());
return 0;
}
qmp = it->second;
}
return qmp->durable_count();
}
size_t waitack_count(const std::string &qname)
{
QueueMessage::ptr qmp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _queue_msgs.find(qname);
if (it == _queue_msgs.end())
{
DLOG("获取队列%s待确认消息数量失败:没有找到消息管理句柄!", qname.c_str());
return 0;
}
qmp = it->second;
}
return qmp->waitack_count();
}
void clear()
{
std::unique_lock<std::mutex> lock(_mutex);
for (auto &qmsg : _queue_msgs)
{
qmsg.second->clear();
}
}
private:
std::mutex _mutex;
std::string _basedir;
std::unordered_map<std::string, QueueMessage::ptr> _queue_msgs;
};
测试:
mqtest/mq_msgtest.cc(编译时需要加上 -lgtest -lsqlite3 -lprotobuf )
cpp
#include "../mqserver/mq_message.hpp"
#include <gtest/gtest.h>
my_mq::MessageManager::ptr mmp;
class MessageTest : public testing::Environment
{
public:
virtual void SetUp() override
{
mmp = std::make_shared<my_mq::MessageManager>("./data/message/");
mmp->initQueueMessage("queue1");
}
virtual void TearDown() override
{
mmp->clear();
}
};
// 新增消息测试:新增消息,然后观察可获取消息数量,以及持久化消息数量
TEST(message_test, insert_test)
{
my_mq::BasicProperties properties;
properties.set_id(my_mq::UUIDHelper::uuid());
properties.set_delivery_mode(my_mq::DeliveryMode::DURABLE);
properties.set_routing_key("news.music.pop");
mmp->insert("queue1", &properties, "Hello World-1", true);
mmp->insert("queue1", nullptr, "Hello World-2", true);
mmp->insert("queue1", nullptr, "Hello World-3", true);
mmp->insert("queue1", nullptr, "Hello World-4", true);
mmp->insert("queue1", nullptr, "Hello World-5", false);
ASSERT_EQ(mmp->getable_count("queue1"), 5);
ASSERT_EQ(mmp->total_count("queue1"), 4);
ASSERT_EQ(mmp->durable_count("queue1"), 4);
ASSERT_EQ(mmp->waitack_count("queue1"), 0);
}
// 获取消息测试:获取一条消息,然后在不进行确认的情况下,查看可获取消息数量,待确认消息数量,以及测试消息获取的顺序
TEST(message_test, select_test)
{
ASSERT_EQ(mmp->getable_count("queue1"), 5);
my_mq::MessagePtr msg1 = mmp->front("queue1");
ASSERT_NE(msg1.get(), nullptr);
ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));
ASSERT_EQ(mmp->getable_count("queue1"), 4);
ASSERT_EQ(mmp->waitack_count("queue1"), 1);
my_mq::MessagePtr msg2 = mmp->front("queue1");
ASSERT_NE(msg2.get(), nullptr);
ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));
ASSERT_EQ(mmp->getable_count("queue1"), 3);
ASSERT_EQ(mmp->waitack_count("queue1"), 2);
my_mq::MessagePtr msg3 = mmp->front("queue1");
ASSERT_NE(msg3.get(), nullptr);
ASSERT_EQ(msg3->payload().body(), std::string("Hello World-3"));
ASSERT_EQ(mmp->getable_count("queue1"), 2);
ASSERT_EQ(mmp->waitack_count("queue1"), 3);
my_mq::MessagePtr msg4 = mmp->front("queue1");
ASSERT_NE(msg4.get(), nullptr);
ASSERT_EQ(msg4->payload().body(), std::string("Hello World-4"));
ASSERT_EQ(mmp->getable_count("queue1"), 1);
ASSERT_EQ(mmp->waitack_count("queue1"), 4);
my_mq::MessagePtr msg5 = mmp->front("queue1");
ASSERT_NE(msg5.get(), nullptr);
ASSERT_EQ(msg5->payload().body(), std::string("Hello World-5"));
ASSERT_EQ(mmp->getable_count("queue1"), 0);
ASSERT_EQ(mmp->waitack_count("queue1"), 5);
}
// 删除消息测试:确认一条消息,查看持久化消息数量,待确认消息数量
TEST(message_test, delete_test)
{
ASSERT_EQ(mmp->getable_count("queue1"), 5);
my_mq::MessagePtr msg1 = mmp->front("queue1");
ASSERT_NE(msg1.get(), nullptr);
ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));
ASSERT_EQ(mmp->getable_count("queue1"), 4);
ASSERT_EQ(mmp->waitack_count("queue1"), 1);
mmp->ack("queue1", msg1->payload().properties().id());
ASSERT_EQ(mmp->waitack_count("queue1"), 0);
ASSERT_EQ(mmp->durable_count("queue1"), 3);
ASSERT_EQ(mmp->total_count("queue1"), 4);
}
// 销毁测试
TEST(message_test, clear)
{
mmp->destroyQueueMessage("queue1");
}
int main(int argc, char *argv[])
{
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new MessageTest);
// testing::AddGlobalTestEnvironment(new MessageTest2);
RUN_ALL_TESTS();
return 0;
}
2.6 虚拟机管理设计
是对上面所有模块的整合,基于数据之间的关联关系进行联合操作
①虚拟机包含下面四个:
- 交换机数据管理模块句柄
- 队列数据管理模块句柄
- 绑定数据管理模块句柄
- 消息数据管理模块句柄
②需要提供下面操作(10个):
- 声明交换机:不存在再创建
- 删除交换机:删除交换机的同时删除关联绑定信息)
- 声明队列:不存在再创建,并且创建队列关联消息管理对象
- 删除队列:删除队列的同时删除关联绑定信息,删除关联消息管理对象及队列所有消息
- 交换机--队列的绑定
- 交换机-队列的解绑
- 获取交换机相关的所有绑定信息
- 新增消息
- 获取指定队列队首消息
- 消息确认删除
mqserver/mq_virtualhost.hpp
cpp
#ifndef __M_HOST_H__
#define __M_HOST_H__
#include "mq_exchange.hpp"
#include "mq_queue.hpp"
#include "mq_binding.hpp"
#include "mq_message.hpp"
namespace my_mq
{
class VirtualHost
{
public:
using ptr = std::shared_ptr<VirtualHost>;
VirtualHost(const std::string &hname, const std::string &basedir, const std::string &dbfile)
: _host_name(hname),
_emp(std::make_shared<ExchangeManager>(dbfile)),
_mqmp(std::make_shared<MsgQueueManager>(dbfile)),
_bmp(std::make_shared<BindingManager>(dbfile)),
_mmp(std::make_shared<MessageManager>(basedir))
{
QueueMap qm = _mqmp->allQueues(); // 获取所有的队列信息
for (auto &e : qm) // 恢复所有队列的信息
_mmp->initQueueMessage(e.first);
}
//-----交换机相关操作-----//
bool declareExchange(const std::string &name,
ExchangeType type, bool durable, bool auto_delete,
std::unordered_map<std::string, std::string> &args) // 声明交换机
{
// 提供需要的信息直接调用即可:名字,交换机类型,持久化,自动删除,其它数据
return _emp->declareExchange(name, type, durable, auto_delete, args);
}
void deleteExchange(const std::string &name) // 删除交换机
{
// 删除交换机的时候,还需要将相关的绑定信息也干掉
_bmp->removeExchangeBindings(name);
return _emp->deleteExchange(name);
}
bool existsExchange(const std::string &name) { return _emp->exists(name); } // 判断交换机是否存在(测试用)
Exchange::ptr selectExchange(const std::string &ename) { return _emp->selectExchange(ename); } // 查询交换机
//-----队列相关操作-----//
bool declareQueue(const std::string &qname,
bool qdurable,
bool qexclusive,
bool qauto_delete,
std::unordered_map<std::string, std::string> &qargs) // 声明队列
{
_mmp->initQueueMessage(qname); // 初始化队列的消息句柄(消息的存储管理)
return _mqmp->declareQueue(qname, qdurable, qexclusive, qauto_delete, qargs); // 创建队列
}
void deleteQueue(const std::string &name) // 删除队列
{
// 删除的时候队列相关的数据有两个:队列的消息,队列的绑定信息
_mmp->destroyQueueMessage(name); // 删除队列里的消息
_bmp->removeMsgQueueBindings(name); // 删除绑定信息
return _mqmp->deleteQueue(name); // 最后删除队列
}
bool existsQueue(const std::string &name) { return _mqmp->exists(name); } // 判断队列是否存在(测试用)
QueueMap allQueues() { return _mqmp->allQueues(); } // 获取所有队列
bool bind(const std::string &ename, const std::string &qname, const std::string &key) // 绑定
{
// 绑定的前提是交换机和队列都必须存在
Exchange::ptr ep = _emp->selectExchange(ename);
if (ep.get() == nullptr)
{
DLOG("队列绑定失败,交换机%s不存在!", ename.c_str());
return false;
}
MsgQueue::ptr mqp = _mqmp->selectQueue(qname);
if (mqp.get() == nullptr)
{
DLOG("队列绑定失败,队列%s不存在!", qname.c_str());
return false;
}
return _bmp->bind(ename, qname, key, ep->durable && mqp->durable); // 只有交换机和队列都是持久化的时候,绑定才能持久化
}
void unBind(const std::string &ename, const std::string &qname) { return _bmp->unBind(ename, qname); } // 解绑
MsgQueueBindingMap exchangeBindings(const std::string &ename) { return _bmp->getExchangeBindings(ename); } // 获取指定交换机所有绑定信息
bool existsBinding(const std::string &ename, const std::string &qname) { return _bmp->exists(ename, qname); } // 绑定是否存在(测试用)
//-----消息相关操作-----//
bool basicPublish(const std::string &qname, BasicProperties *bp, const std::string &body) // 发布消息
{
MsgQueue::ptr mqp = _mqmp->selectQueue(qname);
if (mqp.get() == nullptr)
{
DLOG("发布消息失败,队列%s不存在!", qname.c_str());
return false;
}
return _mmp->insert(qname, bp, body, mqp->durable); // 消息的持久化也取决于队列是否持久化
}
MessagePtr basicConsume(const std::string &qname) { return _mmp->front(qname); } // 消费消息,从队首获取一条消息进行返回
void basicAck(const std::string &qname, const std::string &msgid) { return _mmp->ack(qname, msgid); } // 确认消息
void clear()
{
_emp->clear();
_mqmp->clear();
_bmp->clear();
_mmp->clear();
}
private:
std::string _host_name;
ExchangeManager::ptr _emp; // 交换机管理句柄
MsgQueueManager::ptr _mqmp; // 队列管理句柄
BindingManager::ptr _bmp; // 绑定管理句柄
MessageManager::ptr _mmp; // 消息管理句柄
};
}
#endif
测试:
cpp
#include <gtest/gtest.h>
#include "../mqserver/mq_virtualhost.hpp"
class HostTest : public testing::Test
{
public:
void SetUp() override // 初始化,先插入一些数据
{
std::unordered_map<std::string, std::string> empty_map = std::unordered_map<std::string, std::string>(); // 空对象
_host = std::make_shared<my_mq::VirtualHost>("host1", "./data/host1/message/", "./data/host1/host1.db");
_host->declareExchange("exchange1", my_mq::ExchangeType::DIRECT, true, false, empty_map);
_host->declareExchange("exchange2", my_mq::ExchangeType::DIRECT, true, false, empty_map);
_host->declareExchange("exchange3", my_mq::ExchangeType::DIRECT, true, false, empty_map);
_host->declareQueue("queue1", true, false, false, empty_map);
_host->declareQueue("queue2", true, false, false, empty_map);
_host->declareQueue("queue3", true, false, false, empty_map);
_host->bind("exchange1", "queue1", "news.music.#");
_host->bind("exchange1", "queue2", "news.music.#");
_host->bind("exchange1", "queue3", "news.music.#");
_host->bind("exchange2", "queue1", "news.music.#");
_host->bind("exchange2", "queue2", "news.music.#");
_host->bind("exchange2", "queue3", "news.music.#");
_host->bind("exchange3", "queue1", "news.music.#");
_host->bind("exchange3", "queue2", "news.music.#");
_host->bind("exchange3", "queue3", "news.music.#");
_host->basicPublish("queue1", nullptr, "Hello World-1");
_host->basicPublish("queue1", nullptr, "Hello World-2");
_host->basicPublish("queue1", nullptr, "Hello World-3");
_host->basicPublish("queue2", nullptr, "Hello World-1");
_host->basicPublish("queue2", nullptr, "Hello World-2");
_host->basicPublish("queue2", nullptr, "Hello World-3");
_host->basicPublish("queue3", nullptr, "Hello World-1");
_host->basicPublish("queue3", nullptr, "Hello World-2");
_host->basicPublish("queue3", nullptr, "Hello World-3");
}
void TearDown() override
{
_host->clear();
}
public:
my_mq::VirtualHost::ptr _host;
};
TEST_F(HostTest, init_test)
{
ASSERT_EQ(_host->existsExchange("exchange1"), true);
ASSERT_EQ(_host->existsExchange("exchange2"), true);
ASSERT_EQ(_host->existsExchange("exchange3"), true);
ASSERT_EQ(_host->existsQueue("queue1"), true);
ASSERT_EQ(_host->existsQueue("queue2"), true);
ASSERT_EQ(_host->existsQueue("queue3"), true);
ASSERT_EQ(_host->existsBinding("exchange1", "queue1"), true);
ASSERT_EQ(_host->existsBinding("exchange1", "queue2"), true);
ASSERT_EQ(_host->existsBinding("exchange1", "queue3"), true);
ASSERT_EQ(_host->existsBinding("exchange2", "queue1"), true);
ASSERT_EQ(_host->existsBinding("exchange2", "queue2"), true);
ASSERT_EQ(_host->existsBinding("exchange2", "queue3"), true);
ASSERT_EQ(_host->existsBinding("exchange3", "queue1"), true);
ASSERT_EQ(_host->existsBinding("exchange3", "queue2"), true);
ASSERT_EQ(_host->existsBinding("exchange3", "queue3"), true);
my_mq::MessagePtr msg1 = _host->basicConsume("queue1");
ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));
my_mq::MessagePtr msg2 = _host->basicConsume("queue1");
ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));
my_mq::MessagePtr msg3 = _host->basicConsume("queue1");
ASSERT_EQ(msg3->payload().body(), std::string("Hello World-3"));
my_mq::MessagePtr msg4 = _host->basicConsume("queue1");
ASSERT_EQ(msg4.get(), nullptr);
}
TEST_F(HostTest, remove_exchange)
{
_host->deleteExchange("exchange1");
ASSERT_EQ(_host->existsBinding("exchange1", "queue1"), false);
ASSERT_EQ(_host->existsBinding("exchange1", "queue2"), false);
ASSERT_EQ(_host->existsBinding("exchange1", "queue3"), false);
}
TEST_F(HostTest, remove_queue)
{
_host->deleteQueue("queue1");
ASSERT_EQ(_host->existsBinding("exchange1", "queue1"), false);
ASSERT_EQ(_host->existsBinding("exchange2", "queue1"), false);
ASSERT_EQ(_host->existsBinding("exchange3", "queue1"), false);
my_mq::MessagePtr msg1 = _host->basicConsume("queue1");
ASSERT_EQ(msg1.get(), nullptr);
}
TEST_F(HostTest, ack_message)
{
my_mq::MessagePtr msg1 = _host->basicConsume("queue1");
ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));
_host->basicAck(std::string("queue1"), msg1->payload().properties().id());
my_mq::MessagePtr msg2 = _host->basicConsume("queue1");
ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));
_host->basicAck(std::string("queue1"), msg2->payload().properties().id());
my_mq::MessagePtr msg3 = _host->basicConsume("queue1");
ASSERT_EQ(msg3->payload().body(), std::string("Hello World-3"));
_host->basicAck(std::string("queue1"), msg3->payload().properties().id());
}
int main(int argc, char *argv[])
{
testing::InitGoogleTest(&argc, argv);
RUN_ALL_TESTS();
return 0;
}
bash
g++ -std=c++11 mq_hosttest.cpp ../mqcommon/message.pb.cc -o test_host -lgtest -lsqlite3 -lprotobuf
三,路由交换模块
3.1 说明
客户端将消息发布到指定的交换机,交换机这时候要考虑这条数据该放入到哪些与自己绑定的队列 中,而这个考量是通过交换机类型以及匹配规则来决定的:
- 广播交换:直接将消息交给所有绑定的队列
- 直接交换:队列绑定信息中的binding_key 与消息中的 routing_key 完全⼀致则匹配成功,否则失败
- 主题交换:只有匹配队列主题的消息才会被放入队列中,关于 binding_key 和 routing_key 的规则如下
①路由交换模块
- 功能:判断消息中的 routing_key 与队列的 binding_key 是否合法,并且能否匹配成功
- 取决要素:交换机类型,routing_key 与 binding_key 的匹配
②这个模块主要是队对传输的数据进行处理,因此不需要存储什么数据
③关于匹配
- 广播:不论如何都匹配成功
- 直接:相等则成功
- 主题:按匹配模式,完成则成功
3.2 算法设计
主要针对主题交换判断能否匹配成功的一个算法逻辑,使用动态规划思路
定义⼀个二维数组来标记每次匹配的结果,通过最终数组末尾位置的结果来查看是否整体匹配成功。 使用 routing_key 中的每个单词,与 binding_key 中的单词进行逐个匹配,根据匹配结果来标记数组内容,最终以数组中的末尾标记来确定是否匹配成功
该动态规划的核心主要在推导递推公式,下⾯我们通过几个示例来推导递推公式
①示例一:
- 从上述例⼦中理解,两个单词匹配成功,并不是直接标记为1,还需要考虑父级单词是否匹配 成功,只有父级匹配成功前提下,本次匹配成功才有意义
- 关键:当一个routing_key单词与binding_key单词匹配成功,则应该继承上一个单词(上一行和上一列)的匹配结果
- 单词匹配成功:dp[i][j]=dp[i-1][j-1]
- 在将思想转换为代码时,我们考虑当第一个单词匹配成功时,从左上继承结果,但是这时候是没有左上位置的
- 因此我们的数组大小定义行列分别额外多申请一行一列,并将 dp[0][0]位置置1,如下图:
②示例二:#通配符处理
- 从这个例子中能看出,当出现 # 通配符的时候,如果 bbb 与 # 匹配成功,从左上继承结果,得到的结果是0,匹配失败,但是实际结果应该是成功的
- 因此,当遇到通配符 # 时,可以从上⼀个单词与 # 的匹配结果处 (左边)继承,如下图:
公式为::dp[i][j] =dp[i- 1][j- 1] | dp[i][j- 1]
③示例三:
- binding_key = "aaa.#" 与 routing_key = "aaa.bbb" 与上面的类似,因此当遇到 # 通配符匹配成功时,不仅从左上,左边继承结果,也可以从上面继承结果,公式为:dp[i][j] = dp[i- 1][j- 1] | dp[i][j- 1] | dp[i- 1][j];
- 当binding_key 以 # 为起始符时,可以将#对应行的第0列全置为1,便于后续的匹配结果继承
3.3 实现
/mqserver/mq_route.hpp
cpp
#ifndef __M_ROUTE_H__
#define __M_ROUTE_H__
#include "../mqcommon/log.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/message.pb.h"
namespace my_mq
{
class Router
{
public:
static bool isLegalRoutingKey(const std::string &routing_key) // 判断 routing_key 是否合法
{
// routing_key:只需要判断是否包含有非法字符即可, 合法字符( a~z, A~Z, 0~9, ., _)
for (auto &e : routing_key)
{
if ((e >= 'a' && e <= 'z') || (e >= 'A' && e <= 'Z') ||
(e >= '0' && e <= '9') || (e == '_' || e == '.'))
{
continue;
}
return false;
}
return true;
}
static bool isLegalBindingKey(const std::string &binding_key) // 判断binding_key 是否合法
{
// 1,判断是否包含有非法字符, 合法字符:a~z, A~Z, 0~9, ., _, *, #
for (auto &e : binding_key)
{
if ((e >= 'a' && e <= 'z') || (e >= 'A' && e <= 'Z') ||
(e >= '0' && e <= '9') || (e == '_' || e == '.') ||
(e == '*' || e == '#'))
{
continue;
}
return false;
}
// 2,* 和 # 必须独立存在
std::vector<std::string> sub_words;
StrHelper::split(binding_key, ".", sub_words);
for (auto &word : sub_words)
{
if (word.size() > 1 && (word.find("*") != std::string::npos || word.find("#") != std::string::npos))
return false;
}
// 3. *和#不能连续出现,但是允许 ** (#通配符两边不能出现#和*)
for (int i = 1; i < sub_words.size(); i++)
{
if (sub_words[i] == "#" && sub_words[i - 1] == "*")
return false;
if (sub_words[i] == "#" && sub_words[i - 1] == "#")
return false;
if (sub_words[i] == "*" && sub_words[i - 1] == "#")
return false;
}
return true; // 一切都没问题之后再返回true
}
// 根据路由模式,进行两个字符串的匹配,如果匹配返回true,否则返回false
static bool route(ExchangeType type, const std::string &routing_key, const std::string &binding_key)
{
if (type == ExchangeType::DIRECT) // 直接交换
return (routing_key == binding_key);
else if (type == ExchangeType::FANOUT) // 广播交换
return true;
// 主题交换:进行模式匹配:news.# 与 news.game.pop
// 1,将binding_key 与 routing_key进行字符串分割,得到单个单词
std::vector<std::string> bkeys, rkeys;
int n_bkey = StrHelper::split(binding_key, ".", bkeys);
int n_rkey = StrHelper::split(routing_key, ".", rkeys);
// 2,定义标记数组,并初始化[0][0]位置为true,其他位置为false
std::vector<std::vector<bool>> dp(n_bkey + 1, std::vector<bool>(n_rkey + 1, false));
dp[0][0] = true;
// 3,如果binding_key以#起始,则将#对应行的第0列置为1
for (int i = 1; i <= bkeys.size(); i++)
{
if (bkeys[i - 1] == "#")
{
dp[i][0] = true;
continue;
}
break;
}
// 4,使用routing_key中的每个单词与binding_key中的每个单词进行匹配并标记数组
for (int i = 1; i <= n_bkey; i++)
{
for (int j = 1; j <= n_rkey; j++)
{
if (bkeys[i - 1] == rkeys[j - 1] || bkeys[i - 1] == "*") // 前一个相同
dp[i][j] = dp[i - 1][j - 1]; // 如果当前bkey是个*,或者两个单词相同,表示单词匹配成功,则从左上方继承结果
else if (bkeys[i - 1] == "#") // 如果当前bkey是个#,则需要从左上,左边或上边继承结果
dp[i][j] = dp[i - 1][j - 1] | dp[i][j - 1] | dp[i - 1][j];
}
}
return dp[n_bkey][n_rkey];
}
};
}
#endif
/mqtesr/mq_routetest.cpp
cpp
#include "../mqserver/mq_route.hpp"
#include <gtest/gtest.h>
class QueueTest : public testing::Environment
{
public:
virtual void SetUp() override
{
}
virtual void TearDown() override
{
// bmp->clear();
}
};
TEST(route_test, legal_routing_key)
{
std::string rkey1 = "news.music.pop";
std::string rkey2 = "news..music.pop";
std::string rkey3 = "news.,music.pop";
std::string rkey4 = "news.music_123.pop";
ASSERT_EQ(my_mq::Router::isLegalRoutingKey(rkey1), true);
ASSERT_EQ(my_mq::Router::isLegalRoutingKey(rkey2), true);
ASSERT_EQ(my_mq::Router::isLegalRoutingKey(rkey3), false);
ASSERT_EQ(my_mq::Router::isLegalRoutingKey(rkey4), true);
}
TEST(route_test, legal_binding_key)
{
std::string bkey1 = "news.music.pop";
std::string bkey2 = "news.#.music.pop";
std::string bkey3 = "news.#.*.music.pop"; //
std::string bkey4 = "news.*.#.music.pop"; //
std::string bkey5 = "news.#.#.music.pop"; //
std::string bkey6 = "news.*.*.music.pop";
std::string bkey7 = "news.,music_123.pop"; //
ASSERT_EQ(my_mq::Router::isLegalBindingKey(bkey1), true);
ASSERT_EQ(my_mq::Router::isLegalBindingKey(bkey2), true);
ASSERT_EQ(my_mq::Router::isLegalBindingKey(bkey3), false);
ASSERT_EQ(my_mq::Router::isLegalBindingKey(bkey4), false);
ASSERT_EQ(my_mq::Router::isLegalBindingKey(bkey5), false);
ASSERT_EQ(my_mq::Router::isLegalBindingKey(bkey6), true);
ASSERT_EQ(my_mq::Router::isLegalBindingKey(bkey7), false);
}
TEST(route_test, route)
{
// aaa aaa true
// aaa.bbb aaa.bbb true
// aaa.bbb aaa.bbb.ccc false
// aaa.bbb aaa.ccc false
// aaa.#.bbb aaa.bbb.ccc false
// aaa.bbb.# aaa.ccc.bbb false
// #.bbb.ccc aaa.bbb.ccc.ddd false
// aaa.bbb.ccc aaa.bbb.ccc true
// aaa.* aaa.bbb true
// aaa.*.bbb aaa.bbb.ccc false
// *.aaa.bbb aaa.bbb false
// # aaa.bbb.ccc true
// aaa.# aaa.bbb true
// aaa.# aaa.bbb.ccc true
// aaa.#.ccc aaa.ccc true
// aaa.#.ccc aaa.bbb.ccc true
// aaa.#.ccc aaa.aaa.bbb.ccc true
// #.ccc ccc true
// #.ccc aaa.bbb.ccc true
// aaa.#.ccc.ccc aaa.bbb.ccc.ccc.ccc true
// aaa.#.bbb.*.bbb aaa.ddd.ccc.bbb.eee.bbb true
std::vector<std::string> bkeys = {
"aaa",
"aaa.bbb",
"aaa.bbb",
"aaa.bbb",
"aaa.#.bbb",
"aaa.bbb.#",
"#.bbb.ccc",
"aaa.bbb.ccc",
"aaa.*",
"aaa.*.bbb",
"*.aaa.bbb",
"#",
"aaa.#",
"aaa.#",
"aaa.#.ccc",
"aaa.#.ccc",
"aaa.#.ccc",
"#.ccc",
"#.ccc",
"aaa.#.ccc.ccc",
"aaa.#.bbb.*.bbb"};
std::vector<std::string> rkeys = {
"aaa",
"aaa.bbb",
"aaa.bbb.ccc",
"aaa.ccc",
"aaa.bbb.ccc",
"aaa.ccc.bbb",
"aaa.bbb.ccc.ddd",
"aaa.bbb.ccc",
"aaa.bbb",
"aaa.bbb.ccc",
"aaa.bbb",
"aaa.bbb.ccc",
"aaa.bbb",
"aaa.bbb.ccc",
"aaa.ccc",
"aaa.bbb.ccc",
"aaa.aaa.bbb.ccc",
"ccc",
"aaa.bbb.ccc",
"aaa.bbb.ccc.ccc.ccc",
"aaa.ddd.ccc.bbb.eee.bbb"};
std::vector<bool> result = {
true,
true,
false,
false,
false,
false,
false,
true,
true,
false,
false,
true,
true,
true,
true,
true,
true,
true,
true,
true,
true};
for (int i = 0; i < bkeys.size(); i++)
{
ASSERT_EQ(my_mq::Router::route(my_mq::ExchangeType::TOPIC, rkeys[i], bkeys[i]), result[i]);
}
}
int main(int argc, char *argv[])
{
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new QueueTest);
RUN_ALL_TESTS();
return 0;
}
四,消费者管理模块
客户端这边每当发起一个订阅请求,意味着服务器这边就多了一个订阅者,而这个订阅者是和队列直接关联的,因为订阅请求中会描述当前用户想要订阅哪一个队列的消息。
而一个信道关闭的时候,或者队列被删除的时候,那么这个信道或队列关联的消费者也就没有存在的意义了,因此也需要将相关的消费者信息给删除掉
所以我们也需要对订阅者信息进行管理
#消费者信息结构
- 消费者标识
- 订阅的队列名称
- ⼀个消息的处理回调函数:实现的是当发布一条消息到队列,则选择消费者进行消费,如何消 费?对于服务端来说就是调用这个回调函数进行处理,其内部逻辑就是找到消费者对应的连 接,然后将数据发送给消费者对应的客户端
- void(const std::string&, const BasicProperties&, const std::string&)
- 是否自动应答标志:一个消息被消费者消费后,若自动应答,则直接移除待确认消息,否则 等待客户端确认再移除
#队列消费者管理结构:以队列为单元进行管理,和消息管理类似①操作:
- 新增消费者:信道提供的服务是订阅队列消息的时候创建
- 删除消费者:取消订阅 / 信道关闭 / 连接关闭的时候删除
- 获取消费者:不是单纯的查,是从队列所有的消费者中按序取出一个消费者进行消息的推送
- 判断队列消费者是否为空:后面三个测试用
- 判断指定消费者是否存在
- 清理队列所有消费者
②元素
- 队列名称
- 消费者管理结构:vector数组
- 轮转序号:⼀个队列可能会有多个消费者,但是⼀条消息只需要被⼀个消费者消费即可,因 此采用RR轮转(代码实现时会详细说明)
- 互斥锁:保证线程安全
问题:消费者管理以信道为单元还是以队列为单元
以信道为单元:一个信道关闭的时候,所有关联的消费者都要删除
以队列为单元:一个队列收到了一条新消息,需要找到订阅了队列的消费者进行推送
#对消费者进行统一管理结构初始化 / 删除队列的消费者信息结构:创建/删除队列的时候
向指定队列新增消费者:客户端订阅指定队列消息的时候,新增完成的时候返回消费者对象
从指定队列移除消费者:客户端取消订阅的时候
移除指定队列的所有消费者:队列被删除时销毁,并且删除消费者的队列管理单元对象
从指定队列获取一个消费者:轮询获取--消费者轮换消费起到负载均衡的作用
判断队列中消费者是否为空:后面三个测试用
判断队列中指定消费者是否存在
清理所有消费者
cpp
#ifndef __M_CONSUMER_H__
#define __M_CONSUMER_H__
#include "../mqcommon/log.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/message.pb.h"
#include <iostream>
#include <unordered_map>
#include <mutex>
#include <memory>
#include <vector>
#include <functional>
namespace my_mq
{
//-----消费者信息-----//
using ConsumerCallback = std::function<void(const std::string, const BasicProperties *bp, const std::string)>;
struct Consumer
{
using ptr = std::shared_ptr<Consumer>;
std::string tag; // 消费者标识
std::string qname; // 消费者订阅的队列名称
bool auto_ack; // 自动确认标志
ConsumerCallback callback; // 回调函数
Consumer()
{
DLOG("new Consumer: %p", this);
}
// 消费者标识,要订阅的队列名称,是否自动确认以及一个回调函数
Consumer(const std::string &ctag, const std::string &queue_name, bool ack_flag, const ConsumerCallback &cb)
: tag(ctag), qname(queue_name), auto_ack(ack_flag), callback(std::move(cb))
{
DLOG("new Consumer: %p", this);
}
~Consumer()
{
DLOG("del Consumer: %p", this);
}
};
//-----消费者管理结构(以队列为单元)-----//
class QueueConsumer
{
public:
using ptr = std::shared_ptr<QueueConsumer>;
QueueConsumer(const std::string &qname) : _qname(qname), _rr_seq(0) {}
// 向指定队列新增消费者
Consumer::ptr create(const std::string &ctag, const std::string &queue_name, bool ack_flag, const ConsumerCallback &cb)
{
std::unique_lock<std::mutex> lock(_mutex);
// 1,先判断消费者是否重复
for (auto &e : _consumers) // 遍历队列,一次判断消费者名是否相同
{
if (e->tag == ctag)
return Consumer::ptr();
}
// 2,没有重复则新增消费者对象
auto consumer = std::make_shared<Consumer>(ctag, queue_name, ack_flag, cb);
// 3,添加进队列后返回对象
_consumers.push_back(consumer);
return consumer;
}
// 队列移除消费者
void remove(const std::string &ctag)
{
std::unique_lock<std::mutex> lock(_mutex);
// 2. 遍历查找后删除
for (auto it = _consumers.begin(); it != _consumers.end(); ++it)
{
if ((*it)->tag == ctag)
{
_consumers.erase(it);
return;
}
}
return;
}
// 获取消费者:RR轮转获取,就是当前序号的消费者取完之后,让轮转序号++,让它去取下一个
Consumer::ptr choose()
{
std::unique_lock<std::mutex> lock(_mutex);
if (_consumers.size() == 0) // 没有消费者返回空
return Consumer::ptr();
// 1,获取当前轮转到的下标
int idx = _rr_seq % _consumers.size();
_rr_seq++;
// 2,获取对象,返回
return _consumers[idx];
}
bool empty() // 是否为空
{
std::unique_lock<std::mutex> lock(_mutex);
return _consumers.size() == 0;
}
// 判断指定消费者是否存在
bool exists(const std::string &ctag)
{
std::unique_lock<std::mutex> lock(_mutex);
for (auto it = _consumers.begin(); it != _consumers.end(); ++it) // 遍历查找
{
if ((*it)->tag == ctag)
return true;
}
return false;
}
void clear() // 清理所有消费者
{
std::unique_lock<std::mutex> lock(_mutex);
_consumers.clear();
_rr_seq = 0;
}
private:
std::string _qname;
std::mutex _mutex;
uint64_t _rr_seq; // RR轮转序号
std::vector<Consumer::ptr> _consumers; // 消费者队列本体
};
//-----消费者统一管理结构(真正的对外操作)-----//
class ConsumerManager
{
public:
using ptr = std::shared_ptr<ConsumerManager>;
ConsumerManager() {}
void initQueueConsumer(const std::string &qname) // 初始化队列消费者管理单元
{
std::unique_lock<std::mutex> lock(_mutex);
// 1,重复判断
auto it = _qconsumers.find(qname);
if (it != _qconsumers.end())
return;
// 2,新增
auto qconsumers = std::make_shared<QueueConsumer>(qname);
_qconsumers.insert(std::make_pair(qname, qconsumers));
}
void destroyQueueConsumer(const std::string &qname)
{
std::unique_lock<std::mutex> lock(_mutex);
_qconsumers.erase(qname);
}
// 创建消费者
Consumer::ptr create(const std::string &ctag, const std::string &queue_name, bool ack_flag, const ConsumerCallback &cb)
{
// 获取队列的消费者管理单元句柄,通过句柄完成新建
QueueConsumer::ptr qcp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _qconsumers.find(queue_name);
if (it == _qconsumers.end())
{
DLOG("没有找到队列 %s 的消费者管理句柄!", queue_name.c_str());
return Consumer::ptr();
}
qcp = it->second;
}
// 这个新增不需要加锁保护,因为每个队列里都有自己的锁,下面移除也是同理
return qcp->create(ctag, queue_name, ack_flag, cb);
}
// 删除消费者
void remove(const std::string &ctag, const std::string &queue_name)
{
QueueConsumer::ptr qcp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _qconsumers.find(queue_name);
if (it == _qconsumers.end())
{
DLOG("没有找到队列 %s 的消费者管理句柄!", queue_name.c_str());
return;
}
qcp = it->second;
}
return qcp->remove(ctag);
}
// 从指定队列里获取消费者
Consumer::ptr choose(const std::string &queue_name)
{
QueueConsumer::ptr qcp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _qconsumers.find(queue_name);
if (it == _qconsumers.end())
{
DLOG("没有找到队列 %s 的消费者管理句柄!", queue_name.c_str());
return Consumer::ptr(); // 没找到返回空的
}
qcp = it->second;
}
return qcp->choose();
}
bool empty(const std::string &queue_name)
{
QueueConsumer::ptr qcp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _qconsumers.find(queue_name);
if (it == _qconsumers.end())
{
DLOG("没有找到队列 %s 的消费者管理句柄!", queue_name.c_str());
return false;
}
qcp = it->second;
}
return qcp->empty();
}
bool exists(const std::string &ctag, const std::string &queue_name)
{
QueueConsumer::ptr qcp;
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _qconsumers.find(queue_name);
if (it == _qconsumers.end())
{
DLOG("没有找到队列 %s 的消费者管理句柄!", queue_name.c_str());
return false;
}
qcp = it->second;
}
return qcp->exists(ctag);
}
void clear()
{
std::unique_lock<std::mutex> lock(_mutex);
_qconsumers.clear();
}
private:
std::mutex _mutex;
std::unordered_map<std::string, QueueConsumer::ptr> _qconsumers; // 队列名与队列对象的映射
};
}
#endif
测试,/mqtest/mq_consumertest.cpp
cpp
#include "../mqserver/mq_consumer.hpp"
#include <gtest/gtest.h>
my_mq::ConsumerManager::ptr cmp;
class ConsumerTest : public testing::Environment
{
public:
virtual void SetUp() override
{
cmp = std::make_shared<my_mq::ConsumerManager>();
cmp->initQueueConsumer("queue1");
}
virtual void TearDown() override
{
cmp->clear();
}
};
void cb(const std::string &tag, const my_mq::BasicProperties *bp, const std::string &body)
{
std::cout << tag << " 消费了消息:" << body << std::endl;
}
// 新增消费者测试
TEST(consumer_test, insert_test)
{
cmp->create("consumer1", "queue1", false, cb);
cmp->create("consumer2", "queue1", false, cb);
cmp->create("consumer3", "queue1", false, cb);
ASSERT_EQ(cmp->exists("consumer1", "queue1"), true);
ASSERT_EQ(cmp->exists("consumer2", "queue1"), true);
ASSERT_EQ(cmp->exists("consumer3", "queue1"), true);
}
// 删除消费者测试
TEST(consumer_test, remove_test)
{
cmp->remove("consumer1", "queue1");
ASSERT_EQ(cmp->exists("consumer1", "queue1"), false);
ASSERT_EQ(cmp->exists("consumer2", "queue1"), true);
ASSERT_EQ(cmp->exists("consumer3", "queue1"), true);
}
// 获取消费者测试
TEST(consumer_test, choose_test)
{
my_mq::Consumer::ptr cp = cmp->choose("queue1");
ASSERT_NE(cp.get(), nullptr);
ASSERT_EQ(cp->tag, "consumer2");
cp = cmp->choose("queue1");
ASSERT_NE(cp.get(), nullptr);
ASSERT_EQ(cp->tag, "consumer3");
cp = cmp->choose("queue1");
ASSERT_NE(cp.get(), nullptr);
ASSERT_EQ(cp->tag, "consumer2");
}
int main(int argc, char *argv[])
{
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new ConsumerTest);
RUN_ALL_TESTS();
return 0;
}
五,网络通信
5.1 网络通信协议
可以参照文章:计算机网络(五) ------ 自定义协议简单网络程序-CSDN博客
这里也是和以前一样自己实现一个应用层协议,配合protobuf来进行序列化和反序列化
生产者和消费者都是客户端,它们都需要通过网络和 BrokerServer 进行通信,通信的过程我们使用 Muduo 库来实现,同时在Tcp底层基础上自定义应用层协议,完成客户端对服务器功能的远端调用。所以我们要实现下面的远端调用接口:
- 创建 / 关闭channel
- 创建 / 删除exchange
- 创建 / 删除queue
- 创建 / 删除binding
- 发送 / 订阅message
- 发送ack
- 返回message(服务器->客⼾端)
而关于应用层协议陈硕大佬在设计muduo库的时候已经已经完成了,如下图:
- len:4个字节,表示整个报文长度
- nameLen:4个字节,表示typeName数组的长度
- typeName:是个字节数组,占nameLen个字节,表示请求/响应报文的类型名,作用是分发不同消息到对应的远端接口调用中
- protobufData:也是个字节数组,占len - nameLen - 8个字节,表示请求/响应数据通过protobuf序列化之后的二进制
- checkSum:占4个字节,表示整个消息的校验和,检验请求/响应报文的完整性
所以我们只需要实现protobuf的部分即可(赞美陈大佬!)
我们需要11种不同的服务,所以需要提供11种请求与响应格式,如下代码:
/mqcommon/channel.proto
cpp
syntax = "proto3";
package my_mq;
import "message.proto"; //导入其它proto文件
//信道的打开与关闭
message openChannelRequest
{
string rid = 1; //请求ID
string cid = 2; //创建的信道ID
};
message closeChannelRequest
{
string rid = 1;
string cid = 2;
};
//交换机的声明与删除
message declareExchangeRequest
{
string rid = 1; //请求ID
string cid = 2; //这个请求要交给哪个信道进行处理
string exchange_name = 3; //交换机名称
ExchangeType exchange_type = 4; //类型
bool durable = 5; //持久化标志
bool auto_delete = 6; //是否自动删除
map<string, string> args = 7; //额外参数
};
message deleteExchangeRequest{
string rid = 1;
string cid = 2;
string exchange_name = 3;
};
//队列的声明与删除
message declareQueueRequest{
string rid = 1;
string cid = 2;
string queue_name = 3;
bool exclusive = 4; //是否独占标志
bool durable = 5;
bool auto_delete = 6;
map<string, string> args = 7;
};
message deleteQueueRequest{
string rid = 1;
string cid = 2;
string queue_name = 3;
};
//队列的绑定与解除绑定
message queueBindRequest{
string rid = 1;
string cid = 2;
string exchange_name = 3;
string queue_name = 4;
string binding_key = 5;
};
message queueUnBindRequest{
string rid = 1;
string cid = 2;
string exchange_name = 3;
string queue_name = 4;
};
//消息的发布
message basicPublishRequest {
string rid = 1;
string cid = 2;
string exchange_name = 3; //消息要发布给哪个交换机
string body = 4; //消息内容是什么
BasicProperties properties = 5; //绑定的相关信息,在message.proto 中
};
//消息的确认
message basicAckRequest {
string rid = 1;
string cid = 2;
string queue_name = 3;
string message_id = 4; //对哪个队列的哪个消息进行确认
};
//队列的订阅
message basicConsumeRequest {
string rid = 1;
string cid = 2;
string consumer_tag =3;
string queue_name = 4;
bool auto_ack = 5;
};
//队列订阅取消
message basicCancelRequest {
string rid = 1;
string cid = 2;
string consumer_tag = 3; //哪个消费者针对哪个队列进行取消订阅
string queue_name = 4;
};
//消息的推送:服务器给客户端推送的消息格式
message basicConsumeResponse {
string cid = 1;
string consumer_tag = 2;
string body = 3;
BasicProperties properties = 4;
};
//通用响应
message basicCommonResponse {
string rid = 1; //针对哪一个请求的响应
string cid = 2; //一个连接的哪个信道
bool ok = 3;
}
bash
protoc --cpp_out=./ channel.proto
5.1 信道管理模块
在AMQP模型中,除了通信连接Connection概念外,还有⼀个Channel的概念,这个是针对 Connection连接的一个更细粒度的通信信道,多个Channel可以使用同一个通信连接Connection进行通信,但是同⼀个Connection的多个Channel之间相互独立
信道模块就是再次将上述模块进行整合提供服务的模块
①管理信息
- 信道ID:信道的唯一标识
- 信道关联的消费者:用于消费者信道在关闭的时候取消订阅,删除订阅者信息
- 信道关联的连接:用于向客户端发送数据(响应,推送的消息)
- protobuf协议处理句柄:网络通信前的协议处理
- 消费者管理句柄:信道关闭 / 取消订阅的时候,通过句柄删除订阅者信息
- 虚拟机句柄:交换机 / 队列 / 绑定 / 消息数据管理
- 工作线程池句柄:一条消息被发布到队列后,需要将消息推送给订阅了对应队列的消费者,该过程由线程池完成
②管理操作
- 提供声明 & 删除交换机操作:删除交换机时删除交换机关联的绑定信息
- 提供声明 & 删除队列操作:删除队列的同时,删除队列关联的绑定信息,消息,消费者信息
- 提供绑定 & 解绑队列操作
- 提供订阅 & 取消订阅队列消息操作
- 提供发布 & 确认消息操作
③信道管理:信道的增删查
cpp
#ifndef __M_CHANNEL_H__
#define __M_CHANNEL_H__
#include "muduo/net/TcpConnection.h"
// 下面两个头文件的位置可以参考准备工作章节的3.3,将proto移到third目录下
#include "../third/proto/codec.h"
#include "../third/proto/dispatcher.h"
#include "../mqcommon/log.hpp"
#include "../mqcommon/helper.hpp"
#include "../mqcommon/message.pb.h"
#include "../mqcommon/channel.pb.h"
#include "../mqcommon/ThreadPool.hpp"
#include "mq_consumer.hpp"
#include "mq_virtualhost.hpp"
#include "mq_route.hpp"
namespace my_mq
{
// 后面接口传参数时只需要传入protobuf中不同对象的智能指针即可,不用再传入大量参数了
using ProtobufCodecPtr = std::shared_ptr<ProtobufCodec>;
using openChannelRequestPtr = std::shared_ptr<openChannelRequest>;
using closeChannelRequestPtr = std::shared_ptr<closeChannelRequest>;
using declareExchangeRequestPtr = std::shared_ptr<declareExchangeRequest>;
using deleteExchangeRequestPtr = std::shared_ptr<deleteExchangeRequest>;
using declareQueueRequestPtr = std::shared_ptr<declareQueueRequest>;
using deleteQueueRequestPtr = std::shared_ptr<deleteQueueRequest>;
using queueBindRequestPtr = std::shared_ptr<queueBindRequest>;
using queueUnBindRequestPtr = std::shared_ptr<queueUnBindRequest>;
using basicPublishRequestPtr = std::shared_ptr<basicPublishRequest>;
using basicAckRequestPtr = std::shared_ptr<basicAckRequest>;
using basicConsumeRequestPtr = std::shared_ptr<basicConsumeRequest>;
using basicCancelRequestPtr = std::shared_ptr<basicCancelRequest>;
// 消息的推送和通用响应不是我们要处理的请求,而是推送的消息协议,只需要直接定义对象然后发送数据即可
//-----信道信息类-----//
class Channel
{
public:
using ptr = std::shared_ptr<Channel>;
// 构造时,信道,虚拟机,消费者管理句柄等都需要传进来初始化
Channel(const std::string &id,
const VirtualHost::ptr &host,
const ConsumerManager::ptr &cmp,
const ProtobufCodecPtr &codec,
const muduo::net::TcpConnectionPtr &conn,
const ThreadPool::ptr &pool)
: _cid(id), _conn(conn), _codec(codec), _cmp(cmp), _host(host), _pool(pool)
{
DLOG("new Channel: %p", this);
}
~Channel() // 如果这个信道连接的是消费者,那么这个与消费者的相关信息都要删除
{
if (_consumer.get() != nullptr)
{
_cmp->remove(_consumer->tag, _consumer->qname);
}
DLOG("del Channel: %p", this);
}
//-----交换机相关操作-----//
void declareExchange(const declareExchangeRequestPtr &req) // 交换机声明
{
bool ret = _host->declareExchange(req->exchange_name(),
req->exchange_type(), req->durable(),
req->auto_delete(), req->args());
return basicResponse(ret, req->rid(), req->cid()); // 构建好交换机时响应客户端
}
void deleteExchange(const deleteExchangeRequestPtr &req) // 删除交换机
{
_host->deleteExchange(req->exchange_name());
return basicResponse(true, req->rid(), req->cid());
}
//-----队列相关操作-----//
void declareQueue(const declareQueueRequestPtr &req) // 声明队列
{
bool ret = _host->declareQueue(req->queue_name(),
req->durable(), req->exclusive(),
req->auto_delete(), req->args());
if (ret == false) // 声明队列失败,原因可能是对应交换机不存在等
{
return basicResponse(false, req->rid(), req->cid());
}
_cmp->initQueueConsumer(req->queue_name()); // 初始化队列的消费者管理句柄
return basicResponse(true, req->rid(), req->cid());
}
void deleteQueue(const deleteQueueRequestPtr &req) // 删除队列
{
_cmp->destroyQueueConsumer(req->queue_name()); // 把订阅了队列的消费者都干掉
_host->deleteQueue(req->queue_name()); // 把队列干掉
return basicResponse(true, req->rid(), req->cid());
}
void queueBind(const queueBindRequestPtr &req) // 队列绑定
{
bool ret = _host->bind(req->exchange_name(), req->queue_name(), req->binding_key());
return basicResponse(ret, req->rid(), req->cid());
}
void queueUnBind(const queueUnBindRequestPtr &req) // 队列解绑
{
_host->unBind(req->exchange_name(), req->queue_name());
return basicResponse(true, req->rid(), req->cid());
}
//-----消息的相关操作-----//
void basicPublish(const basicPublishRequestPtr &req) // 消息发布
{
// 这个接口主要与生产者相关,将消息放到队列里,然后服务器自动将该消息发给订阅了该队列的消费者
// 1,先判断交换机是否存在
auto ep = _host->selectExchange(req->exchange_name()); // 获取交换机对象
if (ep.get() == nullptr) // 没有找到
return basicResponse(false, req->rid(), req->cid());
// 2,进行交换路由,判断消息要发布到交换机绑定的哪个队列中
MsgQueueBindingMap mqbm = _host->exchangeBindings(req->exchange_name()); // 获取绑定信息
BasicProperties *properties = nullptr;
std::string routing_key;
if (req->has_properties()) // 先判断有没有绑定信息,如果没有代表该消息是广播的
{
// 如果有则读取绑定信息
properties = req->mutable_properties();
routing_key = properties->routing_key();
}
for (auto &binding : mqbm)
{
if (Router::route(ep->type, routing_key, binding.second->binding_key))
{
// 3,将消息添加到队列中(添加消息的管理)
_host->basicPublish(binding.first, properties, req->body()); // 加入队列名称,属性(如果有)和消息内容
// 4,向线程池中添加一个消息消费任务,也就是向指定队列的订阅者去推送消息
auto task = std::bind(&Channel::consume, this, binding.first); // 进行参数绑定,生成可调用对象
_pool->push(task); // 将该对象作为任务扔线程池里去
}
}
return basicResponse(true, req->rid(), req->cid());
}
void basicAck(const basicAckRequestPtr &req) // 消息确认
{
_host->basicAck(req->queue_name(), req->message_id());
return basicResponse(true, req->rid(), req->cid());
}
void basicConsume(const basicConsumeRequestPtr &req) // 订阅队列消息
{
// 该接口主要与消费者相关,消费者一旦订阅了这个队列,先把消费者的信息保存管理起来起来并添加一个回调函数
// 然后通过这个回调函数将该被订阅的队列里的消息发送给消费者
// 1,判断队列是否存在
bool ret = _host->existsQueue(req->queue_name());
if (ret == false)
return basicResponse(false, req->rid(), req->cid());
// 2,创建队列的消费者
auto cb = std::bind(&Channel::callback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); // 设置回调函数
_consumer = _cmp->create(req->consumer_tag(), req->queue_name(), req->auto_ack(), cb); // 创建队列消费者,消费者名称,订阅的队列,是否自动应答,然后回调函数
return basicResponse(true, req->rid(), req->cid());
}
void basicCancel(const basicCancelRequestPtr &req) // 取消订阅
{
_cmp->remove(req->consumer_tag(), req->queue_name());
// 把对应消费者和其订阅的队列干掉(只是把消费者管理结构中的干掉了,并不是真的干掉了队列,队列真正存在虚拟机里的)
return basicResponse(true, req->rid(), req->cid());
}
private:
void callback(const std::string tag, const BasicProperties *bp, const std::string &body)
{
// 让当前的channel针对参数组织出推送消息请求,将消息推送给channel对应的客户端
// 简单来说就是这个函数就是根据传入的内容组织一个响应,然后发布给客户端当时callback绑定的this的消费者,也就是当前的channel
basicConsumeResponse resp;
resp.set_cid(_cid);
resp.set_body(body);
resp.set_consumer_tag(tag);
if (bp)
{
resp.mutable_properties()->set_id(bp->id());
resp.mutable_properties()->set_delivery_mode(bp->delivery_mode());
resp.mutable_properties()->set_routing_key(bp->routing_key());
}
_codec->send(_conn, resp);
}
void consume(const std::string &qname) // 指定队列消费消息
{
// 1,从消息队列中取出一条消息
MessagePtr mp = _host->basicConsume(qname);
if (mp.get() == nullptr)
{
DLOG("执行消费任务失败,%s 队列没有消息!", qname.c_str());
return;
}
// 2,从订阅队列中取出一个订阅者
Consumer::ptr cp = _cmp->choose(qname);
if (cp.get() == nullptr)
{
DLOG("执行消费任务失败,%s 队列没有消费者!", qname.c_str());
return;
}
// 3,调用订阅者对应的消息处理函数,实现消息的推送
cp->callback(cp->tag, mp->mutable_payload()->mutable_properties(), mp->payload().body());
// 4,判断如果订阅者是否是自动确认,如果是则不需要等待确认直接删除消息,否则需要外部收到消息确认后再删除
if (cp->auto_ack)
_host->basicAck(qname, mp->payload().properties().id());
}
void basicResponse(bool ok, const std::string &rid, const std::string &cid)
{
basicCommonResponse resp;
resp.set_rid(rid);
resp.set_cid(cid);
resp.set_ok(ok);
_codec->send(_conn, resp);
}
private:
std::string _cid; // 信道ID
Consumer::ptr _consumer; // 一个信道只只能连接一个消费者
muduo::net::TcpConnectionPtr _conn; // muduo的连接,通过这个连接发送消息
ProtobufCodecPtr _codec; // 协议处理句柄
ConsumerManager::ptr _cmp; // 消费者管理句柄
VirtualHost::ptr _host; // 虚拟机管理句柄
ThreadPool::ptr _pool; // 线程池
};
//-----信道管理类-----//
class ChannelManager
{
public:
using ptr = std::shared_ptr<ChannelManager>;
ChannelManager() {}
bool openChannel(const std::string &id, const VirtualHost::ptr &host, const ConsumerManager::ptr &cmp,
const ProtobufCodecPtr &codec, const muduo::net::TcpConnectionPtr &conn, const ThreadPool::ptr &pool)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _channels.find(id);
if (it != _channels.end())
{
DLOG("信道:%s 已经存在!", id.c_str());
return false;
}
auto channel = std::make_shared<Channel>(id, host, cmp, codec, conn, pool);
_channels.insert(std::make_pair(id, channel));
return true;
}
void closeChannel(const std::string &id)
{
std::unique_lock<std::mutex> lock(_mutex);
_channels.erase(id);
}
Channel::ptr getChannel(const std::string &id)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _channels.find(id);
if (it == _channels.end())
{
return Channel::ptr();
}
return it->second;
}
private:
std::mutex _mutex;
std::unordered_map<std::string, Channel::ptr> _channels;
};
}
#endif
bash
g++ -std=c++11 mq_channeltest.cpp ../mqcommon/message.pb.cc ../mqcommon/channel.pb.cc -o test_channel -lgtest -lprotobuf -lsqlite3 -I../third/proto -I../mqserver

如果直接编译会出现类似上面的报错,这是因为protobuf自己定义了一个映射类型 google::protobuf::Map,而我们交换机额外数据类型用的是unordered_map,所以需要把前面代码对应部分全改了,包括下面几个地方:





5.2 连接管理模块
向用户提供一个用于实现网络通信的 Connection 对象,从其内部可创建出粒度更轻的 Channel 对象,用于与客户端进行网络通信
①成员信息
- 连接关联的信道管理句柄:实现信道的增删查
- 连接关联的实际用于通信的 muduo::net::Connection 连接
- protobuf 协议处理的句柄:ProtobufCodec对象
- 消费者管理句柄
- 虚拟机句柄
- 异步工作线程池句柄
②连接操作
- 创建Channel信道的
- 删除Channel信道的
③连接管理:连接的增删查
mqserver/mq_connection.hpp
cpp
#include "mq_channel.hpp"
namespace my_mq
{
//-----连接信息类-----//
class Connection
{
public:
using ptr = std::shared_ptr<Connection>;
Connection(const VirtualHost::ptr &host,
const ConsumerManager::ptr &cmp,
const ProtobufCodecPtr &codec,
const muduo::net::TcpConnectionPtr &conn,
const ThreadPool::ptr &pool)
: _conn(conn), _codec(codec), _cmp(cmp), _host(host), _pool(pool), _channels(std::make_shared<ChannelManager>()) // 使用智能指针,因为当连接释放时,其里面的信道也都会跟着一起释放
{
}
void openChannel(const openChannelRequestPtr &req) // 打开一个信道
{
// 判断信道ID是否重复,创建信道
bool ret = _channels->openChannel(req->cid(), _host, _cmp, _codec, _conn, _pool);
if (ret == false)
{
DLOG("创建信道的时候,信道ID重复了");
return basicResponse(false, req->rid(), req->cid());
}
DLOG("%s 信道创建成功!", req->cid().c_str());
return basicResponse(true, req->rid(), req->cid());
}
void closeChannel(const closeChannelRequestPtr &req) // 关闭信道
{
_channels->closeChannel(req->cid());
return basicResponse(true, req->rid(), req->cid());
}
Channel::ptr getChannel(const std::string &cid)
{
return _channels->getChannel(cid);
}
private:
void basicResponse(bool ok, const std::string &rid, const std::string &cid)
{
basicCommonResponse resp;
resp.set_rid(rid);
resp.set_cid(cid);
resp.set_ok(ok);
_codec->send(_conn, resp);
}
private:
muduo::net::TcpConnectionPtr _conn; // 对muduo库的连接进行封装
ProtobufCodecPtr _codec; // 协议处理句柄
ConsumerManager::ptr _cmp; // 消费者管理句柄
VirtualHost::ptr _host; // 虚拟机管理句柄
ThreadPool::ptr _pool; // 线程池
ChannelManager::ptr _channels; // 信道管理句柄
};
//-----连接管理类-----//
class ConnectionManager
{
public:
using ptr = std::shared_ptr<ConnectionManager>;
ConnectionManager() {}
void newConnection(const VirtualHost::ptr &host,
const ConsumerManager::ptr &cmp,
const ProtobufCodecPtr &codec,
const muduo::net::TcpConnectionPtr &conn,
const ThreadPool::ptr &pool)
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _conns.find(conn);
if (it != _conns.end()) // 连接已存在,直接返回
return;
Connection::ptr self_conn = std::make_shared<Connection>(host, cmp, codec, conn, pool);
_conns.insert(std::make_pair(conn, self_conn));
}
void delConnection(const muduo::net::TcpConnectionPtr &conn) // 删除连接
{
std::unique_lock<std::mutex> lock(_mutex);
_conns.erase(conn); // 连接析构,信道析构,然后带着消费者虚拟机啥的全都给析构掉
}
Connection::ptr getConnection(const muduo::net::TcpConnectionPtr &conn) // 获取连接
{
std::unique_lock<std::mutex> lock(_mutex);
auto it = _conns.find(conn);
if (it == _conns.end())
{
return Connection::ptr();
}
return it->second;
}
private:
std::mutex _mutex;
std::unordered_map<muduo::net::TcpConnectionPtr, Connection::ptr> _conns;
};
}
测试未进行功能测试,仅仅测试编译):
mqtest/mq_connectiontest.cpp
cpp
#include "../mqserver/mq_connection.hpp"
int main()
{
my_mq::ConnectionManager::ptr cmp = std::make_shared<my_mq::ConnectionManager>();
cmp->newConnection(std::make_shared<my_mq::VirtualHost>("host1", "./data/host1/message/", "./data/host1/host1.db"),
std::make_shared<my_mq::ConsumerManager>(),
my_mq::ProtobufCodecPtr(),
muduo::net::TcpConnectionPtr(),
ThreadPool::ptr());
return 0;
}
bash
g++ -std=c++11 mq_connectiontest.cpp ../mqcommon/message.pb.cc ../mqcommon/channel.pb.cc -o test_connection -lgtest -lprotobuf -lsqlite3 -I../third/pro
to -I../mqserver
六,服务器实现
我们前面写过一个简单的例子,是使用muduo库实现一个简单的加法和翻译服务器:【项目实战】基于protobuf的发布订阅式消息队列(1)------ 准备工作-CSDN博客
需要管理的对象有:
- _server:Muduo库提供的一个通用Tcp服务器,我们可以封装这个服务器进行TCP通信
- _baseloop:主事件循环器,用于响应IO事件和定时器事件,主要是为了响应监听描述符的 IO事件
- _codec:protobuf 编解码器,主要负责实现应用层协议的解析和封装
- _dispatcher:消息分发器,当 Socket 接收到报文消息后,需要按照消息的类型进行消息分发,也就是根据不同类型的消息分发给相对应的的处理回调函数
- _consumer:服务器中的消费者信息管理句柄
- _threadpool:异步工作线程池,主要用于队列消息的推送工作
- _connections:连接管理句柄,管理当前服务器上的所有已经建立的通信连接
- _virtual_host:服务器持有的虚拟主机,队列、交换机、绑定、消息等数据都是通过这个管理
服务器设计可以参考下面图片:

- 我们要实现的就是这个 BrokerServer 消息队列的服务器,里面有TcpServer,有 ProtobufCodec(协议封装 / 解封处理),有ProcTobufDispate(回调函数分发器),以及EventLoop 对象,通过这几个来搭建服务器,之后还有一些资源管理对象(右边四个)
- 线程池提供异步操作,就是当收到消息时将这个消息推送给客户端这一个需要异步的操作
- 下面一系列对调函数,最终都会注册进回调函数分发器,让服务器根据收到消息的类型进行不同的接口调用
- onProtobufMessage是设置进ProtobufCodec里的回调函数,对收到数据进行protobuf协议的处理,当协议处理完之后,得到请求,再去调用回调函数,针对不同请求进行不同的分发
- 此外还有一个OnMessage接口,是设置给TcpServer的,收到数据之后会调用这个函数进行处理,然后这个函数会对收到的请求进行协议解析,解析之后得到请求,再调用分发器里不同的回调函数,内部再进行不同的处理
代码也和准备工作里的那个例子很接近,只是在原有基础上修改
cpp
#ifndef __M_BROKER_H__
#define __M_BROKER_H__
#include "../third/proto/codec.h"
#include "../third/proto/dispatcher.h"
// 下面这两个可以把muduo库安装的muduo-master/muduo/base目录复制粘贴一份到third目录里
#include "../third/base/Logging.h"
#include "../third/base/Mutex.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpServer.h"
#include "../mqcommon/ThreadPool.hpp"
#include "../mqcommon/message.pb.h"
#include "../mqcommon/channel.pb.h"
#include "../mqcommon/log.hpp"
#include "mq_connection.hpp"
#include "mq_consumer.hpp"
#include "mq_virtualhost.hpp"
namespace my_mq
{
#define DBFILE "/meta.db"
#define P_1 std::placeholders::_1
#define P_2 std::placeholders::_2
#define P_3 std::placeholders::_3
class Server
{
public:
typedef std::shared_ptr<google::protobuf::Message> MessagePtr;
Server(int port, const std::string &basedir)
: _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port), "Server", muduo::net::TcpServer::kReusePort),
_dispatcher(std::bind(&Server::onUnknownMessage, this, P_1, P_2, P_3)),
_codec(std::make_shared<ProtobufCodec>(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, P_1, P_2, P_3))),
_virtual_host(std::make_shared<VirtualHost>("MyVirtualHost", basedir, basedir + DBFILE)),
_consumer_manager(std::make_shared<ConsumerManager>()),
_connection_manager(std::make_shared<ConnectionManager>()),
_threadpool(std::make_shared<ThreadPool>())
{
// 还需要针对历史消息中的所有队列,去初始化队列的消费者管理结构
QueueMap qm = _virtual_host->allQueues(); // 获取所有队列信息
for (auto &q : qm)
{
_consumer_manager->initQueueConsumer(q.first); // 根据队列名称去初始化队列的消费者结构
}
// 注册业务请求回调处理函数
_dispatcher.registerMessageCallback<my_mq::openChannelRequest>(std::bind(&Server::onOpenChannel, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::closeChannelRequest>(std::bind(&Server::onCloseChannel, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::declareExchangeRequest>(std::bind(&Server::onDeclareExchange, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::deleteExchangeRequest>(std::bind(&Server::onDeleteExchange, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::declareQueueRequest>(std::bind(&Server::onDeclareQueue, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::deleteQueueRequest>(std::bind(&Server::onDeleteQueue, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::queueBindRequest>(std::bind(&Server::onQueueBind, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::queueUnBindRequest>(std::bind(&Server::onQueueUnBind, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::basicPublishRequest>(std::bind(&Server::onBasicPublish, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::basicAckRequest>(std::bind(&Server::onBasicAck, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::basicConsumeRequest>(std::bind(&Server::onBasicConsume, this, P_1, P_2, P_3));
_dispatcher.registerMessageCallback<my_mq::basicCancelRequest>(std::bind(&Server::onBasicCancel, this, P_1, P_2, P_3));
_server.setMessageCallback(std::bind(&ProtobufCodec::onMessage, _codec.get(), P_1, P_2, P_3));
_server.setConnectionCallback(std::bind(&Server::onConnection, this, P_1));
}
void start()
{
_server.start();
_baseloop.loop();
}
private: // 有打开关闭信道,声明删除交换机,声明删除队列,绑定解绑队列,消息发布与确认,队列消息订阅与取消订阅
// 打开信道
void onOpenChannel(const muduo::net::TcpConnectionPtr &conn, const openChannelRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("打开信道时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
return mconn->openChannel(message);
}
// 关闭信道
void onCloseChannel(const muduo::net::TcpConnectionPtr &conn, const closeChannelRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("关闭信道时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
return mconn->closeChannel(message);
}
// 声明交换机
void onDeclareExchange(const muduo::net::TcpConnectionPtr &conn, const declareExchangeRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("声明交换机时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
Channel::ptr cp = mconn->getChannel(message->cid());
if (cp.get() == nullptr)
{
DLOG("声明交换机时,没有找到信道!");
return;
}
return cp->declareExchange(message);
}
// 删除交换机
void onDeleteExchange(const muduo::net::TcpConnectionPtr &conn, const deleteExchangeRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("删除交换机时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
Channel::ptr cp = mconn->getChannel(message->cid());
if (cp.get() == nullptr)
{
DLOG("删除交换机时,没有找到信道!");
return;
}
return cp->deleteExchange(message);
}
// 声明队列
void onDeclareQueue(const muduo::net::TcpConnectionPtr &conn, const declareQueueRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("声明队列时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
Channel::ptr cp = mconn->getChannel(message->cid());
if (cp.get() == nullptr)
{
DLOG("声明队列时,没有找到信道!");
return;
}
return cp->declareQueue(message);
}
// 删除队列
void onDeleteQueue(const muduo::net::TcpConnectionPtr &conn, const deleteQueueRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("删除队列时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
Channel::ptr cp = mconn->getChannel(message->cid());
if (cp.get() == nullptr)
{
DLOG("删除队列时,没有找到信道!");
return;
}
return cp->deleteQueue(message);
}
// 队列绑定
void onQueueBind(const muduo::net::TcpConnectionPtr &conn, const queueBindRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("队列绑定时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
Channel::ptr cp = mconn->getChannel(message->cid());
if (cp.get() == nullptr)
{
DLOG("队列绑定时,没有找到信道!");
return;
}
return cp->queueBind(message);
}
// 队列解绑
void onQueueUnBind(const muduo::net::TcpConnectionPtr &conn, const queueUnBindRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("队列解除绑定时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
Channel::ptr cp = mconn->getChannel(message->cid());
if (cp.get() == nullptr)
{
DLOG("队列解除绑定时,没有找到信道!");
return;
}
return cp->queueUnBind(message);
}
// 消息发布
void onBasicPublish(const muduo::net::TcpConnectionPtr &conn, const basicPublishRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("发布消息时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
Channel::ptr cp = mconn->getChannel(message->cid());
if (cp.get() == nullptr)
{
DLOG("发布消息时,没有找到信道!");
return;
}
return cp->basicPublish(message);
}
// 消息确认
void onBasicAck(const muduo::net::TcpConnectionPtr &conn, const basicAckRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("确认消息时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
Channel::ptr cp = mconn->getChannel(message->cid());
if (cp.get() == nullptr)
{
DLOG("确认消息时,没有找到信道!");
return;
}
return cp->basicAck(message);
}
// 队列消息订阅
void onBasicConsume(const muduo::net::TcpConnectionPtr &conn, const basicConsumeRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("队列消息订阅时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
Channel::ptr cp = mconn->getChannel(message->cid());
if (cp.get() == nullptr)
{
DLOG("队列消息订阅时,没有找到信道!");
return;
}
return cp->basicConsume(message);
}
// 队列消息取消订阅
void onBasicCancel(const muduo::net::TcpConnectionPtr &conn, const basicCancelRequestPtr &message, muduo::Timestamp)
{
Connection::ptr mconn = _connection_manager->getConnection(conn);
if (mconn.get() == nullptr)
{
DLOG("队列消息取消订阅时,没有找到连接对应的Connection对象!");
conn->shutdown();
return;
}
Channel::ptr cp = mconn->getChannel(message->cid());
if (cp.get() == nullptr)
{
DLOG("队列消息取消订阅时,没有找到信道!");
return;
}
return cp->basicCancel(message);
}
void onUnknownMessage(const muduo::net::TcpConnectionPtr &conn, const MessagePtr &message, muduo::Timestamp)
{
LOG_INFO << "onUnknownMessage: " << message->GetTypeName();
conn->shutdown();
}
void onConnection(const muduo::net::TcpConnectionPtr &conn)
{
if (conn->connected())
_connection_manager->newConnection(_virtual_host, _consumer_manager, _codec, conn, _threadpool);
else
_connection_manager->delConnection(conn);
}
private:
muduo::net::EventLoop _baseloop;
muduo::net::TcpServer _server; // 服务器对象
ProtobufDispatcher _dispatcher; // 请求分发器对象--要向其中注册请求处理函数
ProtobufCodecPtr _codec; // protobuf协议处理器--针对收到的请求数据进行protobuf协议处理
VirtualHost::ptr _virtual_host; // 虚拟机管理
ConsumerManager::ptr _consumer_manager; // 消费者管理
ConnectionManager::ptr _connection_manager; // 连接管理
ThreadPool::ptr _threadpool; // 线程池
};
}
#endif
bash
g++ -std=c++11 mq_server.cc ../mqcommon/message.pb.cc ../mqcommon/channel.pb.cc ../third/proto/codec.cc -o server -lgtest -lprotobuf -lsqlite3 -I../third/proto -I../mqserver -lmuduo_net -lmuduo_base -lz







