手撕基于AMQP协议的简易消息队列
项目介绍
码云仓库:MessageQueues: 仿Rabbit实现消息队列
文章概要
- 本文将介绍从零搭建一个简易消息队列的方法,目的是了解并学习消息队列的底层原理与逻辑,编写一个独立的服务器程序。从搭建开发环境开始,到编写一些工作组件,再到正式开始编写消息队列。
什么是消息队列?
-
消息队列是一个服务器程序,也可以看做是一个被封装为独立服务器程序的阻塞队列,而阻塞队列的最大用途就是用来实现生产者消费者模型,这个模型可以让我们的程序实现解耦合、支持并发、支持忙闲不均、削峰填谷等功能。
-
市面上成熟的消息队列有很多,如:RabbitMQ、Kafka、RocketMQ、ActiveMQ…
开发环境
- Linux(CentOS-7.6),可根据自己需要更换为Ubuntu
- VSCode
- g++/gdb
- Makefile(也可以使用CMake)
所用技术栈
- C++
- 序列化框架:Protobuf
- 网络通信:自定义应用层协议+Muduo库:对tcp⻓连接的封装、并且使⽤epoll的事件驱动模式,实现⾼并发服务器与客⼾端
- 源数据信息数据库:SQLite3(因为本项目数据量较小,所以选取了一个更加轻量级的数据库)
- 单元测试框架:Gtest
开发环境的搭建
- 这里分别列举CentOS7.6与Ubuntu-22.04的环境配置
CentOS7.6的环境搭建
安装wget
sudo yum install wget
更换软件源
sudo mv /etc/yum.repos.d/CentOS-Base.repo/etc/yum.repos.d/CentOS-Base.repo.bak
sudo wget -O /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
sudo yum clean all
sudo yum makecache
安装scl软件源
sudo yum install centos-release-scl-rh centos-release-scl
安装epel软件源
sudo yum install epel-release
安装lrzsz传输工具
sudo yum install lrzsz
安装高版本gcc/g++编译器
sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++
echo "source /opt/rh/devtoolset-7/enable" >> ~/.bashrc
source ~/.bashrc
安装git
sudo yum install git
安装CMake
sudo yum install cmake
安装Protobuf
- 安装Protobuf依赖库
sudo yum install autoconf automake libtool curl make gcc-c++ unzip
- 下载Protobuf包
wget
https://github.com/protocolbuffers/protobuf/releases/download/v3.20.2/protobuf-all-3.20.2.tar.gz
- 编译安装
tar -zxf protobuf-all-3.20.2.tar.gz//解压缩
cd protobuf-3.20.2/ //切换目录
./autogen.sh
./configure
make //开始编译,大概需要编译15分钟
sudo make install //开始安装
protoc --version//确认是否安装成功
安装Muduo库
git clone https://github.com/chenshuo/Muduo.git //git方式
wget https://gitee.com/hansionz/mq/raw/master/resource/Muduo-master.zip //如果觉得git下的太慢,可以使用这一条指令
sudo yum install gcc-c++ cmake make zlib zlib-devel boost-devel //安装依赖环境
./build.sh
./build.sh install //运行脚本编译安装
安装SQLite3
sudo yum install sqlite-devel
sqlite3 --version //验证是否安装成功
安装Gtest
sudo yum install epel-release
sudo yum install dnf
sudo dnf install dnf-plugins-core
sudo dnf install gtest gtest-devel
- 使用dnf安装软件时可能会报以下错误
Traceback (most recent call last):
File "/usr/bin/dnf", line 57, in <module>
from dnf.cli import main
File "/usr/lib/python2.7/site-packages/dnf/__init__.py", line 30, in <module>
import dnf.base
File "/usr/lib/python2.7/site-packages/dnf/base.py", line 29, in <module>
import libdnf.transaction
File "/usr/lib64/python2.7/site-packages/libdnf/__init__.py", line 3, in
<module>
from . import conf
File "/usr/lib64/python2.7/site-packages/libdnf/conf.py", line 17, in
<module>
_conf = swig_import_helper()
File "/usr/lib64/python2.7/site-packages/libdnf/conf.py", line 16, in
swig_import_helper
return importlib.import_module('_conf')
File "/usr/lib64/python2.7/importlib/__init__.py", line 37, in import_module
__import__(name)
ImportError: No module named _conf
- 这通常是因为python版本过低导致的,可以输入以下命令解决
yum update python* //升级python
yum install dnf-data dnf-plugins-core libdnf-devel libdnf python2-dnf-plugin-migrate dnf-automatic -y # 安装如下软件
-
测试GTest是否安装成功
-
测试代码
#include<gtest/gtest.h> int add(int a,int b){ return a+b; } TEST(testCase,test1){ EXPECT_EQ(add(2,3),5); } int main(int argc,char **argv){ testing::InitGoogleTest(&argc,argv); return RUN_ALL_TESTS(); }
-
编译源文件
g++ test_gtest.cc -o gtest -lgtest
-
运行可执行文件
./gtest
-
成功
[==========] Running 1 test from 1 test case. [----------] Global test environment set-up. [----------] 1 test from testCase [ RUN ] testCase.test1 [ OK ] testCase.test1 (0 ms) [----------] 1 test from testCase (0 ms total) [----------] Global test environment tear-down [==========] 1 test from 1 test case ran. (1 ms total) [ PASSED ] 1 test.
-
第三方库的介绍
Protobuf
什么是Protobuf(Protocol Buffer)?
- Protobuf是数据结构序列化和反序列化框架
Protobuf的特点有哪些?
- 通用性:语⾔⽆关、平台⽆关。即 ProtoBuf ⽀持 Java、C++、Python 等多种语⾔,⽀持多个平台
- ⾼效:即⽐ XML 更⼩、更快、更为简单
- 扩展性、兼容性好:你可以更新数据结构,⽽不影响和破坏原有的旧程序
Protobuf的使用
-
使用流程:
- 编写后缀为.proto的文件,在该文件中定义结果对象及属性内容
- 使⽤ protoc 编译器编译 .proto ⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中
- 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对 .proto ⽂件中定义的字段进⾏设置和获取,和对自定义对象进⾏序列化和反序列化
-
使用示例
-
.proto文件的编写,以学生类为例
//指定proto3语法,若没有指定,编译器会使用proto2语法 syntax = "proto3"; //Protocol Buffers 语⾔版本3,简称 proto3,是 .proto ⽂件最新的语法版本。proto3 简化了 Protocol Buffers 语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤ Java,C++,Python 等多种语⾔⽣成 protocol buffer 代码。在 .proto ⽂件中,要使⽤ syntax = "proto3"; 来指定⽂件语法为 proto3,并且必须写在除去注释内容的第⼀⾏。 如果没有指定,编译器会使⽤proto2语法。 package Student;//确定命名空间 //package 是⼀个可选的声明符,能表⽰ .proto ⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为了避免我们定义的消息出现冲突。 message Student//自定义结构 { uint64 StudentNumber = 1; string StudentName = 2; }; //ProtoBuf 就是以 message 的⽅式来⽀持我们定制协议字段,后期帮助我们形成类和⽅法来使⽤。
-
.proto文件的编写规范:
- ⽂件命名应该使⽤全⼩写字⺟命名,多个字⺟之间⽤ _ 连接,如student.proto
- 书写 .proto ⽂件代码时,应使⽤ 2 个空格的缩进
-
定义消息字段;
-
字段定义格式为:字段类型 字段名 = 字段唯⼀编号;
-
字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _ 连接
-
字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)
.proto 类型 C++ 类型 说明 double
double
双精度浮点数 float
float
单精度浮点数 int32
int32
变长编码的32位整数。负数编码效率低,若字段可能为负值,建议改用 sint32
。int64
int64
变长编码的64位整数。负数编码效率低,若字段可能为负值,建议改用 sint64
。uint32
uint32
变长编码的无符号32位整数 uint64
uint64
变长编码的无符号64位整数 sint32
int32
变长编码的32位整数,ZigZag编码优化负数效率,适用于可能包含负值的场景。 sint64
int64
变长编码的64位整数,ZigZag编码优化负数效率,适用于可能包含负值的场景。 fixed32
uint32
定长4字节的无符号32位整数,数值常大于2^28时更高效(相比 uint32
)。fixed64
uint64
定长8字节的无符号64位整数,数值常大于2^56时更高效(相比 uint64
)。sfixed32
int32
定长4字节的有符号32位整数 sfixed64
int64
定长8字节的有符号64位整数 bool
bool
布尔值( true
/false
)string
std::string
UTF-8或ASCII编码字符串,长度不超过2^32。 bytes
std::string
任意二进制字节序列,长度不超过2^32(实际存储为 std::string
,但语义上区分于字符串)。enum
枚举类型 用户自定义的枚举类型,对应C++的 enum
或enum class
。message
类(class) 用户自定义的消息类型,对应C++中的类结构,可嵌套其他消息或枚举。 repeated
std::vector<T>
动态数组,存储相同类型的多个元素(例如 repeated int32
对应std::vector<int32>
)。map<K, V>
std::unordered_map<K, V>
键值对集合,键类型需为整数、布尔或字符串,值类型可为任意有效类型(例如 map<string, int32>
对应std::unordered_map<string, int32>
)。 -
字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。
-
-
字段唯⼀编号的范围:1 ~ 536,870,911 (2^29 - 1) ,其中 19000 ~ 19999 不可⽤。19000 ~ 19999 不可⽤是因为:在 Protobuf 协议的实现中,对这些数进⾏了预留。如果⾮要在.proto⽂件中使⽤这些预留标识号,例如将 name 字段的编号设置为19000,编译时就会报警
-
-
编译.proto文件
protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto //将.proto文件编译为cpp文件的格式 // protoc 是 Protocol Buffer 提供的命令⾏编译⼯具。 // --proto_path 指定 被编译的.proto⽂件所在⽬录,可多次指定。可简写成 -I IMPORT_PATH 。如不指定该参数,则在当前⽬录进⾏搜索。当某个.proto ⽂件 import 其他.proto ⽂件时,或需要编译的 .proto ⽂件不在当前⽬录下,这时就要⽤-I来指定搜索⽬录。 // --cpp_out= 指编译后的⽂件为 C++ ⽂件。 // OUT_DIR 编译后⽣成⽂件的⽬标路径。 // path/to/file.proto 要编译的.proto⽂件
-
编译student.proto文件命令如下
protoc --cpp_out=./ ./student.proto
-
编译生成的cpp代码内容:
- 对于每个message,都会生成一个对应的消息类
- 在消息类中,编译器为每个字段提供了获取和设置⽅法,以及⼀下其他能够操作字段的⽅法
- 编辑器会针对于每个 .proto ⽂件⽣成 .h 和 .cc ⽂件,分别⽤来存放类的声明与类的实现student.pb.h、student.pb.cc 部分代码展⽰
-
-
序列化与反序列化的使用
-
示例程序
#include<iostream> #include"student.pb.h" int main() { //设置学生的属性 Student::Student s1; s1.set_studentname("张三"); s1.set_studentnumber(1000); //将学生进行序列化 std::string str; str = s1.SerializeAsString(); //将学生信息反序列化 Student::Student ret; ret.ParseFromString(str); //输出结果 std::cout<<ret.studentname()<<std::endl<<ret.studentnumber()<<std::endl; return 0; }
-
对程序编译并执行
g++ -o testStudent student.pb.cc test_student.cc -std=c++11 -lprotobuf //-lprotobuf:链接protobuf库⽂件 -std=c++11:⽀持C++11
-
程序结果
-
-
Muduo库的介绍
什么是Muduo库?
-
Muduo由陈硕⼤佬开发,是⼀个基于⾮阻塞IO和事件驱动的C++⾼并发TCP⽹络编程库。 它是⼀款基于主从Reactor模型的⽹络库,其使⽤的线程模型是one loop per thread, 所谓one loop per thread指的是:
-
⼀个线程只能有⼀个事件循环(EventLoop), ⽤于响应计时器和IO事件
-
⼀个⽂件描述符只能由⼀个线程进⾏读写,换句话说就是⼀个TCP连接必须归属于某个EventLoop管理
-
-
Muduo库常用接⼝介绍
-
muduo::net::TcpServer类基础介绍
// TcpConnection 的共享指针类型别名,用于安全地管理 TcpConnection 对象的生命周期 typedef std::shared_ptr<TcpConnection> TcpConnectionPtr; // 连接事件回调类型(连接建立/关闭时触发) // 参数:const TcpConnectionPtr& 表示关联的 TCP 连接对象 typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback; // 消息到达回调类型(当接收到数据时触发) // 参数: // - const TcpConnectionPtr&: 关联的 TCP 连接对象 // - Buffer*: 指向接收缓冲区的指针,存储待处理的字节数据 // - Timestamp: 消息到达的时间戳(通常为系统时间或相对时间) typedef std::function<void (const TcpConnectionPtr&, Buffer*, Timestamp)> MessageCallback; // IP 地址封装类(继承自可 基类,允许对象拷贝) // 用途:表示一个网络端点(IP + 端口),支持 IPv4/IPv6 class InetAddress : public muduo::copyable { public: // 构造函数 // 参数: // - ip: 字符串形式的 IP 地址(如 "192.168.1.1" 或 "2001:db8::1") // - port: 16 位端口号 // - ipv6: 是否为 IPv6 地址(默认 false,即 IPv4) InetAddress(StringArg ip, uint16_t port, bool ipv6 = false); }; // TCP 服务器类(继承自不可 基类,禁止对象拷贝) // 功能:管理 TCP 连接监听、处理客户端连接和消息收发 class TcpServer : noncopyable { public: // 端口复用选项枚举 enum Option { kNoReusePort, // 不启用 SO_REUSEPORT(默认) kReusePort, // 启用 SO_REUSEPORT,允许多进程/线程绑定同一端口 }; // 构造函数 // 参数: // - loop: EventLoop 事件循环对象,用于驱动网络事件处理 // - listenAddr: 服务器监听的地址和端口 // - nameArg: 服务器名称标识(用于日志或调试) // - option: 端口复用选项(默认 kNoReusePort) TcpServer(EventLoop* loop, const InetAddress& listenAddr, const string& nameArg, Option option = kNoReusePort); // 设置 IO 线程池的大小(用于处理连接的线程数) // 注:通常设置为 CPU 核心数,0 表示所有操作在 EventLoop 线程执行 void setThreadNum(int numThreads); // 启动服务器,开始监听并处理连接 void start(); // 设置新连接建立/关闭时的回调函数 void setConnectionCallback(const ConnectionCallback& cb) { connectionCallback_ = cb; } // 赋值给成员变量 connectionCallback_ // 设置接收到消息时的业务处理回调函数 void setMessageCallback(const MessageCallback& cb) { messageCallback_ = cb; } // 赋值给成员变量 messageCallback_
-
muduo::net::EventLoop类基础介绍
// 事件循环类(不可 ,遵循 one loop per thread 设计) // 核心职责:驱动 IO 事件监听、定时器调度、跨线程任务执行 class EventLoop : noncopyable { public: /// 启动事件循环(无限循环,阻塞直到调用 quit()) /// **必须**在创建该对象的线程中调用 void loop(); /// 终止事件循环 /// **非完全线程安全**:通过裸指针调用时需同步,建议通过 shared_ptr 调用确保安全 void quit(); /// 在指定时间点运行定时器回调 /// 参数: /// - time: 触发时间戳(基于 Timestamp 的时序) /// - cb: 定时器触发时的回调函数 /// 返回值:TimerId 用于后续取消定时器 /// **线程安全**:可从其他线程调用 TimerId runAt(Timestamp time, TimerCallback cb); /// 在延迟指定秒数后运行回调 /// 参数: /// - delay: 延迟时间(单位:秒) /// - cb: 延迟结束后的回调函数 /// 返回值:TimerId /// **线程安全**:可从其他线程调用 TimerId runAfter(double delay, TimerCallback cb); /// 每隔指定间隔重复运行回调 /// 参数: /// - interval: 间隔时间(单位:秒) /// - cb: 每次触发时的回调函数 /// 返回值:TimerId /// **线程安全**:可从其他线程调用 TimerId runEvery(double interval, TimerCallback cb); /// 取消指定定时器 /// 参数: /// - timerId: 由 runAt/runAfter/runEvery 返回的 ID /// **线程安全**:可从其他线程调用 void cancel(TimerId timerId); private: std::atomic<bool> quit_; // 原子标志位,控制循环退出 std::unique_ptr<Poller> poller_; // 事件监听器(如 epoll/kqueue) mutable MutexLock mutex_; // 互斥锁,保护 pendingFunctors_ std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_); // 跨线程提交的待执行任务队列 };
-
muduo::net::TcpConnection类基础介绍
/** * TCP 连接核心类(不可 ,支持共享指针安全访问) * 职责:封装已建立的 TCP 连接,管理连接生命周期及事件处理 * 设计特点:基于 Reactor 模式,通过 Channel 监听 socket 事件,实现非阻塞 IO */ class TcpConnection : noncopyable, // 禁止拷贝语义 public std::enable_shared_from_this<TcpConnection> // 支持安全共享指针[6,7] { public: /** * 构造函数(由 TcpServer/TcpClient 内部调用,用户不应直接创建) * @param loop 所属事件循环(sub-Reactor) * @param nameArg 连接名称标识(用于日志) * @param sockfd 已连接的套接字描述符 * @param localAddr 本地端点地址 * @param peerAddr 对端端点地址 * 初始化流程: * 1. 创建 Socket 和 Channel 对象[2,4] * 2. 注册 Channel 的事件回调(读->handleRead,写->handleWrite)[2,4] * 3. 设置初始状态为 kConnecting */ TcpConnection(EventLoop* loop, const string& name, int sockfd, const InetAddress& localAddr, const InetAddress& peerAddr); /// 连接状态查询(线程安全) bool connected() const { return state_ == kConnected; } // 已建立连接 bool disconnected() const { return state_ == kDisconnected; } // 已断开 /// 数据发送接口(支持多种数据格式,线程安全) void send(string&& message); // C++11 移动语义优化 void send(const void* message, int len); // 原始数据指针 void send(const StringPiece& message); // 字符串片段 void send(Buffer* message); // 交换缓冲区所有权 /// 关闭连接(非线程安全,需避免并发调用) void shutdown(); /// 上下文数据存取(支持任意类型扩展) void setContext(const boost::any& context) { context_ = context; } const boost::any& getContext() const { return context_; } boost::any* getMutableContext() { return &context_; } /// 回调函数设置接口 void setConnectionCallback(const ConnectionCallback& cb) { connectionCallback_ = cb; } // 连接状态变化回调 void setMessageCallback(const MessageCallback& cb) { messageCallback_ = cb; } // 消息到达回调 private: /// 连接状态机(原子操作保障线程安全) enum StateE { kDisconnected, // 初始/已断开状态 kConnecting, // 正在建立连接(Acceptor 创建后) kConnected, // 已建立连接(Channel 注册到 Poller) kDisconnecting // 正在关闭连接(主动调用 shutdown) }; // 核心成员变量 EventLoop* loop_; // 所属事件循环(保证IO操作线程安全) ConnectionCallback connectionCallback_; // 连接建立/关闭回调 MessageCallback messageCallback_; // 数据到达回调 WriteCompleteCallback writeCompleteCallback_; // 发送完成回调(需额外设置)[1] boost::any context_; // 用户自定义上下文(如会话ID) };
-
muduo::net::TcpClient类基础介绍
/** * TCP 客户端核心类(不可 ),基于 muduo 网络库设计 * 职责:管理客户端与服务器的单条 TCP 连接,实现异步连接与消息处理 * 设计特点:通过 Connector 发起连接,连接成功后创建 TcpConnection 管理会话 */ class TcpClient : noncopyable { public: /** * 构造函数 * @param loop 事件循环对象(sub-Reactor) * @param serverAddr 服务器地址(IP + 端口) * @param nameArg 客户端名称标识(用于日志) * 初始化流程: * 1. 创建 Connector 对象发起异步连接 * 2. 连接成功后创建 TcpConnection 对象 */ TcpClient(EventLoop* loop, const InetAddress& serverAddr, const string& nameArg); /// 强制显式定义析构函数(因包含 unique_ptr 成员) ~TcpClient(); /// 发起异步连接(线程安全,可在任意线程调用) void connect(); /// 关闭连接(非线程安全,需在 IO 线程调用) void disconnect(); /// 停止客户端(停止重连机制) void stop(); /// 获取当前连接对象(需加锁保证线程安全) TcpConnectionPtr connection() const { MutexLockGuard lock(mutex_); // 互斥锁保护连接对象 return connection_; } // 回调函数设置接口 void setConnectionCallback(ConnectionCallback cb) { connectionCallback_ = std::move(cb); } // 连接状态变化回调 void setMessageCallback(MessageCallback cb) { messageCallback_ = std::move(cb); } // 消息到达回调 private: EventLoop* loop_; // 所属事件循环(sub-Reactor) ConnectionCallback connectionCallback_; MessageCallback messageCallback_; TcpConnectionPtr connection_ GUARDED_BY(mutex_); // 当前连接(受互斥锁保护) }; /** * 倒计时闩锁类(不可 ),用于线程间同步控制 * 用途:确保异步操作完成前阻塞等待(如等待 TCP 连接建立) * 原理:基于条件变量实现的线程同步原语 */ class CountDownLatch : noncopyable { public: /// 初始化计数器(count > 0) explicit CountDownLatch(int count); /// 阻塞等待直到计数器归零(线程安全) void wait() { MutexLockGuard lock(mutex_); while (count_ > 0) { // 避免虚假唤醒 condition_.wait(); // 条件变量等待 } } /// 计数器减一(线程安全),归零时唤醒所有等待线程 void countDown() { MutexLockGuard lock(mutex_); --count_; if (count_ == 0) { condition_.notifyAll(); // 广播唤醒 } } private: mutable MutexLock mutex_; // 互斥锁 Condition condition_ GUARDED_BY(mutex_); // 条件变量 int count_ GUARDED_BY(mutex_); // 计数器 };
-
muduo::net::Buffer类基础介绍
/** * 网络数据缓冲区类(支持高效拷贝) * 设计目标:提供高效的读写操作,避免内存拷贝,支持协议解析常用操作 * 特性: * - 预留头部空间(kCheapPrepend)便于添加协议头 * - 自动扩容机制保证写入空间 * - 提供网络字节序与主机字节序转换接口 */ class Buffer : public muduo::copyable { public: static const size_t kCheapPrepend = 8; ///< 头部预留空间(用于添加协议头等场景) static const size_t kInitialSize = 1024; ///< 初始缓冲区大小(不包含预留空间) /** * 构造函数 * @param initialSize 初始可用空间大小(默认1024字节) * 总缓冲区大小 = kCheapPrepend + initialSize */ explicit Buffer(size_t initialSize = kInitialSize); /// 交换两个缓冲区内容(高效无拷贝) void swap(Buffer& rhs); /// 获取可读数据字节数 size_t readableBytes() const { return writerIndex_ - readerIndex_; } /// 获取可写空间字节数 size_t writableBytes() const { return buffer_.size() - writerIndex_; } /// 获取指向可读数据的指针(不会消费数据) const char* peek() const { return begin() + readerIndex_; } /// 查找第一个CRLF("\r\n")位置,用于行协议解析 const char* findEOL() const; const char* findEOL(const char* start) const; /// 消费指定长度数据(移动读指针) void retrieve(size_t len); /// 消费整型数据(自动转换网络字节序到主机字节序) void retrieveInt64() { retrieve(sizeof(int64_t)); } void retrieveInt32() { retrieve(sizeof(int32_t)); } void retrieveInt16() { retrieve(sizeof(int16_t)); } void retrieveInt8() { retrieve(sizeof(int8_t)); } /// 获取并消费所有可读数据为字符串 string retrieveAllAsString(); /// 获取并消费指定长度数据为字符串 string retrieveAsString(size_t len); /// 追加数据到缓冲区(自动扩容) void append(const StringPiece& str); void append(const char* data, size_t len); void append(const void* data, size_t len); /// 获取可写空间起始指针(写前需确保足够空间) char* beginWrite() { return begin() + writerIndex_; } const char* beginWrite() const { return begin() + writerIndex_; } /// 确认已写入数据长度(移动写指针) void hasWritten(size_t len) { writerIndex_ += len; } /// 追加整型数据(自动转换主机字节序到网络字节序) void appendInt64(int64_t x); void appendInt32(int32_t x); void appendInt16(int16_t x); void appendInt8(int8_t x); /// 读取并消费整型数据(自动转换网络字节序) int64_t readInt64(); int32_t readInt32(); int16_t readInt16(); int8_t readInt8(); /// 查看整型数据(不移动读指针) int64_t peekInt64() const; int32_t peekInt32() const; int16_t peekInt16() const; int8_t peekInt8() const; /// 在头部预留空间添加数据(常用于添加协议头) void prependInt64(int64_t x); void prependInt32(int32_t x); void prependInt16(int16_t x); void prependInt8(int8_t x); void prepend(const void* data, size_t len); private: std::vector<char> buffer_; ///< 数据存储容器 size_t readerIndex_; ///< 读指针(指向可读数据起始位置) size_t writerIndex_; ///< 写指针(指向可写空间起始位置) static const char kCRLF[]; ///< 行结束符常量"\r\n" /// 获取缓冲区首地址 char* begin() { return &*buffer_.begin(); } const char* begin() const { return &*buffer_.begin(); } /// 空间不足时自动扩容(保证至少有len字节可写空间) void ensureWritableBytes(size_t len); }; // 示例用法: // Buffer buf; // buf.appendInt32(123); // 写入4字节网络序整数 // int32_t n = buf.readInt32(); // 读取并转换为主机序 // buf.prependInt32(0xdeadbeef); // 在头部插入魔数
-
Muduo库的快速上手
-
使用Muduo库搭建一个简单的英译汉服务器和客户端
-
大体流程:
一、服务端搭建流程(TranslateServer)
-
初始化事件循环与服务器
EventLoop loop; // 创建主事件循环 InetAddress listenAddr(8888); // 监听端口 TcpServer server(&loop, listenAddr, "TranslateServer");
-
注册回调函数
// 连接建立/关闭回调 server.setConnectionCallback(std::bind(&onConnection, ...)); // 消息到达回调 server.setMessageCallback(std::bind(&onMessage, ...));
-
启动服务
server.start(); // 启动监听线程 loop.loop(); // 进入事件循环(阻塞在此)
-
实现核心回调函数
void onConnection(const TcpConnectionPtr& conn) { // 连接状态变化处理 if (conn->connected()) { /* 记录新连接 */ } else { /* 清理连接资源 */ } } void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp) { string msg = buf->retrieveAllAsString(); // 读取数据 string result = translate(msg); // 业务处理 conn->send(result); // 发送响应 }
二、客户端搭建流程(TranslateClient)
-
创建IO线程与客户端
EventLoopThread ioThread; // 创建IO线程 TcpClient client(ioThread.startLoop(), serverAddr, "Client");
-
设置回调函数
client.setConnectionCallback(std::bind(&onClientConnection, ...)); client.setMessageCallback(std::bind(&onClientMessage, ...));
-
连接服务器
client.connect(); // 异步连接 latch.wait(); // 等待连接建立(使用CountDownLatch)
-
实现客户端回调
void onClientConnection(const TcpConnectionPtr& conn) { if (conn->connected()) { latch_.countDown(); // 连接成功通知 conn_ = conn; // 保存连接对象 } } void onClientMessage(const TcpConnectionPtr&, Buffer* buf, Timestamp) { cout << "收到响应: " << buf->retrieveAllAsString() << endl; }
-
发送请求
conn_->send("hello"); // 使用保存的连接对象发送数据
三、关键机制说明
-
Reactor模型
-
单线程处理所有IO事件(用户代码无需考虑锁)
-
EventLoop::loop()
驱动事件分发
-
-
回调机制
通过std::bind
绑定成员函数,处理三类事件:-
连接建立/断开
-
消息到达
-
写完成通知(示例未使用)
-
-
Buffer设计
-
自动管理读写指针(
retrieveAllAsString()
消费数据) -
支持高效预置空间(处理协议头)
-
-
线程安全
-
客户端
send()
需在IO线程调用(示例用CountDownLatch
同步) -
服务端无需显式锁(muduo保证回调在IO线程)
-
四、交互流程图解
客户端 服务端 | | | -- connect() --> | | | | ← 连接建立通知(CountDownLatch) -- | | | | -- send("hello") --> | | | | | -- 触发onMessage回调 | | ← 翻译处理 | | -- send("你好") | ← 响应到达触发onClientMessage -- |
-
-
音译汉TCP服务器代码
#include "muduo/net/EventLoop.h" #include "muduo/net/TcpConnection.h" #include "muduo/net/TcpServer.h" #include <functional> #include <iostream> #include <sys/types.h> #include <unordered_map> /** * 基于muduo网络库的简易翻译服务器 * 功能:接收客户端英文单词,返回中文翻译(示例实现简单字典查询) */ class TranslateServer { public: /** * 构造函数 * @param port 服务器监听端口号 * 初始化流程: * 1. 创建事件循环对象(_baseloop) * 2. 创建TCP服务器对象(_server),绑定全零地址(0.0.0.0)表示监听所有网络接口 * 3. 设置连接回调与消息回调 */ TranslateServer(uint16_t port) : _server(&_baseloop, // 事件循环对象 muduo::net::InetAddress("0.0.0.0", port), // 监听地址(IPv4任意地址) "TranslateServer", // 服务器名称(用于日志标识) muduo::net::TcpServer::kReusePort) // 启用端口复用选项(允许多线程监听相同端口) { // 绑定连接状态变化回调(lambda通过std::bind转换为函数对象) _server.setConnectionCallback( std::bind(&TranslateServer::ConnectionCallback, this, std::placeholders::_1)); // 绑定消息到达回调(参数占位符分别表示连接对象、缓冲区、时间戳) _server.setMessageCallback( std::bind(&TranslateServer::MessageCallback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } /// 启动服务器(开始监听并进入事件循环) void Start() { _server.start(); // 启动监听线程 _baseloop.loop(); // 进入主事件循环(阻塞在此处) } private: /** * 连接状态变化回调函数 * @param connection TCP连接智能指针 * 触发时机:当新连接建立或现有连接关闭时 */ void ConnectionCallback(const muduo::net::TcpConnectionPtr &connection) { if (connection->connected()) { std::cout << "新连接建立 [" << connection->peerAddress().toIpPort() << "]" << std::endl; } else { std::cout << "连接关闭 [" << connection->peerAddress().toIpPort() << "]" << std::endl; } } /** * 消息到达回调函数(核心业务逻辑) * @param connection TCP连接对象 * @param buffer 接收缓冲区(存储客户端发送的数据) * @param timestamp 消息到达时间戳 * 处理流程: * 1. 从缓冲区提取全部数据为字符串 * 2. 调用翻译函数获取结果 * 3. 通过TCP连接返回翻译结果 */ void MessageCallback(const muduo::net::TcpConnectionPtr &connection, muduo::net::Buffer *buffer, muduo::Timestamp timestamp) { // 提取缓冲区所有数据(自动移动读指针) std::string request = buffer->retrieveAllAsString(); // 执行翻译(示例实现为内存字典查询) std::string answer = translate(request); // 返回结果给客户端(send方法线程安全) if (!answer.empty()) { connection->send(answer + "\r\n"); // 添加换行符作为消息分隔符 } else { connection->send("未找到翻译: " + request + "\r\n"); } } /** * 简单字典翻译函数(实际项目应替换为数据库查询或API调用) * @param str 待翻译的英文单词 * @return 中文翻译结果,未找到返回空字符串 * 特点: * - 使用静态哈希表减少重复初始化开销 * - 当前仅支持全小写单词查询 */ const std::string translate(const std::string &str) { // 静态字典(初始化一次,后续查询共享) static std::unordered_map<std::string, std::string> dictionary{ {"hello", "你好"}, {"love", "爱"}, {"computer", "计算机"}, {"server", "服务器"} }; auto it = dictionary.find(str); return (it != dictionary.end()) ? it->second : ""; } private: muduo::net::EventLoop _baseloop; // 主事件循环对象(Reactor核心) muduo::net::TcpServer _server; // TCP服务器实例(管理监听和连接) }; int main() { // 创建服务器实例(监听8888端口) TranslateServer tmpserver(8888); std::cout << "翻译服务器启动,监听端口:8888" << std::endl; // 启动服务(阻塞在此处直到程序终止) tmpserver.Start(); return 0; }
-
音译汉客户端代码
#include "muduo/base/CountDownLatch.h" #include "muduo/net/EventLoopThread.h" #include "muduo/net/TcpClient.h" #include "muduo/net/TcpConnection.h" #include <functional> #include <iostream> #include <sys/types.h> /** * 基于muduo网络库的翻译客户端 * 功能:连接翻译服务器,发送英文单词并接收中文翻译结果 */ class TanslateClient // 注意类名拼写建议改为 TranslateClient { public: /** * 构造函数 * @param ip 服务器IP地址(默认本地环回) * @param port 服务器端口(默认8888) * 初始化流程: * 1. 创建事件循环线程(_loopthread) * 2. 创建TCP客户端(_client),绑定到指定服务器地址 * 3. 设置连接和消息回调 */ TanslateClient(const std::string &ip = "127.0.0.1", const int &port = 8888) : _loopthread(), // 事件循环线程(自动启动) _client( _loopthread.startLoop(), // 获取事件循环对象(启动IO线程) muduo::net::InetAddress(ip, port), // 服务器地址 "TanslateClient" // 客户端名称(日志标识) ), _count_down_lantch(1), // 连接同步闩锁(初始计数器1) _conn_ptr(nullptr) // 当前连接指针(线程安全需改进) { // 绑定连接状态变化回调 _client.setConnectionCallback( std::bind(&TanslateClient::ConnectionCallback, this, std::placeholders::_1)); // 绑定消息到达回调 _client.setMessageCallback( std::bind(&TanslateClient::MessageCallback, this, std::placeholders::_1, // TcpConnectionPtr std::placeholders::_2, // Buffer* std::placeholders::_3 // Timestamp )); } /// 连接到服务器(阻塞直到连接建立或失败) void Connect() { _client.connect(); // 发起异步连接 _count_down_lantch.wait(); // 阻塞等待连接成功信号 } /// 发送翻译请求到服务器(线程安全) void Send(const std::string &buffer) { // 需添加互斥锁保护_conn_ptr访问(当前实现存在线程安全隐患) if (_conn_ptr && _conn_ptr->connected()) { _conn_ptr->send(buffer + "\r\n"); // 添加消息分隔符 } else { std::cerr << "连接未就绪,发送失败" << std::endl; } } private: /** * 连接状态变化回调 * @param conn TCP连接智能指针 * 功能:处理连接建立/断开事件,更新连接状态 */ void ConnectionCallback(const muduo::net::TcpConnectionPtr &conn) { _conn_ptr = conn; // 需用互斥锁保护(当前非线程安全) if (conn->connected()) { std::cout << "成功连接服务器 [" << conn->peerAddress().toIpPort() << "]" << std::endl; _count_down_lantch.countDown(); // 释放等待线程 } else { std::cout << "与服务器断开连接" << std::endl; // 建议添加自动重连逻辑 } } /** * 消息到达回调 * @param buffer 接收缓冲区(包含服务器响应) * 功能:处理服务器返回的翻译结果 */ void MessageCallback(const muduo::net::TcpConnectionPtr &conn, muduo::net::Buffer *buffer, muduo::Timestamp timestamp) { // 提取并打印所有接收数据 std::cout << "[翻译结果] " << buffer->retrieveAllAsString() // 清空缓冲区 << std::endl; } private: muduo::net::EventLoopThread _loopthread; // 事件循环线程(IO线程) muduo::net::TcpClient _client; // TCP客户端实例 muduo::net::TcpConnectionPtr _conn_ptr; // 当前连接(需线程安全访问) muduo::CountDownLatch _count_down_lantch; // 连接同步闩锁 }; int main() { // 创建客户端实例(连接指定服务器) TanslateClient myclient("112.74.40.147", 8888); try { myclient.Connect(); // 阻塞直到连接建立 // 交互式发送模式 while (true) { std::cout << "请输入英文单词(输入quit退出): "; std::string input; std::cin >> input; if (input == "quit") break; myclient.Send(input); // 发送查询请求 } } catch (const std::exception& e) { std::cerr << "客户端异常: " << e.what() << std::endl; } return 0; }
-
使用Makefile文件进行编译(记得更改为自己的路径)
# 默认构建目标(当直接运行make时,会构建这两个目标) all: TranslateServer TanslateClient # 构建客户端可执行文件 TanslateClient(注意目标名称可能存在拼写错误,建议改为TranslateClient) # 依赖关系:需要 TanslateClient.cpp 文件 TanslateClient: TanslateClient.cpp # 编译命令说明: # -o $@ : 输出文件名为目标名称(即TanslateClient) # $^ : 输入文件为所有依赖项(即TanslateClient.cpp) # -std=c++11 : 使用C++11标准 # -I ../include : 添加头文件搜索路径(指向上级目录的include文件夹) # -L ../lib : 添加库文件搜索路径(指向上级目录的lib文件夹) # -lmuduo_net -lmuduo_base : 链接muduo网络库和基础库 # -lpthread : 链接pthread线程库 g++ -o $@ $^ -std=c++11 -I ../include -L ../lib -lmuduo_net -lmuduo_base -lpthread # 构建服务端可执行文件 TranslateServer # 依赖关系:需要 TranslateServer.cpp 文件 TranslateServer: TranslateServer.cpp # 参数说明同上 g++ -o $@ $^ -std=c++11 -I ../include -L ../lib -lmuduo_net -lmuduo_base -lpthread # 声明伪目标(防止存在同名文件时make误判) # 注:原写法不够规范,建议改为 .PHONY: all clean .PHONY: # 清理生成的可执行文件 clean: # 强制删除客户端和服务端可执行文件 # 风险提示:rm -rf 需谨慎使用 rm -rf TanslateClient TranslateServer
-
运行结果
-
服务器
-
客户端
-
-
-
基于Muduo库函数实现protobuf协议的通信
-
这里需要用到Muduo库里自带的协议处理器ProtobufCodec,以及自带的请求分发器ProtobufDispatcher,与前面我们使用自己制定的协议不同,在使用Protobuf进行通信时,我们在 设置服务器接受消息时调用的回调函数时(_server.setMessageCallback())并不是我们自己设定的消息回调函数了,而是使用协议处理器ProtobufCodec中的ProtobufCodec::onMessage()函数。也就是说服务器接收到消息之后,立即传给协议处理器ProtobufCodec,由它进行解析后,传给请求分发器ProtobufDispatcher去找到对应的消息处理函数,对应的函数处理完请求后,通过_codec.send(conn, resp);将结果发回给协议处理器,经过协议处理再发给客户端。
-
客户端的接受服务器的Protobuf请求流程与服务器差不多,这里就不过多赘述
-
定义具体的业务请求类型(也就是编写request.proto文件)
// 定义协议版本和包名 syntax = "proto3"; // 使用proto3语法 package HQJ; // 包命名空间(防⽌命名冲突) // 翻译服务请求消息(客户端 -> 服务端) message tanslate_request { string word = 1; // 待翻译的单词(如:输入"hello") }; // 翻译服务响应消息(服务端 -> 客户端) message tanslate_respond { string word = 1; // 翻译结果(如:返回"你好") }; // 加法计算请求消息(客户端 -> 服务端) message add_request { uint64 a = 1; // 第⼀个操作数(示例值:100) uint64 b = 2; // 第⼆个操作数(示例值:200) }; // 加法计算响应消息(服务端 -> 客户端) message add_respond { uint64 result = 1; // 计算结果(示例值:300) };
-
编译request.proto文件
protoc --cpp_out=./ ./request.proto
-
编写服务端代码
#include "muduo/protobuf/codec.h" #include "muduo/protobuf/dispatcher.h" #include "muduo/base/Logging.h" #include "muduo/base/Mutex.h" #include "muduo/net/EventLoop.h" #include "muduo/net/TcpServer.h" #include "request.pb.h" // 包含protobuf生成的头文件 #include <iostream> // 使用muduo库中的noncopyable类禁止拷贝 class protobuf_server : public muduo::noncopyable { public: // 定义protobuf消息的智能指针类型 typedef std::shared_ptr<HQJ::add_request> add_request_ptr; typedef std::shared_ptr<HQJ::add_respond> add_respond_ptr; typedef std::shared_ptr<HQJ::tanslate_request> tanslate_request_ptr; typedef std::shared_ptr<HQJ::tanslate_respond> tanslate_respond_ptr; // 构造函数,初始化服务器和消息处理逻辑 protobuf_server(int port = 8888) : _server(&_loop, muduo::net::InetAddress("0.0.0.0", port), "protobuf_server", muduo::net::TcpServer::kReusePort), // 注册未知消息处理函数 _dispatcher(std::bind(&protobuf_server::onUnknownMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)), // 初始化协议处理器,绑定消息处理函数 _codec(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)) { // 注册特定消息处理函数 _dispatcher.registerMessageCallback<HQJ::tanslate_request>( std::bind(&protobuf_server::onTranslate, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<HQJ::add_request>( std::bind(&protobuf_server::onAdd, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); // 设置连接回调 _server.setConnectionCallback( std::bind(&protobuf_server::onConnection, this, std::placeholders::_1)); // 设置消息回调,用于处理接收到的消息 _server.setMessageCallback( std::bind(&ProtobufCodec::onMessage, &_codec, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } // 启动服务器 void start() { _server.start(); _loop.loop(); } private: // 简单的翻译函数 const std::string translate(const std::string &str) { static std::unordered_map<std::string, std::string> dictionary{{"hello", "你好"}, {"love", "爱"}}; auto cur = dictionary.find(str); if (cur == dictionary.end()) { return ""; } else { return cur->second; } } // 未知消息处理函数 void onUnknownMessage(const muduo::net::TcpConnectionPtr &conn, const MessagePtr &message, muduo::Timestamp) { LOG_INFO << "onUnknownMessage: " << message->GetTypeName(); conn->shutdown(); } // 处理翻译请求 void onTranslate(const muduo::net::TcpConnectionPtr &conn, const tanslate_request_ptr&message, muduo::Timestamp) { std::string request = message->word(); std::string res = translate(request); HQJ::tanslate_respond resp; resp.set_word(res); _codec.send(conn, resp); } // 处理加法请求 void onAdd(const muduo::net::TcpConnectionPtr &conn, const add_request_ptr &message, muduo::Timestamp) { int num1 = message->a(); int num2 = message->b(); int result = num1 + num2; HQJ::add_respond resp; resp.set_result(result); _codec.send(conn, resp); } // 连接回调 void onConnection(const muduo::net::TcpConnectionPtr &connection) { if (connection->connected()) { std::cout << "创建新的连接!" << std::endl; } else { std::cout << "关闭连接!" << std::endl; } } private: muduo::net::EventLoop _loop; // 事件循环 muduo::net::TcpServer _server; // TCP服务器 ProtobufDispatcher _dispatcher; // 请求分发器 ProtobufCodec _codec; // Protobuf协议编解码器 }; int main() { protobuf_server prot_server(8888); prot_server.start(); return 0; }
-
编写客户端代码
#include "muduo/base/CountDownLatch.h" #include "muduo/base/Logging.h" #include "muduo/base/Mutex.h" #include "muduo/net/EventLoop.h" #include "muduo/net/EventLoopThread.h" #include "muduo/net/TcpClient.h" #include "muduo/protobuf/codec.h" #include "muduo/protobuf/dispatcher.h" #include "request.pb.h" #include <iostream> class Client { public: typedef std::shared_ptr<google::protobuf::Message> MessagePtr; typedef std::shared_ptr<HQJ::add_respond> add_respondPtr; typedef std::shared_ptr<HQJ::tanslate_respond> tanslate_respondPtr; Client(const std::string &sip, int sport) : _latch(1), _client(_loopthread.startLoop(), muduo::net::InetAddress(sip, sport), "Client"), _dispatcher(std::bind(&Client::onUnknownMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)), _codec(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)) { // 注册请求处理时的回调函数 _dispatcher.registerMessageCallback<HQJ::tanslate_respond>(std::bind(&Client::onTranslate, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<HQJ::add_respond>(std::bind(&Client::onAdd, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); // 设置客户端接收到消息时调用的函数,交给协议处理机中的消息回调函数,不用我们自己写了 _client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, &_codec, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); // 设置客户端连接成功时调用的函数,这个是需要自己写的 _client.setConnectionCallback(std::bind(&Client::onConnection, this, std::placeholders::_1)); } void connect() { _client.connect(); _latch.wait(); // 阻塞等待,直到连接建立成功 } //制造并发送一个翻译请求 void Translate(const std::string &msg) { HQJ::tanslate_request req; req.set_word(msg); send(&req); } //制造并发送一个加法请求 void Add(int num1, int num2) { HQJ::add_request req; req.set_a(num1); req.set_b(num2); send(&req); } private: // 连接时调用 void onConnection(const muduo::net::TcpConnectionPtr &conn) { if (conn->connected()) { _latch.countDown(); // 唤醒主线程中的阻塞 _conn = conn; } else { // 连接关闭时的操作 _conn.reset(); } } bool send(const google::protobuf::Message *message) { if (_conn->connected()) { // 连接状态正常,再发送,否则就返回false _codec.send(_conn, *message); return true; } return false; } void onUnknownMessage(const muduo::net::TcpConnectionPtr &, const MessagePtr &message, muduo::Timestamp) { LOG_INFO << "onUnknownMessage: " << message->GetTypeName(); } void onTranslate(const muduo::net::TcpConnectionPtr &conn, const tanslate_respondPtr &message, muduo::Timestamp) { std::cout << "翻译结果:" << message->word() << std::endl; } void onAdd(const muduo::net::TcpConnectionPtr &conn, const add_respondPtr &message, muduo::Timestamp) { std::cout << "加法结果:" << message->result() << std::endl; } private: muduo::CountDownLatch _latch; // 实现同步的 muduo::net::EventLoopThread _loopthread; // 异步循环处理线程 muduo::net::TcpConnectionPtr _conn; // 客户端对应的连接 muduo::net::TcpClient _client; // 客户端 ProtobufDispatcher _dispatcher; // 请求分发器 ProtobufCodec _codec; // 协议处理器 }; int main() { Client client("127.0.0.1", 8888); client.connect(); client.Translate("hello"); client.Add(11, 22); sleep(1); return 0; }
-
使用Makefile编译
# 默认构建目标(当直接运行make时,会构建client和server两个目标) all: client server # 构建客户端可执行文件 # 依赖文件:protobuf_client.cpp + request.pb.cc + muduo的protobuf编解码器实现 # 编译说明: # -std=c++11 : 使用C++11标准 # $^ : 表示所有依赖文件(即冒号后的全部文件) # -o $@ : 输出文件名为目标名称(即client) # -I../include : 添加头文件搜索路径(指向muduo头文件目录) # -L../lib : 添加库文件搜索路径(指向muduo库目录) # -l参数链接的库:muduo网络库/基础库、protobuf库、zlib压缩库 # -pthread : 启用POSIX线程支持 client: protobuf_client.cpp request.pb.cc ../include/muduo/protobuf/codec.cc g++ -std=c++11 $^ -o $@ -I../include -L../lib -lmuduo_net -lmuduo_base -pthread -lprotobuf -lz # 构建服务端可执行文件(参数含义与客户端相同) server: protobuf_server.cpp request.pb.cc ../include/muduo/protobuf/codec.cc g++ -std=c++11 $^ -o $@ -I../include -L../lib -lmuduo_net -lmuduo_base -pthread -lprotobuf -lz # 声明伪目标 .PHONY: # 清理生成的可执行文件 clean: rm -rf client server # 强制删除客户端和服务端可执行文件
-
执行结果
-
客户端
-
服务端
-
-
SQLlite3的快速上手
SQLite3的官方文档
https://www.sqlite.org/c3ref/funclist.html
SQLite3是什么?
- SQLite是⼀个进程内的轻量级数据库,它实现了⾃给⾃⾜的、⽆服务器的、零配置的、事务性的 SQL数据库引擎。它是⼀个零配置的数据库,这意味着与其他数据库不⼀样,我们不需要在系统中配置。像其他数据库,SQLite 引擎不是⼀个独⽴的进程,可以按应⽤程序需求进⾏静态或动态连接,SQLite直接访问其存储⽂件
为什么选择SQLite?
-
不需要⼀个单独的服务器进程或操作的系统(⽆服务器的)
-
SQLite 不需要配置
-
⼀个完整的 SQLite 数据库是存储在⼀个单⼀的跨平台的磁盘⽂件
-
SQLite 是⾮常⼩的,是轻量级的,完全配置时⼩于 400KiB,省略可选功能配置时⼩于250KiB
-
SQLite 是⾃给⾃⾜的,这意味着不需要任何外部的依赖
-
SQLite 事务是完全兼容 ACID 的,允许从多个进程或线程安全访问
-
SQLite ⽀持 SQL92(SQL2)标准的⼤多数查询语⾔的功能
-
SQLite 使⽤ ANSI-C 编写的,并提供了简单和易于使⽤的 API
-
SQLite 可在 UNIX(Linux, Mac OS-X, Android, iOS)和 Windows(Win32, WinCE, WinRT)中运⾏
SQLite3 C/C++ API介绍
sqlite3操作流程:
0. 查看当前数据库在编译阶段是否启动了线程安全
int sqlite3_threadsafe(); 0-未启⽤; 1-启⽤
需要注意的是sqlite3是有三种安全等级的:
1. ⾮线程安全模式
2. 线程安全模式(不同的连接在不同的线程/进程间是安全的,即⼀个句柄不能⽤于多线程
间)
3. 串⾏化模式(可以在不同的线程/进程间使⽤同⼀个句柄)
1. 创建/打开数据库⽂件,并返回操作句柄
int sqlite3_open(const char *filename, sqlite3 **ppDb) 成功返回SQLITE_OK
//若在编译阶段启动了线程安全,则在程序运⾏阶段可以通过参数选择线程安全等级
int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const
char *zVfs );
flag:
SQLITE_OPEN_READWRITE -- 以可读可写⽅式打开数据库⽂件
SQLITE_OPEN_CREATE -- 不存在数据库⽂件则创建
SQLITE_OPEN_NOMUTEX--多线程模式,只要不同的线程使⽤不同的连接即可保证线程
安全
SQLITE_OPEN_FULLMUTEX--串⾏化模式
返回:SQLITE_OK表⽰成功
2. 执⾏语句
int sqlite3_exec(sqlite3*, char *sql, int (*callback)
(void*,int,char**,char**),
void* arg, char **err)
int (*callback)(void*,int,char**,char**)
void* : 是设置的在回调时传⼊的arg参数
int:⼀⾏中数据的列数
char**:存储⼀⾏数据的字符指针数组
char**:每⼀列的字段名称
这个回调函数有个int返回值,成功处理的情况下必须返回0,返回⾮0会触发ABORT退出程序
返回:SQLITE_OK表⽰成功
3. 销毁句柄
int sqlite3_close(sqlite3* db); 成功返回SQLITE_OK
int sqlite3_close_v2(sqlite3*); 推荐使⽤--⽆论如何都会返回SQLITE_OK
获取错误信息
const char *sqlite3_errmsg(sqlite3* db);
SQLite3 C/C++ API 的简单使用
- SqliteHelper类的编写
- 用来方便我们进行数据库的操作
#include <iostream>
#include <sqlite3.h>
#include <string>
#include <vector>
#include"Logger.hpp"
class SqliteHelper
{
public:
// 定义一个回调函数类型,用于sqlite3_exec的回调
typedef int (*SqliteCallback)(void *, int, char **, char **);
// 构造函数,接收数据库文件名
SqliteHelper(const std::string dbfilename)
: _dbfilename(dbfilename)
{
}
// 打开数据库
// 参数safe_leve用于指定打开数据库的附加模式,默认为SQLITE_OPEN_FULLMUTEX
bool open(int safe_leve = SQLITE_OPEN_FULLMUTEX)
{
// 使用sqlite3_open_v2函数打开或创建数据库
int ret = sqlite3_open_v2(_dbfilename.c_str(), &_handler, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | safe_leve, nullptr);
if (ret != SQLITE_OK)
{
// std::cout << "创建/打开sqlite数据库失败: ";
// std::cout << sqlite3_errmsg(_handler) << std::endl;
ELOG("创建/打开sqlite数据库失败: %s",sqlite3_errmsg(_handler));
return false;
}
return true;
}
// 关闭数据库
void close()
{
// 使用sqlite3_close_v2函数关闭数据库
if (_handler)
sqlite3_close_v2(_handler);
}
// 执行SQL语句
// 参数sql为要执行的SQL语句,cb为回调函数,arg为回调函数的参数
bool exec(const std::string &sql, SqliteCallback cb, void *arg)
{
// 使用sqlite3_exec函数执行SQL语句
int ret = sqlite3_exec(_handler, sql.c_str(), cb, arg, nullptr);
if (ret != SQLITE_OK)
{
// std::cout << sql << std::endl;
// std::cout << "执行语句失败: ";
// std::cout << sqlite3_errmsg(_handler) << std::endl;
ELOG("执行语句:%s 失败!\t错误原因: %s",sql.c_str(),sqlite3_errmsg(_handler));
return false;
}
return true;
}
private:
std::string _dbfilename; // 数据库文件名
sqlite3 *_handler; // 数据库句柄
};
-
SqliteHelper类的测试,编写main.cc文件
#include "test_sqlite.hpp" // 包含SQLite操作的辅助类头文件 #include <cassert> // 包含断言头文件,用于检查程序中的假设 // SQLite查询回调函数,用于处理查询结果 int select_stu_callback(void *arg, int col_count, char **result, char **fields_name) { std::vector<std::string> *arry = (std::vector<std::string> *)arg; // 将void*类型的参数转换为std::vector<std::string>*类型 arry->push_back(result[0]); // 将查询结果的第一列添加到向量中 return 0; // 返回0表示成功 } int main() { SqliteHelper helper("./test.db"); // 创建一个SqliteHelper对象,用于操作数据库 // 1. 创建/打开库文件 assert(helper.open()); // 打开数据库文件,如果文件不存在则创建 // 2. 创建表(不存在则创建), 学生信息: 学号,姓名,年龄 std::string ct = "create table if not exists student(sn int primary key, name varchar(32), age int);"; assert(helper.exec(ct, nullptr, nullptr)); // 执行创建表的SQL语句 // 3. 新增数据 , 修改, 删除, 查询 std::string insert_sql = "insert into student values(1, '小明', 18), (2, '小黑', 19), (3, '小红', 18);"; assert(helper.exec(insert_sql, nullptr, nullptr)); // 执行插入数据的SQL语句 std::string update_sql = "update student set name='张小明' where sn=1"; assert(helper.exec(update_sql, nullptr, nullptr)); // 执行更新数据的SQL语句 std::string delete_sql = "delete from student where sn=3"; assert(helper.exec(delete_sql, nullptr, nullptr)); // 执行删除数据的SQL语句 std::string select_sql = "select name from student;"; std::vector<std::string> arry; assert(helper.exec(select_sql, select_stu_callback, &arry)); // 执行查询SQL语句,并使用回调函数处理查询结果 for (auto &name : arry) { std::cout << name << std::endl; // 输出查询结果 } // 4. 关闭数据库 helper.close(); // 关闭数据库连接 return 0; }
-
编译
g++ -std=c++11 main.cc -o main -lsqlite3
-
运行结果
对GTest的快速上手
什么是GTest?
- GTest是⼀个跨平台的 C++单元测试框架,由google公司发布。gtest是为了在不同平台上为编写C++单元测试⽽⽣成的。它提供了丰富的断⾔、致命和⾮致命判断、参数化等等
GTest的使用
-
TEST宏
TEST(test_case_name, test_name) TEST_F(test_fixture,test_name)
-
TEST:主要⽤来创建⼀个简单测试, 它定义了⼀个测试函数, 在这个函数中可以使⽤任何C++代码并且使⽤框架提供的断⾔进⾏检查
-
TEST_F:主要⽤来进⾏多样测试,适⽤于多个测试场景如果需要相同的数据配置的情况, 即相同的数据测不同的⾏为
-
GTest中的断言
-
分类:
- ASSERT_系列:如果当前点检测失败则退出当前函数_
- EXPECT_系列:如果当前点检测失败则继续往下执⾏
-
常用断言的介绍
// bool值检查 ASSERT_TRUE(参数),期待结果是true ASSERT_FALSE(参数),期待结果是false //数值型数据检查 ASSERT_EQ(参数1,参数2),传⼊的是需要⽐较的两个数 equal ASSERT_NE(参数1,参数2),not equal,不等于才返回true ASSERT_LT(参数1,参数2),less than,⼩于才返回true ASSERT_GT(参数1,参数2),greater than,⼤于才返回true ASSERT_LE(参数1,参数2),less equal,⼩于等于才返回true ASSERT_GE(参数1,参数2),greater equal,⼤于等于才返回true
-
简单的断言测试程序(assert.cpp)
#include <gtest/gtest.h> // 引入Google Test框架的头文件 #include <iostream> using std::cout; using std::endl; // 定义一个测试用例,属于"test"测试套件,用例名称为"testname_less_than" TEST(test, testname_less_than) { int age = 20; // 定义一个整型变量age,并初始化为20 EXPECT_LT(age, 18); // 使用EXPECT_LT断言宏,期望age小于18,若不满足则测试失败,但继续执行后续测试 } // 定义一个测试用例,属于"test"测试套件,用例名称为"testname_great_than" TEST(test, testname_great_than) { int age = 20; // 定义一个整型变量age,并初始化为20 EXPECT_GT(age, 18); // 使用EXPECT_GT断言宏,期望age大于18,若满足则测试通过,否则测试失败,但继续执行后续测试 } // 程序的主函数,程序的执行入口 int main(int argc,char* argv[]) { testing::InitGoogleTest(&argc,argv); // 初始化Google Test框架 RUN_ALL_TESTS(); // 运行所有已定义的测试用例 return 0; // 程序正常结束,返回0 }
-
编译
g++ -o assert assert.cpp -std=c++11 -lgtest
-
结果
-
GTest中的事件机制
- 事件机制的最⼤好处就是能够为我们各个测试⽤例提前准备好测试环境,并在测试完毕后⽤于销毁环境,这样有个好处就是如果我们有⼀端代码需要进⾏多种不同⽅法的测试,则可以通过测试机制在每个测试⽤例进⾏之前初始化测试环境和数据,并在测试完毕后清理测试造成的影响。
-
测试程序:⼀个测试程序只有⼀个main函数,也可以说是⼀个可执⾏程序是⼀个测试程序。该级别的事件机制是在程序的开始和结束执⾏
-
测试套件:代表⼀个测试⽤例的集合体,该级别的事件机制是在整体的测试案例开始和结束执⾏
-
测试⽤例:该级别的事件机制是在每个测试⽤例开始和结束都执⾏
-
GTest提供的三种常见事件:
-
全局事件:针对整个测试程序。实现全局的事件机制,需要创建⼀个⾃⼰的类,然后继承
testing::Environment类,然后分别实现成员函数 SetUp 和 TearDown ,同时在main函数内进
⾏调⽤ testing::AddGlobalTestEnvironment(new MyEnvironment); 函数添加全局的事件机制
-
简单的测试程序
#include <gtest/gtest.h> // 引入Google Test框架的头文件 #include <iostream> #include <map> using std::cout; // 使用std命名空间中的cout对象,用于标准输出 using std::endl; // 使用std命名空间中的endl对象,用于输出换行符 using std::string; // 自定义环境类,继承自testing::Environment class MyEnvironment : public testing::Environment { public: // 重写SetUp方法,用于单元测试前的环境初始化 virtual void SetUp() override { std::cout << "单元测试执行前的环境初始化!!\n"; } // 重写TearDown方法,用于单元测试后的环境清理 virtual void TearDown() override { std::cout << "单元测试执行完毕后的环境清理!!\n"; } }; // 定义两个测试用例,均属于MyEnvironment测试套件 TEST(MyEnvironment, test1) { std::cout << "单元测试1\n"; } TEST(MyEnvironment, test2) { std::cout << "单元测试2\n"; } // 定义一个全局的map,用于测试 std::map<string, string> mymap; // 自定义的MyMapTest环境类,继承自testing::Environment class MyMapTest : public testing::Environment { public: // 重写SetUp方法,用于单元测试前的环境初始化,向map中插入数据 virtual void SetUp() override { cout << "测试mymap单元测试" << endl; mymap.insert(std::make_pair("hello", "你好")); mymap.insert(std::make_pair("no", "不要")); } // 重写TearDown方法,用于单元测试后的环境清理,清空map virtual void TearDown() override { mymap.clear(); cout << "单元测试执行完毕" << endl; } }; // 定义两个测试用例,均属于MyMapTest测试套件 TEST(MyMapTest, test1) { // 期望mymap的大小为2 EXPECT_EQ(mymap.size(), 2); // 从mymap中删除键为"no"的元素 mymap.erase("no"); } TEST(MyMapTest, test2) { // 期望mymap的大小仍为2(但由于test1中已经删除了一个元素,这个期望实际上是不正确的) EXPECT_EQ(mymap.size(), 2); } // 程序的主函数,程序的执行入口 int main(int argc, char* argv[]) { testing::InitGoogleTest(&argc, argv); // 初始化Google Test框架 testing::AddGlobalTestEnvironment(new MyMapTest); // 注册MyMapTest环境 testing::AddGlobalTestEnvironment(new MyEnvironment); // 注册MyEnvironment环境 RUN_ALL_TESTS(); // 运行所有已定义的测试用例 return 0; // 程序正常结束,返回0 }
- 编译
g++ -o global global.cpp -std=c++11 -lgtest
-
结果
-
-
TestSuite事件:针对⼀个个测试套件。测试套件的事件机制我们同样需要去创建⼀个类,继承⾃testing::Test ,实现两个静态函数 SetUpTestCase 和 TearDownTestCase ,测试套件的事件机制不需要像全局事件机制⼀样在 main 注册,⽽是需要将我们平时使⽤的 TEST 宏改为 TEST_F 宏。
-
SetUpTestCase() 函数是在测试套件第⼀个测试⽤例开始前执⾏
-
TearDownTestCase() 函数是在测试套件最后⼀个测试⽤例结束后执⾏
-
需要注意TEST_F的第⼀个参数是我们创建的类名,也就是当前测试套件的名称,这样在
TEST_F宏的测试套件中就可以访问类中的成员了
-
简单的测试程序
#include<iostream> // 包含标准输入输出流库 #include<gtest/gtest.h> // 包含Google Test框架的头文件 #include<map> // 包含标准库中的map容器 // 使用声明,避免每次调用时都需要std::前缀 using std::string; using std::cout; using std::endl; using std::make_pair; // 定义测试套件SuitTest,继承自testing::Test class SuitTest : public testing::Test { public: // 在测试套件中的所有测试开始之前调用 static void SetUpTestCase() { std::cout << "环境1第一个TEST之前调用\n"; } // 在测试套件中的所有测试结束之后调用 static void TearDownTestCase() { std::cout << "环境1最后一个TEST之后调用\n"; } // 在每个测试用例之前调用,用于初始化测试环境 virtual void SetUp() override { mymap.insert(make_pair("hsq","哈士奇")); cout<<"这是每个单元测试自己的初始化"<<endl; } // 在每个测试用例之后调用,用于清理测试环境 virtual void TearDown() { cout<<"这是每个单元自己的结束函数"<<endl; } public: std::map<std::string,std::string> mymap; // 测试用例共享的成员变量 }; // 定义第一个测试用例testInsert,测试插入操作 TEST_F(SuitTest,testInsert) { mymap.insert(make_pair("nihao","你好")); EXPECT_EQ(mymap.size(),1); // 期望map的大小为1 } // 定义第二个测试用例testSize,测试map的大小 TEST_F(SuitTest,testSize) { EXPECT_EQ(mymap.size(),1); // 期望map的大小为1 } // 主函数,程序的入口点 int main(int argc,char* argv[]) { testing::InitGoogleTest(&argc,argv); // 初始化Google Test RUN_ALL_TESTS(); // 运行所有测试用例 return 0; }
-
编译
g++ -o suit suit.cpp -std=c++11 -lgtest
-
结果
-
-
-
TestCase事件: 针对⼀个个测试⽤例。测试⽤例的事件机制的创建和测试套件的基本⼀样,不同地⽅在于测试⽤例实现的两个函数分别是 SetUp 和 TearDown , 这两个函数也不是静态函数
- SetUp()函数是在⼀个测试⽤例的开始前执⾏
- TearDown()函数是在⼀个测试⽤例的结束后执⾏
- 也就是说,在TestSuite/TestCase事件中,每个测试⽤例,虽然它们同⽤同⼀个事件环境类,可以访问其中的资源,但是本质上每个测试⽤例的环境都是独⽴的,这样我们就不⽤担⼼不同的测试⽤例之间会有数据上的影响了,保证所有的测试⽤例都使⽤相同的测试环境进⾏测试。
-
前置工具类的编写
基于C++11的异步操作的线程池的编写
异步操作类future的介绍
-
std::future:
-
介绍:std::future是C++11标准库中的⼀个模板类,它表⽰⼀个异步操作的结果。当我们在多线程编程中使⽤异步任务时,std::future可以帮助我们在需要的时候获取任务的执⾏结果。std::future的⼀个重要特性是能够阻塞当前线程,直到异步操作完成,从⽽确保我们在获取结果时不会遇到未完成的操作。
-
应用场景:
-
异步任务: 当我们需要在后台执⾏⼀些耗时操作时,如⽹络请求或计算密集型任务等,std::future可以⽤来表⽰这些异步任务的结果。通过将任务与主线程分离,我们可以实现任务的并⾏处理,从⽽提⾼程序的执⾏效率
-
并发控制: 在多线程编程中,我们可能需要等待某些任务完成后才能继续执⾏其他操作。通过使⽤std::future,我们可以实现线程之间的同步,确保任务完成后再获取结果并继续执⾏后续操作
-
结果获取:std::future提供了⼀种安全的⽅式来获取异步任务的结果。我们可以使⽤std::future::get()函数来获取任务的结果,此函数会阻塞当前线程,直到异步操作完成。这样,在
调⽤get()函数时,我们可以确保已经获取到了所需的结果
-
-
三种搭配future执行异步操作的使用示例
-
使⽤ std::async关联异步任务
-
std::async是⼀种将任务与std::future关联的简单⽅法。它创建并运⾏⼀个异步任务,并返回⼀个与该任务结果关联的std::future对象。默认情况下,std::async是否启动⼀个新线程,或者在等待future时,任务是否同步运⾏都取决于你给的 参数。
-
std::launch类型:
- std::launch::deferred 表明该函数会被延迟调⽤,直到在future上调⽤get()或者wait()才会开始执⾏任务
- std::launch::async 表明函数会在⾃⼰创建的线程上运⾏
- std::launch::deferred | std::launch::async 内部通过系统等条件⾃动选择策略
-
示例代码(async.cpp)
-
#include <future> // 异步操作库 #include <iostream> // 标准输入输出 #include <thread> // 线程支持库 #include <unistd.h> // POSIX系统调用(如sleep) /** * 加法函数(模拟耗时操作) * @param num1 第一个加数 * @param num2 第二个加数 * @return 两数之和 * 注:通过sleep模拟长时间计算过程 */ int Add(int num1, int num2) { std::cout << "加法函数开始执行...\n"; // 阶段1提示 sleep(20); // 模拟20秒耗时操作 std::cout << "加法函数即将返回结果...\n"; // 阶段2提示 return num1 + num2; } int main() { std::cout << "--------主程序开始----------\n"; // 创建异步任务(延迟启动模式) // std::launch::deferred 表示延迟执行,直到调用get()/wait()时才运行 // 对比:std::launch::async 会立即在独立线程执行 std::future<int> result = std::async(std::launch::deferred, Add, 12, 13); sleep(10); // 主线程休眠10秒(此时异步任务尚未启动) std::cout << "--------准备获取结果----------\n"; // 获取异步任务结果(此时才真正执行Add函数) // get()会阻塞直到任务完成 int sum = result.get(); std::cout << "计算结果: " << sum << std::endl; std::cout << "--------程序结束----------\n"; return 0; }
- 编译
g++ -o async async.cpp -std=c++11 -lpthread
-
结果
-
当std::launch::deferred时
-
当std::launch::async时
-
-
使⽤std::packaged_task和std::future配合
-
std::packaged_task就是将任务和 std::feature 绑定在⼀起的模板,是⼀种对任务的封装。我们可以通过std::packaged_task对象获取任务相关联的std::feature对象,通过调⽤get_future()⽅法获得。std::packaged_task的模板参数是函数签名
-
可以把std::future和std::async看成是分开的, ⽽ std::packaged_task则是⼀个整体。
-
示例代码(package_task.cpp)
#include <future> // 引入future库,用于处理异步操作 #include <iostream> // 引入输入输出流库 #include <thread> // 引入线程库 // 定义一个普通的加法函数 int add(int x, int y) { return x + y; } int main() { // 创建一个packaged_task对象,它包装了add函数 std::packaged_task<int(int, int)> packTask(add); // 从packaged_task对象获取一个future对象,用于访问异步操作的结果 std::future<int> fu = packTask.get_future(); // 创建一个线程,执行packaged_task对象。注意,我们需要传递packaged_task的引用给线程函数, // 但由于std::thread的构造函数会 其参数,而packaged_task是不可 的,所以我们必须传递一个右值引用。 // 使用std::move可以将packTask转换为右值引用,从而允许线程函数接收并执行它。 std::thread th1(std::move(packTask), 520, 1314); // 在主线程中等待异步操作完成,并获取结果 int res = fu.get(); // 打印结果 std::cout << res << std::endl; // 等待线程执行完毕 th1.join(); return 0; }
-
编译
g++ -o package_task package_task.cpp -std=c++11 -lpthread
-
结果
-
-
-
使⽤std::promise和std::future配合
-
std::promise提供了⼀种设置值的⽅式,它可以在设置之后通过相关联的std::future对象进⾏读取。
-
换种说法就是之前说过std::future可以读取⼀个异步函数的返回值了, 但是要等待就绪, ⽽std::promise就提供⼀种 ⽅式⼿动让 std::future就绪
-
示例代码(promise.cpp)
#include <future> // 引入future库,用于处理异步操作 #include <iostream> // 引入输入输出流库 #include <thread> // 引入线程库 // 定义一个函数,用于计算两个整数的和,并通过promise对象返回结果 void add(int x, int y, std::promise<int> &prom) { std::cout << "add function" << std::endl; // 打印信息,表示add函数被调用 prom.set_value(x + y); // 设置promise的值,这个值会被future对象获取 } int main() { std::promise<int> prom; // 创建一个promise对象,用于存储类型为int的结果 std::future<int> fut = prom.get_future(); // 从promise对象获取一个future对象,用于访问结果 // 创建一个线程,执行add函数,传入两个整数和一个promise对象的引用 std::thread th1(add, 520, 1314, std::ref(prom)); // 在主线程中获取add函数的计算结果 int res = fut.get(); // 调用future对象的get方法,获取结果,并等待异步操作完成 std::cout << res << std::endl; // 打印结果 th1.join(); // 等待线程th1执行完毕 return 0; }
-
编译
g++ -o promise promise.cpp -std=c++11 -lpthread
-
结果
-
-
-
C++11线程池的实现
-
异步操作的搭配选择:基于线程池执⾏任务的时候,⼊⼝函数内部执⾏逻辑是固定的,因此选择std::packaged_task加上std::future的组合来实现
-
线程池的工作思想:⽤⼾传⼊要执⾏的函数,以及需要处理的数据(函数的参数),由线程池中的⼯作线程来执⾏函数完成任务
-
线程池需要管里的成员:
- 任务池:⽤vector维护的⼀个函数任务池⼦
- 互斥锁 & 条件变量: 实现同步互斥
- ⼀定数量的⼯作线程:⽤于不断从任务池取出任务执⾏任务
- 结束运⾏标志:以便于控制线程池的结束
-
线程池需要管理的操作
- ⼊队任务:⼊队⼀个函数和参数
- 停⽌运⾏:终⽌线程池
-
线程池的实现代码(ThreadPool.cpp)
#include <condition_variable> #include <functional> #include <future> #include <iostream> #include <memory> #include <mutex> #include <thread> #include <vector> class ThreadPool { public: // 定义任务类型为无参数无返回值的函数 using Task = std::function<void(void)>; // 构造函数,初始化线程池 // 参数为线程池中的线程数量,默认为1 // 初始化停止标志为false ThreadPool(int thr_count = 1) : _stop(false) { for (int i = 0; i < thr_count; i++) { _threads.emplace_back(&ThreadPool::Entry, this); } } // 析构函数,用于清理线程池 // 停止所有线程并等待它们完成 ~ThreadPool() { Stop(); } // 模板函数,用于向线程池中添加任务 // 返回一个future,包含任务的返回值 template <typename F, typename... Args> auto push(const F &&func, Args &&...args)-> std::future<decltype(func(args...))> { //无参函数对象->packaged_task->任务对象智能指针 // 将传入的函数及函数的参数绑定为一个无参函数对象,之后再构造一个智能指针去管理一个packaged_task对象,最后封装为任务指针,放入任务池 //封装为任务指针是因为packaged_task并不是一个函数对象,不可以直接传给线程执行 using return_type = decltype(func(args...)); auto function = std::bind(std::forward<F>(func), std::forward<Args>(args)...);//参数包的展开 auto task = std::make_shared<std::packaged_task<return_type()>>(function); std::future<return_type> fu = task->get_future(); { std::unique_lock<std::mutex> uniLock(_mutex); // 将任务添加到任务队列中,这里添加进去的是lamda函数对象,并不是task指针 _tasks.emplace_back([task](){ (*task)(); }); // 通知一个等待线程有任务可处理 _conv.notify_one(); } return fu; } // 停止线程池中的所有线程 bool Stop() { // 如果已经停止,则直接返回true if (_stop == true)return true; _stop = true; // 通知所有等待线程停止 _conv.notify_all(); // 等待所有线程完成 for (auto &th : _threads) { th.join(); } return true; } private: void Entry() { // 当_stop标志为false时,表示线程池仍在运行,持续处理任务 while (false == _stop) { std::vector<Task> tmp_tasks; // 创建一个临时任务池,用于存储待处理的任务 { // 加锁,保护对共享资源(如_tasks和_stop)的访问 std::unique_lock<std::mutex> lock(_mutex); // 等待条件满足:任务池不为空,或者收到停止信号 _conv.wait(lock, [this](){ return _stop || !_tasks.empty(); }); // 条件满足后,将_tasks中的任务转移到临时任务池tmp_tasks中 // 这样做是为了避免在持有锁的情况下执行任务,从而提高效率 tmp_tasks.swap(_tasks); } // 锁已经释放,现在可以安全地执行所有取出的任务 for (auto &tmp_task : tmp_tasks) { tmp_task(); // 执行任务 } } // 当_stop为true时,退出循环,函数返回 return; } private: std::mutex _mutex; // 互斥锁,保护任务和停止标志 std::vector<Task> _tasks; // 存储待处理的任务队列 std::condition_variable _conv; // 条件变量,用于线程间的同步和通知 std::atomic<bool> _stop; // 原子标志,指示线程池是否应停止 std::vector<std::thread> _threads; // 存储线程池中的所有工作线程 }; int Add(int num1, int num2) { return num1 + num2; } int main() { ThreadPool pool; for (int i = 0; i < 10; i++) { std::future<int> fu = pool.push(Add, 11, i); std::cout << fu.get() << std::endl; } pool.Stop(); return 0; }
-
编译
g++ -o ThreadPool ThreadPool.cpp -std=c++11 -lpthread
-
结果
日志打印工具
-
为了便于编写项⽬中能够快速定位程序的错误位置,因此编写⼀个⽇志打印类,进⾏简单的⽇志打印。
-
实现代码
#ifndef __M_LOG_H__ // 防止头文件重复包含 #define __M_LOG_H__ #include <ctime> // 时间相关函数 #include <iostream> // 标准输入输出 // ==================== 日志级别定义 ==================== #define DBG_LEVEL 0 // 调试级别(最低级别) #define INF_LEVEL 1 // 信息级别 #define ERR_LEVEL 2 // 错误级别(最高级别) #define DEFAULT_LEVEL DBG_LEVEL // 默认输出级别(当前设为调试级别) // ==================== 核心日志宏 ==================== /** * @brief 通用日志输出宏(内部使用) * @param lev_str 级别字符串(如"DBG") * @param level 日志级别数值 * @param format 格式化字符串(类似printf) * @param ... 可变参数(对应format中的占位符) * 功能:根据日志级别决定是否输出,带时间戳和代码位置信息 */ #define LOG(lev_str, level, format, ...) \ { \ if (level >= DEFAULT_LEVEL) /* 级别过滤 */ \ { \ time_t tmp = time(nullptr); /* 获取当前时间戳 */ \ struct tm *time = localtime(&tmp); /* 转换为本地时间 */ \ char str_time[32]; /* 时间格式化缓冲区 */ \ size_t ret = strftime(str_time, 31, "%D %H:%M:%S", time); /* 格式化为 MM/DD/YY HH:MM:SS */ \ printf("[%s][%s][%s:%d]\t" format "\n", /* 输出格式: \ lev_str, /* 日志级别标记 */ \ str_time, /* 格式化时间 */ \ __FILE__, /* 源代码文件名 */ \ __LINE__, /* 代码行号 */ \ ##__VA_ARGS__); /* 用户日志内容 */ \ } \ } // ==================== 快捷日志宏 ==================== /// 调试日志(DBG_LEVEL) #define DLOG(format, ...) LOG("DBG", DBG_LEVEL, format, ##__VA_ARGS__) /// 信息日志(INF_LEVEL) #define ILOG(format, ...) LOG("INF", INF_LEVEL, format, ##__VA_ARGS__) /// 错误日志(ERR_LEVEL) #define ELOG(format, ...) LOG("ERR", ERR_LEVEL, format, ##__VA_ARGS__) #endif // __M_LOG_H__
文件基础操作类
-
需要提供的操作:
-
⽂件是否存在判断
-
⽂件⼤⼩获取
-
⽂件读/写
-
⽂件创建/删除
-
⽬录创建/删除
-
-
实现代码
//文件操作类 class FileHelper { public: // 构造函数 FileHelper(const std::string &filename) : _filename(filename) { } // 文件大小 size_t size() { struct stat st; int ret = stat(_filename.c_str(), &st); if (ret != 0) return 0; return st.st_size; } // 是否存在 bool exists() { struct stat st; return stat(_filename.c_str(), &st) == 0; } // 读操作 bool read(char *body, size_t offset, size_t len) { // 打开文件 std::ifstream ifs(_filename, std::ios::binary | std::ios::in); // 以二进制形式打开 if (ifs.is_open() == false) { ELOG("%s 文件打开失败!", _filename.c_str()); return false; } // 跳转到指定位置 ifs.seekg(offset, std::ios::beg); // 读取文件 ifs.read(body, len); if (ifs.good() == false) { ELOG("%s 文件读取数据失败!!", _filename.c_str()); ifs.close(); return false; } // 关闭文件 ifs.close(); return true; } bool read(std::string &body) { // 获取文件大小,根据文件大小调整body的空间 size_t fsize = this->size(); body.resize(fsize); return read(&body[0], 0, fsize); } // 写操作 bool write(const char *body, size_t offset, size_t len) { // 打开文件 std::fstream fs(_filename, std::ios::binary | std::ios::in | std::ios::out); if (fs.is_open() == false) { ELOG("%s 文件打开失败!", _filename.c_str()); return false; } // 跳转到文件指定位置 fs.seekp(offset, std::ios::beg); // 写入数据 fs.write(body, len); if (fs.good() == false) { ELOG("%s 文件写入数据失败!!", _filename.c_str()); fs.close(); return false; } // 关闭文件 fs.close(); return true; } bool write(const std::string &body) { //如果文件有内容,则删除源文件,重新生成名为filename的文件 if (size() > 0) { removeFile(_filename); FileHelper::createFile(_filename); } return write(body.c_str(), 0, body.size()); } // 重命名 bool rename(const std::string &newfilename) { return (::rename(_filename.c_str(), newfilename.c_str()) == 0); } // 获取父目录 static std::string parentDirectory(const std::string &filename) { int pos = filename.find_last_of("/"); if (pos == std::string::npos) { return "./"; } std::string ppath = filename.substr(0, pos); return ppath; } // 创建文件 static bool createFile(const std::string &filename) { std::fstream ofs(filename, std::ios::out | std::ios::binary); if (!ofs.is_open()) { 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) { int pos = 0, idx = 0; while (idx < path.size()) { pos = path.find("/", idx); 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) { std::string command = "rm -rf " + path; return (::system(command.c_str()) != -1); } private: std::string _filename; };
SQLite基础操作类
-
需要提供的操作:
- 判断库是否存在
- 创建并打开/关闭/删除库
- 启动/提交/回滚事物
- 执行语句
-
实现代码
class SqliteHelper { public: // 定义一个回调函数类型,用于sqlite3_exec的回调 typedef int (*SqliteCallback)(void *, int, char **, char **); SqliteHelper(){} // 构造函数,接收数据库文件名 SqliteHelper(const std::string dbfilename) : _dbfilename(dbfilename) { } // 打开数据库 // 参数safe_leve用于指定打开数据库的附加模式,默认为SQLITE_OPEN_FULLMUTEX bool open(int safe_leve = SQLITE_OPEN_FULLMUTEX) { // 使用sqlite3_open_v2函数打开或创建数据库 int ret = sqlite3_open_v2(_dbfilename.c_str(), &_handler, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | safe_leve, nullptr); if (ret != SQLITE_OK) { // std::cout << "创建/打开sqlite数据库失败: "; // std::cout << sqlite3_errmsg(_handler) << std::endl; ELOG("创建/打开sqlite数据库失败: %s", sqlite3_errmsg(_handler)); return false; } return true; } // 关闭数据库 void close() { // 使用sqlite3_close_v2函数关闭数据库 if (_handler) sqlite3_close_v2(_handler); } // 执行SQL语句 // 参数sql为要执行的SQL语句,cb为回调函数,arg为回调函数的参数 bool exec(const std::string &sql, SqliteCallback cb, void *arg) { // 使用sqlite3_exec函数执行SQL语句 int ret = sqlite3_exec(_handler, sql.c_str(), cb, arg, nullptr); if (ret != SQLITE_OK) { // std::cout << sql << std::endl; // std::cout << "执行语句失败: "; // std::cout << sqlite3_errmsg(_handler) << std::endl; ELOG("执行语句:%s 失败!\t错误原因: %s", sql.c_str(), sqlite3_errmsg(_handler)); return false; } return true; } private: std::string _dbfilename; // 数据库文件名 sqlite3 *_handler; // 数据库句柄 };
字符串操作类
-
一共字符串分割功能
-
实现代码
class StrHelper { public: // 静态成员函数,用于分割字符串 // 参数:待分割的字符串str,分隔符sep,存储结果的向量result // 返回值:分割后的子字符串数量 static size_t split(const std::string &str, const std::string &sep, std::vector<std::string> &result) { int pos = 0; // 用于记录分隔符在字符串中的位置 int idx = 0; // 用于遍历字符串的索引 // 遍历字符串,直到索引超出字符串长度 while (idx < str.size()) { // 从当前索引开始查找分隔符,返回分隔符的起始位置 pos = str.find(sep, idx); // 如果未找到分隔符,说明已经到达字符串末尾或分隔符不在剩余部分 if (pos == std::string::npos) { // 将剩余部分作为最后一个子字符串添加到结果向量中 std::string tmp = str.substr(idx); result.push_back(tmp); // 返回结果向量的大小,即子字符串的数量 return result.size(); } // 如果分隔符的起始位置与当前索引相同,说明两个分隔符之间没有数据 if (pos == idx) { // 更新索引,跳过这个分隔符 idx = pos + sep.size(); continue; } // 提取两个分隔符之间的子字符串,并添加到结果向量中 std::string tmp = str.substr(idx, pos - idx); result.push_back(tmp); // 更新索引,跳过这个分隔符和已经处理的子字符串 idx = pos + sep.size(); } // 返回结果向量的大小,即子字符串的数量 return result.size(); } };
UUID⽣成器类
-
UUID(Universally Unique Identifier), 也叫通⽤唯⼀识别码,通常由32位16进制数字字符组成。
-
在本项目中,uuid⽣成,我们采⽤⽣成8个随机数字,加上8字节序号,共16字节数组⽣成32位16进制字符的组合形式来确保全局唯⼀的同时能够根据序号来分辨数据
-
实现代码
/** * @class UUIDHelper * @brief UUID生成工具类(伪随机实现) * * 生成格式:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx * 实现特点: * 1. 前16字符为随机数(std::mt19937_64生成) * 2. 后16字符为递增序列号(原子操作保证线程安全) * 3. 16进制表示,小写字母 * 注意:非标准UUID实现,不适合高安全场景 */ class UUIDHelper { public: /** * @brief 生成伪随机UUID字符串 * @return 格式化的UUID字符串(36字符) * * 生成逻辑: * 1. 前16字符:8个随机字节(显示为16个十六进制字符) * 2. 中间3个连字符"-" * 3. 后16字符:8字节递增序列号(显示为16个十六进制字符) * 4. 最后1个连字符"-" */ static std::string uuid() { // 初始化随机数生成器 std::random_device rd; // 真随机数种子 std::mt19937_64 gernator(rd()); // 64位梅森旋转算法 std::uniform_int_distribution<int> distribution(0, 255); // 生成0-255的随机数 std::stringstream ss; // 生成前16位随机部分(8字节) for (int i = 0; i < 8; i++) { // 格式化为2位十六进制(不足补零) ss << std::setw(2) << std::setfill('0') << std::hex << distribution(gernator); // 在特定位置插入分隔符 if (i == 3 || i == 5 || i == 7) // 第4、6、8字节后 { ss << "-"; } } // 生成后16位序列号部分(原子计数器) static std::atomic<size_t> seq(1); // 静态原子计数器 size_t num = seq.fetch_add(1); // 原子自增 // 将序列号转为十六进制字符串 for (int i = 7; i >= 0; i--) // 从高字节到低字节 { // 每次处理1字节(2个十六进制字符) ss << std::setw(2) << std::setfill('0') << std::hex << ((num >> (i * 8)) & 0xff); // 提取字节 // 在倒数第2字节后插入分隔符 if (i == 6) ss << "-"; } return ss.str(); // 示例:3e2a5f8c-d4b1-47e2-9a3b-000000000001 } }; /* 典型输出格式(示例): 前半随机部分 分隔 递增序列号 ┌───────────┐ ┬ ┌───────────┐ 3e2a5f8c-d4b1-47e2-9a3b-000000000001 │ └── 序列号从1开始 └─ 固定位置的分隔符 */
需求分析
核心概念需求
- ⽣产者 (Producer)
- 消费者 (Consumer)
- 中间⼈ (Broker)
- 发布 (Publish)
- 订阅 (Subscribe)
整体模型需求
-
一个生产者一个消费者
-
N个⽣产者, N个消费者
-
不难看出Broker Server是最核⼼的部分, 负责消息的存储和转发
Broker Server模块需求
Broker Server模块概览
-
为了实现AMQP(Advanced Message Queuing Protocol)-⾼级消息队列协议模型,对于消息中间件服务器Broker中,又需要一下模块:
- 虚拟机 (VirtualHost): 类似于 MySQL 的 “database”, 是⼀个逻辑上的集合。⼀个 BrokerServer 上可以存在多个 VirtualHost
- 交换机 (Exchange): ⽣产者把消息先发送到 Broker 的 Exchange 上,再根据不同的规则, 把消息转发给不同的 Queue队列 (Queue): 真正⽤来存储消息的部分, 每个消费者决定⾃⼰从哪个 Queue 上读取消息
- 绑定 (Binding): Exchange 和 Queue 之间的关联关系,Exchange 和 Queue 可以理解成 "多对多"关系,使⽤⼀个关联表就可以把这两个概念联系起来
- 消息 (Message): 传递的内容
-
BrokerServer结构图
- 大体上是BrokerServer中包含多个虚拟机,但本项目为了简化代码,仅支持定义一个虚拟机,同志们可以在这做拓展
-
VirtualHost结构图
- 对于生产客户端来说,其生产的消息应该交给交换机进行处理,由交换机根据交换机类型决定该消息应该推送到哪些队列当中去,而不是由生产客户端直接推送到队列当中
- 对于消费客户端来说,其要消费得消息应该直接从队列里面进行拿取。
Broker Server模块核心API
- Broker Server需要提供相应的接口来给客户端进行操作
- 创建交换机 (exchangeDeclare)
- 销毁交换机 (exchangeDelete)
- 创建队列 (queueDeclare)
- 销毁队列 (queueDelete)
- 创建绑定 (queueBind)
- 解除绑定 (queueUnbind)
- 发布消息 (basicPublish)
- 订阅消息 (basicConsume)
- 确认消息 (basicAck)
- 取消订阅 (basicCancel)
交换机类型需求
-
Direct: ⽣产者发送消息时, 直接指定被该交换机绑定的队列名
-
Fanout: ⽣产者发送的消息会被 到该交换机的所有队列中
-
Topic: 绑定队列到交换机上时, 指定⼀个字符串为 bindingKey。发送消息指定⼀个字符串为routingKey。当 routingKey 和 bindingKey 满⾜⼀定的匹配条件的时候, 则把消息投递到指定队列
持久化需求
- 需要持久化的数据:
- Exchange:交换机数据
- Queue:队列数据
- Binding:绑定数据,消费者与队列与交换机之间的关系
- Message:未被消费得数据
网络通信需求
- ⽣产者和消费者都是客⼾端程序, Broker 则是作为服务器,通过⽹络进⾏通信。
- 在⽹络通信的过程中, 客⼾端部分要提供对应的 api, 来实现对服务器的操作
- 客户端需要提供的api:
- 创建 Connection
- 关闭 Connection
- 创建 Channel
- 关闭 Channel
- 创建队列 (queueDeclare)
- 销毁队列 (queueDelete)
- 创建交换机 (exchangeDeclare)
- 销毁交换机 (exchangeDelete)
- 创建绑定 (queueBind)解除绑定 (queueUnbind)
- 发布消息 (basicPublish)
- 订阅消息 (basicConsume)
- 确认消息 (basicAck)
- 取消订阅(basicCancel)
消息应答种类需求
-
⾃动应答: 消费者只要消费了消息, 就算应答完毕了,Broker 直接删除这个消息
-
⼿动应答: 消费者⼿动调⽤应答接⼝, Broker 收到应答请求之后, 才真正删除这个消息
模块划分
三层处理架构
项目模块关系图
服务端模块
服务器的整体结构图
- _server:Muduo库提供的⼀个通⽤TCP服务器, 我们可以封装这个服务器进⾏TCP通信
- _baseloop:主事件循环器, ⽤于响应IO事件和定时器事件,主loop主要是为了响应监听描述符的IO事件
- _codec: ⼀个protobuf编解码器, 我们在TCP服务器上设计了⼀层应⽤层协议, 这个编解码器主要就是负责实现应⽤层协议的解析和封装, 下边具体讲解
- _dispatcher:⼀个消息分发器, 当Socket接收到⼀个报⽂消息后, 我们需要按照消息的类型, 即上⾯提到的typeName进⾏消息分发, 会将不同类型的消息分发相对应的的处理函数中,下边具体讲解
- _consumer: 服务器中的消费者信息管理句柄。
- _threadpool: 异步⼯作线程池,主要⽤于队列消息的推送⼯作。
- _connections: 连接管理句柄,管理当前服务器上的所有已经建⽴的通信连接。
- _virtual_host:服务器持有的虚拟主机。 队列、交换机 、绑定、消息等数据都是通过虚拟主机管理
关键组件协作
持久化数据管理中⼼模块
- 在数据管理模块中管理交换机,队列,队列绑定,消息等部分数据数据。
-
交换机管理:
-
管理信息:名称,类型,是否持久化标志,是否(⽆⼈使⽤时)⾃动删除标志,其他参数
-
管理操作:恢复历史信息,声明,删除,获取,判断是否存在
-
-
队列管理:
-
管理信息:名称,是否持久化标志,是否独有标志,是否(⽆⼈使⽤时)⾃动删除标志,其他参数…
-
管理操作:恢复历史信息,声明,删除,获取,判断是否存在
-
-
绑定管理:
-
管理信息:交换机名称,队列名称,绑定主题
-
管理操作:恢复历史信息,绑定,解绑,解除交换机关联绑定信息,解除队列关联绑定信息,获取交换机关联绑定信息
-
消息管理:
-
管理信息:
- 属性消息ID, 路由主题,持久化模式标志
- 消息内容
- 持久化有效标志
- 持久化位置
- 持久化消息长度
-
管理操作:恢复历史消息、向指定队列新增消息,获取指定队列队首消息,确认移除消息。以上消息都应该在内存和硬盘中存储。
-
以内存存储为主,主要是保证快速查找信息进⾏处理
-
以硬盘存储为辅,主要是保证服务器重启之后,之前的信息都可以正常保持
-
-
虚拟机管理模块
-
因为交换机/队列/绑定都是以虚拟机为单元整体进⾏操作的,因此虚拟机是对以上数据管理模块的整合模块
-
虚拟机管理信息:
-
交换机数据管理模块句柄
-
队列数据管理模块句柄
-
绑定数据管理模块句柄
-
消息数据管理模块句柄
-
-
虚拟机对外的接口
-
提供虚拟机内交换机声明/删除操作
-
提供虚拟机内队列声明/删除操作
-
提供虚拟机内交换机-队列绑定/解绑操作
-
获取交换机的相关绑定信息
-
-
对虚拟机的管理操作
-
创建虚拟机
-
查询虚拟机
-
删除虚拟机
-
交换路由模块
-
当客⼾端发布⼀条消息到交换机后,这条消息,应该被⼊队到该交换机绑定的哪些队列中?答案是由交换路由模块来决定
-
在绑定信息中有⼀个binding_key,⽽每条发布的消息中有⼀个routing_key,能否⼊队取决于两个要素:交换机类型和key
-
不同交换机类型对应的路由操作:
- ⼴播:将消息⼊队到该交换机的所有绑定队列中
- 直接:将消息⼊队到绑定信息中binding_key与消息routing_key⼀致的队列中
- 主题:将消息⼊队到绑定信息中binding_key与routing_key是匹配成功的队列中
-
binding_key:
- 是由数字字⺟下划线构成的, 并且使⽤ . 分成若⼲部分,比如:news.music.#
- ⽀持 * 和 # 两种通配符, 但是 * # 只能作为 . 切分出来的独⽴部分, 不能和其他数字字⺟混⽤,比如:
- ⽐如 a.*.b 是合法的, a.*a.b 是不合法的
- * 可以匹配任意⼀个单词(注意是单词不是字⺟)
- # 可以匹配任意零个或者多个单词(注意是单词不是字⺟)
-
routing_key:
- 是由数据、字⺟和下划线构成, 并且使⽤ . 划分成若⼲部分,如:news.music.pop
-
路由匹配算法详解
这个路由匹配算法用于消息队列系统中,根据交换机类型和路由键来决定消息如何路由到队列。算法主要处理三种交换机类型:DIRECT(直接)、FANOUT(扇出)和TOPIC(主题)。
-
算法概述
-
该算法实现了AMQP协议中的路由匹配规则,特别是对TOPIC交换机的复杂模式匹配。算法采用动态规划方法解决主题交换机中的通配符匹配问题。
-
三种交换机类型的处理
- DIRECT(直接交换机)
if (type == ExchangeType::DIRECT) { return (routing_key == binding_key); }
特点:
-
精确匹配路由键和绑定键
-
只有当
routing_key
完全等于binding_key
时才匹配成功 -
时间复杂度:O(1)
示例:
-
routing_key: “order.create”
-
匹配的binding_key: “order.create”
-
不匹配的binding_key: “order.delete”
- FANOUT(扇出交换机)
else if (type == ExchangeType::FANOUT) { return true; }
特点:
-
无条件匹配所有绑定
-
消息会被路由到所有绑定的队列
-
不考虑路由键和绑定键
-
时间复杂度:O(1)
示例:
- 无论routing_key是什么,都会匹配所有binding_key
- TOPIC(主题交换机)
else if (type == ExchangeType::TOPIC) { // 详细匹配逻辑... }
特点:
-
支持通配符匹配
-
使用动态规划算法处理复杂模式
-
时间复杂度:O(m*n),其中m和n分别是binding_key和routing_key的分段数
TOPIC交换机匹配算法详解
- 键分割
std::vector<std::string> bkeys, rkeys; int count_binding_key = StrHelper::split(binding_key, ".", bkeys); int count_routing_key = StrHelper::split(routing_key, ".", rkeys);
-
将binding_key和routing_key按"."分割成多个部分
-
例如:“news.music.pop” → [“news”, “music”, “pop”]
- 动态规划表初始化
std::vector<std::vector<bool>> dp(count_binding_key + 1, std::vector<bool>(count_routing_key + 1, false)); dp[0][0] = true;
-
创建(m+1)×(n+1)的二维布尔数组dp
-
dp[i][j]表示binding_key前i部分能否匹配routing_key前j部分
-
初始状态:空键匹配空键(dp[0][0] = true)
- 处理"#"开头的binding_key
for (int i = 1; i <= count_binding_key; i++) { if (bkeys[i - 1] == "#") { dp[i][0] = true; continue; } break; }
-
"#"表示匹配零个或多个单词
-
如果binding_key以"#"开头,则能匹配空routing_key
-
例如binding_key: "#.news"可以匹配routing_key: “news”
- 动态规划填充
for (int i = 1; i <= count_binding_key; i++) { for (int j = 1; j <= count_routing_key; j++) { // 情况1:当前单词相同或binding_key为"*" if (bkeys[i - 1] == rkeys[j - 1] || bkeys[i - 1] == "*") { dp[i][j] = dp[i - 1][j - 1]; } // 情况2:当前binding_key为"#" else if (bkeys[i - 1] == "#") { dp[i][j] = dp[i - 1][j - 1] | dp[i][j - 1] | dp[i - 1][j]; } } }
匹配规则:
-
精确匹配或"*"通配符:
-
"*"匹配单个单词
-
如果当前binding_key部分等于routing_key部分,或binding_key部分为"*"
-
继承左上角的结果(dp[i][j] = dp[i-1][j-1])
-
-
"#"通配符:
-
"#"匹配零个或多个单词
-
可以从三个方向继承结果:
-
左上(dp[i-1][j-1]):"#"匹配当前单词
-
左(dp[i][j-1]):"#"匹配更多单词
-
上(dp[i-1][j]):"#"匹配零个单词
-
-
-
返回结果
return dp[count_binding_key][count_routing_key];
-
返回动态规划表右下角的值
-
表示整个binding_key能否匹配整个routing_key
示例分析
示例1:
-
binding_key: “news.*.pop”
-
routing_key: “news.music.pop”
匹配过程:
-
分割:
-
bkeys: [“news”, “*”, “pop”]
-
rkeys: [“news”, “music”, “pop”]
-
-
动态规划表:
-
dp[1][1] = true (“news” == “news”)
-
dp[2][2] = true ("*“匹配"music”)
-
dp[3][3] = true (“pop” == “pop”)
-
-
结果:true
示例2:
-
binding_key: “news.#”
-
routing_key: “news.music.pop.jazz”
匹配过程:
-
分割:
-
bkeys: [“news”, “#”]
-
rkeys: [“news”, “music”, “pop”, “jazz”]
-
-
动态规划表:
-
dp[1][1] = true (“news” == “news”)
-
dp[2][2] = true ("#“匹配"music”)
-
dp[2][3] = true ("#“继续匹配"pop”)
-
dp[2][4] = true ("#“继续匹配"jazz”)
-
-
结果:true
示例3:
-
binding_key: “news.music.#.jazz”
-
routing_key: “news.music.pop.jazz”
匹配过程:
-
分割:
-
bkeys: [“news”, “music”, “#”, “jazz”]
-
rkeys: [“news”, “music”, “pop”, “jazz”]
-
-
动态规划表:
-
dp[1][1] = true (“news” == “news”)
-
dp[2][2] = true (“music” == “music”)
-
dp[3][3] = true ("#“匹配"pop”)
-
dp[4][4] = true (“jazz” == “jazz”)
-
-
结果:true
-
消费者管理模块
- 消费者管理是以队列为单元的,因为每个消费者都会在开始的时候订阅⼀个队列的消息,当队列中有消息后,会将队列消息轮询推送给订阅了该队列的消费者。
- 因此操作流程通常是,从队列关联的消息管理中取出消息,从队列关联的消费者中取出⼀个消费者,然后将消息推送给消费者(这就是发布订阅中负载均衡的⽤法)
- 需要管理的消费者信息:
- 标识
- 订阅队列名称
- ⾃动应答标志(决定了⼀条消息推送给消费者后,是否需要等待收到确认后再删除消息)
- 消息处理回调函数指针(⼀个消息发布后调⽤回调,选择消费者进⾏推送)
- 对消费者管理的操作:
- 添加
- 删除
- 轮询获取指定队列的消费者
- 移除队列所有消费者
信道管理模块
-
在AMQP模型中,除了通信连接Connection概念外,还有⼀个Channel的概念,Channel是针对Connection连接的⼀个更细粒度的通信信道,多个Channel可以使⽤同⼀个通信连接Connection,但同一个Connection的Channel之间相互独⽴
-
需要管理的信道信息
- 信道ID
- 信道关联的消费者
- 信道关联的连接
- 信道关联的虚拟机
- ⼯作线程池(⼀条消息被发布到队列后,需要将消息推送给订阅了对应队列的消费者,过程由线程池完成)
-
需要管理的操作:
-
提供声明&删除交换机操作(删除交换机的同时删除交换机关联的绑定信息)
-
提供声明&删除队列操作(删除队列的同时,删除队列关联的绑定信息,消息,消费者信息)
-
提供绑定&解绑队列操作
-
提供订阅&取消订阅队列消息操作
-
提供发布&确认消息操作
-
链接管理模块
-
链接模块的典型工作流程
-
管理结构图
-
虽然Muduo库里面提供了链接模块,但不能完全满足我们的要求,所以需要进行二次封装。
-
除了基础的增删查链接外,还需要能够操作信道
-
需要管理的链接信息:
- 链接关联的信道
- 链接关联的Muduo库Connection
-
需要管理的操作:
- 新增链接
- 删除链接
- 获取链接
- 打开信道
- 关闭信道
Broker服务器模块
- 整合以上所有模块,并搭建⽹络通信服务器,实现与客⼾端⽹络通信,能够识别客⼾端请求,并提供客⼾端请求的处理服务
- 需要管理的服务器信息:
- 虚拟机管理模块句柄
- 消费者管理模块句柄
- 连接管理模块句柄
- ⼯作线程池句柄
- muduo库通信所需元素
客户端模块
消费者管理
-
消费者在客⼾端的存在感⽐较低,因为在⽤⼾的使⽤⻆度中,只要创建⼀个信道后,就可以通过信道完成所有的操作,因此对于消费者的感官更多是在订阅的时候传⼊了⼀个消费者标识,且当前的简单实现也仅仅是⼀个信道只能创建订阅⼀个队列,也就是只能创建⼀个消费者,它们⼀⼀对应,因此更是弱化了消费者的存在
-
需要管理的消费者信息
-
标识
b. 订阅队列名称
c. ⾃动应答标志(决定了⼀条消息推送给消费者后,是否需要等待收到确认后再删除消息)
d. 消息处理回调函数指针(⼀个消息发布后调⽤回调,选择消费者进⾏推送)
-
-
需要管理的消费者操作:
- 添加
- 删除
- 轮询获取指定队列的消费者
- 移除队列所有消费者等操作
信道请求模块
-
与服务端的信道类似,客⼾端这边在AMQP模型中,也是除了通信连接Connection概念外,还有⼀个Channel的概念,Channel是针对Connection连接的⼀个更细粒度的通信信道,多个Channel可以使⽤同⼀个通信连接Connection进⾏通信,但是同⼀个Connection的Channel之间相互独⽴
-
需要管理的信道信息:
-
信道ID
-
信道关联的通信连接
-
信道关联的消费者
-
请求对应的响应信息队列(这⾥队列使⽤hash表,以便于查找指定的响应)
-
互斥锁&条件变量(⼤部分的请求都是阻塞操作,发送请求后需要等到响应才能继续,但是muduo库的通信是异步的,因此需要我们⾃⼰在收到响应后,通过判断是否是等待的指定响应
来进⾏同步)
-
-
需要管理的信道操作:
- 提供创建信道操作
- 提供删除信道操作
- 提供声明交换机操作(强断⾔-有则OK,没有则创建)
- 提供删除交换机
- 提供创建队列操作(强断⾔-有则OK,没有则创建)
- 提供删除队列操作
- 提供交换机-队列绑定操作
项目实现
在Linux机器上规划开发目录
-
MQClient:存放客户端模块的代码
-
MQCommon:存放通用代码,如上文提到的前置工具类、.proto文件
-
MQServer:用于存放服务端模块代码
-
MQTest:用于编写单元测试
-
TestCode:是我之前学习库函数时写的代码,可以忽略
-
ThirdLib:存放第三方库文件
在MQCommon中编写公共类
在MQCommon中编写message.proto文件
- 在开始正式项⽬功能模块代码编写之前,我们需要先提前做⼀件事情,就是将消息类型定义出来。⽽消息最终是需要进⾏持久化存储的,因此涉及到数据的序列化和反序列化,因此消息的类型定义我们使⽤protobuf来进⾏⽣成
// 协议版本声明
syntax = "proto3";
// 包命名空间(防止命名冲突)
package MQ;
/* ==================== 交换机类型枚举 ====================
* 定义消息队列中交换机的路由策略类型
*/
enum ExchangeType {
UNKNOWTYPE = 0; // 未知类型(默认值)
DIRECT = 1; // 直连交换机(精确匹配routing_key)
FANOUT = 2; // 扇出交换机(广播到所有绑定队列)
TOPIC = 3; // 主题交换机(支持通配符匹配)
};
/* ==================== 消息传递模式枚举 ====================
* 定义消息的持久化特性
*/
enum DeliveryMode {
UNKNOWMODE = 0; // 未知模式(默认值)
UNDURABLE = 1; // 非持久化(内存存储,重启丢失)
DURABLE = 2; // 持久化(磁盘存储,重启保留)
};
/* ==================== 消息基础属性 ====================
* 包含消息的元数据信息
*/
message BasicProperties{
string id = 1; // 消息唯一标识符(建议UUID格式)
DeliveryMode delivery_mode = 2; // 持久化模式(默认UNKNOWMODE)
string routing_key = 3; // 路由键(用于交换机消息分发)
};
/* ==================== 消息有效载荷 ====================
* 包含实际传输的数据内容
*/
message Payload {
BasicProperties properties = 1; // 消息属性(必填)
string body = 2; // 消息内容主体(业务数据)
string valid = 3; // 有效性标记(可扩展为校验码/签名)
};
/* ==================== 完整消息结构 ====================
* 包含载荷及存储位置信息
*/
message Message {
Payload payload = 1; // 消息载荷(必填)
uint32 offset = 2; // 消息在存储中的偏移量(字节单位)
uint32 length = 3; // 消息长度(字节单位)
};
-
编译得到message.pb.h 与message.pb.cc两个文件
protoc --cpp_out=./ ./message.proto
在MQCommon中编写request.proto文件
-
该文件负责自定义网络通信协议
-
实现的请求类型有:
-
创建channel
-
关闭 channel
-
创建 exchange
-
删除 exchange
-
创建 queue
-
删除 queue
-
创建 binding
-
删除 binding
-
发送 message
-
订阅 message
-
发送 ack
-
返回 message (服务器 -> 客⼾端)
-
syntax = "proto3";
package MQ;
import "message.proto";
//信道的打开与关闭
message openChannelRequest{
string rid = 1;
string cid = 2;
};
message closeChannelRequest{
string rid = 1;
string cid = 2;
};
//交换机的声明与删除
message declareExchangeRequest{
string rid = 1;
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 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;
}
-
编译得到request.pb.h 与request.pb.cc两个文件
protoc --cpp_out=./ ./request.proto
在MQCommon中编写Helper.hpp文件
- 该文件包含的工具类(已在**“前置工具类的编写”**一节给出源码):
- UUID生成器类
- 字符串操作类
- SQLite操作类
- 文件基础操作类
在MQCommon中编写Logger.hpp文件
- 该文件里包含日志打印类,已在**“前置工具类的编写”**一节给出源码
在MQCommon中编写ThreadPool.hpp文件
- 该文件包含异步工作线程池类,已在**“前置工具类的编写”**一节给出源码
在MQServer中编写服务端模块代码
在MQServer中编写makefile文件来编译服务端模块
.PHONY: server
CFLAG= -I../ThirdLib/lib/include
LFLAG= -L../ThirdLib/lib/lib -lgtest -lprotobuf -lsqlite3 -pthread -lmuduo_net -lmuduo_base -lz
server:server.cpp ../MQCommon/message.pb.cc ../MQCommon/request.pb.cc ../ThirdLib/lib/include/muduo/protobuf/codec.cc
g++ -g -std=c++11 $(CFLAG) $^ -o $@ $(LFLAG)
在MQServer中编写Exchange.hpp文件实现交换机数据管理
-
在此文件中要实现的功能:
-
定义交换机数据类
-
交换机名称
-
交换机类型
-
是否持久化标志
-
是否⾃动删除标志
-
其他参数
-
-
定义交换机数据持久化类(数据持久化的sqlite3数据库中)
- 创建/删除交换机数据表
- 新增交换机数据
- 移除交换机数据
- 查询所有交换机数据
- 查询指定交换机数据(根据名称)
-
定义交换机数据管理类
- 声明交换机,并添加管理(存在则OK,不存在则创建)
- 删除交换机
- 获取指定交换机
- 销毁所有交换机数据
-
-
实现代码
#ifndef __M_EXCHANGE_H__ #define __M_EXCHANGE_H__ #include "../MQCommon/Logger.hpp" #include "../MQCommon/Helper.hpp" #include "../MQCommon/message.pb.h" #include <google/protobuf/map.h> #include <iostream> #include <unordered_map> #include <mutex> #include <memory> namespace MQ { //1. 定义交换机类 struct Exchange { using ptr = std::shared_ptr<Exchange>; //1. 交换机名称 std::string _name; //2. 交换机类型 MQ::ExchangeType _type; //3. 交换机持久化标志 bool _durable; //4. 是否自动删除标志 bool _auto_delete; //5. 其他参数 google::protobuf::Map<std::string, std::string> _args; Exchange() {} Exchange(const std::string &ename, MQ::ExchangeType etype, bool edurable, bool eauto_delete, const google::protobuf::Map<std::string, std::string> &eargs): _name(ename), _type(etype), _durable(edurable), _auto_delete(eauto_delete), _args(eargs) {} //args存储键值对,在存储数据库的时候,会组织一个格式字符串进行存储 key=val&key=val.... //内部解析str_args字符串,将内容存储到成员中 void setArgs(const std::string &str_args) { //key=val&key=val& 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; } } //将args中的内容进行序列化后,返回一个字符串 std::string getArgs() { std::string result; for (auto start = _args.begin(); start != _args.end(); ++start) { result += start->first + "=" + start->second + "&"; } return result; } }; using ExchangeMap = std::unordered_map<std::string, Exchange::ptr>; //2. 定义交换机数据持久化管理类--数据存储在sqlite数据库中 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) { 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); return result; } private: 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 = ( MQ::ExchangeType)std::stoi(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; }; //3. 定义交换机数据内存管理类 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, MQ::ExchangeType type, bool durable, bool auto_delete, const google::protobuf::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) { bool ret = _mapper.insert(exp); if (ret == 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; } size_t size() { std::unique_lock<std::mutex> lock(_mutex); return _exchanges.size(); } //清理所有交换机数据 void clear() { std::unique_lock<std::mutex> lock(_mutex); _mapper.removeTable(); _exchanges.clear(); } private: std::mutex _mutex; ExchangeMapper _mapper; ExchangeMap _exchanges; }; } #endif
在MQServer中编写Queue.hpp文件实现队列数据管理
-
在此文件中要实现的功能:
-
定义队列描述数据类
- 队列名称
- 是否持久化标志
- 是否独占标志
- 是否⾃动删除标志
-
定义队列数据持久化类(数据持久化的sqlite3数据库中)
- 创建/删除队列数据表
- 新增队列数据
- 移除队列数据
- 查询所有队列数据
-
定义队列数据管理类
- 创建队列,并添加管理(存在则OK,不存在则创建)
- 删除队列
- 获取指定队列
- 获取所有队列
- 判断指定队列是否存在
- 获取队列数量
- 销毁所有队列数据
-
-
实现代码
#ifndef __M_Queue_H__ #define __M_Queue_H__ #include "../MQCommon/Helper.hpp" #include "../MQCommon/Logger.hpp" #include "../MQCommon/message.pb.h" #include <google/protobuf/map.h> #include <iostream> #include <memory> #include <mutex> #include <unordered_map> namespace MQ { //基础队列结构 struct Queue { using ptr = std::shared_ptr<Queue>; std::string _name; bool _durable; bool _exclusive; bool _auto_delete; google::protobuf::Map<std::string, std::string> _args; Queue() {} Queue(const std::string &name, bool durable, bool exclusive, bool auto_delete, const google::protobuf::Map<std::string, std::string> &args) : _name(name), _durable(durable), _exclusive(exclusive), _auto_delete(auto_delete), _args(args) { } bool setArgs(const std::string &str_args) { std::vector<std::string> sub_args; StrHelper::split(str_args, "&", sub_args); for (auto &str : sub_args) { auto pos = str.find("="); if (pos == std::string::npos) { ELOG("Invalid args: "); return false; } std::string key = str.substr(0, pos); std::string value = str.substr(pos + 1); _args[key] = value; } return true; } std::string getArgs() { std::string str_args; for (auto &arg : _args) { str_args += arg.first + "=" + arg.second + "&"; } return str_args; } }; using QueueMap = std::unordered_map<std::string, std::shared_ptr<Queue>>; //队列数据持久化类 class QueueMapper { public: QueueMapper() {} QueueMapper(const std::string &db_file) : _sqliteHelper(db_file) { // 首先创建数据库文件的父级目录 // 随后打开数据库文件,创建数据库表 std::string path = FileHelper::parentDirectory(db_file); FileHelper::createDirectory(path); assert(_sqliteHelper.open()); createTable(); } // 创建队列表 bool createTable() { // create table if not exists queue_table(name varchar(255) not null primary key,durable int,exclusive int,auto_delete int,args varchar(255)); std::stringstream ss; ss << "create table if not exists queue_table("; ss << "name varchar(255) not null primary key,"; ss << "durable int,"; ss << "exclusive int,"; ss << "auto_delete int,"; ss << "args varchar(255));"; if (!_sqliteHelper.exec(ss.str(), nullptr, nullptr)) { ELOG("创建表: queue_table 失败"); abort(); // 如果创建表失败,则直接退出程序,并报异常错误 return false; } return true; } bool removeTable() { // drop table if exists queue_table; std::string sql = "drop table if exists queue_table;"; if (!_sqliteHelper.exec(sql, nullptr, nullptr)) { ELOG("删除表: queue_table 失败"); return false; } return true; } // 向数据库中插入一个队列 bool insert(const Queue::ptr &queue) { // insert into queue_table(name,durable,exclusive,auto_delete,args); std::stringstream ss; ss << "insert into queue_table values("; ss << "'" << queue->_name << "',"; ss << queue->_durable << ","; ss << queue->_exclusive << ","; ss << queue->_auto_delete << ","; ss << "'" << queue->getArgs() << "');"; if (!_sqliteHelper.exec(ss.str(), nullptr, nullptr)) { ELOG("插入队列:%s 失败", queue->_name.c_str()); return false; } return true; } // 根据名字删除一个队列 bool deleteQueue(const std::string &name) { // delete from queue_table where name = 'name'; std::stringstream ss; ss << "delete from queue_table where name = '"; ss << name << "';"; if (!_sqliteHelper.exec(ss.str(), nullptr, nullptr)) { ELOG("删除队列:%s 失败", name.c_str()); return false; } return true; } QueueMap recovery() { // select name, durable, exclusive, auto_delete, args from queue_table; QueueMap result; std::string sql = "select name, durable, exclusive, auto_delete, args from queue_table;"; _sqliteHelper.exec(sql, selectCallback, &result); return result; } ~QueueMapper() { _sqliteHelper.close(); } private: // 查询时传递的回调函数 static int selectCallback(void *arg, int numcol, char **row, char **fields) { QueueMap *result = (QueueMap *)arg; Queue::ptr queue = std::make_shared<Queue>(); queue->_name = row[0]; queue->_durable = (bool)std::stoi(row[1]); queue->_exclusive = (bool)std::stoi(row[2]); queue->_auto_delete = (bool)std::stoi(row[3]); if (row[4]) queue->setArgs(row[4]); result->insert(std::make_pair(queue->_name, queue)); return 0; } private: SqliteHelper _sqliteHelper; }; class QueueManager { public: using ptr = std::shared_ptr<QueueManager>; QueueManager(const std::string &db_file) : _queueMapper(db_file) { _queues = _queueMapper.recovery(); } bool declareQueue(const std::string &name, bool durable, bool exclusive, bool auto_delete, const google::protobuf::Map<std::string, std::string> &args) { std::unique_lock<std::mutex> lock(_mutex); // 查找该队列是否存在 auto it = _queues.find(name); if (it != _queues.end()) { return true; } // 如果该队列不存在,则创建该队列 Queue::ptr pqueue = std::make_shared<Queue>(name, durable, exclusive, auto_delete, args); // 如果标记为持久化存储,这里需要将该队列插入数据库 if (durable == true) { bool ret = _queueMapper.insert(pqueue); if (ret == false) return false; } // 将该队列插入到队列管理器中 _queues.insert(std::make_pair(name, pqueue)); return true; } bool deleteQueue(const std::string &name) { std::unique_lock<std::mutex> lock(_mutex); auto it = _queues.find(name); // 如果没找到,说明无需删除 if (it == _queues.end()) { return true; } // 如果该队列是持久化的,则在数据库中也删除该队列 if (it->second->_durable) { _queueMapper.deleteQueue(name); } // 删除该队列 _queues.erase(it); return true; } Queue::ptr selectQueue(const std::string &name) { std::unique_lock<std::mutex> lock(_mutex); auto it = _queues.find(name); if (it == _queues.end()) { return Queue::ptr(); } return it->second; } bool exist(const std::string &name) { std::unique_lock<std::mutex> lock(_mutex); auto it = _queues.find(name); if (it == _queues.end()) { return false; } return true; } size_t size() { std::unique_lock<std::mutex> lock(_mutex); return _queues.size(); } QueueMap allQueues() { std::unique_lock<std::mutex> lock(_mutex); return _queues; } void clear() { std::unique_lock<std::mutex> lock(_mutex); _queues.clear(); _queueMapper.removeTable(); } private: std::mutex _mutex; QueueMap _queues; QueueMapper _queueMapper; }; } #endif
在MQServer中编写Binding.hpp文件实现绑定信息(交换机-队列)管理
-
在此文件中要实现的功能:
-
定义绑定信息类
- 交换机名称
- 队列名称
- binding_key(分发匹配规则-决定了哪些数据能被交换机放⼊队列)
-
定义绑定信息数据持久化类(数据持久化的sqlite3数据库中)
- 创建/删除绑定信息数据表
- 新增绑定信息数据
- 移除指定绑定信息数据
- 移除指定交换机相关绑定信息数据:移除交换机的时候会被调⽤
- 移除指定队列相关绑定信息数据:移除队列的时候会被调⽤
- 查询所有绑定信息数据:⽤于重启服务器时进⾏历史数据恢复
-
定义绑定信息数据管理类
- 创建绑定信息,并添加管理(存在则OK,不存在则创建)
- 解除指定的绑定信息
- 删除指定队列的所有绑定信息
- 删除交换机相关的所有绑定信息
- 获取交换机相关的所有绑定信息:交换机收到消息后,需要分发给⾃⼰关联的队列
- 判断指定绑定信息是否存在
- 获取当前绑定信息数量
- 销毁所有绑定信息数据
-
-
实现代码
#ifndef __M_Bingding_H__ #define __M_Bingding_H__ #include "../MQCommon/Helper.hpp" #include "../MQCommon/Logger.hpp" #include "../MQCommon/message.pb.h" #include <google/protobuf/map.h> #include <iostream> #include <memory> #include <mutex> #include <unordered_map> namespace MQ { struct Binding { using ptr = std::shared_ptr<Binding>; Binding(const std::string &name_exchange, const std::string &name_queue, const std::string &binding_key) : name_exchange(name_exchange), name_queue(name_queue), binding_key(binding_key) { } std::string name_exchange; std::string name_queue; std::string binding_key; }; // 队列与绑定关系的映射 using QueueBindingMap = std::unordered_map<std::string, Binding::ptr>; // 交换机与队列的映射,方便后续删除绑定信息 using ExchangeQueueMap = std::unordered_map<std::string, QueueBindingMap>; class BindingMapper { public: BindingMapper(const std::string &db_file_path) : _sqlite_helper(db_file_path) { std::string parent_path = FileHelper::parentDirectory(db_file_path); FileHelper::createDirectory(parent_path); _sqlite_helper.open(); creatTable(); } bool creatTable() { std::stringstream ss; ss << "create table if not exists binding_table("; ss << "name_exchange varchar(255) not null,"; ss << " name_queue varchar(255) not null,"; ss << " binding_key varchar(255) not null);"; if (!_sqlite_helper.exec(ss.str(), nullptr, nullptr)) { // 如果创建表失败就直接报异常错误 ELOG("创建binding_table失败"); abort(); } return true; } bool removeTable() { std::stringstream ss; ss << "drop table if exists binding_table"; if (!_sqlite_helper.exec(ss.str(), nullptr, nullptr)) { ELOG("删除binding_table失败"); return false; } return true; } bool insert(const Binding::ptr &binding) { // insert into binding_table values(name_exchange,name_queue,binding_key) std::stringstream ss; ss << "insert into binding_table values("; ss << "'" << binding->name_exchange << "',"; ss << "'" << binding->name_queue << "',"; ss << "'" << binding->binding_key << "');"; if (!_sqlite_helper.exec(ss.str(), nullptr, nullptr)) { ELOG("插入binding_table失败"); return false; } return true; } bool remove(const std::string &name_exchange, const std::string &name_queue) { // delete from binding_table where name_exchange=xxx and name_queue=xxx std::stringstream ss; ss << "delete from binding_table where name_exchange='" << name_exchange << "' and name_queue='" << name_queue << "';"; if (!_sqlite_helper.exec(ss.str(), nullptr, nullptr)) { // 删除绑定信息失败 ELOG("remove删除绑定信息失败"); return false; } return true; } bool removeByExchange(const std::string &name_exchange) { std::stringstream ss; ss << "delete from binding_table where name_exchange='" << name_exchange << "' ;"; if (!_sqlite_helper.exec(ss.str(), nullptr, nullptr)) { // 删除绑定信息失败 ELOG("removeByExchange删除绑定信息失败"); return false; } return true; } bool removeByQueue(const std::string &name_queue) { std::stringstream ss; ss << "delete from binding_table where name_queue='" << name_queue << "';"; if (!_sqlite_helper.exec(ss.str(), nullptr, nullptr)) { // 删除绑定信息失败 ELOG("removeByQueue删除绑定信息失败"); return false; } return true; } ExchangeQueueMap recover() { // select name_exchange,name_queue,binding_key from binding_table ExchangeQueueMap result; std::stringstream ss; ss << "select name_exchange,name_queue,binding_key from binding_table"; _sqlite_helper.exec(ss.str(), selectCallback, &result); return result; } ~BindingMapper() {} static int selectCallback(void *arg, int numcol, char **row, char **fields) { ExchangeQueueMap *result = (ExchangeQueueMap *)arg; Binding::ptr binding = std::make_shared<Binding>(row[0], row[1], row[2]); // 防止交换机信息已经存在,导致原信息被覆盖 // 所以利用unordered_map的[]运算符重载,有则拿出对应交换机所对应的队列哈希表,若没有则创建 QueueBindingMap &queue_binding_map = (*result)[binding->name_exchange]; queue_binding_map.insert(std::make_pair(binding->name_queue, binding)); return 0; } private: SqliteHelper _sqlite_helper; }; class BindingManager { public: using ptr = std::shared_ptr<BindingManager>; BindingManager(const std::string &db_file_path) : _binding_mapper(db_file_path) { _exchange_queue_map = _binding_mapper.recover(); } bool bind(const std::string &name_exchange, const std::string &name_queue, const std::string &binding_key, bool durable) { std::unique_lock<std::mutex> lock(_mutex); auto binding = std::make_shared<Binding>(name_exchange, name_queue, binding_key); auto exchange_it = _exchange_queue_map.find(name_exchange); // 如果绑定信息存在,则不用绑定 // 先查交换机是否存在,再查队列是否存在 if (exchange_it != _exchange_queue_map.end()) { auto queue_it = exchange_it->second.find(name_queue); if (queue_it != exchange_it->second.end()) { DLOG("binding信息已经存在,不用绑定"); return true; } } QueueBindingMap &queue_bingding_map = _exchange_queue_map[binding->name_exchange]; queue_bingding_map.insert(std::make_pair(binding->name_queue, binding)); if (durable) _binding_mapper.insert(binding); return true; } void unbind(const std::string &name_exchange, const std::string &name_queue) { std::unique_lock<std::mutex> lock(_mutex); auto exchange_it = _exchange_queue_map.find(name_exchange); if (exchange_it == _exchange_queue_map.end()) { return; } auto queue_it = exchange_it->second.find(name_queue); if (queue_it == exchange_it->second.end()) { return; } _exchange_queue_map[name_exchange].erase(name_queue); _binding_mapper.remove(name_exchange, name_queue); return; } void unbindByExchange(const std::string &name_exchange) { std::unique_lock<std::mutex> lock(_mutex); auto exchange_it = _exchange_queue_map.find(name_exchange); if (exchange_it == _exchange_queue_map.end()) { return; } _exchange_queue_map.erase(name_exchange); _binding_mapper.removeByExchange(name_exchange); return; } void unbindByQueue(const std::string &name_queue) { std::unique_lock<std::mutex> lock(_mutex); for (auto &exchange_queue_binding : _exchange_queue_map) { exchange_queue_binding.second.erase(name_queue); _binding_mapper.removeByQueue(name_queue); } } bool exist(const std::string &name_exchange, const std::string &name_queue) { std::unique_lock<std::mutex> lock(_mutex); auto exchange_it = _exchange_queue_map.find(name_exchange); if (exchange_it == _exchange_queue_map.end()) { return false; } auto queue_it = exchange_it->second.find(name_queue); if (queue_it == exchange_it->second.end()) { return false; } return true; } void clear() { _binding_mapper.removeTable(); _exchange_queue_map.clear(); } size_t size() { std::unique_lock<std::mutex> lock(_mutex); size_t count = 0; for(auto &exchange_queue_binding : _exchange_queue_map) { count += exchange_queue_binding.second.size(); } return count; } Binding::ptr getBinding(const std::string &name_exchange, const std::string &name_queue) { std::unique_lock<std::mutex> lock(_mutex); auto exchange_it = _exchange_queue_map.find(name_exchange); if (exchange_it == _exchange_queue_map.end()) { return Binding::ptr(); } auto queue_it = exchange_it->second.find(name_queue); if (queue_it == exchange_it->second.end()) { return Binding::ptr(); } return queue_it->second; } QueueBindingMap getExchangeBindings(const std::string &name_exchange) { std::unique_lock<std::mutex> lock(_mutex); auto exchange_it = _exchange_queue_map.find(name_exchange); if(exchange_it == _exchange_queue_map.end()) { ELOG("交换机不存在"); return QueueBindingMap(); } return exchange_it->second; } ~BindingManager() { } private: std::mutex _mutex; ExchangeQueueMap _exchange_queue_map; BindingMapper _binding_mapper; }; } #endif
在MQServer中编写Message.hpp文件实现绑定消息管理
-
需要在此文件中实现的功能:
-
消息的持久化管理
-
管理数据
- 队列消息⽂件存储的路径
- 队列消息的存储⽂件名
- 队列消息的临时交换⽂件名
-
管理操作
- ⽇志消息存储在⽂件中(4B⻓度+(属性+内容+有效位)序列化消息,连续存储即可)
- 提供队列消息⽂件创建/删除功能
- 提供队列消息的新增持久化/删除持久化
- 提供持久化内容的垃圾回收(其实就是重新加载出所有有效消息返回,并重新⽣成新的消息存储⽂件)
-
-
消息的管理(以队列为单位进⾏管理)
-
队列消息管理数据
- 队列名称
- 待推送消息链表
- 持久化消息hash
- 待确认消息hash
- 有效消息数量
- 已经持久化消息总量
- 持久化管理句柄
-
队列管理操作
- 新增消息
- 获取队⾸消息(获取的同时将消息加⼊待确认队列)
- 移除指定待确认消息
- 获取队列待消费&待确认消息数量
- 恢复队列历史消息。
- 销毁队列所有消息
- 判断队列消息是否为空
-
-
消息的总体对外管理
- 管理的操作
- 初始化新建队列的消息管理结构,并创建消息存储⽂件
- 删除队列的消息管理结构,以及消息存储⽂件
- 向指定队列新增消息
- 获取指定队列队⾸消息
- 确认指定队列待确认消息(删除)
- 判断指定队列消息是否为空
- 管理的操作
-
-
实现代码
#ifndef __M_MESSAGE_H__ #define __M_MESSAGE_H__ #include "../MQCommon/Helper.hpp" #include "../MQCommon/Logger.hpp" #include "../MQCommon/message.pb.h" #include <google/protobuf/map.h> #include <iostream> #include <list> #include <memory> #include <mutex> #include <unordered_map> namespace MQ { #define DATAFILE_SUBFIX ".message_data" #define TMPFILE_SUBFIX ".message_data.tmp" using MessagePtr = std::shared_ptr<MQ::Message>; class MessageMapper { public: MessageMapper(std::string &path, const std::string &queue_name) : _name_queue(queue_name) { if (path.back() != '/') path += '/'; _name_data_file = path + queue_name + DATAFILE_SUBFIX; _name_temp_file = path + queue_name + TMPFILE_SUBFIX; if (!FileHelper(path).exists()) { assert(FileHelper::createDirectory(path)); } if (!FileHelper(_name_data_file).exists()) creatFile(); } bool insertDataFile(MessagePtr &message) { return insert(_name_data_file, message); } bool insertTempFile(MessagePtr &message) { return insert(_name_temp_file, message); } bool remove(MessagePtr &message) { std::string filename = _name_data_file; size_t offset = message->offset(); size_t length = message->length(); // 修改文件的有效标志 message->mutable_payload()->set_valid("0"); std::string new_load = message->payload().SerializeAsString(); // 判断新的载荷与原载荷长度是否相同,来防止覆盖掉别的消息 if (new_load.size() != length) { ELOG("消息载荷长度与原载荷长度不同"); return false; } // 将修改合法标志的数据写入文件(覆盖原载荷的位置) FileHelper file_helper(filename); file_helper.write(new_load.c_str(), offset, new_load.size()); return true; } std::list<MessagePtr> garbageCollection() { bool ret; std::list<MessagePtr> result; ret = load(result); if (ret == false) { DLOG("加载有效数据失败!\n"); return result; } // DLOG("垃圾回收,得到有效消息数量:%d", result.size()); // 2. 将有效数据,进行序列化存储到临时文件中 FileHelper::createFile(_name_temp_file); for (auto &msg : result) { DLOG("向临时文件写入数据: %s", msg->payload().body().c_str()); ret = insert(_name_temp_file, msg); if (ret == false) { DLOG("向临时文件写入消息数据失败!!"); return result; } } // DLOG("垃圾回收后,向临时文件写入数据完毕,临时文件大小: %ld", FileHelper(_name_temp_file).size()); // 3. 删除源文件 ret = FileHelper::removeFile(_name_data_file); if (ret == false) { DLOG("删除源文件失败!"); return result; } // 4. 修改临时文件名,为源文件名称 ret = FileHelper(_name_temp_file).rename(_name_data_file); if (ret == false) { DLOG("修改临时文件名称失败!"); return result; } // 5. 返回新的有效数据 return result; } void removeFile() { FileHelper::removeFile(_name_data_file); FileHelper::removeFile(_name_temp_file); } private: bool load(std::list<MessagePtr> &result) { // 挑选出有效信息 FileHelper file_helper(_name_data_file); size_t offset = 0, length = 0; while (offset < file_helper.size()) { // 读取消息长度 file_helper.read((char *)&length, offset, sizeof(size_t)); offset += sizeof(size_t); // 读取消息 std::string load_str(length, '\0'); file_helper.read(&load_str[0], offset, length); offset += length; // 反序列化消息 MessagePtr message = std::make_shared<MQ::Message>(); message->mutable_payload()->ParseFromString(load_str); // 判断消息是否有效 if (message->payload().valid() == std::string("0")) { DLOG("该消息无效,不用插入队列"); continue; } // 将有效消息插入队列 result.push_back(message); } return true; } bool creatFile() { if (FileHelper(_name_data_file).exists()) { return true; } return FileHelper::createFile(_name_data_file); } bool insert(const std::string &filename, MessagePtr &message) { bool ret = true; // 将消息中的消息载荷序列化 std::string load = message->payload().SerializeAsString(); // 找到消息位置的偏移量 FileHelper file_helper(filename); // 偏移量 size_t offset = file_helper.size(); // 消息的长度 size_t length = load.size(); // 先将消息的长度写入文件 ret = file_helper.write((char *)&length, offset, sizeof(size_t)); if (ret == false) { ELOG("写入消息长度失败"); return false; } // 将消息插入文件 ret = file_helper.write(load.c_str(), offset + sizeof(size_t), length); if (ret == false) { ELOG("写入消息失败"); return false; } // 设置message的偏移量和长度 message->set_offset(offset + sizeof(size_t)); message->set_length(length); return true; } private: std::string _name_data_file; std::string _name_temp_file; std::string _name_queue; }; // 队列消息类,主要是负责消息与队列之间的关系 class QueueMessage { public: using ptr = std::shared_ptr<QueueMessage>; QueueMessage(std::string &path, const std::string &qname) : _qname(qname), _valid_count(0), _total_count(0), _mapper(path, qname) { } ~QueueMessage() {} // 传入队列消息的属性、消息体、是否持久化 bool insert(const BasicProperties *bp, const std::string &body, bool queue_is_durable) { // 1. 构造消息对象 MessagePtr msg = std::make_shared<MQ::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 { 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. 进行持久化存储 bool ret = _mapper.insertDataFile(msg); if (ret == 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. 内存的管理 _msgs.push_back(msg); return true; } 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; // 持久化文件中有效消息数量 -1 garbageCollection(); // 内部判断是否需要垃圾回收,需要的话则回收一下 } // 4. 删除内存中的信息 _waitack_msgs.erase(msg_id); // DLOG("确认消息后,删除消息的管理成功:%s", it->second->payload().body().c_str()); return true; } bool recovery() { // 恢复历史消息 std::unique_lock<std::mutex> lock(_mutex); _msgs = _mapper.garbageCollection(); for (auto &msg : _msgs) { _durable_msgs.insert(std::make_pair(msg->payload().properties().id(), msg)); } _valid_count = _total_count = _msgs.size(); return true; } // 从队首取出消息 MessagePtr front() { std::unique_lock<std::mutex> lock(_mutex); if (_msgs.empty()) { return MessagePtr(); } // 获取一条队首消息:从_msgs中取出数据 MessagePtr msg = _msgs.front(); _msgs.pop_front(); // 将该消息对象,向待确认的hash表中添加一份,等到收到消息确认后进行删除 _waitack_msgs.insert(std::make_pair(msg->payload().properties().id(), msg)); return msg; } size_t getAbleCount() { std::unique_lock<std::mutex> lock(_mutex); return _msgs.size(); } size_t getTotalCount() { std::unique_lock<std::mutex> lock(_mutex); return _total_count; } size_t getDurableCount() { std::unique_lock<std::mutex> lock(_mutex); return _durable_msgs.size(); } size_t getWaitackCount() { std::unique_lock<std::mutex> lock(_mutex); return _waitack_msgs.size(); } void clear() { std::unique_lock<std::mutex> lock(_mutex); _mapper.removeFile(); _msgs.clear(); _durable_msgs.clear(); _waitack_msgs.clear(); _valid_count = 0; _total_count = 0; } private: bool garbageCollectionCheck() { // 持久化的消息总量大于2000, 且其中有效比例低于50%则需要持久化 if (_total_count > 2000 && _valid_count * 10 / _total_count < 5) { return true; } return false; } void garbageCollection() { // 1. 进行垃圾回收,获取到垃圾回收后,有效的消息信息链表 if (garbageCollectionCheck() == false) return; std::list<MessagePtr> msgs = _mapper.garbageCollection(); 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 }; class MessageManager { public: using ptr = std::shared_ptr<MessageManager>; MessageManager(const std::string &basedir) : _basedir(basedir) {} ~MessageManager() {} 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 clear() { std::unique_lock<std::mutex> lock(_mutex); for (auto &qmsg : _queue_msgs) { qmsg.second->clear(); } } 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()) { ELOG("向队列%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()) { ELOG("确认队列%s消息%s失败:没有找到消息管理句柄!", qname.c_str(), msg_id.c_str()); return; } qmp = it->second; } qmp->remove(msg_id); return; } size_t getAbleCount(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->getAbleCount(); } size_t getTotalCount(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->getTotalCount(); } size_t getDurableCount(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->getDurableCount(); } size_t getWaitAckCount(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->getWaitackCount(); } private: std::mutex _mutex; std::string _basedir; std::unordered_map<std::string, QueueMessage::ptr> _queue_msgs; }; } #endif
在MQServer中编写VirtualHost.hpp文件实现虚拟机管理
-
虚拟机模块是对上述几个数据管理模块的整合,并基于数据之间的关联关系进⾏联合操作。
-
在此文件要实现的功能有:
-
虚拟机需要管理的数据:
- 交换机数据管理模块句柄
- 队列数据管理模块句柄
- 绑定数据管理模块句柄
- 消息数据管理模块句柄
-
虚拟机需要管理的操作:
- 提供声明交换机的功能(存在则OK,不存在则创建)
- 提供删除交换机的功能(删除交换机的同时删除关联绑定信息)
- 提供声明队列的功能(存在则OK,不存在则创建,创建的同时创建队列关联消息管理对象)
- 提供删除队列的功能(删除队列的同时删除关联绑定信息,删除关联消息管理对象及队列所有消息)
- 提供交换机-队列绑定的功能
- 提供交换机-队列解绑的功能
- 提供获取交换机相关的所有绑定信息功能
- 提供新增消息的功能
- 提供获取指定队列队⾸消息的功能
- 提供消息确认删除的功能
-
-
实现代码
#ifndef __M_Virtualhost_H__ #define __M_Virtualhost_H__ #include "../MQCommon/Helper.hpp" #include "../MQCommon/Logger.hpp" #include "Binding.hpp" #include "Exchange.hpp" #include "Message.hpp" #include "Queue.hpp" #include <google/protobuf/map.h> #include <iostream> #include <memory> #include <mutex> #include <unordered_map> namespace MQ { class VirtualHost { public: using ptr = std::shared_ptr<VirtualHost>; VirtualHost(const std::string &host_name, const std::string &base_dir, const std::string &db_file) : _host_name(host_name), _exchange_manager_pointer(std::make_shared<ExchangeManager>(db_file)), _queue_manager_pointer(std::make_shared<QueueManager>(db_file)), _message_manager_pointer(std::make_shared<MessageManager>(base_dir)), _binding_manager_pointer(std::make_shared<BindingManager>(db_file)) { QueueMap queue_map = _queue_manager_pointer->allQueues(); for (auto &queue_pair : queue_map) { _message_manager_pointer->initQueueMessage(queue_pair.first); } } bool declareExchange(const std::string &name, ExchangeType type, bool durable, bool auto_delete, const google::protobuf::Map<std::string, std::string> &args) { return _exchange_manager_pointer->declareExchange(name, type, durable, auto_delete, args); } void deleteExchange(const std::string &name) { // 删除交换机的时候,需要将交换机相关的绑定信息也删除掉。 _binding_manager_pointer->unbindByExchange(name); return _exchange_manager_pointer->deleteExchange(name); } bool existExchange(const std::string &name) { return _exchange_manager_pointer->exists(name); } Exchange::ptr selectExchange(const std::string &ename) { return _exchange_manager_pointer->selectExchange(ename); } bool declareQueue(const std::string &qname, bool qdurable, bool qexclusive, bool qauto_delete, const google::protobuf::Map<std::string, std::string> &qargs) { // 初始化队列的消息句柄(消息的存储管理) // 队列的创建 _message_manager_pointer->initQueueMessage(qname); return _queue_manager_pointer->declareQueue(qname, qdurable, qexclusive, qauto_delete, qargs); } bool deleteQueue(const std::string &name) { // 删除的时候队列相关的数据有两个:队列的消息,队列的绑定信息 _message_manager_pointer->destroyQueueMessage(name); _binding_manager_pointer->unbindByQueue(name); return _queue_manager_pointer->deleteQueue(name); } bool existQueue(const std::string &name) { return _queue_manager_pointer->exist(name); } QueueMap allQueues() { return _queue_manager_pointer->allQueues(); } bool bind(const std::string &ename, const std::string &qname, const std::string &key) { Exchange::ptr ep = _exchange_manager_pointer->selectExchange(ename); if (ep.get() == nullptr) { DLOG("进行队列绑定失败,交换机%s不存在!", ename.c_str()); return false; } Queue::ptr mqp = _queue_manager_pointer->selectQueue(qname); if (mqp.get() == nullptr) { DLOG("进行队列绑定失败,队列%s不存在!", qname.c_str()); return false; } return _binding_manager_pointer->bind(ename, qname, key, ep->_durable && mqp->_durable); } void unBind(const std::string &ename, const std::string &qname) { return _binding_manager_pointer->unbind(ename, qname); } QueueBindingMap exchangeBindings(const std::string &ename) { return _binding_manager_pointer->getExchangeBindings(ename); } bool existBinding(const std::string &ename, const std::string &qname) { return _binding_manager_pointer->exist(ename, qname); } bool basicPublish(const std::string &qname, BasicProperties *bp, const std::string &body) { Queue::ptr mqp = _queue_manager_pointer->selectQueue(qname); if (mqp.get() == nullptr) { DLOG("发布消息失败,队列%s不存在!", qname.c_str()); return false; } return _message_manager_pointer->insert(qname, bp, body, mqp->_durable); } MessagePtr basicConsume(const std::string &qname) { return _message_manager_pointer->front(qname); } void basicAck(const std::string &qname, const std::string &msgid) { return _message_manager_pointer->ack(qname, msgid); } void clear() { _exchange_manager_pointer->clear(); _queue_manager_pointer->clear(); _binding_manager_pointer->clear(); _message_manager_pointer->clear(); } ~VirtualHost() {} private: std::string _host_name; ExchangeManager::ptr _exchange_manager_pointer; QueueManager::ptr _queue_manager_pointer; MessageManager::ptr _message_manager_pointer; BindingManager::ptr _binding_manager_pointer; }; } #endif
在MQServer中编写Route.hpp文件实现交换机路由管理
-
路由模块负责判断生产者发布的消息应该被推送到那个队列中区
-
在此文件需要实现的功能:
- 键验证功能:验证RoutingKey和BindingKey是否合法
- 路由匹配功能:通过比较RoutingKey和BindingKey来进行分配
-
实现代码
#ifndef __M_Route_H__ #define __M_Route_H__ #include "../MQCommon/Helper.hpp" #include "../MQCommon/message.pb.h" #include <string> #include <vector> namespace MQ { class RouteManager { public: // 判断是否是合法的routingkey,routingkey是设置在交换机里的 static bool isValidRoutingKey(const std::string &routing_key) { // routing_key:只需要判断是否包含有非法字符即可, 合法字符( a~z, A~Z, 0~9, ., _) for (int i = 0; i < routing_key.size(); i++) { if ((routing_key[i] >= 'a' && routing_key[i] <= 'z') || (routing_key[i] >= 'A' && routing_key[i] <= 'Z') || (routing_key[i] >= '0' && routing_key[i] <= '9') || (routing_key[i] == '_') || (i - 1 >= 0 && routing_key[i] == '.' && routing_key[i - 1] != routing_key[i])) { continue; } return false; } return true; } // 判断是否是合法的bindingkey,bindingkey是发布客户端给的 static bool isValidBindingKey(const std::string &binding_key) { // 1. 判断是否包含有非法字符, 合法字符:a~z, A~Z, 0~9, ., _, *, # for (auto &ch : binding_key) { if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || (ch == '_' || ch == '.') || (ch == '*' || ch == '#')) { continue; } return false; } // 2. *和#必须独立存在: news.music#.*.# std::vector<std::string> sub_words; StrHelper::split(binding_key, ".", sub_words); for (std::string &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; } static bool route(ExchangeType type, const std::string &routing_key, const std::string &binding_key) { // 如果是直接交换机,只需要判断routing_key是否与binding_key相同 if (type == ExchangeType::DIRECT) { return (routing_key == binding_key); } else if (type == ExchangeType::FANOUT) // 如果是广播交换机,则无需操作 { return true; } else if (type == ExchangeType::TOPIC) // 主题交换:要进行模式匹配 news.# & news.music.pop { // 1. 将binding_key与routing_key进行字符串分割,得到各个的单词数组 std::vector<std::string> bkeys, rkeys; int count_binding_key = StrHelper::split(binding_key, ".", bkeys); int count_routing_key = StrHelper::split(routing_key, ".", rkeys); // 2. 定义标记数组,并初始化[0][0]位置为true,其他位置为false std::vector<std::vector<bool>> dp(count_binding_key + 1, std::vector<bool>(count_routing_key + 1, false)); dp[0][0] = true; // 3. 如果binding_key以#起始,则将#对应行的第0列置为1. for (int i = 1; i <= count_binding_key; i++) { if (bkeys[i - 1] == "#") { dp[i][0] = true; continue; } break; } // 4. 使用routing_key中的每个单词与binding_key中的每个单词进行匹配并标记数组 for (int i = 1; i <= count_binding_key; i++) { for (int j = 1; j <= count_routing_key; j++) { // 如果当前bkey是个*,或者两个单词相同,表示单词匹配成功,则从左上方继承结果 if (bkeys[i - 1] == rkeys[j - 1] || bkeys[i - 1] == "*") { dp[i][j] = dp[i - 1][j - 1]; } else if (bkeys[i - 1] == "#") { // 如果当前bkey是个#,则需要从左上,左边,上边继承结果 dp[i][j] = dp[i - 1][j - 1] | dp[i][j - 1] | dp[i - 1][j]; } } } return dp[count_binding_key][count_routing_key]; } } }; } #endif
在MQServer中编写Consumer.hpp文件实现队列消费者/订阅者管理
-
消费者管理–以队列为单元进⾏管理-队列消费者管理结构
-
需要管理的操作:
- 新增消费者:信道提供的服务是订阅队列消息的时候创建
- 删除消费者:取消订阅 / 信道关闭 / 连接关闭 的时候删除
- 获取消费者:从队列所有的消费者中按序取出⼀个消费者进⾏消息的推送
- 判断队列消费者是否为空
- 判断指定消费者是否存在
- 清理队列所有消费者
-
需要管理的数据
- 消费者管理结构:vector
- 轮转序号:⼀个队列可能会有多个消费者,但是⼀条消息只需要被⼀个消费者消费即可,因此采⽤RR轮转
- 互斥锁:保证线程安全
- 队列名称
-
对消费者进⾏统⼀管理结构
- 初始化/删除队列的消费者信息结构(创建/删除队列的时候初始化)
- 向指定队列新增消费者(客⼾端订阅指定队列消息的时候):新增完成的时候返回消费者对象
- 从指定队列移除消费者(客⼾端取消订阅的时候)
- 移除指定队列的所有消费者(队列被删除时销毁):删除消费者的队列管理单元对象
- 从指定队列获取⼀个消费者(轮询获取-消费者轮换消费起到负载均衡的作⽤)
- 判断队列中消费者是否为空
- 判断队列中指定消费者是否存在
- 清理所有消费者
-
实现代码
#ifndef __M_CONSUMER_H__ #define __M_CONSUMER_H__ #include "../MQCommon/Helper.hpp" #include "../MQCommon/Logger.hpp" #include "../MQCommon/message.pb.h" #include <google/protobuf/map.h> #include <iostream> #include <memory> #include <mutex> #include <unordered_map> namespace MQ { // 回调函数 // 第一个参数为消息标识,第二个参数为消息属性,第三个参数为要处理的消息体 using ConsumerCallback = std::function<void(const std::string, const BasicProperties *bp, const std::string)>; struct Consumer { // 自动应答标志 bool _auto_ack; // 订阅的队列名称 std::string _subscribe_queue_name; // 消费者标识 std::string _consumer_tag; // 消费者回调函数 ConsumerCallback _callback; // 指针 using ptr = std::shared_ptr<Consumer>; // 构造函数 Consumer() {} Consumer(const std::string &consumer_tag, const std::string &subscribe_queue_name, bool auto_ack, const ConsumerCallback &callback) : _auto_ack(auto_ack), _subscribe_queue_name(subscribe_queue_name), _consumer_tag(consumer_tag), _callback(callback) {} // 析构函数 virtual ~Consumer() {} }; // 以队列为单元的消费者管理结构 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, ConsumerCallback& cb) { // 1. 加锁 std::unique_lock<std::mutex> lock(_mutex); // 2. 判断消费者是否重复 for (auto &consumer : _consumers) { if (consumer->_consumer_tag == ctag) { return Consumer::ptr(); } } // 3. 没有重复则新增--构造对象 auto consumer = std::make_shared<Consumer>(ctag, queue_name, ack_flag, cb); // 4. 添加管理后返回对象 _consumers.push_back(consumer); return consumer; } // 队列移除消费者 void remove(const std::string &ctag) { // 1. 加锁 std::unique_lock<std::mutex> lock(_mutex); // 2. 遍历查找-删除 for (auto it = _consumers.begin(); it != _consumers.end(); ++it) { if ((*it)->_consumer_tag == ctag) { _consumers.erase(it); return; } } return; } // 队列获取消费者:RR轮转获取 Consumer::ptr choose() { // 1. 加锁 std::unique_lock<std::mutex> lock(_mutex); if (_consumers.size() == 0) { return Consumer::ptr(); } // 2. 获取当前轮转到的下标 int idx = _rr_seq % _consumers.size(); _rr_seq++; // 3. 获取对象,返回 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); // 2. 遍历查找 for (auto it = _consumers.begin(); it != _consumers.end(); ++it) { if ((*it)->_consumer_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; // 轮转序号 std::vector<Consumer::ptr> _consumers; }; class ConsumerManager { public: using ptr = std::shared_ptr<ConsumerManager>; ConsumerManager() {} void initQueueConsumer(const std::string &qname) { // 1. 加锁 std::unique_lock<std::mutex> lock(_mutex); // 2. 重复判断 auto it = _qconsumers.find(qname); if (it != _qconsumers.end()) { return; } // 3. 新增 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 createConsumer(const std::string &ctag, const std::string &queue_name, bool ack_flag, 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 removeConsumer(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 chooseConsumer(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 isEmpty(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 isExist(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
在MQServer中编写Channel.hpp文件实现队列信道管理
-
在AMQP模型中,除了通信连接Connection概念外,还有⼀个Channel的概念,Channel是针对Connection连接的⼀个更细粒度的通信信道,多个Channel可以使⽤同⼀个通信连接Connection进⾏通信,但是同⼀个Connection的Channel之间相互独⽴
-
⽽信道模块就是再次将上述模块进⾏整合提供服务的模块
-
在该文件中要实现的功能:
-
信道管理的信息:
- 信道ID:信道的唯⼀标识
- 信道关联的消费者:⽤于消费者信道在关闭的时候取消订阅,删除订阅者信息
- 信道关联的连接:⽤于向客⼾端发送数据(响应,推送的消息)
- protobuf协议处理句柄:⽹络通信前的协议处理
- 消费者管理句柄:信道关闭/取消订阅的时候,通过句柄删除订阅者信息
- 虚拟机句柄:交换机/队列/绑定/消息数据管理
- ⼯作线程池句柄(⼀条消息被发布到队列后,需要将消息推送给订阅了对应队列的消费者,过程由线程池完成)
-
信道管理的操作:
- 提供声明&删除交换机操作(删除交换机的同时删除交换机关联的绑定信息)
- 提供声明&删除队列操作(删除队列的同时,删除队列关联的绑定信息,消息,消费者信息)
- 提供绑定&解绑队列操作
- 提供订阅&取消订阅队列消息操作
- 提供发布&确认消息操作
-
-
实现代码
#ifndef __M_Channel_H__ #define __M_Channel_H__ #include "../MQCommon/Helper.hpp" #include "../MQCommon/Logger.hpp" #include "../MQCommon/ThreadPool.hpp" #include "../MQCommon/message.pb.h" #include "../MQCommon/request.pb.h" #include "Consumer.hpp" #include "Route.hpp" #include "VirtualHost.hpp" #include "muduo/net/TcpConnection.h" #include "muduo/protobuf/codec.h" namespace MQ { // 指针的定义 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_channel, const VirtualHost::ptr &virtualhost_ptr, const ConsumerManager::ptr &consumer_manager_ptr, const ProtobufCodecPtr &codec_ptr, const muduo::net::TcpConnectionPtr &connection_ptr, const ThreadPool::ptr &threadpool_ptr) : _id_channel(id_channel), _virtualhost_ptr(virtualhost_ptr), _consumer_manager_ptr(consumer_manager_ptr), _connection_ptr(connection_ptr), _threadpool_ptr(threadpool_ptr) { DLOG("new Channel: %p", this); } ~Channel() { if (_consumer_ptr.get() != nullptr) { _consumer_manager_ptr->removeConsumer(_consumer_ptr->_consumer_tag, _consumer_ptr->_subscribe_queue_name); } DLOG("del Channel: %p", this); } // 交换机的声明与删除 void declareExchange(const declareExchangeRequestPtr &req) { bool ret = _virtualhost_ptr->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) { _virtualhost_ptr->deleteExchange(req->exchange_name()); return basicResponse(true, req->rid(), req->cid()); } // 队列的声明与删除 void declareQueue(const declareQueueRequestPtr &req) { bool ret = _virtualhost_ptr->declareQueue(req->queue_name(), req->durable(), req->exclusive(), req->auto_delete(), req->args()); if (ret == false) { return basicResponse(false, req->rid(), req->cid()); } _consumer_manager_ptr->initQueueConsumer(req->queue_name()); // 初始化队列的消费者管理句柄 return basicResponse(true, req->rid(), req->cid()); } void deleteQueue(const deleteQueueRequestPtr &req) { _consumer_manager_ptr->destroyQueueConsumer(req->queue_name()); _virtualhost_ptr->deleteQueue(req->queue_name()); return basicResponse(true, req->rid(), req->cid()); } // 队列的绑定与解除绑定 void queueBind(const queueBindRequestPtr &req) { bool ret = _virtualhost_ptr->bind(req->exchange_name(), req->queue_name(), req->binding_key()); return basicResponse(ret, req->rid(), req->cid()); } void queueUnBind(const queueUnBindRequestPtr &req) { _virtualhost_ptr->unBind(req->exchange_name(), req->queue_name()); return basicResponse(true, req->rid(), req->cid()); } // 消息的发布 void basicPublish(const basicPublishRequestPtr &req) { // 1. 判断交换机是否存在 auto ep = _virtualhost_ptr->selectExchange(req->exchange_name()); if (ep.get() == nullptr) { return basicResponse(false, req->rid(), req->cid()); } // 2. 进行交换路由,判断消息可以发布到交换机绑定的哪个队列中 QueueBindingMap mqbm = _virtualhost_ptr->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 (RouteManager::route(ep->_type, routing_key, binding.second->binding_key)) { // 3. 将消息添加到队列中(添加消息的管理) _virtualhost_ptr->basicPublish(binding.first, properties, req->body()); // 4. 向线程池中添加一个消息消费任务(向指定队列的订阅者去推送消息--线程池完成) auto task = std::bind(&Channel::consume, this, binding.first); _threadpool_ptr->push(task); } } return basicResponse(true, req->rid(), req->cid()); } // 消息的确认 void basicAck(const basicAckRequestPtr &req) { _virtualhost_ptr->basicAck(req->queue_name(), req->message_id()); return basicResponse(true, req->rid(), req->cid()); } // 订阅队列消息 void basicConsume(const basicConsumeRequestPtr &req) { // 1. 判断队列是否存在 bool ret = _virtualhost_ptr->existQueue(req->queue_name()); if (ret == false) { return basicResponse(false, req->rid(), req->cid()); } // 2. 创建队列的消费者 auto callback_func = std::bind(&Channel::callback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); // 创建了消费者之后,当前的channel角色就是个消费者 _consumer_ptr = _consumer_manager_ptr->createConsumer(req->consumer_tag(), req->queue_name(), req->auto_ack(), callback_func); return basicResponse(true, req->rid(), req->cid()); } // 取消订阅 void basicCancel(const basicCancelRequestPtr &req) { _consumer_manager_ptr->removeConsumer(req->consumer_tag(), req->queue_name()); return basicResponse(true, req->rid(), req->cid()); } private: void callback(const std::string tag, const BasicProperties *base_properties, const std::string &body) { // 针对参数组织出推送消息请求,将消息推送给channel对应的客户端 basicConsumeResponse resp; resp.set_cid(_id_channel); resp.set_body(body); resp.set_consumer_tag(tag); if (base_properties) { resp.mutable_properties()->set_id(base_properties->id()); resp.mutable_properties()->set_delivery_mode(base_properties->delivery_mode()); resp.mutable_properties()->set_routing_key(base_properties->routing_key()); } _codec_ptr->send(_connection_ptr, resp); } void consume(const std::string &qname) { // 指定队列消费消息 // 1. 从队列中取出一条消息 MessagePtr mp = _virtualhost_ptr->basicConsume(qname); if (mp.get() == nullptr) { DLOG("执行消费任务失败,%s 队列没有消息!", qname.c_str()); return; } // 2. 从队列订阅者中取出一个订阅者 Consumer::ptr cp = _consumer_manager_ptr->chooseConsumer(qname); if (cp.get() == nullptr) { DLOG("执行消费任务失败,%s 队列没有消费者!", qname.c_str()); return; } // 3. 调用订阅者对应的消息处理函数,实现消息的推送 cp->_callback(cp->_consumer_tag, mp->mutable_payload()->mutable_properties(), mp->payload().body()); // 4. 判断如果订阅者是自动确认---不需要等待确认,直接删除消息,否则需要外部收到消息确认后再删除 if (cp->_auto_ack) _virtualhost_ptr->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_ptr->send(_connection_ptr, resp); } private: // 信道ID std::string _id_channel; // 消费者指针 Consumer::ptr _consumer_ptr; // 链接指针 muduo::net::TcpConnectionPtr _connection_ptr; // 协议处理器 ProtobufCodecPtr _codec_ptr; // 消费者管理器 ConsumerManager::ptr _consumer_manager_ptr; // 虚拟主机 VirtualHost::ptr _virtualhost_ptr; // 线程池 ThreadPool::ptr _threadpool_ptr; }; 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
在MQServer中编写Connection.hpp文件实现链接管理
-
向⽤⼾提供⼀个⽤于实现⽹络通信的Connection对象,从其内部可创建出粒度更轻的Channel对象,⽤于与客⼾端进⾏⽹络通信
-
在该文件中要实现的功能:
-
需要管理的信息:
- 连接关联的信道管理句柄(实现信道的增删查)
- 连接关联的实际⽤于通信的muduo::net::Connection连接
- protobuf协议处理的句柄(ProtobufCodec对象)
- 消费者管理句柄
- 虚拟机句柄
- 异步⼯作线程池句柄
-
连接操作:
- 提供创建Channel信道的操作
- 提供删除Channel信道的操作
-
-
实现代码
#ifndef __M_Connection_H__ #define __M_Connection_H__ #include "Channel.hpp" namespace MQ { /** * @class Connection * @brief 表示单个客户端连接的管理类 * * 负责管理连接生命周期内的信道操作,包括: * - 信道创建/关闭 * - 请求响应处理 * - 与虚拟主机、消费者管理器的交互 */ class Connection { public: using ptr = std::shared_ptr<Connection>; /** * @brief 构造函数(依赖注入核心组件) * @param host 所属虚拟主机管理接口 * @param cmp 消费者管理器 * @param codec Protobuf编解码器 * @param conn 底层TCP连接对象 * @param pool 线程池(用于异步任务处理) */ Connection(const VirtualHost::ptr &host, const ConsumerManager::ptr &cmp, const ProtobufCodecPtr &codec, const muduo::net::TcpConnectionPtr &conn, const ThreadPool::ptr &pool) : _host_ptr(host), _consumer_manager_ptr(cmp), _codec_ptr(codec), _tcp_connection_ptr(conn), _threadpool_ptr(pool), _channels_ptr(std::make_shared<ChannelManager>()) // 初始化信道管理器 { } /** * @brief 打开新信道(线程安全) * @param req 包含信道ID的请求对象 * 流程: * 1. 检查信道ID唯一性 * 2. 通过ChannelManager创建信道 * 3. 发送操作响应 */ void openChannel(const openChannelRequestPtr &req) { bool ret = _channels_ptr->openChannel( req->cid(), _host_ptr, _consumer_manager_ptr, _codec_ptr, _tcp_connection_ptr, _threadpool_ptr); if (!ret) { DLOG("信道创建失败:重复的CID %s", req->cid().c_str()); return basicResponse(false, req->rid(), req->cid()); } DLOG("信道创建成功 CID: %s", req->cid().c_str()); basicResponse(true, req->rid(), req->cid()); } /** * @brief 关闭指定信道 * @param req 包含目标信道ID的请求 * 注意:立即发送操作确认响应 */ void closeChannel(const closeChannelRequestPtr &req) { _channels_ptr->closeChannel(req->cid()); basicResponse(true, req->rid(), req->cid()); } /** * @brief 获取指定信道对象 * @param cid 信道标识符 * @return 信道智能指针(不存在时返回空指针) */ Channel::ptr getChannel(const std::string &cid) { return _channels_ptr->getChannel(cid); } private: /** * @brief 发送通用响应消息 * @param ok 操作是否成功 * @param rid 请求ID(用于请求-响应匹配) * @param cid 连接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_ptr->send(_tcp_connection_ptr, resp); // 通过编解码器发送响应 } private: // 核心组件依赖 muduo::net::TcpConnectionPtr _tcp_connection_ptr; // 底层TCP连接 ProtobufCodecPtr _codec_ptr; // 协议编解码器 ConsumerManager::ptr _consumer_manager_ptr; // 消费者管理 VirtualHost::ptr _host_ptr; // 所属虚拟主机 ThreadPool::ptr _threadpool_ptr; // 异步任务线程池 // 信道管理 ChannelManager::ptr _channels_ptr; // 多信道管理容器 }; /** * @class ConnectionManager * @brief 全局连接管理类(单例模式) * * 功能: * - 维护所有活跃连接 * - 提供连接查找功能 * - 保证线程安全的连接操作 */ class ConnectionManager { public: using ptr = std::shared_ptr<ConnectionManager>; ConnectionManager() = default; /** * @brief 创建并注册新连接 * @param host 虚拟主机实例 * @param cmp 消费者管理器 * @param codec 编解码器 * @param conn 新建立的TCP连接 * @param pool 线程池 * 线程安全:通过互斥锁保证注册操作的原子性 */ 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); if (!_connections.contains(conn)) { auto connection = std::make_shared<Connection>( host, cmp, codec, conn, pool); _connections.emplace(conn, connection); } } /** * @brief 删除指定连接 * @param conn 要移除的TCP连接 */ void delConnection(const muduo::net::TcpConnectionPtr &conn) { std::unique_lock<std::mutex> lock(_mutex); _connections.erase(conn); } /** * @brief 获取连接对象 * @param conn TCP连接指针 * @return 关联的Connection对象(不存在时返回空指针) */ Connection::ptr getConnection(const muduo::net::TcpConnectionPtr &conn) { std::unique_lock<std::mutex> lock(_mutex); auto it = _connections.find(conn); return (it != _connections.end()) ? it->second : nullptr; } private: std::mutex _mutex; // 连接映射表互斥锁 std::unordered_map<muduo::net::TcpConnectionPtr, Connection::ptr> _connections; }; } // namespace MQ #endif
在MQServer中编写Broker.hpp文件实现服务器模块
-
BrokerServer模块是对整体服务器所有模块的整合,接收客⼾端的请求,并提供服务
-
实现代码
#ifndef __M_Broker_H__ #define __M_Broker_H__ #include "../MQCommon/Logger.hpp" #include "../MQCommon/ThreadPool.hpp" #include "../MQCommon/message.pb.h" #include "../MQCommon/request.pb.h" #include "Connection.hpp" #include "Consumer.hpp" #include "VirtualHost.hpp" #include "muduo/base/Logging.h" #include "muduo/base/Mutex.h" #include "muduo/net/EventLoop.h" #include "muduo/net/TcpServer.h" #include "muduo/protobuf/codec.h" #include "muduo/protobuf/dispatcher.h" namespace MQ { #define DBFILE "/meta.db" #define HOSTNAME "MyVirtualHost" class BrokerServer { public: typedef std::shared_ptr<google::protobuf::Message> MessagePtr; // 构造函数 BrokerServer(int port, const std::string &basedir) : _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port), "Server", muduo::net::TcpServer::kReusePort), // muduo服务器对象初始化 _dispatcher(std::bind(&BrokerServer::onUnknownMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)), // 请求分发器对象初始化 _codec(std::make_shared<ProtobufCodec>(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))), // protobuf协议处理器对象初始化 _virtual_host(std::make_shared<VirtualHost>(HOSTNAME, basedir, basedir + DBFILE)), // 虚拟主机对象初始化 _consumer_manager(std::make_shared<ConsumerManager>()), // 消费者管理器对象初始化 _connection_manager(std::make_shared<ConnectionManager>()), // 连接管理器对象初始化 _threadpool(std::make_shared<ThreadPool>()) // 线程池对象初始化 { // 针对历史消息中的所有队列,别忘了,初始化队列的消费者管理结构 QueueMap queue_map = _virtual_host->allQueues(); for (auto &q : queue_map) { _consumer_manager->initQueueConsumer(q.first); DLOG("%s",q.first.c_str()); } // 注册业务请求处理函数 _dispatcher.registerMessageCallback<MQ::openChannelRequest>(std::bind(&BrokerServer::onOpenChannel, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::closeChannelRequest>(std::bind(&BrokerServer::onCloseChannel, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::declareExchangeRequest>(std::bind(&BrokerServer::onDeclareExchange, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::deleteExchangeRequest>(std::bind(&BrokerServer::onDeleteExchange, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::declareQueueRequest>(std::bind(&BrokerServer::onDeclareQueue, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::deleteQueueRequest>(std::bind(&BrokerServer::onDeleteQueue, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::queueBindRequest>(std::bind(&BrokerServer::onQueueBind, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::queueUnBindRequest>(std::bind(&BrokerServer::onQueueUnBind, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::basicPublishRequest>(std::bind(&BrokerServer::onBasicPublish, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::basicAckRequest>(std::bind(&BrokerServer::onBasicAck, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::basicConsumeRequest>(std::bind(&BrokerServer::onBasicConsume, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<MQ::basicCancelRequest>(std::bind(&BrokerServer::onBasicCancel, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _server.setMessageCallback(std::bind(&ProtobufCodec::onMessage, _codec.get(), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _server.setConnectionCallback(std::bind(&BrokerServer::onConnection, this, std::placeholders::_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
在MQServer中编写Server.cpp文件实现服务器示例
-
实现代码
#include "Broker.hpp" int main() { MQ::BrokerServer server(8085, "./data/"); server.start(); return 0; }
在MQClient中编写客户端模块代码
在MQClient中编写makefile文件来编译客户端模块
.PHONY:all
all:PublichClient ConsumeClient
PublichClient : PublichClient.cpp ../MQCommon/request.pb.cc ../MQCommon/message.pb.cc ../ThirdLib/lib/include/muduo/protobuf/codec.cc
g++ -g -std=c++11 $^ -o $@ -I../ThirdLib/lib/include -L../ThirdLib/lib/lib -lmuduo_net -lmuduo_base -pthread -lprotobuf -lz
ConsumeClient : ConsumeClient.cpp ../MQCommon/request.pb.cc ../MQCommon/message.pb.cc ../ThirdLib/lib/include/muduo/protobuf/codec.cc
g++ -g -std=c++11 $^ -o $@ -I../ThirdLib/lib/include -L../ThirdLib/lib/lib -lmuduo_net -lmuduo_base -pthread -lprotobuf -lz
在MQClient中编写Subscriber.hpp文件实现订阅者模块
-
与服务端,并⽆太⼤差别
-
在该文件中,应该实现的功能:
-
订阅者信息:
- 订阅者标识
- 订阅队列名
- 是否⾃动确认标志
- 回调处理函数(收到消息后该如何处理的回调函数对象)
-
实现函数
#ifndef __M_Subscriber_H__ #define __M_Subscriber_H__ #include "../MQCommon/Helper.hpp" #include "../MQCommon/Logger.hpp" #include "../MQCommon/message.pb.h" #include <google/protobuf/map.h> #include <iostream> #include <memory> #include <mutex> #include <unordered_map> namespace MQ { using SubscriberCallback = std::function<void(const std::string, const BasicProperties *bp, const std::string)>; class Subscriber { public: using ptr = std::shared_ptr<Subscriber>; // 构造函数 Subscriber() {} Subscriber(const std::string &consumer_tag, const std::string &subscribe_queue_name, bool auto_ack, const SubscriberCallback &callback) : _auto_ack(auto_ack), _subscribe_queue_name(subscribe_queue_name), _subscribe_queue_tag(consumer_tag), _callback(callback) { } // 析构函数 virtual ~Subscriber() {} public: // 自动应答标志 bool _auto_ack; // 订阅的队列名称 std::string _subscribe_queue_name; // 消费者标识 std::string _subscribe_queue_tag; // 消费者回调函数 SubscriberCallback _callback; }; } #endif
-
在MQClient中编写Channel.hpp文件实现信道管理模块
-
同样的,客⼾端也有信道,其功能与服务端⼏乎⼀致,或者说不管是客⼾端的channel还是服务端的channel都是为了⽤⼾提供具体服务⽽存在的,只不过服务端是为客⼾端的对应请求提供服务,⽽客⼾端的接⼝服务是为了⽤⼾具体需要服务,也可以理解是⽤⼾通过客⼾端channel的接⼝调⽤来向服务端发送对应请求,获取请求的服务
-
在该文件中,应该实现的功能:
-
信道信息:
- 信道ID
- 信道关联的⽹络通信连接对象
- protobuf协议处理对象
- 信道关联的消费者
- 请求对应的响应信息队列(这⾥队列使⽤<请求ID,响应>hash表,以便于查找指定的响应)
- 互斥锁&条件变量(⼤部分的请求都是阻塞操作,发送请求后需要等到响应才能继续,但是muduo库的通信是异步的,因此需要我们⾃⼰在收到响应后,通过判断是否是等待的指定响应来进⾏同步)
-
信道操作:
- 提供创建信道操作
- 提供删除信道操作
- 提供声明交换机操作(强断⾔-有则OK,没有则创建)
- 提供删除交换机
- 提供创建队列操作(强断⾔-有则OK,没有则创建)
- 提供删除队列操作
- 提供交换机-队列绑定操作
- 提供交换机-队列解除绑定操作
- 提供添加订阅操作
- 提供取消订阅操作
- 提供发布消息操作
- 提供确认消息操作
-
信道管理:
- 创建信道
- 查询信道
- 删除信道
-
-
实现代码
#ifndef __M_Channel_H__ #define __M_Channel_H__ #include "../MQCommon/Helper.hpp" #include "../MQCommon/Logger.hpp" #include "../MQCommon/ThreadPool.hpp" #include "../MQCommon/message.pb.h" #include "../MQCommon/request.pb.h" #include "Subscriber.hpp" #include "muduo/net/TcpConnection.h" #include "muduo/protobuf/codec.h" #include <condition_variable> #include <iostream> #include <mutex> #include <unordered_map> namespace MQ { using MessagePtr = std::shared_ptr<google::protobuf::Message>; using ProtobufCodecPtr = std::shared_ptr<ProtobufCodec>; using basicConsumeResponsePtr = std::shared_ptr<basicConsumeResponse>; using basicCommonResponsePtr = std::shared_ptr<basicCommonResponse>; class Channel { public: using ptr = std::shared_ptr<Channel>; // 构造函数 Channel(const muduo::net::TcpConnectionPtr &conn, const ProtobufCodecPtr &codec) : _channel_id(UUIDHelper::uuid()), _connection_ptr(conn), _codec_ptr(codec) { } // 析构函数 ~Channel() { basicCancel(); } std::string cid() { return _channel_id; } bool openChannel() { std::string rid = UUIDHelper::uuid(); openChannelRequest req; req.set_rid(rid); req.set_cid(_channel_id); _codec_ptr->send(_connection_ptr, req); basicCommonResponsePtr resp = waitResponse(rid); return resp->ok(); } void closeChannel() { std::string rid = UUIDHelper::uuid(); closeChannelRequest req; req.set_rid(rid); req.set_cid(_channel_id); _codec_ptr->send(_connection_ptr, req); waitResponse(rid); return; } bool declareExchange( const std::string &name, ExchangeType type, bool durable, bool auto_delete, google::protobuf::Map<std::string, std::string> &args) { // 构造一个声明虚拟机的请求对象, std::string rid = UUIDHelper::uuid(); declareExchangeRequest req; req.set_rid(rid); req.set_cid(_channel_id); req.set_exchange_name(name); req.set_exchange_type(type); req.set_durable(durable); req.set_auto_delete(auto_delete); req.mutable_args()->swap(args); // 然后向服务器发送请求 _codec_ptr->send(_connection_ptr, req); // 等待服务器的响应 basicCommonResponsePtr resp = waitResponse(rid); // 返回 return resp->ok(); } void deleteExchange(const std::string &name) { std::string rid = UUIDHelper::uuid(); deleteExchangeRequest req; req.set_rid(rid); req.set_cid(_channel_id); req.set_exchange_name(name); _codec_ptr->send(_connection_ptr, req); waitResponse(rid); return; } bool declareQueue( const std::string &qname, bool qdurable, bool qexclusive, bool qauto_delete, google::protobuf::Map<std::string, std::string> &qargs) { std::string rid = UUIDHelper::uuid(); declareQueueRequest req; req.set_rid(rid); req.set_cid(_channel_id); req.set_queue_name(qname); req.set_durable(qdurable); req.set_auto_delete(qauto_delete); req.set_exclusive(qexclusive); req.mutable_args()->swap(qargs); _codec_ptr->send(_connection_ptr, req); basicCommonResponsePtr resp = waitResponse(rid); return resp->ok(); } void deleteQueue(const std::string &qname) { std::string rid = UUIDHelper::uuid(); deleteQueueRequest req; req.set_rid(rid); req.set_cid(_channel_id); req.set_queue_name(qname); _codec_ptr->send(_connection_ptr, req); waitResponse(rid); return; } bool queueBind( const std::string &ename, const std::string &qname, const std::string &key) { std::string rid = UUIDHelper::uuid(); queueBindRequest req; req.set_rid(rid); req.set_cid(_channel_id); req.set_exchange_name(ename); req.set_queue_name(qname); req.set_binding_key(key); _codec_ptr->send(_connection_ptr, req); basicCommonResponsePtr resp = waitResponse(rid); return resp->ok(); } void queueUnBind(const std::string &ename, const std::string &qname) { std::string rid = UUIDHelper::uuid(); queueUnBindRequest req; req.set_rid(rid); req.set_cid(_channel_id); req.set_exchange_name(ename); req.set_queue_name(qname); _codec_ptr->send(_connection_ptr, req); waitResponse(rid); return; } void basicPublish( const std::string &ename, const BasicProperties *bp, const std::string &body) { std::string rid = UUIDHelper::uuid(); basicPublishRequest req; req.set_rid(rid); req.set_cid(_channel_id); req.set_body(body); req.set_exchange_name(ename); if (bp != nullptr) { req.mutable_properties()->set_id(bp->id()); req.mutable_properties()->set_delivery_mode(bp->delivery_mode()); req.mutable_properties()->set_routing_key(bp->routing_key()); } _codec_ptr->send(_connection_ptr, req); waitResponse(rid); return; } void basicAck(const std::string &msgid) { if (_subscriber_ptr.get() == nullptr) { DLOG("消息确认时,找不到消费者信息!"); return; } std::string rid = UUIDHelper::uuid(); basicAckRequest req; req.set_rid(rid); req.set_cid(_channel_id); req.set_queue_name(_subscriber_ptr->_subscribe_queue_name); req.set_message_id(msgid); _codec_ptr->send(_connection_ptr, req); waitResponse(rid); return; } void basicCancel() { if (_subscriber_ptr.get() == nullptr) { return; } std::string rid = UUIDHelper::uuid(); basicCancelRequest req; req.set_rid(rid); req.set_cid(_channel_id); req.set_queue_name(_subscriber_ptr->_subscribe_queue_name); req.set_consumer_tag(_subscriber_ptr->_subscribe_queue_tag); _codec_ptr->send(_connection_ptr, req); waitResponse(rid); _subscriber_ptr.reset(); return; } bool basicConsume( const std::string &consumer_tag, const std::string &queue_name, bool auto_ack, const SubscriberCallback &cb) { if (_subscriber_ptr.get() != nullptr) { DLOG("当前信道已订阅其他队列消息!"); return false; } std::string rid = UUIDHelper::uuid(); basicConsumeRequest req; req.set_rid(rid); req.set_cid(_channel_id); req.set_queue_name(queue_name); req.set_consumer_tag(consumer_tag); req.set_auto_ack(auto_ack); _codec_ptr->send(_connection_ptr, req); basicCommonResponsePtr resp = waitResponse(rid); if (resp->ok() == false) { DLOG("添加订阅失败!"); return false; } DLOG("添加订阅成功!订阅者:%s,订阅队列:%s", consumer_tag.c_str(), queue_name.c_str()); _subscriber_ptr = std::make_shared<Subscriber>(consumer_tag, queue_name, auto_ack, cb); return true; } public: // 连接收到基础响应后,向hash_map中添加响应 void putBasicResponse(const basicCommonResponsePtr &resp) { std::unique_lock<std::mutex> lock(_mutex); _basic_resp.insert(std::make_pair(resp->rid(), resp)); _cv.notify_all(); } // 连接收到消息推送后,需要通过信道找到对应的消费者对象,通过回调函数进行消息处理 void consume(const basicConsumeResponsePtr &resp) { if (_subscriber_ptr.get() == nullptr) { DLOG("消息处理时,未找到订阅者信息!"); return; } if (_subscriber_ptr->_subscribe_queue_tag != resp->consumer_tag()) { DLOG("收到的推送消息中的消费者标识,与当前信道消费者标识不一致!"); return; } _subscriber_ptr->_callback(resp->consumer_tag(), resp->mutable_properties(), resp->body()); } private: basicCommonResponsePtr waitResponse(const std::string &rid) { std::unique_lock<std::mutex> lock(_mutex); _cv.wait(lock, [&rid, this]() { return _basic_resp.find(rid) != _basic_resp.end(); }); // while(condition()) _cv.wait(); basicCommonResponsePtr basic_resp = _basic_resp[rid]; _basic_resp.erase(rid); return basic_resp; } private: std::string _channel_id; muduo::net::TcpConnectionPtr _connection_ptr; ProtobufCodecPtr _codec_ptr; Subscriber::ptr _subscriber_ptr; std::mutex _mutex; std::condition_variable _cv; std::unordered_map<std::string, basicCommonResponsePtr> _basic_resp; }; class ChannelManager { public: using ptr = std::shared_ptr<ChannelManager>; ChannelManager() {} Channel::ptr create(const muduo::net::TcpConnectionPtr &conn, const ProtobufCodecPtr &codec) { std::unique_lock<std::mutex> lock(_mutex); auto channel = std::make_shared<Channel>(conn, codec); _channels.insert(std::make_pair(channel->cid(), channel)); return channel; } void remove(const std::string &cid) { std::unique_lock<std::mutex> lock(_mutex); _channels.erase(cid); } Channel::ptr get(const std::string &cid) { std::unique_lock<std::mutex> lock(_mutex); auto it = _channels.find(cid); if (it == _channels.end()) { return Channel::ptr(); } return it->second; } private: std::mutex _mutex; std::unordered_map<std::string, Channel::ptr> _channels; }; } #endif
在MQClient中编写Worker.hpp文件实现异步⼯作线程模块
-
客⼾端这边存在两个异步⼯作线程,
-
⼀个是muduo库中客⼾端连接的异步循环线程EventLoopThread,
-
⼀个是当收到消息后进⾏异步处理的⼯作线程池。
-
-
这两项都不是以连接为单元进⾏创建的,⽽是创建后,可以⽤以多个连接中,因此单独进⾏封装。
-
实现代码:
#ifndef __M_WORKER_H__ #define __M_WORKER_H__ #include "../MQCommon/Helper.hpp" #include "../MQCommon/Logger.hpp" #include "../MQCommon/ThreadPool.hpp" #include "muduo/net/EventLoopThread.h" namespace MQ { class AsyncWorker { public: using ptr = std::shared_ptr<AsyncWorker>; muduo::net::EventLoopThread loopthread; MQ::ThreadPool pool; }; } #endif
在MQClient中编写Connection.hpp文件实现连接管理模块
-
在客⼾端这边,RabbitMQ弱化了客⼾端的概念,因为⽤⼾所需的服务都是通过信道来提供的,因此操作思想转换为先创建连接,通过连接创建信道,通过信道提供服务这⼀流程。
-
这个模块同样是针对muduo库客⼾端连接的⼆次封装,向⽤⼾提供创建channel信道的接⼝,创建信道后,可以通过信道来获取指定服务
-
实现代码
#ifndef __M_CONNECTION_H__ #define __M_CONNECTION_H__ #include "muduo/base/CountDownLatch.h" #include "muduo/base/Logging.h" #include "muduo/base/Mutex.h" #include "muduo/net/EventLoop.h" #include "muduo/net/EventLoopThread.h" #include "muduo/net/TcpClient.h" #include "muduo/protobuf/codec.h" #include "muduo/protobuf/dispatcher.h" #include "Channel.hpp" #include "Subscriber.hpp" #include "Worker.hpp" namespace MQ { class Connection { public: using ptr = std::shared_ptr<Connection>; Connection(const std::string &sip, int sport, const AsyncWorker::ptr &worker) : _latch(1), _client(worker->loopthread.startLoop(), muduo::net::InetAddress(sip, sport), "Client"), _dispatcher(std::bind(&Connection::onUnknownMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)), _codec(std::make_shared<ProtobufCodec>(std::bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3))), _worker(worker), _channel_manager(std::make_shared<ChannelManager>()) { _dispatcher.registerMessageCallback<basicCommonResponse>(std::bind(&Connection::basicResponse, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<basicConsumeResponse>(std::bind(&Connection::consumeResponse, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, _codec.get(), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _client.setConnectionCallback(std::bind(&Connection::onConnection, this, std::placeholders::_1)); _client.connect(); _latch.wait(); // 阻塞等待,直到连接建立成功 } // 打开信道 Channel::ptr openChannel() { Channel::ptr channel = _channel_manager->create(_conn, _codec); bool ret = channel->openChannel(); if (ret == false) { DLOG("打开信道失败!"); return Channel::ptr(); } return channel; } // 关闭信道 void closeChannel(const Channel::ptr &channel) { channel->closeChannel(); _channel_manager->remove(channel->cid()); } private: void basicResponse(const muduo::net::TcpConnectionPtr &conn, const basicCommonResponsePtr &message, muduo::Timestamp) { // 1. 找到信道 Channel::ptr channel = _channel_manager->get(message->cid()); if (channel.get() == nullptr) { DLOG("未找到信道信息!"); return; } // 2. 将得到的响应对象,添加到信道的基础响应hash_map中 channel->putBasicResponse(message); } void consumeResponse(const muduo::net::TcpConnectionPtr &conn, const basicConsumeResponsePtr &message, muduo::Timestamp) { // 1. 找到信道 Channel::ptr channel = _channel_manager->get(message->cid()); if (channel.get() == nullptr) { DLOG("未找到信道信息!"); return; } // 2. 封装异步任务(消息处理任务),抛入线程池 _worker->pool.push([channel, message]() { channel->consume(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()) { _latch.countDown(); // 唤醒主线程中的阻塞 _conn = conn; } else { // 连接关闭时的操作 _conn.reset(); } } private: muduo::CountDownLatch _latch; // 实现同步的 muduo::net::TcpConnectionPtr _conn; // 客户端对应的连接 muduo::net::TcpClient _client; // 客户端 ProtobufDispatcher _dispatcher; // 请求分发器 ProtobufCodecPtr _codec; // 协议处理器 AsyncWorker::ptr _worker; // 异步工作线程 ChannelManager::ptr _channel_manager; // 信道管理器 }; } #endif
-
在MQClient中编写ConsumeClient.cpp文件和PublichClient.cpp文件实现消费者客户端与发布者客户端
-
消费者客户端的实现
#include "Connection.hpp" void cb(MQ::Channel::ptr &channel, const std::string consumer_tag, const MQ::BasicProperties *bp, const std::string &body) { std::cout << consumer_tag << "消费了消息:" << body << std::endl; channel->basicAck(bp->id()); } int main(int argc, char *argv[]) { if (argc != 2) { std::cout << "usage: ./consume_client queue1\n"; return -1; } //1. 实例化异步工作线程对象 MQ::AsyncWorker::ptr awp = std::make_shared<MQ::AsyncWorker>(); //2. 实例化连接对象 MQ::Connection::ptr conn = std::make_shared<MQ::Connection>("127.0.0.1", 8085, awp); //3. 通过连接创建信道 MQ::Channel::ptr channel = conn->openChannel(); //4. 通过信道提供的服务完成所需 // 1. 声明一个交换机exchange1, 交换机类型为广播模式 google::protobuf::Map<std::string, std::string> tmp_map; channel->declareExchange("exchange1", MQ::ExchangeType::DIRECT, true, false, tmp_map); // 2. 声明一个队列queue1 channel->declareQueue("queue1", true, false, false, tmp_map); // 3. 声明一个队列queue2 channel->declareQueue("queue2", true, false, false, tmp_map); // 4. 绑定queue1-exchange1,且binding_key设置为queue1 channel->queueBind("exchange1", "queue1", "queue1"); // 5. 绑定queue2-exchange1,且binding_key设置为news.music.# channel->queueBind("exchange1", "queue2", "news.music.#"); auto functor = std::bind(cb, channel, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); channel->basicConsume("consumer1", argv[1], false, functor); while(1) std::this_thread::sleep_for(std::chrono::seconds(3)); conn->closeChannel(channel); return 0; }
-
发布者客户端的实现
#include "Connection.hpp" int main() { //1. 实例化异步工作线程对象 MQ::AsyncWorker::ptr awp = std::make_shared<MQ::AsyncWorker>(); //2. 实例化连接对象 MQ::Connection::ptr conn = std::make_shared<MQ::Connection>("127.0.0.1", 8085, awp); //3. 通过连接创建信道 MQ::Channel::ptr channel = conn->openChannel(); //4. 通过信道提供的服务完成所需 // 1. 声明一个交换机exchange1, 交换机类型为广播模式 google::protobuf::Map<std::string, std::string> tmp_map; channel->declareExchange("exchange1", MQ::ExchangeType::DIRECT, true, false, tmp_map); // 2. 声明一个队列queue1 channel->declareQueue("queue1", true, false, false, tmp_map); // 3. 声明一个队列queue2 channel->declareQueue("queue2", true, false, false, tmp_map); // 4. 绑定queue1-exchange1,且binding_key设置为queue1 channel->queueBind("exchange1", "queue1", "queue1"); // 5. 绑定queue2-exchange1,且binding_key设置为news.music.# channel->queueBind("exchange1", "queue2", "news.music.#"); //5. 循环向交换机发布消息 for (int i = 0; i < 10; i++) { MQ::BasicProperties bp; bp.set_id(UUIDHelper::uuid()); bp.set_delivery_mode(MQ::DeliveryMode::DURABLE); bp.set_routing_key("queue1"); channel->basicPublish("exchange1", &bp, "Hello World-" + std::to_string(i)); } MQ::BasicProperties bp; bp.set_id(UUIDHelper::uuid()); bp.set_delivery_mode(MQ::DeliveryMode::DURABLE); bp.set_routing_key("queue1"); channel->basicPublish("exchange1", &bp, "Hello Bite"); bp.set_routing_key("news.sport"); channel->basicPublish("exchange1", &bp, "Hello chileme?"); //6. 关闭信道 conn->closeChannel(channel); return 0; }
在MQTest中编写模块的单元测试
在MQTest中编写makefile文件来编译客户端模块
all:Test_FileHelper Test_Exchange Test_Queue Test_Binding Test_Message Test_VirtualHost Test_Route Test_Consumer Test_Channel Test_Connection
Test_VirtualHost:Test_VirtualHost.cpp ../MQCommon/message.pb.cc
g++ -g -o $@ $^ -std=c++11 -lgtest -lprotobuf -lsqlite3 -pthread
Test_Binding:Test_Binding.cpp
g++ -g -o $@ $^ -std=c++11 -lgtest -lprotobuf -lsqlite3 -pthread
Test_Queue:Test_Queue.cpp
g++ -g -o $@ $^ -std=c++11 -lgtest -lprotobuf -lsqlite3 -pthread
Test_FileHelper:Test_FileHelper.cpp
g++ -o $@ $^ -std=c++11
Test_Exchange:Test_Exchange.cpp
g++ -g -std=c++11 $^ -o $@ -lgtest -lprotobuf -lsqlite3
Test_Message:Test_Message.cpp ../MQCommon/message.pb.cc
g++ -g -std=c++11 $^ -o $@ -lgtest -lprotobuf -lsqlite3
Test_Route:Test_Route.cpp ../MQCommon/message.pb.cc
g++ -g -std=c++11 $^ -o $@ -lgtest -lprotobuf -lsqlite3
Test_Consumer:Test_Consumer.cpp ../MQCommon/message.pb.cc
g++ -g -std=c++11 $^ -o $@ -lgtest -lprotobuf -lsqlite3
Test_Channel:Test_Channel.cpp ../MQCommon/message.pb.cc ../MQCommon/request.pb.cc
g++ -g -std=c++11 $^ -o $@ -lgtest -lprotobuf -lsqlite3 -I../ThirdLib/lib/include
Test_Connection:Test_Connection.cpp ../MQCommon/message.pb.cc ../MQCommon/request.pb.cc
g++ -g -std=c++11 $^ -o $@ -lgtest -lprotobuf -lsqlite3 -I../ThirdLib/lib/include
.PHONY:
clean:
rm -rf Test_FileHelper Test_Exchange Test_Queue Test_Binding Test_Message Test_VirtualHost Test_Route Test_Consumer Test_Channel Test_Connection
在MQTest中编写Test_Exchange.cpp文件来编译交换机模块单元测试
#include "../MQServer/Exchange.hpp"
#include <gtest/gtest.h>
#include<unordered_map>
MQ::ExchangeManager::ptr emp;
class ExchangeTest : public testing::Environment
{
public:
virtual void SetUp() override
{
emp = std::make_shared<MQ::ExchangeManager>("./data/meta.db");
}
virtual void TearDown() override
{
emp->clear();
std::cout << "最后的清理!!\n";
}
};
TEST(exchange_test, insert_test)
{
std::unordered_map<std::string, std::string> tmp = {{"k1","v1"},{"k2","v2"}};
std::unordered_map<std::string, std::string> map;
map.insert(tmp.begin(),tmp.end());
for(auto& a : map)
{
std::cout<<a.first<<' '<<a.second<<std::endl;
}
emp->declareExchange("exchange1", MQ::ExchangeType::DIRECT, true, false, map);
emp->declareExchange("exchange2", MQ::ExchangeType::DIRECT, true, false, map);
emp->declareExchange("exchange3", MQ::ExchangeType::DIRECT, true, false, map);
emp->declareExchange("exchange4", MQ::ExchangeType::DIRECT, true, false, map);
ASSERT_EQ(emp->size(), 4);
}
TEST(exchange_test, select_test)
{
ASSERT_EQ(emp->exists("exchange1"), false);
ASSERT_EQ(emp->exists("exchange2"), true);
ASSERT_EQ(emp->exists("exchange3"), true);
ASSERT_EQ(emp->exists("exchange4"), true);
MQ::Exchange::ptr exp = emp->selectExchange("exchange1");
ASSERT_NE(exp.get(), nullptr);
ASSERT_EQ(exp->_name, "exchange1");
ASSERT_EQ(exp->_durable, true);
ASSERT_EQ(exp->_auto_delete, false);
ASSERT_EQ(exp->_type, MQ::ExchangeType::DIRECT);
ASSERT_EQ(exp->getArgs(), std::string("k1=v1&k2=v2&"));
}
TEST(exchange_test, remove_test)
{
emp->deleteExchange("exchange2");
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;
}
在MQTest中编写Test_Binding.cpp文件来编译绑定模块单元测试
#include "../MQServer/Binding.hpp"
#include <gtest/gtest.h>
MQ::BindingManager::ptr bmp;
class QueueTest : public testing::Environment {
public:
virtual void SetUp() override {
bmp = std::make_shared<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->exist("exchange1", "queue1"), false);
ASSERT_EQ(bmp->exist("exchange1", "queue2"), false);
ASSERT_EQ(bmp->exist("exchange1", "queue3"), false);
ASSERT_EQ(bmp->exist("exchange2", "queue1"), true);
ASSERT_EQ(bmp->exist("exchange2", "queue2"), false);
ASSERT_EQ(bmp->exist("exchange2", "queue3"), true);
}
TEST(queue_test, select_test) {
ASSERT_EQ(bmp->exist("exchange1", "queue1"), false);
ASSERT_EQ(bmp->exist("exchange1", "queue2"), false);
ASSERT_EQ(bmp->exist("exchange1", "queue3"), false);
ASSERT_EQ(bmp->exist("exchange2", "queue1"), true);
ASSERT_EQ(bmp->exist("exchange2", "queue2"), false);
ASSERT_EQ(bmp->exist("exchange2", "queue3"), true);
MQ::Binding::ptr bp = bmp->getBinding("exchange2", "queue1");
ASSERT_NE(bp.get(), nullptr);
ASSERT_EQ(bp->name_exchange, std::string("exchange2"));
ASSERT_EQ(bp->name_queue, std::string("queue1"));
ASSERT_EQ(bp->binding_key, std::string("news.music.pop"));
}
TEST(queue_test, select_exchange_test) {
MQ::QueueBindingMap mqbm = bmp->getExchangeBindings("exchange2");
ASSERT_EQ(mqbm.size(), 2);
ASSERT_NE(mqbm.find("queue1"), mqbm.end());
ASSERT_EQ(mqbm.find("queue2"), mqbm.end());
ASSERT_NE(mqbm.find("queue3"), mqbm.end());
}
// // e2-q3
// TEST(queue_test, remove_exchange_test) {
// bmp->unbindByExchange("exchange1");
// ASSERT_EQ(bmp->exist("exchange1", "queue1"), false);
// ASSERT_EQ(bmp->exist("exchange1", "queue2"), false);
// ASSERT_EQ(bmp->exist("exchange1", "queue3"), false);
// }
// TEST(queue_test, remove_single_test) {
// ASSERT_EQ(bmp->exist("exchange2", "queue2"), true);
// bmp->unbind("exchange2", "queue2");
// ASSERT_EQ(bmp->exist("exchange2", "queue2"), false);
// ASSERT_EQ(bmp->exist("exchange2", "queue3"), true);
// }
int main(int argc,char *argv[])
{
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new QueueTest);
RUN_ALL_TESTS();
return 0;
}
在MQTest中编写Test_Channel.cpp文件来编译信道模块单元测试
#include "../MQServer/Channel.hpp"
int main()
{
MQ::ChannelManager::ptr cmp = std::make_shared<MQ::ChannelManager>();
cmp->openChannel("c1",
std::make_shared<MQ::VirtualHost>("host1", "./data/host1/message/", "./data/host1/host1.db"),
std::make_shared<MQ::ConsumerManager>(),
MQ::ProtobufCodecPtr(),
muduo::net::TcpConnectionPtr(),
MQ::ThreadPool::ptr());
return 0;
}
在MQTest中编写Test_Connection.cpp文件来编译链接模块单元测试
#include "../MQServer/Connection.hpp"
int main()
{
MQ::ConnectionManager::ptr cmp = std::make_shared<MQ::ConnectionManager>();
cmp->newConnection(std::make_shared<MQ::VirtualHost>("host1", "./data/host1/message/", "./data/host1/host1.db"),
std::make_shared<MQ::ConsumerManager>(),
MQ::ProtobufCodecPtr(),
muduo::net::TcpConnectionPtr(),
MQ::ThreadPool::ptr());
return 0;
}
在MQTest中编写Test_Consumer.cpp文件来编译消费者模块单元测试
#include "../MQServer/Consumer.hpp"
#include <gtest/gtest.h>
MQ::ConsumerManager::ptr cmp;
class ConsumerTest : public testing::Environment {
public:
virtual void SetUp() override {
cmp = std::make_shared<MQ::ConsumerManager>();
cmp->initQueueConsumer("queue1");
}
virtual void TearDown() override {
cmp->clear();
}
};
void cb(const std::string &tag, const MQ::BasicProperties *bp, const std::string &body)
{
std::cout << tag << " 消费了消息:" << body << std::endl;
}
TEST(consumer_test, insert_test) {
cmp->createConsumer("consumer1", "queue1", false, cb);
cmp->createConsumer("consumer2", "queue1", false, cb);
cmp->createConsumer("consumer3", "queue1", false, cb);
ASSERT_EQ(cmp->isExist("consumer1", "queue1"), true);
ASSERT_EQ(cmp->isExist("consumer2", "queue1"), true);
ASSERT_EQ(cmp->isExist("consumer3", "queue1"), true);
}
TEST(consumer_test, remove_test) {
cmp->removeConsumer("consumer1", "queue1");
ASSERT_EQ(cmp->isExist("consumer1", "queue1"), false);
ASSERT_EQ(cmp->isExist("consumer2", "queue1"), true);
ASSERT_EQ(cmp->isExist("consumer3", "queue1"), true);
}
TEST(consumer_test, choose_test) {
MQ::Consumer::ptr cp = cmp->chooseConsumer("queue1");
ASSERT_NE(cp.get(), nullptr);
ASSERT_EQ(cp->_consumer_tag, "consumer2");
cp = cmp->chooseConsumer("queue1");
ASSERT_NE(cp.get(), nullptr);
ASSERT_EQ(cp->_consumer_tag, "consumer3");
cp = cmp->chooseConsumer("queue1");
ASSERT_NE(cp.get(), nullptr);
ASSERT_EQ(cp->_consumer_tag, "consumer2");
}
int main(int argc,char *argv[])
{
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new ConsumerTest);
RUN_ALL_TESTS();
return 0;
}
在MQTest中编写Test_FileHelper.cpp文件来文件操作类单元测试
#include "../MQCommon/Helper.hpp"
int main()
{
FileHelper helper("../MQCommon/Helper.hpp");
DLOG("是否存在:%d", helper.exists());
DLOG("文件大小:%ld", helper.size());
FileHelper tmp_helper("./aaa/bbb/ccc/tmp.hpp");
if (tmp_helper.exists() == false)
{
std::string path = FileHelper::parentDirectory("./aaa/bbb/ccc/tmp.hpp");
DLOG("path:%s",path.c_str());
if (FileHelper(path).exists() == false)
{
FileHelper::createDirectory(path);
}
FileHelper::createFile("./aaa/bbb/ccc/tmp.hpp");
}
std::string body;
helper.read(body);
DLOG("读测试,读出的内容:%s",body.c_str());
tmp_helper.write(body);
FileHelper tmp_helper1("./aaa/bbb/ccc/tmp.hpp");
char str[16] = {0};
tmp_helper.read(str, 8, 11);
DLOG("[%s]", str);
tmp_helper.write("12345678901", 8, 11);
tmp_helper.rename("./aaa/bbb/ccc/test.hpp");
FileHelper::removeFile("./aaa/bbb/ccc/test.hpp");
FileHelper::removeDirectory("./aaa");
return 0;
}
在MQTest中编写Test_Message.cpp文件来编译消息模块单元测试
#include "../MQServer/Message.hpp"
#include <gtest/gtest.h>
MQ::MessageManager::ptr mmp;
class MessageTest : public testing::Environment {
public:
virtual void SetUp() override {
mmp = std::make_shared<MQ::MessageManager>("./data/message/");
mmp->initQueueMessage("queue1");
}
virtual void TearDown() override {
// mmp->clear();
}
};
//新增消息测试:新增消息,然后观察可获取消息数量,以及持久化消息数量
// TEST(message_test, insert_test)
// {
// MQ::BasicProperties properties;
// properties.set_id(UUIDHelper::uuid());
// properties.set_delivery_mode(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->getAbleCount("queue1"), 5);
// ASSERT_EQ(mmp->getTotalCount("queue1"), 4);
// ASSERT_EQ(mmp->getDurableCount("queue1"), 4);
// ASSERT_EQ(mmp->getWaitAckCount("queue1"), 0);
// }
//获取消息测试:获取一条消息,然后在不进行确认的情况下,查看可获取消息数量,待确认消息数量,以及测试消息获取的顺序
// TEST(message_test, select_test) {
// ASSERT_EQ(mmp->getAbleCount("queue1"), 5);
// MQ::MessagePtr msg1 = mmp->front("queue1");
// ASSERT_NE(msg1.get(), nullptr);
// ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));
// ASSERT_EQ(mmp->getAbleCount("queue1"), 4);
// ASSERT_EQ(mmp->getWaitAckCount("queue1"), 1);
// MQ::MessagePtr msg2 = mmp->front("queue1");
// ASSERT_NE(msg2.get(), nullptr);
// ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));
// ASSERT_EQ(mmp->getAbleCount("queue1"), 3);
// ASSERT_EQ(mmp->getWaitAckCount("queue1"), 2);
// MQ::MessagePtr msg3 = mmp->front("queue1");
// ASSERT_NE(msg3.get(), nullptr);
// ASSERT_EQ(msg3->payload().body(), std::string("Hello World-3"));
// ASSERT_EQ(mmp->getAbleCount("queue1"), 2);
// ASSERT_EQ(mmp->getWaitAckCount("queue1"), 3);
// MQ::MessagePtr msg4 = mmp->front("queue1");
// ASSERT_NE(msg4.get(), nullptr);
// ASSERT_EQ(msg4->payload().body(), std::string("Hello World-4"));
// ASSERT_EQ(mmp->getAbleCount("queue1"), 1);
// ASSERT_EQ(mmp->getWaitAckCount("queue1"), 4);
// MQ::MessagePtr msg5 = mmp->front("queue1");
// ASSERT_NE(msg5.get(), nullptr);
// ASSERT_EQ(msg5->payload().body(), std::string("Hello World-5"));
// ASSERT_EQ(mmp->getAbleCount("queue1"), 0);
// ASSERT_EQ(mmp->getWaitAckCount("queue1"), 5);
// }
//删除消息测试:确认一条消息,查看持久化消息数量,待确认消息数量
// TEST(message_test, delete_test) {
// ASSERT_EQ(mmp->getAbleCount("queue1"), 5);
// MQ::MessagePtr msg1 = mmp->front("queue1");
// ASSERT_NE(msg1.get(), nullptr);
// ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));
// ASSERT_EQ(mmp->getAbleCount("queue1"), 4);
// ASSERT_EQ(mmp->getWaitAckCount("queue1"), 1);
// mmp->ack("queue1", msg1->payload().properties().id());
// ASSERT_EQ(mmp->getWaitAckCount("queue1"), 0);
// ASSERT_EQ(mmp->getDurableCount("queue1"), 3);
// ASSERT_EQ(mmp->getTotalCount("queue1"), 4);
// }
// 销毁测试
// TEST(message_test, clear) {
// mmp->destroyQueueMessage("queue1");
// }
//恢复历史数据测试
MQ::MessageManager::ptr mmp1;
class MessageTest2 : public testing::Environment {
public:
virtual void SetUp() override {
mmp1 = std::make_shared<MQ::MessageManager>("./data/message/");
}
virtual void TearDown() override {
//mmp->clear();
}
};
TEST(message_test2, recovery_test) {
mmp1->initQueueMessage("queue1");
ASSERT_EQ(mmp1->getAbleCount("queue1"), 4);
MQ::MessagePtr msg1 = mmp1->front("queue1");
ASSERT_NE(msg1.get(), nullptr);
ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));
ASSERT_EQ(mmp1->getAbleCount("queue1"), 3);
ASSERT_EQ(mmp1->getWaitAckCount("queue1"), 1);
MQ::MessagePtr msg2 = mmp1->front("queue1");
ASSERT_NE(msg2.get(), nullptr);
ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));
ASSERT_EQ(mmp1->getAbleCount("queue1"), 2);
ASSERT_EQ(mmp1->getWaitAckCount("queue1"), 2);
MQ::MessagePtr msg3 = mmp1->front("queue1");
ASSERT_NE(msg3.get(), nullptr);
ASSERT_EQ(msg3->payload().body(), std::string("Hello World-3"));
ASSERT_EQ(mmp1->getAbleCount("queue1"), 1);
ASSERT_EQ(mmp1->getWaitAckCount("queue1"), 3);
MQ::MessagePtr msg4 = mmp1->front("queue1");
ASSERT_NE(msg4.get(), nullptr);
ASSERT_EQ(msg4->payload().body(), std::string("Hello World-4"));
ASSERT_EQ(mmp1->getAbleCount("queue1"), 0);
ASSERT_EQ(mmp1->getWaitAckCount("queue1"), 4);
}
int main(int argc,char *argv[])
{
testing::InitGoogleTest(&argc, argv);
//testing::AddGlobalTestEnvironment(new MessageTest);
testing::AddGlobalTestEnvironment(new MessageTest2);
RUN_ALL_TESTS();
return 0;
}
在MQTest中编写Test_Queue.cpp文件来编译队列模块单元测试
#include "../MQServer/Queue.hpp"
#include <gtest/gtest.h>
MQ::QueueManager::ptr mqmp;
class QueueTest : public testing::Environment {
public:
virtual void SetUp() override {
mqmp = std::make_shared<MQ::QueueManager>("./data/meta.db");
DLOG("创建数据库文件");
}
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->exist("queue1"), true);
ASSERT_EQ(mqmp->exist("queue2"), true);
ASSERT_EQ(mqmp->exist("queue3"), false);
ASSERT_EQ(mqmp->exist("queue4"), true);
MQ::Queue::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->exist("queue3"), false);
}
int main(int argc,char *argv[])
{
testing::InitGoogleTest(&argc, argv);
testing::AddGlobalTestEnvironment(new QueueTest);
RUN_ALL_TESTS();
return 0;
}
在MQTest中编写Test_Route.cpp文件来编译路由模块单元测试
#include "../MQServer/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(MQ::RouteManager::isValidRoutingKey(rkey1), true);
ASSERT_EQ(MQ::RouteManager::isValidRoutingKey(rkey2), false);
ASSERT_EQ(MQ::RouteManager::isValidRoutingKey(rkey3), false);
ASSERT_EQ(MQ::RouteManager::isValidRoutingKey(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(MQ::RouteManager::isValidBindingKey(bkey1), true);
ASSERT_EQ(MQ::RouteManager::isValidBindingKey(bkey2), true);
ASSERT_EQ(MQ::RouteManager::isValidBindingKey(bkey3), false);
ASSERT_EQ(MQ::RouteManager::isValidBindingKey(bkey4), false);
ASSERT_EQ(MQ::RouteManager::isValidBindingKey(bkey5), false);
ASSERT_EQ(MQ::RouteManager::isValidBindingKey(bkey6), true);
ASSERT_EQ(MQ::RouteManager::isValidBindingKey(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(MQ::RouteManager::route(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;
}
在MQTest中编写Test_VirtualHost.cpp文件来编译虚拟机模块单元测试
#include <gtest/gtest.h>
#include "../MQServer/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<MQ::VirtualHost>("host1", "./data/host1/message/", "./data/host1/host1.db");
_host->declareExchange("exchange1", MQ::ExchangeType::DIRECT, true, false, empty_map);
_host->declareExchange("exchange2", MQ::ExchangeType::DIRECT, true, false, empty_map);
_host->declareExchange("exchange3", 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:
MQ::VirtualHost::ptr _host;
};
TEST_F(HostTest, init_test) {
ASSERT_EQ(_host->existExchange("exchange1"), true);
ASSERT_EQ(_host->existExchange("exchange2"), true);
ASSERT_EQ(_host->existExchange("exchange3"), true);
ASSERT_EQ(_host->existQueue("queue1"), true);
ASSERT_EQ(_host->existQueue("queue2"), true);
ASSERT_EQ(_host->existQueue("queue3"), true);
ASSERT_EQ(_host->existBinding("exchange1", "queue1"), true);
ASSERT_EQ(_host->existBinding("exchange1", "queue2"), true);
ASSERT_EQ(_host->existBinding("exchange1", "queue3"), true);
ASSERT_EQ(_host->existBinding("exchange2", "queue1"), true);
ASSERT_EQ(_host->existBinding("exchange2", "queue2"), true);
ASSERT_EQ(_host->existBinding("exchange2", "queue3"), true);
ASSERT_EQ(_host->existBinding("exchange3", "queue1"), true);
ASSERT_EQ(_host->existBinding("exchange3", "queue2"), true);
ASSERT_EQ(_host->existBinding("exchange3", "queue3"), true);
MQ::MessagePtr msg1 = _host->basicConsume("queue1");
ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));
MQ::MessagePtr msg2 = _host->basicConsume("queue1");
ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));
MQ::MessagePtr msg3 = _host->basicConsume("queue1");
ASSERT_EQ(msg3->payload().body(), std::string("Hello World-3"));
MQ::MessagePtr msg4 = _host->basicConsume("queue1");
ASSERT_EQ(msg4.get(), nullptr);
}
TEST_F(HostTest, remove_exchange) {
_host->deleteExchange("exchange1");
ASSERT_EQ(_host->existBinding("exchange1", "queue1"), false);
ASSERT_EQ(_host->existBinding("exchange1", "queue2"), false);
ASSERT_EQ(_host->existBinding("exchange1", "queue3"), false);
}
TEST_F(HostTest, remove_queue) {
_host->deleteQueue("queue1");
ASSERT_EQ(_host->existBinding("exchange1", "queue1"), false);
ASSERT_EQ(_host->existBinding("exchange2", "queue1"), false);
ASSERT_EQ(_host->existBinding("exchange3", "queue1"), false);
MQ::MessagePtr msg1 = _host->basicConsume("queue1");
ASSERT_EQ(msg1.get(), nullptr);
}
TEST_F(HostTest, ack_message) {
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());
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());
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;
}
r::isValidBindingKey(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::vectorstd::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::vectorstd::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 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(MQ::RouteManager::route(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;
}
#### 在MQTest中编写Test_VirtualHost.cpp文件来编译虚拟机模块单元测试
```c++
#include <gtest/gtest.h>
#include "../MQServer/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<MQ::VirtualHost>("host1", "./data/host1/message/", "./data/host1/host1.db");
_host->declareExchange("exchange1", MQ::ExchangeType::DIRECT, true, false, empty_map);
_host->declareExchange("exchange2", MQ::ExchangeType::DIRECT, true, false, empty_map);
_host->declareExchange("exchange3", 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:
MQ::VirtualHost::ptr _host;
};
TEST_F(HostTest, init_test) {
ASSERT_EQ(_host->existExchange("exchange1"), true);
ASSERT_EQ(_host->existExchange("exchange2"), true);
ASSERT_EQ(_host->existExchange("exchange3"), true);
ASSERT_EQ(_host->existQueue("queue1"), true);
ASSERT_EQ(_host->existQueue("queue2"), true);
ASSERT_EQ(_host->existQueue("queue3"), true);
ASSERT_EQ(_host->existBinding("exchange1", "queue1"), true);
ASSERT_EQ(_host->existBinding("exchange1", "queue2"), true);
ASSERT_EQ(_host->existBinding("exchange1", "queue3"), true);
ASSERT_EQ(_host->existBinding("exchange2", "queue1"), true);
ASSERT_EQ(_host->existBinding("exchange2", "queue2"), true);
ASSERT_EQ(_host->existBinding("exchange2", "queue3"), true);
ASSERT_EQ(_host->existBinding("exchange3", "queue1"), true);
ASSERT_EQ(_host->existBinding("exchange3", "queue2"), true);
ASSERT_EQ(_host->existBinding("exchange3", "queue3"), true);
MQ::MessagePtr msg1 = _host->basicConsume("queue1");
ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));
MQ::MessagePtr msg2 = _host->basicConsume("queue1");
ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));
MQ::MessagePtr msg3 = _host->basicConsume("queue1");
ASSERT_EQ(msg3->payload().body(), std::string("Hello World-3"));
MQ::MessagePtr msg4 = _host->basicConsume("queue1");
ASSERT_EQ(msg4.get(), nullptr);
}
TEST_F(HostTest, remove_exchange) {
_host->deleteExchange("exchange1");
ASSERT_EQ(_host->existBinding("exchange1", "queue1"), false);
ASSERT_EQ(_host->existBinding("exchange1", "queue2"), false);
ASSERT_EQ(_host->existBinding("exchange1", "queue3"), false);
}
TEST_F(HostTest, remove_queue) {
_host->deleteQueue("queue1");
ASSERT_EQ(_host->existBinding("exchange1", "queue1"), false);
ASSERT_EQ(_host->existBinding("exchange2", "queue1"), false);
ASSERT_EQ(_host->existBinding("exchange3", "queue1"), false);
MQ::MessagePtr msg1 = _host->basicConsume("queue1");
ASSERT_EQ(msg1.get(), nullptr);
}
TEST_F(HostTest, ack_message) {
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());
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());
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;
}