当前位置: 首页 > news >正文

专业购物网站baidu百度首页官网

专业购物网站,baidu百度首页官网,重庆公司网站开发,wordpress建视频网站可以吗1.项目介绍 曾经在学习Linux的过程中,我们学习过阻塞队列 (BlockingQueue) 。 当时我们说阻塞队列最大的用途, 就是用来实现生产者消费者模型。 生产者消费者模型是后端开发的常用编程方式, 它存在诸多好处: 解耦合支持并发支持忙闲不均削峰…

1.项目介绍

曾经在学习Linux的过程中,我们学习过阻塞队列 (BlockingQueue) 。 当时我们说阻塞队列最大的用途, 就是用来实现生产者消费者模型。 生产者消费者模型是后端开发的常用编程方式, 它存在诸多好处:

  • 解耦合
  • 支持并发
  • 支持忙闲不均
  • 削峰填谷

在实际的后端开发中, 尤其是分布式系统里, 跨主机之间使用生产者消费者模型, 也是非常普遍的需求。因此, 我们通常会把阻塞队列封装成一个独立的服务器程序, 并且赋予其更丰富的功能。 这样的服务程序我们就称为消息队列 (Message Queue, MQ)。

MQ(Message Queue)消息队列,是基础数据结构中“先进先出”的一种数据机构。指把要传输的数据(消息)放在队列中,用队列机制来实现消息传递——生产者产生消息并把消息放入队列,然后由消费者去处理。消费者可以到指定队列拉取消息,或者订阅相应的队列,由MQ服务端给其推送消息。(来源:百度百科)

市面上成熟的消息队列非常多:

  • RabbitMQ
  • Kafka
  • RocketMQ
  • ActiveMQ

其中 RabbitMQ 是一个非常知名、功能强大且广泛使用的消息队列。咱们就仿照RabbitMQ 模拟实现一个简单的消息队列。

RabbitMQ的优点

  • 基于AMQP协议
  • 高并发(是一个容量的概念,服务器可以接受的大任务数量)
  • 高性能(是一个速度的概念,单位时间内服务器可以处理的任务数)
  • 高可用(是一个持久的概念,单位时间内服务器可以正常工作的时间比例)
  • 强大的社区支持,以及很多公司都在使用
  • 支持插件
  • 支持多语言

2.开发环境

  • Linux(Ubuntu-22.04)
  • VSCode/Vim
  • g++/gdb
  • Makefile

3.技术选型

  • 开发主语言: C++
  • 序列化框架: Protobuf 二进制序列化
  • 网络通信:
    • 方案一:自定义应用层协议+原生socket(不推荐,难度较高)
    • 方案二:自定义应用层协议+muduo库(对tcp长连接的封装、并且使用epoll的事件驱动模式,实现高并发服务器与客户端
  • 原数据信息数据库:SQLite3
  • 单元测试框架: Gtest

4.环境搭建

本人使用的是Ubuntu22.4版本,环境搭建指令如下:

  • 安装wget
sudo apt-get install wget
  • 安装lrzsz传输工具
 sudo apt-get install lrzsz
  • gcc/g++编译器
sudo apt-get install gcc g++
  • 安装项目构建工具make
sudo apt-get install make
  • 安装调试器
sudo apt-get install gdb
  • 安装 git
sudo apt-get install git
  • cmake
sudo apt-get install cmake
  • 安装 Protobuf
#安装protobuf依赖库
sudo apt-get install autoconf automake libtool curl unzip gzip#下载protobuf包
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.2/protobuf-all-3.20.2.tar.gz
#若 github 下载过慢,则到下方链接进行下载
wget https://gitee.com/qigezi/bitmq/blob/master/mqthird/protobufall-3.20.2.tar.gz# 解压缩
tar -zxf protobuf-all-3.20.2.tar.gz
# 切换目录
cd protobuf-3.20.2/
# 如果下载的是具体的某一门语言,不需要执行这一步
./autogen.sh
# 配置安装路径 默认安装在 /usr/local 目录, lib、 bin 都是分散的
./configure
# 开始编译 15 分钟左右
make
# 开始安装, 需要 root 权限
sudo make install
# 确认是否安装成功
protoc --version

最后的查看protobuf版本的代码可能并不能成功打印出对应的版本,并不是因为安装失败了,而是安装后的库存放于 /usr/local/lib 中, 导致运行程序时默认检索不到。可以修改/etc/ld.so.conf 文件,添加库文件路径到其中。

vim /etc/ld.so.conf

我们编写一个后缀位.proto的文件,内容如下:

编写完毕之后,在命令行输入如下内容:

protoc --cpp_out=. test.proto

编译成功之后,会生成两个文件:.pb.h和.pb.cc

这个pb.h就是c++中的头文件,pb.cc是源文件。

此时我们再创建一个test.cc文件,编写如下代码(注意,头文件要包含刚刚生成的pb.h文件)

#include <iostream>
#include "test.pb.h" // 引入编译生成的头文件
using namespace std;int main()
{string people_str;// 序列化{// .proto 文件声明的 package,通过 protoc 编译后,会为编译生成的C++代码声明同名的命名空间// 其范围是在.proto 文件中定义的内容contacts::PeopleInfo people;people.set_age(20);people.set_name("张三");// 调用序列化方法,将序列化后的二进制序列存入 string 中if (!people.SerializeToString(&people_str)){cout << "序列化联系人失败." << endl;}// 打印序列化结果cout << "序列化后的 people_str: " << people_str.size() << endl;}// 反序列化{contacts::PeopleInfo people;// 调用反序列化方法,读取 string 中存放的二进制序列,并反序列化出对象if (!people.ParseFromString(people_str)){cout << "反序列化出联系人失败." << endl;}// 打印结果cout << "Parse age: " << people.age() << endl;cout << "Parse name: " << people.name() << endl;}return 0;
}

编写完毕之后,使用如下命令进行编译:

g++ -std=c++11 test.cc test.pb.cc -o test -lprotobuf

编译成功后,就会生成一个可执行文件test,我们运行该文件:

如果最终能成功显示出上面的结果,则说明安装是没有问题的。

安装 Muduo

# 1.下载muduo库
# git 方式
git clone https://github.com/chenshuo/muduo.git
# 备用下载地址
wget https://gitee.com/hansionz/mq/raw/master/resource/muduo-master.zip# 2.解压muduo-master压缩包
unzip muduo-master.zip# 3.安装依赖库
sudo apt-get install libz-dev libboost-all-dev# 4.运行脚本编译安装
cd muduo-master/
./build.sh
./build.sh install

同样,我们也需要测试一下是否安装成功,在执行完上面的步骤后,muduo-master同级目录下会生成一个build目录,我们进入该目录下。

然后继续进入release-cpp11目录下的bin目录

cd release-cpp11/bin

这个目录下包含很多可执行程序,我们运行其中的protobuf_server,启动后他会卡在那里,表示服务端已经就绪,要等待客户端连接。

我们重新启动一个窗口,然后打开这个目录下的protobuf_client(客户端),并绑定好ip和端口。

在输入回车的一瞬间就打印出很多消息,说明与服务端连接成功了。

并且服务端也打印出了日志。

如果以上步骤都没问题,则说明muduo库安装成功。

  • 安装 SQLite3
sudo apt-get install sqlite3 libsqlite3-dev
# 验证是否安装成功
sqlite3 --version
  • 安装 Gtest
sudo apt-get install libgtest-dev

测试 GTest 是否安装成功,编写下面C++代码:

#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程序

如果可以成功运行出结果,则说明安装成功了。

5.第三方库快速上手

在正式编写项目代码之前,我们还需要熟悉一下前面安装的各种第三方库,使用这些库的好处在于,我们可以直接使用库中已经编写好的代码,而不用什么都去自己手动实现,大大节省了编写代码的时间和难度。

下面我将会简单的介绍一下每个库如何使用,带大家快速上手。

5.1 Protobuf

笔者在之前写过一篇博客,详细介绍了protobuf的语法,感兴趣的同学可以去看看:从入门到精通——ProtoBuf_proto message和协议-CSDN博客

Protobuf 是什么

ProtoBuf(全称 Protocol Buffer)是数据结构序列化和反序列化框架,它具有以下特点:

  • 语言无关、平台无关: 即 ProtoBuf 支持 Java、 C++、 Python 等多种语言,支持多个平台
  • 高效: 即比 XML 更小、更快、更为简单
  • 扩展性、兼容性好: 你可以更新数据结构,而不影响和破坏原有的旧程序

Protobuf 使用流程介绍

  • 编写 .proto 文件,描述我们想要定义的结构化对象(描述对象中,包含什么成员,每个成员具有什么属性),不理解的同学就认为定义了一个C语言中的结构体。
  • 使用 protoc 编译器编译 .proto 文件,生成一系列接口代码,.h中定义了我们所描述的数据结构对象类,.cc定义了结构化对象数据的访问、操作、序列化、反序列化
  • 依赖生成的接口,将编译生成的头文件包含进我们的代码中,实现对 .proto 文件中定义的字段进行设置和获取,和对 message 对象进行序列化和反序列化

ProtoBuf 快速上手

我们以一个简单通讯录的实现来驱动对 Protobuf 的学习。在通讯录 demo 中,我们将实现:

  • 对一个联系人的信息使用 Protobuf 进行序列化,并将结果打印出来
  • 对序列化后的内容使用 Protobuf 进行反序列,解析出联系人信息并打印出来
  • 联系人包含以下信息: 姓名、年龄

通过通讯录 demo,我们能快速的了解 ProtoBuf 的使用流程。

  • 1)创建.proto文件

我们为通讯录 demo 新建文件: contacts.proto

  • 2)指定 proto3 语法

在 .proto 文件中,要使用 syntax = "proto3"; 来指定文件语法为 proto3,并且必须写在除去注释内容的第一行。 如果没有指定,编译器会使用 proto2 语法。

在通讯录 demo 的 contacts.proto 文件中,可以为文件指定 proto3 语法,内容如下:

  • 3)package 声明符

package 是一个可选的声明符,能表示 .proto 文件的命名空间,在项目中要有唯一性。它的作用是给生成的代码添加命名空间,避免我们定义的消息出现冲突(和C++中的命名空间是一个意思)。

在通讯录 demo 的 contacts.proto 文件中,可以声明其命名空间,内容如下:

  • 4)定义消息(message)

消息(message) : 要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。(和C语言中的结构体比较像)

在通讯录 demo 中我们就需要为联系人定义一个 message:

注意:消息类型名非常重要,这个名字就是将来生成的类的名字,我们这里将其命名为PeopleInfo表示这个结构化对象表示的是联系人信息。

  • 5)定义消息字段

在 message 中我们可以定义其属性字段,字段定义格式为: 字段类型 字段名 = 字段唯一编号

  • 字段名称命名规范:全小写字母,多个字母之间用 _ 连接。
  • 字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)。
  • 字段唯一编号:用来标识字段,一旦开始使用就不能够再改变。

下面的表格展示了定义于消息体中的标量数据类型,以及编译 .proto 文件之后自动生成的类中与之对应的字段类型。在这里展示了与 C++ 语言对应的类型

注意:变长编码[1]是指:经过 protobuf 编码后,原本 4 字节或 8 字节的数可能会被变为其他字节数

更新 contacts.proto, 新增姓名、年龄字段

  • 6)编译contacts.proto文件

编译命令:

protoc --cpp_out=. contacts.proto

编译成功会生成两个文件,一个是pb.cc,一个是pb.h。

我们打开pb.h后可以看到如下内容(简化后的)

namespace contacts {class PeopleInfo{public:// string name = 1;void clear_name();const std::string& name() const;template <typename ArgT0 = const std::string&, typename... ArgT>void set_name(ArgT0&& arg0, ArgT... args);std::string* mutable_name();PROTOBUF_NODISCARD std::string* release_name();void set_allocated_name(std::string* name);private:const std::string& _internal_name() const;inline PROTOBUF_ALWAYS_INLINE void _internal_set_name(const std::string& value);std::string* _internal_mutable_name();public:// uint32 age = 2;void clear_age();uint32_t age() const;void set_age(uint32_t value);private:uint32_t _internal_age() const;void _internal_set_age(uint32_t value);}
}

我们再来对比一下之前的.proto文件,就能很明显的发现生成之后的代码都是根据我们之前写的内容生成的。

protoc编译器会自动帮我们生成很多的接口方法,如set_name()就是用来设置name字段,name()就是获取name字段,并且还支持进行序列化以及反序列化。

我们自己定义的消息类会继承于Message类,Message会继承MessageLite类,而MessageLite中提供了读写消息实例的方法,包括序列化方法和反序列化方法。

class MessageLite 
{
public://序列化:bool SerializeToOstream(ostream* output) const; // 将序列化后数据写入文件流bool SerializeToArray(void *data, int size) const;bool SerializeToString(string* output) const;//反序列化:bool ParseFromIstream(istream* input); // 从流中读取数据,再进行反序列化动作bool ParseFromArray(const void* data, int size);bool ParseFromString(const string& data);
};

既然消息类是继承于MessageLite的,所以上面的方法我们也可以直接使用。

注意:

序列化的结果为二进制字节序列,而非文本格式。

  • 7)序列化与反序列化的使用

创建一个测试文件 info.cc,方法中我们实现:

  • 对一个联系人的信息使用 PB 进行序列化,并将序列化结果打印出来
  • 对序列化后的内容使用 PB 进行反序列,解析出联系人信息并打印出来
#include <iostream>
#include "./contacts.pb.h" // 引入编译生成的头文件
using namespace std;int main()
{string people_str;// 序列化{//设置创建一个结构化对象contacts::PeopleInfo people;people.set_age(20);people.set_name("张三");// 调用序列化方法,将序列化后的二进制序列存入 string 中if (!people.SerializeToString(&people_str)) {cout << "序列化联系人失败." << endl;}// 打印序列化结果cout << "序列化成功,结果: " << people_str.size() << endl;}cout << endl;// 反序列化{contacts::PeopleInfo people;// 调用反序列化方法,读取 string 中存放的二进制序列,并反序列化出对象if (!people.ParseFromString(people_str)) {cout << "反序列化出联系人失败." << endl;}// 打印结果cout << "反序列化成功" << endl;cout << "Parse age: " << people.age() << endl;cout << "Parse name: " << people.name() << endl;}return 0;
}

编写完代码之后,我们进行编译,生成可执行程序:

g++ test.cc contacts.pb.cc -o Test -std=c++11 -lprotobuf

运行可执行程序:

由于 ProtoBuf 是把联系人对象序列化成了二进制序列,这里用 string 来作为接收二进制序列的容器。所以在终端打印的时候会有换行等一些乱码显示。另外相对于 xml 和JSON 来说,因为 PB 被编码成二进制,破解成本增大, ProtoBuf 编码是相对安全的。

ProtoBuf整体来说还是比较简单的,重点就在于如何编写.proto文件,有了该文件之后我们就可以转化成C++代码,直接进行使用了。

5.2 Muduo

什么是Muduo

Muduo是一个基于非阻塞 IO 和事件驱动的 C++高并发 TCP 网络编程库。 它是一款基于主从 Reactor 模型的网络库,其使用的线程模型是 one loop per thread, 所谓 one loop per thread 指的是:

  • 一个线程只能有一个事件循环(EventLoop), 用于响应计时器和 IO 事件
  • 一个文件描述符只能由一个线程进行读写,换句话说就是一个 TCP 连接必须归属于某个 EventLoop 管理

muduo库有下面几个特点:

  • 1.基于主从reactor模型的高性能服务器框架
    • reactor模型:基于事件触发的模型(基于epoll进行IO事件监控)
    • 主从reactor模型:将IO事件监控进行进一步的层次划分
      • 主reactor:只对新建连接事件进行监控(保证不受IO阻塞影响实现高效的新建连接获取)
      • 从reactor:针对新建连接进行IO事件监控(进行IO操作和业务处理)
      • 主从reactor必然是一个多执行流的并发模式——one thread one loop

Muduo库 服务器常见接口介绍

  • muduo::net::TcpServer 类基础介绍
typedef std::shared_ptr<TcpConnection> TcpConnectionPtr;
typedef std::function<void (const TcpConnectionPtr&)> ConnectionCallback;
typedef std::function<void (const TcpConnectionPtr&, Buffer*, Timestamp)> MessageCallback;class InetAddress : public muduo::copyable
{
public:InetAddress(StringArg ip, uint16_t port, bool ipv6 = false);
};class TcpServer : noncopyable
{public:enum Option{kNoReusePort,    kReusePort,};TcpServer(EventLoop* loop,const InetAddress& listenAddr,const string& nameArg,Option option = kNoReusePort);void setThreadNum(int numThreads);void start();/// 当一个新连接建立成功的时候被调用void setConnectionCallback(const ConnectionCallback& cb){ connectionCallback_ = cb; }/// 消息的业务处理回调函数---这是收到新连接消息的时候被调用的函数void setMessageCallback(const MessageCallback& cb){ messageCallback_ = cb; }
};

创建TcpServer服务类后会给我们创建一个服务器,我们就不需要手动创建套接字了。

TcpServer构造函数各个参数的含义:

  • 1.loop:事件监控(封装了epoll)
  • 2.listenAddr:需要传入一个InetAddress类型的对象,该对象表示服务器绑定的IP和端口号。
  • 3.nameArg:服务器名称,用于标识不同的服务器
  • 4.option:如果我们主动关闭了服务器,服务器会进入TIME_WAIT状态,在这段时间内我们不能使用上次的IP和端口了,必须强制等待一段时间,为了解决这个问题,我么可以开启地址重用,这样主动断开连接之后,也可以立刻重启服务器。

枚举类型Option内容如下:

其中kReusePort用于设置地址重用,kNoReusePort用于关闭地址重用。

setThreadNum:

void setThreadNum(int numThreads);

前面说到过muduo库是基于主从reactor模型的服务器,意味着在服务器内部除了创建一个主线程,还要创建很多的从线程,从线程并不是在事件到来时才被创建,而是会预先创建一批从线程(线程池),每到来一个事件,就让线程池中的某一个线程去执行。

setThreadNum就是用于设置服务器中从线程的个数

start:

void start();

用于启动服务器。服务器并不是在我们创建好TcpServer对象时就启动了,创建TcpServer对象只是设置了服务器的一些参数(如IP,端口等),要手动调用start函数才能正式启动服务器。

回调函数:

// 当一个新连接建立成功的时候被调用
void setConnectionCallback(const ConnectionCallback& cb)
{ connectionCallback_ = cb; 
}// 消息的业务处理回调函数---这是收到新连接消息的时候被调用的函数
void setMessageCallback(const MessageCallback& cb)
{ messageCallback_ = cb; 
}

当一个新连接建立成功时,希望执行什么动作就可以设置setConnectionCallback函数,我们将需要执行的动作写到一个函数中,并将该函数传递给setConnectionCallback函数的参数,这样在新连接建立成功时,服务器会自动帮我们调用我们传递的函数。

setMessageCallback是消息的业务处理回调函数,新连接建立成功后会收到来自客户端的消息,当收到客户端消息时就会自动调用我们传递的函数。

注意:

回调函数并不是想怎么写就怎么写的,必须按照指定的格式:



  • muduo::net::EventLoop 类基础介绍
class EventLoop : noncopyable
{
public:void loop();void quit();TimerId runAt(Timestamp time, TimerCallback cb);TimerId runAfter(double delay, TimerCallback cb);TimerId runEvery(double interval, TimerCallback cb);void cancel(TimerId timerId);private:std::atomic<bool> quit_;std::unique_ptr<Poller> poller_;mutable MutexLock mutex_;std::vector<Functor> pendingFunctors_ GUARDED_BY(mutex_);
};
  • EventLoopmuduo库中的核心对象之一。它主要用于处理事件循环,相当于一个事件分发器。在网络编程场景下,它负责处理 I/O 事件(如套接字的可读、可写事件)以及定时器事件等。
  • EventLoop会不断地循环等待事件的发生,一旦有事件就绪,就会调用相应的回调函数来处理该事件。这种事件驱动的机制是高效处理大量并发 I/O 操作的关键。
  • EventLoop内部维护了一个epoll来管理众多的文件描述符(套接字)。它使用这些机制来高效地检测哪些文件描述符上有事件发生。

EventLoop中有很多接口,但是我们这个项目只会用到loop()接口,内部是一个死循环,用于事件监控,当触发事件时会进行IO处理。



  • muduo::net::TcpConnection 类基础介绍
class TcpConnection : noncopyable,
public
std::enable_shared_from_this<TcpConnection>
{
public: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++11void send(const void* message, int len);void send(const StringPiece& message);void send(Buffer* message); // this one will swap data//关闭连接void shutdown(); // NOT thread safe, no simultaneous callingvoid 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, kConnected, kDisconnecting };EventLoop* loop_;ConnectionCallback connectionCallback_;MessageCallback messageCallback_;WriteCompleteCallback writeCompleteCallback_;boost::any context_;
};

TcpConnection用于表示一个 TCP 连接的类。它封装了一个 TCP 连接的状态、对应的套接字以及相关的读写操作和事件处理。

下面是我们会用到的TcpConnection中的接口: 

  • connected用于判断是否建立了连接
  • disconnected用于判断连接是否断开了
  • send用于向套接字中写入数据(向客户端发送数据)
  • shutdown用于关闭连接。


  • muduo::net::Buffer 类基础介绍
class Buffer : public muduo::copyable
{
public:static const size_t kCheapPrepend = 8;static const size_t kInitialSize = 1024;explicit Buffer(size_t initialSize = kInitialSize): buffer_(kCheapPrepend + initialSize), readerIndex_(kCheapPrepend), writerIndex_(kCheapPrepend){}void swap(Buffer& rhs);size_t readableBytes() const;size_t writableBytes() const;const char* peek() const;const char* findEOL() const;const char* findEOL(const char* start) const;void retrieve(size_t len);void retrieveInt64();void retrieveInt32();void retrieveInt16();void retrieveInt8();string retrieveAllAsString();string retrieveAsString(size_t len);void append(const StringPiece& str);void append(const char* /*restrict*/ data, size_t len);void append(const void* /*restrict*/ data, size_t len);char* beginWrite();const char* beginWrite() const;void hasWritten(size_t 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* /*restrict*/ data, size_t len);
private:std::vector<char> buffer_;size_t readerIndex_;size_t writerIndex_;static const char kCRLF[];
};

Buffer就是一个缓冲区,它可以临时存储从网络套接字读取的数据,或者将要发送到网络套接字的数据。

介绍比较重要的几个接口:

  • 1.string retrieveAllAsString(); 从缓冲区中读取数据,读入到字符串中
  • 2.retrieve(size_t len); 从缓冲区中读取len个字节的数据。
  • 3.readableBytes(); 获取缓冲区的可读数据的大小
  • 4.append();向发送缓冲区中追加数据

使用Muduo库搭建服务器

我们会搭建一个简易的翻译服务,客户端上传需要翻译的单词,服务端进行解析后返回给客户端。

服务器整体框架如下:

#include <iostream>
#include "include/muduo/net/TcpServer.h";
#include "include/muduo/net/EventLoop.h";
#include "include/muduo/net/TcpConnection.h";
using namespace std;class TranslateServer
{
public:TranslateServer(int port) //要绑定的端口: _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port),"TcpServer", muduo::net::TcpServer::Option::kReusePort) //启动地址重用{}void Start();private://新连接建立成功时的回调函数void onConnection(const muduo::net::TcpConnectionPtr& conn);//收到请求时的回调函数void onMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buf, muduo::Timestamp);private:muduo::net::EventLoop _baseloop;muduo::net::TcpServer _server;
};int main()
{TranslateServer server(8080);server.Start();return 0;
}

在main函数中创建了一个TranslateServer对象,并传入需要绑定的端口8080,在TranslateServer对象内部会自动帮我处理好:新连接建立成功时应该做什么,当收到连接中发来的请求应该做什么。

所以在main函数中只需要掉哟Start()函数即可启动服务器。

细节实现:

#include <iostream>
#include <functional>
#include <unordered_map>
#include "include/muduo/net/TcpServer.h"
#include "include/muduo/net/EventLoop.h"
#include "include/muduo/net/TcpConnection.h"
using namespace std;class TranslateServer
{
public:TranslateServer(int port) //要绑定的端口: _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port),"TcpServer", muduo::net::TcpServer::Option::kReusePort) //启动地址重用{auto fun1 = bind(&TranslateServer::onConnection, this, std::placeholders::_1);_server.setConnectionCallback(fun1);  //不能直接设置为onConnection函数,因为包含一个隐藏的this指针auto fun2 = bind(&TranslateServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);_server.setMessageCallback(fun2);     //不能直接设置为onMessage函数,因为包含一个隐藏的this指针}void start(){_server.start();  //开始事件监听_baseloop.loop(); //开始事件监控(是一个死循环)}private://新连接建立成功时的回调函数void onConnection(const muduo::net::TcpConnectionPtr& conn){//该函数在建立连接和关闭连接时都会被调用if (conn->connected()){cout << "连接建立成功!" << endl;}else {cout << "关闭连接" << endl;}}string translate(const string& word){static unordered_map<string, string> dict = {{"hello", "你好"},{"sun", "太阳"},{"left", "左"}};auto it = dict.find(word);if (it != dict.end()){return it->second;}else {return "查找失败";}}//收到请求时的回调函数void onMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buf, muduo::Timestamp){//1.将缓冲区的数据读取出来放到word中string word = buf->retrieveAllAsString();//2.通过内置词典,翻译word对应的中文string res = translate(word);//3.将翻译的结果返回给客户端conn->send(res);}private:muduo::net::EventLoop _baseloop;muduo::net::TcpServer _server;
};int main()
{TranslateServer server(8080);server.start();return 0;
}
  • 1.TranslateServer的构造函数:需要调用setConnectionCallback和setMessageCallback设置好对应的回调函数,即onConnection和onMessage函数,这两个函数的参数都必须符合特定的格式,才能作为setConnectionCallback和setMessageCallback的参数,而类的成员函数默认会携带一个隐藏的this指针,无法直接传递,需要依靠C++11中的包装器来完成。
  • 2.start函数:start是用于启动服务器,_server.start()是启动事件的监听,而_baseloop.loop()是开始事件的监控(前面说到过muduo库是基于epoll模型的),所以当调用该函数后,当套接字中读取到有事件就绪时,直接会调用设置好的回调函数。
  • 3.onConnection函数:新连接到来时判断连接有没有成功建立,如果有则打印。
  • 4.onMessage函数:进行业务处理的函数,当客户端发送翻译请求时,会被监控到,并调用该函数,该函数就能读取到用户请求,并对请求进行处理,最后将处理结果返回给用户。

Muduo库 客户端常见接口介绍

class TcpClient : noncopyable
{
public:TcpClient(EventLoop* loop,const InetAddress& serverAddr,const string& nameArg);~TcpClient(); // force out-line dtor, for std::unique_ptr members.void connect();//连接服务器void disconnect();//关闭连接void stop();//获取客户端对应的通信连接 Connection 对象的接口,发起 connect 后,有可能还没有连接建立成功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_;ConnectionCallback connectionCallback_;MessageCallback messageCallback_;WriteCompleteCallback writeCompleteCallback_;TcpConnectionPtr connection_ GUARDED_BY(mutex_);
};

TcpClient类中的接口和TcpServer类中的接口基本上是一样的,下面只介绍与TcpServer不同的地方。

1.connect(); 用于连接服务器(向服务器发起TCP连接)

  • 2.disconnect(); 用于关闭连接
  • 3.stop(); 停止客户端运行
  • 4.connection(); 获取客户端对应的通信连接 Connection 对象的接口,通过该对象可以向服务器send数据

需要注意的是,因为 muduo 库不管是服务端还是客户端都是异步操作,对于客户端来说如果我们在连接还没有完全建立成功的时候,想要调用connection()获取Connection对象,此时获取的就是一个空的Connection,如果再调用send就会直接崩溃。

为了解决这个问题,我们可以使用内置的 CountDownLatch 类进行同步控制

class CountDownLatch : noncopyable
{
public: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();}}int getCount() const;private:mutable MutexLock mutex_;Condition condition_ GUARDED_BY(mutex_);int count_ GUARDED_BY(mutex_);
};

CountDownLatch类中包含两个函数,一个是wait,顾名思义就是调用这个接口时就会进行阻塞等待;另一个是countDown,调用这个函数时会唤醒阻塞。

所以正确的流程如下:

使用Muduo库搭建客户端

整体框架如下:

#include <iostream>
#include <functional>
#include <unordered_map>
#include "include/muduo/net/TcpClient.h"
#include "include/muduo/net/EventLoopThread.h"
#include "include/muduo/net/TcpConnection.h"
#include "include/muduo/base/CountDownLatch.h"
using namespace std;class TranslateClient
{
public:TranslateClient(const std::string& ip, int port): _latch(1), _client(_baseloop.startLoop(), muduo::net::InetAddress(ip, port), "TcpClient"){ }void connect();void send(const std::string& str);private://连接建立成功时的回调函数,连接建立成功后,唤醒阻塞void onConnect(const muduo::net::TcpConnectionPtr& conn);void onMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buf,muduo::Timestamp timestamp);private:muduo::CountDownLatch _latch;muduo::net::EventLoopThread _baseloop;muduo::net::TcpClient _client;muduo::net::TcpConnectionPtr _conn;
};int main()
{TranslateClient client("127.0.0.1", 8080);client.connect();while (true){string str;cout << "请输入要翻译的单词:";getline(cin, str);if (str == "quit")break;client.send(str);sleep(1);}return 0;
}

main函数中创建了一个TranslateClient对象,并绑定对应的IP和端口,然后连接该IP和端口的服务器,然后会进入一个死循环,每次读取一个字符串,然后将该字符串发送给服务端。

和服务端一样,客户端的TranslateClient构造函数也需要绑定回调函数onConnection和onMessage。

注意客户端使用的是EventLoopThread而不是EventLoop,EventLoopThread 能更好地管理 EventLoop 的生命周期,包括启动、停止和资源清理。而直接使用 EventLoop,客户端代码要自己负责这些复杂操作,使用 EventLoopThread 可让客户端专注于业务逻辑。

实现细节:

#include <iostream>
#include <functional>
#include <unordered_map>
#include "include/muduo/net/TcpClient.h"
#include "include/muduo/net/EventLoopThread.h"
#include "include/muduo/net/TcpConnection.h"
#include "include/muduo/base/CountDownLatch.h"
using namespace std;class TranslateClient
{
public:TranslateClient(const std::string& ip, int port): _latch(1), _client(_baseloop.startLoop(), muduo::net::InetAddress(ip, port), "TcpClient"){auto fun1 = bind(&TranslateClient::onConnect, this, placeholders::_1);_client.setConnectionCallback(fun1);auto fun2 = bind(&TranslateClient::onMessage, this, placeholders::_1, placeholders::_2, placeholders::_3);_client.setMessageCallback(fun2);}void connect(){_client.connect();_latch.wait(); //阻塞等待,直到连接建立成功}void send(const std::string& str){if (_conn->connected())_conn->send(str);}private://连接建立成功时的回调函数,连接建立成功后,唤醒阻塞void onConnect(const muduo::net::TcpConnectionPtr& conn){if (conn->connected()){_latch.countDown(); //唤醒阻塞_conn = conn;}else {std::cout << "连接断开!!" << endl;_conn.reset();}}void onMessage(const muduo::net::TcpConnectionPtr& conn, muduo::net::Buffer* buf,muduo::Timestamp timestamp){string res = buf->retrieveAllAsString();std::cout << "翻译结果:" << res << endl;}private:muduo::CountDownLatch _latch;muduo::net::EventLoopThread _baseloop;muduo::net::TcpClient _client;muduo::net::TcpConnectionPtr _conn;
};int main()
{TranslateClient client("127.0.0.1", 8080);client.connect();while (true){string str;cout << "请输入要翻译的单词:";getline(cin, str);if (str == "quit")break;client.send(str);sleep(1);}return 0;
}
  • 1.TranslateClient的构造函数:需要调用setConnectionCallback和setMessageCallback设置好对应的回调函数,即onConnection和onMessage函数,这两个函数的参数都必须符合特定的格式,才能作为setConnectionCallback和setMessageCallback的参数,而类的成员函数默认会携带一个隐藏的this指针,无法直接传递,需要依靠C++11中的包装器来完成。
  • 2.connect函数:会调用_client.connect()函数,该函数会向服务端发起连接,但是这个操作是异步的,所以不能直接获取Connection对象,而是要确保连接连接成功之后才获取。所以这里会调用一个wait函数,等到onConnection函数被调用时,则说明连接建立成功。
  • 3.send函数:由于是异步的,连接可能已经被关闭了,这种情况下不能发送数据,需要特判。
  • 4.onConnection函数:当连接建立成功后,会唤醒阻塞,此时再获取的一定是非空的Connection。
  • 5.onMessage函数:对收到的服务器响应直接打印。

运行结果:

通过muduo库,我们就能快速搭建一个TCP通信模型,这比我们自己手写一个要快很多,并且性能也要更好。

5.2.1 基于muduo库函数实现protobuf协议的通信

在上面我们使用muduo库实现了一个简单的翻译服务器和客户端,但是这个应用中还存在一些问题:我们没有制定协议去解决Tcp粘包问题

前面的应用中客户端给服务器发送请求中,请求中只包含请求信息的字符串,但是这种方式是不太好的,因为TCP会存在粘包问题(两次请求的数据粘到一起,无法将他们分开),所以我们需要制定合理的协议并加上protobuf来实现真正的网络通讯模型。

正好muduo库中也包含了一些使用案例,其中一个就是使用protobuf+自定义协议的方式来解决TCP粘包问题的例子,我们一起来看一下陈硕大佬是如何处理这个问题的。

在muduo-master/examples中有一个protobuf文件夹:

我们打开其中的codec,内容如下:

由于代码中包含较多回调函数,直接看代码非常复杂,这里就不直接介绍代码了,而是将其中比较重要的接口简化成图描述他们之间的关系,等了解他们的关系之后再看代码就容易很多了。

下面是项目中自定义的协议,一个完整的请求包含下面几个部分:

  • 1.len:固定大小4个字节,表示除了len之外整个报文的长度。
  • 2.nameLen:表示typeName的长度。
  • 3.typeName:表示某种类型的名称。这个类型名称用于区分不同种类的消息、请求或对象。
  • 4.protobufData:真正要发送给服务器的数据,通过protobuf进行序列化后的二进制字节流。
  • 5.checkSum:数据校验和,判断数据在传输过程中是否发生损坏或者篡改。

将项目代码简化成流程图,如下所示:

  • 在ProtobufDispatcher类中,会通过registerMessageCallback函数进行业务注册,将不同请求对应的回调函数保存至关系映射表中。
  • ProtobufCodec中的onMessage会将缓冲区中的数据进行处理,读取到一个完整的protobufData数据,这一步实际上就是处理TCP粘包的步骤。
  • 在TcpClient(客户端类)中设置消息回调函数为ProtobufCodec中的onMessage,即每收到一个用户请求后都会将其中的protobufData数据读取出来。
  • onMessage不仅仅会将缓冲区中的数据处理成protobuf结构对象,还会通过protobuf请求,调用ProtobufDispatcher中的onProtobufMessage函数,他会帮我们根据不同的protobuf请求调用不同的处理函数。

onMessage的定义如下:

void ProtobufCodec::onMessage(const TcpConnectionPtr& conn,Buffer* buf,Timestamp receiveTime)
{while (buf->readableBytes() >= kMinMessageLen + kHeaderLen){const int32_t len = buf->peekInt32();if (len > kMaxMessageLen || len < kMinMessageLen){errorCallback_(conn, buf, receiveTime, kInvalidLength);break;}else if (buf->readableBytes() >= implicit_cast<size_t>(len + kHeaderLen)){ErrorCode errorCode = kNoError;MessagePtr message = parse(buf->peek()+kHeaderLen, len, &errorCode);if (errorCode == kNoError && message){messageCallback_(conn, message, receiveTime);buf->retrieve(kHeaderLen+len);}else{errorCallback_(conn, buf, receiveTime, errorCode);break;}}else{break;}}
}

其中的parse函数就会解析出一个完整的protoData数据,然后通过messageCallback函数调用对应的执行方法。

ProtobufDispatcher:

class ProtobufDispatcher
{public:typedef std::function<void (const muduo::net::TcpConnectionPtr&,const MessagePtr& message,muduo::Timestamp)> ProtobufMessageCallback;explicit ProtobufDispatcher(const ProtobufMessageCallback& defaultCb): defaultCallback_(defaultCb){}void onProtobufMessage(const muduo::net::TcpConnectionPtr& conn,const MessagePtr& message,muduo::Timestamp receiveTime) const{CallbackMap::const_iterator it = callbacks_.find(message->GetDescriptor());if (it != callbacks_.end()){it->second->onMessage(conn, message, receiveTime);}else{defaultCallback_(conn, message, receiveTime);}}template<typename T>void registerMessageCallback(const typename CallbackT<T>::ProtobufMessageTCallback& callback){std::shared_ptr<CallbackT<T> > pd(new CallbackT<T>(callback));callbacks_[T::descriptor()] = pd;}private:typedef std::map<const google::protobuf::Descriptor*, std::shared_ptr<Callback> > CallbackMap;CallbackMap callbacks_;ProtobufMessageCallback defaultCallback_;
};

我们直接使用上面的protobuf协议方式,完成一个可以完成翻译和计算器的服务器+客户端项目。

编写proto文件

编写好了之后通过指令生成对应的.pb.cc和.pb.h文件

完成服务端代码

#include <iostream>
#include <functional>
#include <memory>
#include "request.pb.h"
#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher_lite.h"
#include "muduo/proto/dispatcher.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpServer.h"
using namespace std;class ProtoServer
{typedef shared_ptr<request::TranslateRequest> TranslateRequestPtr;typedef shared_ptr<request::TranslateResponse> TranslateResponsePtr;typedef shared_ptr<request::CalculatorRequest> CalculatorRequestPtr;typedef shared_ptr<request::CalculatorResponse> CalculatorResponsePtr;
public:ProtoServer(int port): _server(&_baseloop, muduo::net::InetAddress(port), "ProtoServer", muduo::net::TcpServer::kReusePort), _dispatcher(bind(&ProtoServer::onUnknownMessage, this, placeholders::_1, placeholders::_2, placeholders::_3)), _codec(bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, placeholders::_1, placeholders::_2, placeholders::_3)){//注册业务处理函数_dispatcher.registerMessageCallback<request::TranslateRequest>(std::bind(&ProtoServer::onTranslate, this,placeholders::_1, placeholders::_2, placeholders::_3));_dispatcher.registerMessageCallback<request::CalculatorRequest>(std::bind(&ProtoServer::onCalculator, this,placeholders::_1, placeholders::_2, placeholders::_3));//设置回调函数_server.setConnectionCallback(bind(&ProtoServer::onConnection, this, placeholders::_1));_server.setMessageCallback(bind(&ProtobufCodec::onMessage, &_codec, placeholders::_1, placeholders::_2, placeholders::_3));}void start(){_server.start();_baseloop.loop();}private:string translate(const string& word){static unordered_map<string, string> dict = {{"sun", "太阳"},{"left", "左"},{"right", "右"},{"hello", "你好"}};auto it = dict.find(word);if (it != dict.end())return it->second;else  return "未找到";}void onTranslate(const muduo::net::TcpConnectionPtr& conn, const TranslateRequestPtr& req, muduo::Timestamp rt){//读取请求字符串string word = req->word();//将请求字符串进行翻译string tranStr = translate(word);//构建TranslateResponse响应request::TranslateResponse resp;resp.set_result(tranStr);//发送响应_codec.send(conn, resp);}void onCalculator(const muduo::net::TcpConnectionPtr& conn, const CalculatorRequestPtr& req, muduo::Timestamp rt){int num1 = req->num1();int num2 = req->num2();char op = req->op()[0];request::CalculatorResponse resp;switch(op){case '+':resp.set_result(num1 + num2);resp.set_code(true);break;case '-':resp.set_result(num1 - num2);resp.set_code(true);break;case '*':resp.set_result(num1 * num2);resp.set_code(true);break;case '/':if (num2 == 0){resp.set_code(false);}else {resp.set_result(num1 / num2);resp.set_code(true);}break;                                }_codec.send(conn, resp);}void onConnection(const muduo::net::TcpConnectionPtr& conn){if (conn->connected()){cout << "连接成功!!!" << endl;}else {cout << "断开连接" << endl;}}void onUnknownMessage(const muduo::net::TcpConnectionPtr& conn, const MessagePtr& message, muduo::Timestamp){std::cout << "onUnknownMessage: " << message->GetTypeName() << std::endl;}private:muduo::net::EventLoop _baseloop;muduo::net::TcpServer _server;ProtobufDispatcher _dispatcher;ProtobufCodec _codec;
};int main()
{ProtoServer server(8080);server.start();return 0;
}

构造函数内容如下:

注意事项:

  • 成员变量相对于没有协议之前的版本,新增了一个ProtobufDispatcher(事件分配器)和一个ProtobufCodec(协议处理器)
    • ProtobufDispatcher的作用是将用户的请求事件分配给不同的回调函数,根据不同的请求执行不同的业务。
    • ProtobufCodec用于协议处理,他能从缓冲区中的字符流中读取到一个完整的ProtobufData数据,然后将其反序列化为Protobuf结构化对象(请求的protobuf);也可以将Protobuf结构化对象(响应的protobuf)序列化成ProtobufData,然后封装成报文发送给客户端。
  • _dispatcher在注册事件时,在模板参数中需要填写指定的请求类型,当服务端收到对应的请求类型时,就会调用对应的回调函数。
  • _dispatcher绑定的回调函数需要注意,其第二个参数是用智能指针(shard_ptr)的方式。
  • _在服务端给客户端发送响应时不能直接调用conn->send(str),因为此时直接发送就就是字节流,没有使用protobuf对应的response和应用层定制协议。客户端会无法识别。所以应该交给_codec处理,他会将结构化对象进行序列化并封装成完整报文再发送给对端。

完成客户端代码

#include <iostream>
#include <functional>
#include <memory>
#include "request.pb.h"
#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher_lite.h"
#include "muduo/proto/dispatcher.h"
#include "muduo/net/EventLoopThread.h"
#include "muduo/net/TcpClient.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/base/CountDownLatch.h"
#include "muduo/base/Mutex.h"
using namespace std;class ProtoClient
{
public:typedef shared_ptr<google::protobuf::Message> MessagePtr;typedef shared_ptr<request::TranslateRequest> TranslateRequestPtr;typedef shared_ptr<request::TranslateResponse> TranslateResponsePtr;typedef shared_ptr<request::CalculatorRequest> CalculatorRequestPtr;typedef shared_ptr<request::CalculatorResponse> CalculatorResponsePtr;ProtoClient(const std::string& ip, int port): _latch(1), _client(_loopthread.startLoop(), muduo::net::InetAddress(ip, port), "Client"), _dispatcher(bind(&ProtoClient::onUnknownMessage, this, placeholders::_1, placeholders::_2, placeholders::_3)), _codec(bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, placeholders::_1, placeholders::_2, placeholders::_3)){//注册业务回调函数_dispatcher.registerMessageCallback<request::TranslateResponse>(bind(&ProtoClient::translateResp, this, placeholders::_1, placeholders::_2, placeholders::_3));_dispatcher.registerMessageCallback<request::CalculatorResponse>(bind(&ProtoClient::calculatorResp, this, placeholders::_1, placeholders::_2, placeholders::_3));//注册连接以及消息回调_client.setConnectionCallback(std::bind(&ProtoClient::onConnection, this, placeholders::_1));_client.setMessageCallback(bind(&ProtobufCodec::onMessage, &_codec, placeholders::_1, placeholders::_2, placeholders::_3));}void connect(){_client.connect();_latch.wait();}void translate(){std::cout << "请输入你要翻译的单词:";string word;getline(cin, word);request::TranslateRequest req;req.set_word(word);send(req);}void calculator(){std::cout << "请输入操作数1:";int num1 = 0;cin >> num1;getchar();std::cout << "请输入操作符:";string op;getline(cin, op);std::cout << "请输入操作数2:";int num2 = 0;cin >> num2;getchar();request::CalculatorRequest req;req.set_num1(num1);req.set_op(op);req.set_num2(num2);send(req);}private:void translateResp(const muduo::net::TcpConnectionPtr& conn, const TranslateResponsePtr& req, muduo::Timestamp rt){cout << "结果为:" << req->result() << endl;}void calculatorResp(const muduo::net::TcpConnectionPtr& conn, const CalculatorResponsePtr& req, muduo::Timestamp rt){if (req->code() == false){cout << "发生错误" << endl;}else {cout << "结果为:" << req->result() << endl;}}//多态void send(const google::protobuf::Message& msg){if (_conn->connected())_codec.send(_conn, msg);}void onConnection(const muduo::net::TcpConnectionPtr& conn){if (conn->connected()){cout << "连接成功!!!" << endl;_latch.countDown();_conn = conn;}else {cout << "断开连接" << endl;_conn.reset();}}void onUnknownMessage(const muduo::net::TcpConnectionPtr& conn, const MessagePtr& message, muduo::Timestamp){std::cout << "onUnknownMessage: " << message->GetTypeName() << std::endl;conn->shutdown();}private:muduo::CountDownLatch _latch; //实现同步muduo::net::EventLoopThread _loopthread; //异步循环处理线程muduo::net::TcpConnectionPtr _conn; //客户端与服务器的连接muduo::net::TcpClient _client;    //客户端ProtobufDispatcher _dispatcher; //请求分发器ProtobufCodec _codec; //协议处理器
};void menu()
{cout << "**********************" << endl;cout << "*     1.translate    *" << endl;cout << "*     2.calculator   *" << endl;cout << "*     0.exit         *" << endl;cout << "**********************" << endl;
}int main()
{ProtoClient client("127.0.0.1", 8080);client.connect();int choose = 1;while (choose){menu();cout << "请选择:";cin >> choose;getchar();switch (choose){case 1:client.translate();break;case 2:client.calculator();break;case 0:break;default:cout << "选择失败,请重新选择" << endl;break;}}return 0;
}

注意:

客户端与服务端的编写比较类似,不同的地方在于服务端使用的是EventLoop,而客户端使用的是EventLoopThread,并且由于客户端是异步的,不能保证发送连接请求之后连接就建立成功了,而是等到onConnection函数被调用之后才认为连接建立成功了,所以调用connect()发起连接之后,需要等待,等到onConnection被调用才允许向服务端发送请求。

编写makefile

all:server clientserver:protobuf_server.cc request.pb.cc ../../../..third/muduo-master/examples/protobuf/codec/codec.ccg++ -o $@ $^ -std=c++11 -I../../../../include -L../../../../lib -lmuduo_net -lmuduo_base -lprotobuf -lpthread -lzclient:protobuf_client.cc request.pb.cc ../../../../third/muduo-master/examples/protobuf/codec/codec.ccg++ -o $@ $^ -std=c++11 -I../../../../include -L../../../../lib -lmuduo_net -lmuduo_base -lprotobuf -lpthread -lz.PHONY:clean
clean:rm -rf server client

运行结果

先启动服务器,然后运行客户端

客户端连接成功后会打印一个菜单。

再试一下对应的服务:

如果发生除0错误,直接设置错误码,避免服务器计算结果造成崩溃:

总结

上面就是基于muuduo库实现protobuf协议通信的简单小demo,后面我们的rabbitmq也是基于上面那一套模型,所以上面的内容一定要理解清楚,知道每一句代码是什么意思,有什么作用。

5.3 SQLite3

什么是 SQLite

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 介绍

C/C++ API 是 SQLite3 数据库的一个客户端, 提供一种用 C/C++操作数据库的方法。

下面我们介绍一下常见的几个接口:

SQLite3 官方文档: List Of SQLite Functions

1.查看当前数据库在编译阶段是否启动了线程安全

int sqlite3_threadsafe(); 0-未启用; 1-启用

需要注意的是 sqlite3 是有三种安全等级的:

  • 1. 非线程安全模式
  • 2. 线程安全模式(不同的连接在不同的线程/进程间是安全的,即一个句柄不能用于多线程间)
  • 3. 串行化模式(可以在不同的线程/进程间使用同一个句柄)

2.创建/打开数据库文件,并返回操作句柄

int sqlite3_open(const char *filename, sqlite3 **ppDb) 
//成功返回SQLITE_OK//若在编译阶段启动了线程安全,则在程序运行阶段可以通过参数选择线程安全等级
int sqlite3_open_v2(const char *filename, sqlite3 **ppDb, int flags, const char *zVfs ); 
//成功返回SQLITE_OK

其中flag有以下几个选项:

  • SQLITE_OPEN_READWRITE -- 以可读可写方式打开数据库文件
  • SQLITE_OPEN_CREATE -- 不存在数据库文件则创建
  • SQLITE_OPEN_NOMUTEX--多线程模式,只要不同的线程使用不同的连接即可保证线程安全
  • SQLITE_OPEN_FULLMUTEX--串行化模式

3.执行语句

int sqlite3_exec(sqlite3*, char *sql, int (*callback)(void*,int,char**,char**), void* arg, char **err) 
//返回: SQLITE_OK 表示成功

我们的sql语句可能会进行查询操作,我们需要对查询到的结果进行处理,所以需要用到回调函数callbck,他的各种参数含义如下:int (*callback)(void*,int,char**,char**)。

  • void* : 是设置的在回调时传入的 arg 参数
  • int:一行中数据的列数
  • char**:存储一行数据的字符指针数组(一个回调处理一行数据,如果有多行则会循环调用回调函数)
  • char**:每一列的字段名称

这个回调函数有个 int 返回值,成功处理的情况下必须返回 0,返回非 0会触发 ABORT 退出程序。

4.销毁句柄

int sqlite3_close(sqlite3* db); //成功返回 SQLITE_OK
int sqlite3_close_v2(sqlite3*); //推荐使用--无论如何都会返回SQLITE_OK

5.获取错误信息

const char *sqlite3_errmsg(sqlite3* db);

封装SQLite3操作类

下面我们将这几个接口封装成一个类,快速上手这几个接口。

#include <iostream>
#include <string>
#include <vector>
#include <sqlite3.h>class SqliteHelper
{typedef int (*sqliteCallback)(void*, int, char**, char**);
public:SqliteHelper(const std::string& dbfile): _dbfile(dbfile), _handler(nullptr){}bool open(int flag = SQLITE_OPEN_FULLMUTEX) //默认为串行化{//不存在则创建 | 以可读可写方式int ret = sqlite3_open_v2(_dbfile.c_str(), &_handler, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE  | flag, nullptr);if (ret != SQLITE_OK){std::cout << "创建/打开数据库失败: ";std::cout << sqlite3_errmsg(_handler) << std::endl;return false;}return true;}bool exec(const std::string& sql, sqliteCallback cb, void* arg){int ret = sqlite3_exec(_handler, sql.c_str(), cb, arg, nullptr);if (ret != SQLITE_OK){std::cout << "执行语句失败: ";std::cout << sqlite3_errmsg(_handler) << std::endl;return false;}return true;}void close(){if (_handler)sqlite3_close_v2(_handler);}private:std::string _dbfile;sqlite3* _handler;
};

整体代码都是比较简单的,只要明白了每个函数参数的意思,一个一个往里面填即可。

测试代码

上面我们完成了对sqlite3函数的封装,这里我们对刚刚完成的代码进行测试。

我们先测试一下新增功能:

#include <iostream>
#include "sqlite.hpp"int main()
{//创建库文件SqliteHelper helper("./test.db");helper.open();//1.创建/打开表 (学生信息表:编号, 姓名,年龄)const char* ct = "create table if not exists student(id int primary key, name varchar(32), age int);";helper.exec(ct, nullptr, nullptr);  //创建表不需要回调函数//2.增删改查const char* insert_sql1 = "insert into student values(1, '张三', 18);";const char* insert_sql2 = "insert into student values(2, '李四', 20);";const char* insert_sql3 = "insert into student values(3, '王五', 32);";helper.exec(insert_sql1, nullptr, nullptr);helper.exec(insert_sql2, nullptr, nullptr);helper.exec(insert_sql3, nullptr, nullptr);//3.关闭数据库helper.close();return 0;
}

上面的代码很简单:插入3条语句,我们将程序进行编译后运行。

运行没有报错,还生成了一个test.db,这个就是我们刚刚创建的数据库(sqlite3中一个库对应一个文件)。

我们使用sqlite3命令打开该文件。

查看该库下的表所使用的命令与MySQL不同,MySQL的命令是show tables,而这里是.tables。

现在我们使用select命令就能查询到刚刚插入的数据了。

接下来我们测试一下删除功能。

#include <iostream>
#include "sqlite.hpp"int main()
{//创建库文件SqliteHelper helper("./test.db");helper.open();//1.创建/打开表 (学生信息表:编号, 姓名,年龄)const char* ct = "create table if not exists student(id int primary key, name varchar(32), age int);";helper.exec(ct, nullptr, nullptr);  //创建表不需要回调函数//2.增删改查const char* update_sql = "delete from student where id = 2";helper.exec(update_sql, nullptr, nullptr);//3.关闭数据库helper.close();return 0;
}

运行之后,查看数据库,果然id为2的学生信息被删除了。

每次都要去数据库中查看结果非常麻烦,接下来我们使用代码来查询数据库中的结果。

#include <iostream>
#include "sqlite.hpp"int select_stu_callback(void* arg, int col_count, char** result, char** fields_name)
{//result表示的是某一行的数据for (int i = 0; i < col_count; i++){std::cout << result[i] << " ";}std::cout << std::endl;return 0;  //非常重要,否则会发生错误
}int main()
{//创建库文件SqliteHelper helper("./test.db");helper.open();//1.创建/打开表 (学生信息表:编号, 姓名,年龄)const char* ct = "create table if not exists student(id int primary key, name varchar(32), age int);";helper.exec(ct, nullptr, nullptr);  //创建表不需要回调函数//2.增删改查// const char* insert_sql1 = "insert into student values(1, '张三', 18);";// const char* insert_sql2 = "insert into student values(2, '李四', 20);";// const char* insert_sql3 = "insert into student values(3, '王五', 32);";// helper.exec(insert_sql1, nullptr, nullptr);// helper.exec(insert_sql2, nullptr, nullptr);// helper.exec(insert_sql3, nullptr, nullptr);// const char* update_sql = "delete from student where id = 2";// helper.exec(update_sql, nullptr, nullptr);const char* select_sql = "select * from student";helper.exec(select_sql, select_stu_callback, nullptr);//3.关闭数据库helper.close();return 0;
}

运行结果:

5.4 GTest

GTest 是一个跨平台的 C++单元测试框架,由 google 公司发布。 gtest 是为了在不同平台上为编写 C++单元测试而生成的。它提供了丰富的断言、致命和非致命判断、参数化等等.

GTest 使用

使用分为两个方向:

  • 1.简单的宏断言
  • 2.事件机制(全局,单独用例)

宏断言

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

下面我们做个测试, 使用 TEST 宏及断言,断言age大于18

#include <iostream>
#include <gtest/gtest.h>//test为单元测试名
//greate_than为单元测试下的一个测试用例的名称
TEST(test, greate_than)
{int age = 20;ASSERT_GT(age, 18);printf("OK!\n");
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);RUN_ALL_TESTS();  //运行所有的单元测试return 0;
}

注意:ASSERT_GT一定要放在宏函数中,放在main函数或者其他函数内部会报错。

运行结果:

上面的断言是age大于18,如果修改成小于18会出现什么结果(将上面的ASSERT_GT换成ASSERT_LT)

可以看到在断言失败之后,OK并没有被打印,这就是ASSERT_系列(如果当前点检测失败则退出当前函数),如果我们将其修改成EXPECT系列呢。

#include <iostream>
#include <gtest/gtest.h>//test为单元测试名
//greate_than为单元测试下的一个测试用例的名称
TEST(test, less_than)
{int age = 20;EXPECT_LT(age, 18);printf("OK!\n");
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);RUN_ALL_TESTS();  //运行所有的单元测试return 0;
}

运行结果:

虽然断言失败了,但是没有直接退出函数,而是继续执行打印出了OK。

如果想要测试两个用例,则取不同测试用例名称即可

#include <iostream>
#include <gtest/gtest.h>//test为单元测试名
//greate_than为单元测试下的一个测试用例的名称
TEST(test, less_than)
{int age = 20;EXPECT_LT(age, 18);printf("OK!\n");
}TEST(test, greater_than)
{int age = 20;EXPECT_GT(age, 18);printf("OK!\n");
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);RUN_ALL_TESTS();  //运行所有的单元测试return 0;
}

结果如下:



在学习完断言宏之后,你可能会觉得没有什么用,这个断言宏还没有C语言中的assert好用,因为assert可以在任意位置使用,而断言宏只能在单元测试宏函数中使用。

其实GTest中真正有用的是事件机制。

事件机制

GTest 中的事件机制是指在测试前和测试后提供给用户自行添加操作的机制,而且该机制也可以让同一测试套件下的测试用例共享数据。 GTest 框架中事件的结构层次:

  • 测试程序:一个测试程序只有一个 main 函数,也可以说是一个可执行程序是一个测试程序。该级别的事件机制是在程序的开始和结束执行
  • 测试套件:代表一个测试用例的集合体,该级别的事件机制是在整体的测试案例开始和结束执行(可以理解为一个测试环境,可以在单元测试之前进行测试环境初始化,测试完毕之后进行环境清理。
  • 测试用例:该级别的事件机制是在每个测试用例开始和结束都执行

事件机制的最大好处就是能够为我们各个测试用例提前准备好测试环境,并在测试完毕后用于销毁环境,这样有个好处就是如果我们有一段代码需要进行多种不同方法的测试,则可以通过测试机制在每个测试用例进行之前初始化测试环境和数据,并在测试完毕后清理测试造成的影响。

测试套件分为两类:

  • 1.全局测试套件:在整体的测试中,只会初始化一次环境,在所有测试用例完毕后,才会清理环境。
  • 2.用例测试套件:在每次的单元测试中,都会重新初始化测试环境,完毕后清理环境。

GTest 提供了两种常见的的事件:

  • 全局事件:针对整个测试程序。实现全局的事件机制,需要创建一个自己的类,然后继承 testing::Environment 类,然后分别实现成员函数 SetUp 和 TearDown,同时在 main 函数内进行调用 testing::AddGlobalTestEnvironment(new MyEnvironment);函数添加全局的事件机制。

全局测试套件,其实就是我们用户自己,定义一个全局测试环境类。

#include <iostream>
#include <gtest/gtest.h>
//全局事件:针对整个测试程序,提供全局事件机制,能够在测试之前配置测试环境数据,测试完毕后清理数据//先定义环境类,通过继承 testing::Environment 的派生类来完成
class MyEnvironment : public testing::Environment
{
public:virtual void SetUp() override //重写的虚函数接口 SetUp 会在测试之前被调用{std::cout << "单元测试执行前的环境初始化!" << std::endl;}virtual void  TearDown() override //TearDown 会在测试完毕后调用.{std::cout << "单元测试执行后的环境清理!" << std::endl;}
};TEST(MyEnvironment, test1)
{std::cout << "单元测试1" << std::endl;
}TEST(MyEnvironment, test2)
{std::cout << "单元测试2" << std::endl;
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);testing::AddGlobalTestEnvironment(new MyEnvironment);RUN_ALL_TESTS();return 0;
}

运行结果:

执行顺序如图所示:

其实SetUp函数类似于C++中的构造函数,TearDown类似于析构函数。

全局测试套件的最大好处就是初始化测试环境以及测试完毕之后对测试环境进行清理。



  • TestSuite 事件:针对一个个测试套件。测试套件的事件机制我们同样需要去创建一个类,继承自 testing::Test,实现两个静态函数 SetUpTestCase 和TearDownTestCase,测试套件的事件机制不需要像全局事件机制一样在 main 注册,而是需要将我们平时使用的 TEST 宏改为 TEST_F 宏。
  1. SetUpTestCase() 函数是在测试套件第一个测试用例开始前执行
  2. TearDownTestCase() 函数是在测试套件最后一个测试用例结束后执行
  3. 需要注意 TEST_F 的第一个参数是我们创建的类名,也就是当前测试套件的名称,这样在 TEST_F 宏的测试套件中就可以访问类中的成员了

环境测试类中,可以定义成员变量,成员变量是独立的,是与当前测试套件相关的单元测试才能访问。

模板如下所示:

class HashTestEnv : public testing::Test //不再像全局那样,继承Environment
{
public:static void SetUpTestCase() { } //在所有单元测试前执行,初始化测试环境static void TearDownTestCase() { } //在所有单元测试后执行,清理测试环境
public://成员变量//注意:成员变量必须是公有的。
};//单元测试宏,变成了TEST_F
//注意:测试的命名,必须与套件环境类名一致(为了能直接访问类成员变量)
TEST_F(HashTestEnv, test1)
{}

我们现在根据这个模板来编写一下代码。

#include <iostream>
#include <gtest/gtest.h>
#include <unordered_map>class MyTest : public testing::Test
{
public:static void SetUpTestCase(){std::cout << "所有单元测试前执行,初始化总环境" << std::endl;}static void TearDownTestCase(){std::cout << "所有单元测试完毕后执行,清理总环境" << std::endl;}
public:std::unordered_map<std::string, std::string> _mymap;
};TEST_F(MyTest, insert_test)
{_mymap.insert({"hello", "你好"});_mymap.insert({"bye", "再见"});
}TEST_F(MyTest, size_test)
{ASSERT_EQ(_mymap.size(), 2);
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);RUN_ALL_TESTS();return 0;
}

运行结果:

在insert_test单元测试中明明已经向_mymap中插入了两个数据,为什么在size_test中进行宏断言时却说_mymap的大小为0?

每个单元测试中的成员变量都是独立的,每一次都会重新初始化。

如果我们将这个成员变量设置为全局呢?

运行结果:

现在两个单元测试使用的就是同一个map了,所以在insert_test中插入了两个新数据,在size_test中调用size得到的大小不再是0,而是2了。

如果想将map设置为成员变量,如何完成添加数据

我们需要创建独立测试,我们之前创建的SetUpTestCase和TearDownTestCase都是静态的,他们用于整体环境的初始化和清理,而非静态的SetUp和TearDownTest是针对每一个单元测试进行初始化和清理。

#include <iostream>
#include <gtest/gtest.h>
#include <unordered_map>class MyTest : public testing::Test
{
public:static void SetUpTestCase(){std::cout << "所有单元测试前执行,初始化总环境" << std::endl;}void SetUp() override{std::cout << "单元测试初始化" << std::endl;_mymap.insert({"hello", "你好"});_mymap.insert({"bye", "再见"});}static void TearDownTestCase(){std::cout << "所有单元测试完毕后执行,清理总环境" << std::endl;}void TearDown(){std::cout << "单元测试清理" << std::endl;}
public:std::unordered_map<std::string, std::string> _mymap;
};TEST_F(MyTest, insert_test)
{_mymap.insert({"yes", "是的"});ASSERT_EQ(_mymap.size(), 3);
}TEST_F(MyTest, size_test)
{ASSERT_EQ(_mymap.size(), 2);
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);RUN_ALL_TESTS();return 0;
}

运行结果:

所以SetUpTestCase可以初始化设置为所有单元测试共有的数据,而SetUp可以插入每个单元测试所需的独立的测试数据。

5.5 C++11 异步操作实现线程池

介绍

std::future 是 C++11 标准库中的一个模板类,它表示一个异步操作的结果。当我们在多线程编程中使用异步任务时, std::future 可以帮助我们在需要的时候获取任务的执行结果。 std::future 的一个重要特性是能够阻塞当前线程,直到异步操作完成,从而确保我们在获取结果时不会遇到未完成的操作。

应用场景

  • 异步任务: 当我们需要在后台执行一些耗时操作时,如网络请求或计算密集型任务等, std::future 可以用来表示这些异步任务的结果。通过将任务与主线程分离,我们可以实现任务的并行处理,从而提高程序的执行效率
  • 并发控制: 在多线程编程中,我们可能需要等待某些任务完成后才能继续执行其他操作。通过使用 std::future,我们可以实现线程之间的同步,确保任务完成后再获取结果并继续执行后续操作
  • 结果获取: std::future 提供了一种安全的方式来获取异步任务的结果。我们可以使用 std::future::get()函数来获取任务的结果,此函数会阻塞当前线程,直到异步操作完成。这样,在调用 get()函数时,我们可以确保已经获取到了所需的结果

用法示例

5.5.1 使用 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 内部通过系统等条件自动选择策略

std::launch::deferred演示代码:

#include <iostream>
#include <thread>
#include <future>int Add(int num1, int num2)
{std::cout << "加法" << std::endl;return num1 + num2;
}int main()
{std::cout << "111" << std::endl;std::future<int> result = std::async(std::launch::deferred, Add, 2, 3);std::cout << "222" << std::endl;int sum = result.get();std::cout << "333" << std::endl;std::cout << sum << std::endl;return 0;
}

在上图中,我们创建了一个简单的函数:Add,然后我们使用async进行异步关联,此时Add函数并不会被调用,直到future类型的sum调用了get之后才会被调用。

运行结果:

可以看到打印加法并不在111和222之间,进一步说明了Add函数并不是在async关联时被调用,而是在调用get之后才被调用的。

std::launch::async演示代码:

async模式下会在内部创建工作线程,异步的完成任务。

#include <iostream>
#include <thread>
#include <future>int Add(int num1, int num2)
{std::cout << "加法" << std::endl;return num1 + num2;
}int main()
{std::cout << "111" << std::endl;std::future<int> result = std::async(std::launch::async, Add, 2, 3);std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "222" << std::endl;int sum = result.get();std::cout << "333" << std::endl;std::cout << sum << std::endl;return 0;
}

为了防止主线程走的太快,我们让主线程停止一秒等待异步线程执行完毕。

与前面不同的是,在使用async关联异步后,Add函数就会被调用,所以加法的打印是在111和222之间的,此时的get直接获取刚刚调用的结果,不会再次调用Add函数了。

我们再次修改代码,在Add函数中也添一个sleep。

#include <iostream>
#include <thread>
#include <future>int Add(int num1, int num2)
{std::cout << "加法 111" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(5));std::cout << "加法 222" << std::endl;return num1 + num2;
}int main()
{std::cout << "111" << std::endl;std::future<int> result = std::async(std::launch::async, Add, 2, 3);std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "222" << std::endl;int sum = result.get();std::cout << "333" << std::endl;std::cout << sum << std::endl;return 0;
}

运行结果:

在调用async函数时会创建一个工作线程去执行Add函数,并休眠5秒钟,主线程休眠一秒后会向下执行,直到遇到get函数,但是此时的get还没有获取到结果(工作线程还没执行完),所以主线程会阻塞在get函数等待工作线程执行完毕。



5.5.2 使用 std::promise 和 std::future 配合

std::promise 提供了一种设置值的方式,它可以在设置之后通过相关联的 std::future 对象进行读取。换种说法就是之前说过 std::future 可以读取一个异步函数的返回值了, 但是要等待就绪,而 std::promise 就提供一种方式手动让 std::future 就绪(通过在线程中对promise对象设置数据,其他线程通过future获取设置数据的方式实现获取异步执行结果)。

简单来说其作用是:它用于在一个线程中存储一个值或异常,并在另一个线程中通过 future 来获取这个值或异常。

#include <iostream>
#include <thread>
#include <future>int Add(int num1, int num2, std::promise<int>& prom)
{prom.set_value(num1 + num2);return num1 + num2;
}int main()
{std::promise<int> prom;//通过std::promise的get_future()函数与fu绑定std::future<int> fu = prom.get_future();//将函数Add及对象prom放在线程里面执行std::thread thr(Add, 10, 20, std::ref(prom));int res = fu.get();std::cout << "sum: " << res << std::endl;thr.join();return 0;
}

这段代码,首先创建了一个promise对象和一个future对象,并将两者进行绑定,随后通过thread线程调用Add函数,注意Add函数中需要传递promise对象进去,并且通过set_value函数保存num1+num2的计算结果结果。最后主线程就能通过通过与promise绑定的future对象,获取到结果。



5.5.3 使用 std::packaged_task 和 std::future 配合

std::packaged_task 就是将任务和 std::future 绑定在一起的模板,是一种对任务的封装。我们可以通过 std::packaged_task 对象获取任务相关联的 std::future 对象(通过调用 get_future()方法获得)。 std::packaged_task 的模板参数是函数签名。

可以把 std::future 和 std::async 看成是分开的, 而 std::packaged_task 则是一个整体。

#include <iostream>
#include <thread>
#include <future>int Add(int num1, int num2)
{return num1 + num2;
}int main()
{std::packaged_task<int(int, int)> task(Add);std::future<int> fu = task.get_future();//task可以看作是一个可调用对象来调用执行任务,如task(11, 22);//但是又不能完成当作一个函数来使用//std::async(std::launch::async, task, 11, 22); //error//std::thread(task, 11, 22); //errorint sum = fu.get();std::cout << sum << std::endl;
}

通过packaged_task对Add函数进行二次封装得到task对象,task可以看作是一个可调用对象,通过task(11, 22)就能直接调用Add函数。但是他又不能完全看作是一个函数,例如下面的两种写法都是错误的。

  • std::async(std::launch::async, task, 11, 22); //error
  • std::thread(task, 11, 22); //error

我们更倾向于将task定义成一个指针,传递到线程中,通过解引用的方式执行目标函数。但是单纯的指针,存在生命周期的问题,可能会出现风险,所以我们可以在堆上new对象,并使用智能指针来管理。

#include <iostream>
#include <thread>
#include <memory>
#include <future>int Add(int num1, int num2)
{return num1 + num2;
}int main()
{//std::packaged_task<int(int, int)> task(Add);auto ptask = std::make_shared<std::packaged_task<int(int, int)>>(Add);std::future<int> fu = ptask->get_future();std::thread thr([ptask](){(*ptask)(11, 22);});int sum = fu.get();std::cout << sum << std::endl;thr.join();return 0;
}

5.5.4 实现线程池

基于线程池执行任务的时候,入口函数内部执行逻辑是固定的,因此选择std::packaged_task 加上 std::future 的组合来实现。

1.线程池的工作思想:

用户传入要执行的函数,以及需要处理的数据(函数的参数),由线程池中的工作线程来执行函数完成任务

2.如何实现:

管理的成员:

  • 任务池:用 vector 维护的一个函数任务池子
  • 互斥锁 & 条件变量: 实现同步互斥
  • 一定数量的工作线程:用于不断从任务池取出任务执行任务
  • 结束运行标志:以便于控制线程池的结束。

管理的操作:

  • 入队任务:入队一个函数和参数
  • 停止运行:终止线程池

线程池整体框架如下:

#include <iostream>
#include <functional>
#include <memory>
#include <thread>
#include <future>
#include <vector>using functor = std::function<void()>;class threadpool
{
public:threadpool(int count = 1);~threadpool();//push的参数:F为用户需要执行的函数,args是函数中的参数//push内部,会将这个函数封装成一个异步任务,抛入线程池中,由工作线程取出并执行template<typename F, typename ...Args>auto push(F &&func, Args&& ...args) -> std::future<decltype(func(args...))>; //通过decltype推导出函数运行的返回值类型void stop();
private://线程入口函数——内部不断从任务池中取出任务执行。void entry();private:std::atomic_bool _stop;   //结束标志std::mutex _mutex;   //互斥锁std::condition_variable _cv;   //条件变量std::vector<std::thread> _threads;  //线程池std::vector<functor> _tasks;  //任务池
};
  • 值得注意的是上面的push函数,因为将来我们并不能确定用户传递进来的函数返回值,参数。所以我们要设置成模板+可变参数包。
  • push函数内部还会将运行结果返回给用户,但是不同的函数可能会有不同类型的返回值,我们需要使用auto和decltype来推导出函数结果对应的返回值类型,最终还要加上future,我们期望用户能使用future来获取异步执行的结果。
  • push函数会将用户传递的任务(函数+参数)封装成一个异步任务(package_task)并加入到任务池_tasks当中,线程池会指派一个线程执行该任务,并将执行的结果保存至future对象中,然后返回给用户。

代码如下:

#include <iostream>
#include <functional>
#include <memory>
#include <thread>
#include <future>
#include <vector>using functor = std::function<void()>;class threadpool
{
public:threadpool(int count = 1): _stop(false){for (int i = 0; i < count; i++){_threads.emplace_back(&threadpool::entry, this);}}~threadpool(){stop();}//push的参数:F为用户需要执行的函数,args是函数中的参数//push内部,会将这个函数封装成一个异步任务,抛入线程池中,由工作线程取出并执行template<typename F, typename ...Args>auto push(F &&func, Args&& ...args) -> std::future<decltype(func(args...))> //通过decltype推导出函数运行的返回值类型{using return_type = decltype(func(args...));//1.将传入的函数封装成一个packaged_task任务auto fun = std::bind(std::forward<F>(func), std::forward<Args>(args)...);auto task = std::make_shared<std::packaged_task<return_type()>>(fun);std::future<return_type> fu = task->get_future();//2.构造一个lambda匿名对象(捕获任务对象){std::unique_lock<std::mutex> lock(_mutex);_tasks.push_back([task](){(*task)();});//3.将构造出来的匿名对象,抛入任务池中(线程池会指派一个线程去执行该线程)_cv.notify_one();}return fu;}void stop(){if (_stop == false){_stop = true;_cv.notify_all(); //唤醒所有线程for (auto& thread : _threads){thread.join();}}}
private://线程入口函数——内部不断从任务池中取出任务执行。void entry(){while (_stop == false) {std::vector<functor> tmp_tasks;{//加锁:等待任务池不为空,或者_stop被设置为truestd::unique_lock<std::mutex> lock(_mutex);_cv.wait(lock, [&](){return _stop || !_tasks.empty();});tmp_tasks.swap(_tasks); //将任务队列中所有的任务取出放到临时任务池中}//取出任务执行for (auto& task : tmp_tasks)task();}}private:std::atomic_bool _stop;   //结束标志std::mutex _mutex;   //互斥锁std::condition_variable _cv;   //条件变量std::vector<std::thread> _threads;  //线程池std::vector<functor> _tasks;  //任务池
};
  • threadpool构造函数:用户传入一个count表示线程池预先创建的线程个数,_threads内部添加count个线程,每个线程都去执行entry函数。
  • push函数:用户传入一个函数以及函数对应的参数,在push函数内部会将函数以及参数封装成fun,再使用packaged_task和future对fun进行绑定。然后加锁,向任务池中添加任务,任务的内容就是执行task对应的函数,添加完毕之后唤醒一个线程去执行,最后将future返回给用户,用户能通过get获取运行结果。
  • stop函数:唤醒所有的线程,并且等待所有的线程。
  • entry函数:线程入口函数,是一个while循环,结束条件就是stop被设置为ture(即调用了stop),每个线程都会调用该函数,并被锁阻塞住,当获取到锁之后,会继续判断如果任务池不为空,就停止等待了,将tmp_tasks与_tasks进行交换,获取任务池中所有的线程,依次执行。

编写测试代码:

int Add(int num1, int num2)
{return num1 + num2;
}int main()
{threadpool pool(5);for (int i = 0; i < 10; i++){int rand1 = rand() % 100;int rand2 = rand() % 100;std::future<int> fu = pool.push(Add, rand1, rand2);std::cout << rand1 << "+" << rand2 << "=" << fu.get() << std::endl;sleep(1);}pool.stop();return 0;
}

运行结果:

6.需求分析

6.1 核心概念

我们以前学习过生产者消费者模型,而我们要实现的消息队列就是生产者消费者模型,只不过生产者不再是同一主机上的进程了,而是一个客户端。


其中, 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): 传递的内容

那么如何理解交换机和队列之间的关系呢?

消费队列服务器中会存在很多的队列,假如说这是一个新闻发布系统的消息队列,那么会存在音乐新闻队列,体育新闻队列,花边新闻队列等。而音乐新闻又可以分成歌曲新闻和歌手新闻。

当消息发布客户端想发布一条消息到消息队列服务器中时,需要把消息加入到对应的队列当中去,如果发布的是一个歌曲新闻,那么这个消息不能只加入到歌曲队列当中,还要加入到音乐队列中,也就是说生产者和队列之间不是单对单的关系,而是多对多的。

所以我们使用交换机来帮我们维护生成者与队列之间的关系,在生产者与消费者之间添加一个交换机,并且交换机会绑定很多的队列。

生产者会将消息先发送给交换机,交换机根据匹配规则,将消息转发到不同的队列中去。例如,生产者发布了一个歌曲新闻,交换机会将该新闻转发到音乐新闻队列和歌曲新闻队列中。

总结:所谓的 Exchange 和 Queue 可以理解成 "多对多" 关系, 和数据库中的 "多对多" 一样. 意思是:一个 Exchange 可以绑定多个 Queue (可以向多个 Queue 中转发消息)一个 Queue 也可以被多个 Exchange 绑定 (一个 Queue 中的消息可以来自于多个Exchange)

注意:上述数据结构, 既需要在内存中存储, 也需要在硬盘中存储

  • 内存存储: 方便使用
  • 硬盘存储: 重启数据不丢失

6.2 核心API

对于 Broker 来说, 要实现以下核心 API,通过这些 API 来实现消息队列的基本功能

  • 1. 创建交换机 (exchangeDeclare)
  • 2. 销毁交换机 (exchangeDelete)
  • 3. 创建队列 (queueDeclare)
  • 4. 销毁队列 (queueDelete)
  • 5. 创建绑定 (queueBind)
  • 6. 解除绑定 (queueUnbind)
  • 7. 发布消息 (basicPublish)
  • 8. 订阅消息 (basicConsume)
  • 9. 确认消息 (basicAck)
  • 10. 取消订阅 (basicCancel)

另一方面, Producer 和 Consumer 则通过网络的方式, 远程调用这些 API, 实现生产者消费者模型

关于 VirtualHost(虚拟机):对于 RabbitMQ 来说, VirtualHost 也是可以随意创建删除的此处咱们暂时不做这部分功能(实现起来也比较简单, 咱们的代码中会完成部分和虚拟主机相关的结构设计)

6.3 交换机类型

对于 RabbitMQ 来说, 主要支持四种交换机类型(交换机决定了一个消息应该放到哪个队列):

  • Direct: 生产者发送消息时, 直接指定被该交换机绑定的队列名
  • Fanout: 生产者发送的消息会被复制到该交换机的所有队列中
  • Topic: 绑定队列到交换机上时, 指定一个字符串为 bindingKey。发送消息指定一个字符串为 routingKey。当 routingKey 和 bindingKey 满足一定的匹配条件的时候, 则把消息投递到指定队列
  • Header

其中 Header 这种方式比较复杂, 比较少见。常用的是前三种交换机类型,项目中也主要实现这三种

这三种操作就像给 qq 群发红包

  • Direct 是发一个专属红包, 只有指定的人能领
  • Fanout 是使用了魔法, 发一个 10 块钱红包, 群里的每个人都能领 10 块钱
  • Topic 是发一个画图红包, 发 10 块钱红包, 同时出个题, 得画的像的人, 才能领.也是每个领到的人都能领 10 块钱

6.4 持久化

Exchange, Queue, Binding, Message 等数据都有持久化需求

当程序重启 / 主机重启, 保证上述内容不丢失。

6.5 网络通信

生产者和消费者都是客户端程序, Broker 则是作为服务器,通过网络进行通信。在网络通信的过程中, 客户端部分要提供对应的 api, 来实现对服务器的操作。

  • 1. 创建 Connection
  • 2. 关闭 Connection
  • 3. 创建 Channel
  • 4. 关闭 Channel
  • 5. 创建队列 (queueDeclare)
  • 6. 销毁队列 (queueDelete)
  • 7. 创建交换机 (exchangeDeclare)
  • 8. 销毁交换机 (exchangeDelete)
  • 9. 创建绑定 (queueBind)
  • 10. 解除绑定 (queueUnbind)
  • 11. 发布消息 (basicPublish)
  • 12. 订阅消息 (basicConsume)
  • 13. 确认消息 (basicAck)
  • 14. 取消订阅(basicCancel)

可以看到, 在 Broker 的基础上, 客户端还要增加 Connection 操作和 Channel 操作

  • Connection 对应一个 TCP 连接
  • Channel 则是 Connection 中的逻辑通道

一个 Connection 中可以包含多个 Channel。 Channel 和 Channel 之间的数据是独立的,不会相互干扰。这样做主要是为了能够更好的复用 TCP 连接, 达到长连接的效果,避免频繁的创建关闭 TCP 连接。

Connection 可以理解成一根网线. Channel 则是网线里具体的线缆

6.6 消息应答

被消费的消息, 需要进行应答。应答模式分成两种:

  • 自动应答: 消费者只要消费了消息, 就算应答完毕了, Broker 直接删除这个消息
  • 手动应答: 消费者手动调用应答接口, Broker 收到应答请求之后, 才真正删除这个消息

手动应答的目的, 是为了保证消息确实被消费者处理成功了. 在一些对于数据可靠性要求高的场景, 比较常见

7.模块划分

7.1 服务端模块

7.1.1 持久化数据管理中心模块

在数据管理模块中管理交换机,队列,队列绑定,消息等部分数据数据。

7.1.1.1 交换机数据管理

1.要管理的数据:描述了一个交换机应该有什么数据

  • 1)交换机名称:唯一标识
  • 2)交换机类型:决定了消息的转发方式
    • 每个队列绑定中有个bingding_key,每条消息中有个routing_key
    • 1.直接交换:binding_key与routing_key相同,则将消息放入队列
    • 2.广播交换:将消息放入交换机绑定的所有队列中
    • 3.主题交换:routing_key与多个绑定队列的binding_key有个匹配规则,匹配成功则放入
  • 3)持久化标志:决定了当前交换机信息是否需要持久化存储
  • 4)删除标志:指的是关联了当前交换机的所有客户端都退出了,是否要自动删除交换机。
  • 5)其他参数:(暂未使用,方便扩展)

2.对交换机的管理操作:

  • 1)创建交换机:这里的创建本质上是声明的意思(有则OK,没有则创建)
  • 2)删除交换机:注意——每个交换机都会绑定一个或多个队列(意味着会有一个或多个绑定信息),因此删除交换机需要删除相关的绑定信息
  • 3)获取指定名称交换机。
  • 4)获取当前交换机数量

7.1.1.2 队列数据管理

1.要管理的数据:

  • 1)队列名称:唯一的标识
  • 2)持久化存储标志:决定了是否将队列信息持久化存储起来,决定了重启后这个队列是否存在。
  • 3)是否独占标志:独占指的是,只有当前客户端自己能够订阅队列消息,其他客户端无法订阅该队列的消息。
  • 4)自动删除标志:当订阅了当前队列的所有客户端都退出后,是否删除该队列。
  • 5)其他参数:(暂未使用,方便扩展)

2.提供的管理操作:

  • 1)创建队列
  • 2)删除队列
  • 3)获取指定队列信息
  • 4)获取队列数量
  • 5)获取所有队列名称:当系统重启后,需要重新加载数据和历史消息(消息以队列为单元存储在文件中),而加载消息需要知道队列名称,因为在存储消息时,存储文件以队列名称进行命名。

注意:如果一个队列的持久化标志为false,意味着重启后,队列就没了,也没有客户端能够订阅队列的消息,因此这个队列里的消息如果持久化存储了,是没有意义的,因此通常一个队列的持久化标志是false,那么它的消息也就不需要持久化。

7.1.1.3 绑定数据管理

队列绑定用于描述哪个队列和哪个交换机绑定到了一起。

1.管理的数据:

  • 1)交换机名称
  • 2)队列名称
  • 3)binding_key:绑定密钥——描述了交换机的主题交换和直接交换的消息发布匹配规则,由数字,字符,_#. 组成,例如(binding_key: news.music.#)

2.管理的操作:

  • 1)添加绑定
  • 2)解除绑定
  • 3)获取交换机相关的所有绑定信息:
    • 1.删除交换机的时候,删除相关的绑定信息
    • 2.当消息发布到交换机,交换机通过绑定信息将消息发布到指定队列
  • 4)获取队列相关的所有绑定信息:删除队列的时候,要删除相关的绑定信息
  • 5)获取绑定信息数量

7.1.1.4 消息数据管理

1.管理的数据

  • 1)ID:消息的唯一标识
  • 2)持久化标志:表示是否对消息进行持久化(还要取决于队列的持久化标志)
  • 3)routing_key:决定了当前消息要发布的队列(消息发布到交换机后,根据绑定队列的binding_key决定是否发布到指定队列)
  • 4)消息主体:消息内容

————以下是服务端为了管理所添加的信息————

  • 5)存储偏移量:消息以队列为单元存储在文件中,这个偏移量是当前消息相较于文件起始位置的偏移量
  • 6)消息长度:从偏移量位置取出指定长度的消息(解决粘包问题)
  • 7)是否有效标志:标识当前消息是否已被删除(当一个文件中,有效消息占据总消息比例不到50%,且数据量超过2000,则进行垃圾回收,重新整理文件数据存储。当系统重启,也只需要重新加载有效消息)

2.消息的管理

管理方式:以队列为单元进行管理(消息的所有操作都是以队列为单元的)

管理数据:

  • 1)消息链表:保存所有的待推送消息
  • 2)待确认消息(hash):消息推送给客户端后,会等待客户端进行消息确认,收到确认后,才会真正删除消息
  • 3)持久化消息(hash):假设消息都会进行持久化存储,操作过程中会存在垃圾回收操作,但是垃圾回收会改变消息的存储位置,因此每次垃圾回收后,都需要用新的位置,去更新持久化消息的信息。
  • 4)持久化的有效消息数量
  • 5)持久化的总的消息数量:决定了什么时候进行垃圾回收

管理操作:

  • 1)向队列新增消息
  • 2)获取队首消息:获取消息后,就会将消息从待推送消息链表删除(不再是待发送消息,而是待确认消息)
  • 3)确认消息:从待确认消息中移除消息,并进行持久化数据的删除
  • 4)恢复队列历史消息:主要是在构造函数中进行(只有在重启时才会进行)
  • 5)垃圾回收(消息持久化子模块完成):持久化文件中有效消息比例小于50%,且总消息数量超过2000进行垃圾回收
  • 6)删除队列相关消息文件:当一个队列被删除了,则消息没有存在的意义了。

上面两个是内部管理单元,而下面是向外(客户端)提供的。

队列消息管理:

  • 1)初始化队列消息结构
  • 2)移除队列消息结构
  • 3)向队列新增消息
  • 4)对队列消息进行确认
  • 5)恢复队列历史消息

7.1.2 虚拟机管理模块

虚拟机其实就是交换机+队列+绑定+消息的整体逻辑单元,因此虚拟机管理其实就是将上面四个模块的合并管理。

1.要管理的数据:

  • 1)交换机数据管理句柄
  • 2)队列数据管理句柄
  • 3)绑定信息数据管理句柄
  • 4)消息数据管理句柄

2.要管理的操作:

  • 1)声明/删除交换机(注意:在删除交换机时需要删除相关的绑定信息)
  • 2)声明/删除队列(注意:在删除队列时,要删除相关的绑定信息以及消息数据)
  • 3)队列的绑定/解除绑定(注意:绑定的时候,交换机和队列必须是存在的)
  • 4)获取指定队列的消息
  • 5)对指定队列的指定消息进行确认
  • 6)获取交换机相关的所有绑定信息(一条消息要发布给指定交换机时,交换机要获取所有的绑定信息,来确定消息要发布到哪个队列)

7.1.3 交换路由模块

消息的发布是将一条新消息发布到交换机上,由交换机决定放入哪些队列,而决定交给哪个队列,其中交换机类型起了很大作用(直接交换,广播交换,主题交换)

直接交换和广播交换思想较为简单,而主题交换涉及到了一个规则匹配的流程,交换路由模块就是专门做匹配过程的

  • 每个队列和交换机的绑定信息中,都有一个binding_key:这是队列发布的匹配规则(binding_key只能由数字、字母、_. #*构成,其中 #* 是通配符,用于匹配0个或多个单词,用于匹配一个单词) 
  • 每条要发布的消息中,都有一个routing_key:是消息的发布规则(routing_key只能由数字、字母、_. 构成)

交换机的三种交换模式:

  1. 广播:直接将消息发布给交换机的所有绑定队列
  2. 直接:routing_key与binding_key完全一致则匹配成功
  3. 主题:binding_key是匹配规则,routing_key是消息规则(例如binding_key是news.music.#,routing_key是news.music.pop,则两者可以匹配成功)

路由匹配模块没有要管理的数据,只有向外提供的路由匹配操作(提供判断routing_key与binding_key是否能够匹配成功的接口)

7.1.4 消费者管理模块

消费者指的是订阅了一个队列消息的客户端,一旦这个队列有了消息就会推送给这个客户端。

在核心API中有个订阅消息的服务(这里的订阅并不是订阅某条消息,而是订阅了某个队列的消息),当前主要实现了消息推送功能,因此一旦有了消息就要能找到消费者相关信息(消费者对应的信道)。

1.消费者信息:

  • 1)消费者标识:区分不同的消费者
  • 2)订阅队列名称:当前队列有消息就会推送给这个客户端,以及当客户端收到消息后,需要对指定队列的消息进行确认
  • 3)自动确认标志:
    • 自动确认——推送消息后,直接删除消息不需要额外确认
    • 手动确认——需要等到收到确认回复后再去删除
  • 4)消费处理回调函数指针:队列收到一条消息后,通过回调函数指针进行处理(该函数内部逻辑固定——向指定客户端推送消息)

2.消费者管理:

管理思想:以队列为单元进行管理(每个消费者订阅的都是指定队列的消息,消费者对消息进行确认也是以队列进行确认,更关键的是:当指定队列中有消息了,必然是向订阅了这个队列的消费者进行消息推送)。

消费者管理结构:

数据信息:消费者链表——保存当前队列的所有消费者信息(RR轮转,每次取出下一个消费者进行消息推送)

(注意:一条消息在推送时,并不会发送给所有订阅该队列的客户端,而是只需要其中一个客户端发布即可,所以消费者信息采用链表存储,每次发布信息时从链表中依次选取消费者,可以达到负载均衡的效果)

管理操作:

  • 1)向指定队列新增消费者
  • 2)RR轮转获取指定队列消费者
  • 3)删除指定队列消费者
  • 4)获取指定队列消费者数量
  • 5)判断指定队列中消费者数量是否为空
  • 6)初始化队列消费者结构
  • 7)删除队列消费者结构

7.1.5 信道管理模块

网络通信时必然都是通过网络通信连接来完成,为了能够更加充分的利用资源,因此对通信连接又进行了进一步的细化,细化出了通信信道。

一个通信连接可以创建多个通信通道,每个通信信道之间,在用户眼中是相互独立的,而本质的底层它们使用的是同一个通信连接进行网络通信,一旦某个客户端要关闭通信,关闭的不是连接,而是自己对应的通信通道,关闭信道我们就需要将客户端的订阅给取消。

信道是用户眼中的一个通信通道,所以所有的网络通信服务都是由信道提供的。

信道提供的服务操作:

  • 1)声明 / 删除交换机
  • 2)声明 / 删除队列
  • 3)绑定 / 解绑定队列与交换机
  • 4)发布消息 / 队列消息确认
  • 5)订阅队列消息 /  取消队列订阅

信道要管理的数据:

  • 1)信道ID:信道的唯一标识
  • 2)信道关联的虚拟机句柄
  • 3)信道关联的消费者句柄:当信道关闭时,所有关联的消费者订阅都要取消,相当于删除所有的相关消费者。
  • 4)工作线程池句柄:信道进行消息发布到指定队列操作后,从指定队列中获取一个消费者,对这条消息进行消费(也就是将消息推送给客户端的操作交给线程池执行)。

注意:并非每个信道都有一个线程池,而是整个服务器就一个线程池,所有的信道都是通过同一个线程池进行异步操作。

信道的管理:

  • 1)创建一个信道
  • 2)关闭一个信道
  • 3)获取指定信道句柄

7.1.6 连接管理模块

就是一个网络通信对应的连接,因此当一个连接要关闭时,就应该把连接关联的信道全部关闭。

  • 在网络通信模块中,我们使用muduo库来实现底层通信,muduo库中本身就有Connection连接的概念和对象类。但是在我们的连接中,还有一个上层通信信道的概念,这个概念在muduo库中是没有的。
  • 因此我们需要在用户层面,对这个muduo库中的Connection连接进行二次封装,形成我们自己所需的连接管理。

管理数据:

  • 1)muduo库的通信连接
  • 2)当前连接关联的通信管理句柄

连接提供的操作:

  • 1)创建信道
  • 2)关闭信道

管理的操作:

  • 1)新增连接
  • 2)关闭连接
  • 3)获取指定连接信息

7.1.7 Broker 服务器模块

整合以上所有模块,并搭建网络通信服务器,实现与客户端网络通信,能够识别客户端请求,并提供客户端请求的处理服务。

管理信息:

  • 1)虚拟机管理模块句柄
  • 2)消费者管理模块句柄
  • 3)连接管理模块句柄
  • 4)工作线程池句柄
  • 5)muduo 库通信所需元素

注意:

  • 1)一个服务器有一个工作线程池,其他所有的信道操作都是同一个线程池
  • 2)一个服务器有一个虚拟机,其他所有交换机,队列,消息的操作都是针对这个虚拟机进行的

7.2 客户端模块

7.2.1 消费者管理 

当订阅客户端订阅一个队列消息时,就相当于创建了一个消费者。

管理的数据:

  • 1)消费者标识
  • 2)订阅的队列名称
  • 3)自动确认标志
  • 4)消息回调处理函数指针

当当前消费者订阅了一个队列的消息,这个队列有了消息之后,就会将这个消息推送给客户端,这时候收到了消息则使用消息回调函数进行处理,处理完毕后根据确认标志决定是否进行消息确认。

管理的操作:

  • 1)新增消费者
  • 2)删除消费者
  • 3)查找消费者

7.2.2 信道管理模块

所有提供的操作与服务端类似,因为客户端给用户提供什么服务,服务器就要给客户端提供什么活动。 

管理信息:

  • 1)信道ID
  • 2)消费者管理句柄:每个信道都有自己相关的消费者
  • 3)线程池句柄:对推送过来的消息进行回调处理,处理过程通过工作线程来进行
  • 4)信道关联的连接

信道提供的服务:

  • 1)声明 / 删除交换机
  • 2)声明 / 删除队列
  • 3)绑定 / 解绑定队列与交换机
  • 4)发布消息 / 队列消息确认
  • 5)订阅队列消息 /  取消队列订阅
  • 6)创建 / 关闭信道

信道的管理:

  • 1)新增一个信道
  • 2)删除一个信道
  • 3)查找指定ID的信道

7.2.3 连接管理模块

所有的服务都是通过信道来完成的(而不是连接),信道在用户的角度就是一个通信通道,因此所有的请求都是通过信道来完成的,连接的管理就包含了客户端资源的整合。

面对用户,不需要有客户端的概念,连接对于用户来说就是客户端,通过连接创建信道,通过信道完成所需服务。

管理操作:

  • 1)连接服务器
  • 2)创建信道
  • 3)关闭信道
  • 4)关闭连接

管理的资源:

  • 1)工作线程池
  • 2)连接关联的信道管理句柄

7.2.4 异步工作池模块

muduo库中的TcpClient只有通信连接,没有事件监控,它不知道什么时候有消息到来,所以TcpClient模块需要一个EventLoopThread模块进行IO事件监控。

当收到推送消息后,需要对推送的消息进行处理,因此需要一个线程池来帮我们完成消息处理的过程。

注意:所有的连接用的都是一个EventLoopThread进行IO事件监控,所有的推送消息处理也只需要有一个线程池就够了,并不需要每个连接都有一个EventLoop,也不需要每个信道的消息处理都有自己的线程池。

7.3 项目模块关系图

8. 服务端核心模块实现

8.1 项目目录结构

Linux 机器上创建 mq 项目, 并且规划开发目录, 使用 Makefile 组织项目。

root@hcss-ecs-918d:~/work/rabbitmq# tree rabbitmq/
rabbitmq/
|-- demo
|-- mqclient
|-- mqcommon
|-- mqserver
|-- mqtest
|-- third
  • demo:编写一些功能用例时所在的目录(前面第三方库介绍时所写代码都放在该目录下)
  • mqcommon: 公共模块代码(线程池,数据库访问,文件访问,日志打印, pb 相关,以及其他的一些琐碎功能模块代码)
  • mqclient: 客户端模块代码
  • mqserver: 服务器模块代码
  • mqtest: 单元测试
  • third: 用到的第三方库存放目录

在创建完目录结构后,我们需要将muduo库中的include和lib移动到third目录下。

良好的目录结构可以帮助我们更好的查找代码,项目整体更加清晰明了。

8.2 工具类模块实现

在项目开始前,我们需要实现一些工具模块,有了这些工具,可以更好的帮我们实现整个项目,正所谓磨刀不误砍柴工,接下来让我们一起实现吧。

8.2.1 日志打印工具

为了便于编写项目中能够快速定位程序的错误位置,因此编写一个日志打印类,进行简单的日志打印。

目标:封装一个日志宏,通过日志宏进行日志打印,在打印的信息中带有系统时间以及文件名和行号。

1.文件名和行号可以通过下面两个宏来实现

  • __FILE__:显示文件名
  • __LINE__:显示执行代码所在行的行号。

2.显示当前时间可以通过strftime函数实现

其中tm是当前时间的结构体,format是以什么格式来转换,s是保存转换完后的字符串。

tm结构体内容如下所示。

而调用localtime函数可以帮我们生成一个tm结构体

3.代码演示:

#include <iostream>
#include <ctime>int main()
{time_t t = time(nullptr);struct tm* ptm = localtime(&t);char time_str[32];strftime(time_str, 31, "%Y-%m-%d %H:%M:%S", ptm);printf("[%s][%s:%d] Hello World!\n", time_str, __FILE__, __LINE__);return 0;
}

执行结果:

接下来我们需要将上面的代码封装成宏。

#include <iostream>
#include <ctime>#define DBG_LEVEL 0
#define INF_LEVEL 1
#define ERR_LEVEL 2
#define DEFAULT_LEVEL DBG_LEVEL //默认日志等级,控制哪些日志能输出,哪些不能
#define LOG(level, format, ...) {\if (level >= DEFAULT_LEVEL) {\time_t t = time(nullptr);\struct tm* ptm = localtime(&t);\char time_str[32];\strftime(time_str, 31, "%Y-%m-%d %H:%M:%S", ptm);\printf("[%s][%s:%d]" format "\n", time_str, __FILE__, __LINE__, ##__VA_ARGS__);\}\
} int main()
{LOG(DBG_LEVEL, "Hello %s--%d", "World", 5);return 0;
}

注意:

  • 宏必须写在同一行,所以宏函数的每一行最后都需要添加 \ 进行转义
  • LOG宏函数的第三个参数是不定参,需要使用__VA_ARGS__表示
  • 如果format是单纯的字符串,没有不定参,这种情况下会报错,解决方法就是在__VA_ARGS__宏前面添加两个#。

我们还可以给每个等级额外添加一个宏函数,并且在日志中打印出日志等级。

#include <iostream>
#include <unordered_map>
#include <ctime>#define DBG_LEVEL 0
#define INF_LEVEL 1
#define ERR_LEVEL 2
#define DEFAULT_LEVEL DBG_LEVEL //默认日志等级,控制哪些日志能输出,哪些不能std::unordered_map<int, std::string> hash_level = {{DBG_LEVEL, "Debug"}, {INF_LEVEL, "Info "},{ERR_LEVEL, "Error"}
};#define LOG(level, format, ...) {\if (level >= DEFAULT_LEVEL) {\time_t t = time(nullptr);\struct tm* ptm = localtime(&t);\char time_str[32];\strftime(time_str, 31, "%Y-%m-%d %H:%M:%S", ptm);\printf("[%s][%s][%s:%d]   " format "\n", bitmq::hash_level[level].c_str(), time_str, __FILE__, __LINE__, ##__VA_ARGS__);\}\
} #define DLOG(format, ...) LOG(DBG_LEVEL, format, ##__VA_ARGS__);
#define ILOG(format, ...) LOG(INF_LEVEL, format, ##__VA_ARGS__);
#define ELOG(format, ...) LOG(ERR_LEVEL, format, ##__VA_ARGS__);int main()
{LOG(ERR_LEVEL, "Hello %s--%d", "World", 5);DLOG("Hello %s--%d", "World", 5);ILOG("Hello %s--%d", "World", 5);ELOG("Hello %s--%d", "World", 5);return 0;
}

执行结果:

测试完毕后,代码没有问题,我们可以将宏函数放到mqcommon文件夹下。

8.2.2 使用Helper工具

Helper 工具类中要完成的是项目中需要的一些辅助零碎的功能代码实现,其中包括文件的基础操作,字符串的额外操作等在项目中用到的零碎功能。

8.2.2.1 sqlite基础操作类

sqlite类在认识第三方库章节已经实现过了,所以我们直接将之前的代码复制回来即可,但是需要注意的是,我们之前打印错误信息使用的是cout,而如今我们已经实现了自己的日志打印,所以我们使用自己写的LOG宏函数来完成日志打印工作。

#include <iostream>
#include <string>
#include <vector>
#include <sqlite3.h>
#include "logger.hpp"namespace cola
{
class SqliteHelper
{typedef int (*sqliteCallback)(void*, int, char**, char**);
public:SqliteHelper(const std::string& dbfile): _dbfile(dbfile), _handler(nullptr){}bool open(int flag = SQLITE_OPEN_FULLMUTEX) //默认为串行化{//不存在则创建 | 以可读可写方式int ret = sqlite3_open_v2(_dbfile.c_str(), &_handler, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE  | flag, nullptr);if (ret != SQLITE_OK){ELOG("创建/打开sqlite数据库失败: %s", sqlite3_errmsg(_handler));return false;}return true;}bool exec(const std::string& sql, sqliteCallback cb, void* arg){int ret = sqlite3_exec(_handler, sql.c_str(), cb, arg, nullptr);if (ret != SQLITE_OK){ELOG("%s 执行语句失败: %s",sql.c_str(), sqlite3_errmsg(_handler));return false;}return true;}void close(){if (_handler)sqlite3_close_v2(_handler);}private:std::string _dbfile;sqlite3* _handler;
};
}

8.2.2.2 字符串操作类

在这个类我们需要实现字符串分割功能,给定一个str,以sep为分隔符,将str进行分割分割后的结果放入res数组当中。

#include <iostream>
#include <vector>class StringHelper
{
public:static size_t splite(const std::string& str, const std::string& sep, std::vector<std::string>& res){int index = 0, pos = 0;while (index < str.size()){pos = str.find(sep, index);if (pos == std::string::npos){res.push_back(str.substr(index));break;}if (index != pos){res.push_back(str.substr(index, pos - index));}index = pos + sep.size();}return res.size();}
};int main()
{std::string str = "...news....music...#.pop...";std::vector<std::string> array;StringHelper::splite(str, ".", array);for (auto& str : array){std::cout << str << std::endl;}return 0;
}

运行结果:

8.2.2.3 UUID 生成器类

UUID(Universally Unique Identifier), 也叫通用唯一识别码,通常由 32 位 16 进制数字字符组成。

UUID 的标准型式包含 32 个 16 进制数字字符,以连字号分为五段,形式为 8-4-4-4-12的 32 个字符,如: 550e8400-e29b-41d4-a716-446655440000。

uuid 生成思想:

  • 1.生成8个0~255之间的随机数
  • 2.取出一个8字节的序号

通过以上数据,组成16字节的数据,转换成32个16进制字符。

我们生成随机数的方法是通过一个机器随机数作为生成伪随机数的种子。 (不能直接使用机器随机数来生成,因为机器随机数效率较低)

  • 生成机器随机数可使用std::random_device
  • 伪随机数生成使用std::mt19937_64
#include <iostream>
#include <random>int main()
{std::random_device rd;std::mt19937_64 gernator(rd());//限制数字区间std::uniform_int_distribution<int> distribution(0, 255);std::cout << distribution(gernator) << std::endl;return 0;
}

接下来我们需要将十进制转换成16进制

int main()
{std::random_device rd;std::mt19937_64 gernator(rd());//限制数字区间std::uniform_int_distribution<int> distribution(0, 255);auto randNum = distribution(gernator);std::cout << randNum << "  ->  ";//将生成的数字转换为16进制数字字符std::stringstream ss;ss << std::setw(2) << std::setfill('0') << std::hex << randNum;std::cout << ss.str() << std::endl;return 0;
}

可以通过std::hex,但是如果随机数是小于16,则需要在前面补0,所以我们使用setw限制位数为2位,使用setfill填充0。

现在我们可以直接生成8个随机数,并转化成16进制。

int main()
{std::random_device rd;std::mt19937_64 gernator(rd());//限制数字区间std::uniform_int_distribution<int> distribution(0, 255);//将生成的数字转换为16进制数字字符std::stringstream ss;for (int i = 0; i < 8; i++){ss << std::setw(2) << std::setfill('0') << std::hex << distribution(gernator);if (i == 3 || i == 5 || i == 7)ss << "-";}std::cout << ss.str() << std::endl;return 0;
}

不要忘记uuid的格式(8-4-4-4-12),需要在对应位置添加 '-'。

现在已经解决了第一步了,唯一性已经比较强了,接下来我们还要给uuid添加编号,初始值为1,每次创建都会增加编号值,所以编号的类型必须为static。

int main()
{std::random_device rd;std::mt19937_64 gernator(rd());//限制数字区间std::uniform_int_distribution<int> distribution(0, 255);//将生成的数字转换为16进制数字字符std::stringstream ss;for (int i = 0; i < 8; i++){ss << std::setw(2) << std::setfill('0') << std::hex << distribution(gernator);if (i == 3 || i == 5 || i == 7)ss << "-";}//定义一个原子类型整形,初始值为1static std::atomic<size_t> seq(1);size_t num = seq.fetch_add(1);for (int i = 7; i >= 0; i--){ss << std::setw(2) << std::setfill('0') << std::hex << ((num >> (i * 8)) & 0xff);if (i == 6)ss << "-";}std::cout << ss.str() << std::endl;return 0;
}

到这里,uuid的生成就完成了,接下来我们需要对其进行封装。

class UUIDHelper
{
public:static std::string uuid(){std::random_device rd;std::mt19937_64 gernator(rd());//限制数字区间std::uniform_int_distribution<int> distribution(0, 255);//将生成的数字转换为16进制数字字符std::stringstream ss;for (int i = 0; i < 8; i++){ss << std::setw(2) << std::setfill('0') << std::hex << distribution(gernator);if (i == 3 || i == 5 || i == 7)ss << "-";}//定义一个原子类型整形,初始值为1static std::atomic<size_t> seq(1);size_t num = seq.fetch_add(1);for (int i = 7; i >= 0; i--){ss << std::setw(2) << std::setfill('0') << std::hex << ((num >> (i * 8)) & 0xff);if (i == 6)ss << "-";}return ss.str();}
};

8.2.2.4 文件基础操作

这个类中我们需要实现以下功能

  • 文件是否存在判断
  • 文件大小获取
  • 文件读/写
  • 文件创建/删除
  • 目录创建/删除

文件操作类模板:

class FileHelper
{
public:FileHelper(const std::string& filename): _filename(filename){}bool exists();  //判断是否存在size_t size();  //获取文件大小bool read(std::string& buffer);  //读取文件中所有内容到buffer中 bool read(char* buffer, size_t offset, size_t len);  //从offset位置读取len个字符bool write(const std::string& buffer);   //将buffer写入文件当中bool write(char* buffer, size_t offset, size_t len);  //将buffer写入指定位置static bool createFile(const std::string& filename)  //创建文件static bool removeFile(const std::string& filename)  //删除文件static bool createDirectory(const std::string& path) //创建目录static bool removeDirectory(const std::string& path) //删除目录static std::string parentDirectory(const std::string& filename) //获取文件路径bool renameFile(const std::string& newName);  //修改文件名称private:std::string _filename;
};

整体代码:

class FileHelper
{
public:FileHelper(const std::string& filename): _filename(filename){}bool exists()  //判断是否存在{//使用stat获取文件状态,获取失败则表示文件不存在struct stat st;return stat(_filename.c_str(), &st) == 0;//成功返回0,不成功返回-1}size_t size()  //获取文件大小{struct stat st;int ret = stat(_filename.c_str(), &st);if (ret < 0)return 0;return st.st_size;}bool read(std::string& buffer)  //读取文件中所有内容到buffer中 {//获取文件大小,根据文件大小调整buffer的空间size_t fsize = size();buffer.resize(fsize);return read(&buffer[0], 0, fsize);}bool read(char* buffer, size_t offset, size_t len)  //从offset位置读取len个字符{//1.打开文件std::ifstream ifs(_filename, std::ios::binary | std::ios::in);if (ifs.is_open() == false){ELOG("%s 文件打开失败", _filename.c_str());return false;}//2.跳转文件读写位置ifs.seekg(offset, std::ios::beg);//3.读取文件数据ifs.read(buffer, len);if (ifs.good() == false){ELOG("% 文件读取数据失败", _filename.c_str());ifs.close();return false;}//4.关闭文件ifs.close();return true;}bool write(const std::string& buffer)   //将buffer写入文件当中{   return write(buffer.c_str(), 0, buffer.size());}bool write(const char* buffer, size_t offset, size_t len)  //将buffer写入指定位置{//1.打开文件std::fstream ofs(_filename, std::ios::binary | std::ios::out | std::ios::in); //跳转位置需要读权限if (ofs.is_open() == false){ELOG("%s 文件打开失败, 失败原因:%s", _filename.c_str(), strerror(errno));return false;           }//2.跳转到文件指定位置ofs.seekp(offset, std::ios::beg);//3.写入数据ofs.write(buffer, len);if (ofs.good() == false){ELOG("% 文件写入数据失败", _filename.c_str());ofs.close();return false;   }//4.关闭文件ofs.close();return true;}bool renameFile(const std::string& newName)  //修改文件名称{return ::rename(_filename.c_str(), newName.c_str()) == 0;}static bool createFile(const std::string& filename)  //创建文件{std::fstream ofs(filename, std::ios::binary | std::ios::out);if (ofs.is_open() == false){ELOG("%s 文件打开失败", filename.c_str());return false;           }ofs.close();return true;}static bool removeFile(const std::string& filename)  //删除文件{return ::remove(filename.c_str()) == 0;}static bool createDirectory(const std::string& path) //创建目录{//多级路径创建中,需要从第一个父级目录开始创建size_t pos = 0, index = 0;while (index < path.size()){pos = path.find('/', index);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;}index = pos + 1;}return true;}static bool removeDirectory(const std::string& path) //删除目录{std::string cmd = "rm -rf " + path;return system(cmd.c_str()) != -1;}static std::string parentDirectory(const std::string& filename) //获取文件路径{size_t pos = filename.rfind('/');if (pos == std::string::npos){//文件名没有/,返回当前目录return "./";}std::string path = filename.substr(0, pos);return path;}private:std::string _filename;
};

测试代码:

#include "helper.hpp"
#include "logger.hpp"int main()
{bitmq::FileHelper helper("copy.hpp");DLOG("是否存在: %d", helper.exists());DLOG("文件大小: %ld", helper.size());bitmq::FileHelper temp("./aaa/bbb/ccc/temp.hpp");if (temp.exists() == false){std::string path = bitmq::FileHelper::parentDirectory("./aaa/bbb/ccc/temp.hpp"); //获取文件父路径if (bitmq::FileHelper(path).exists() == false) //如果父路径不存在则创建{bitmq::FileHelper::createDirectory(path);}bitmq::FileHelper::createFile("./aaa/bbb/ccc/temp.hpp");  //在路径下创建文件}//读取/写入整个文件std::string buffer;helper.read(buffer);DLOG("%s", buffer.c_str());temp.write(buffer);//从指定位置读取/写入char str[16] = { 0 };temp.read(str, 8, 4);DLOG("%s", str);temp.write("123456789", 8, 11);//修改名称temp.renameFile("./aaa/bbb/ccc/test.hpp");//删除文件bitmq::FileHelper::removeFile("./aaa/bbb/ccc/test.hpp");//删除目录bitmq::FileHelper::removeDirectory("./aaa");return 0;
}

代码一定要测试,如果测试成功,将来出错就说明错误不是出在工具类的代码上,这样可以提高查错效率。

8.2.2.5 整体代码
#pragma once#include <iostream>
#include <string>
#include <vector>
#include <random>
#include <sstream>
#include <iomanip>
#include <atomic>
#include <fstream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cerrno>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <sqlite3.h>
#include "mq_logger.hpp"namespace bitmq
{class SqliteHelper{typedef int (*sqliteCallback)(void*, int, char**, char**);public:SqliteHelper(const std::string& dbfile): _dbfile(dbfile), _handler(nullptr){}bool open(int flag = SQLITE_OPEN_FULLMUTEX) //默认为串行化{//不存在则创建 | 以可读可写方式int ret = sqlite3_open_v2(_dbfile.c_str(), &_handler, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE  | flag, nullptr);if (ret != SQLITE_OK){ELOG("创建/打开sqlite数据库失败: %s", sqlite3_errmsg(_handler));return false;}return true;}bool exec(const std::string& sql, sqliteCallback cb, void* arg){int ret = sqlite3_exec(_handler, sql.c_str(), cb, arg, nullptr);if (ret != SQLITE_OK){ELOG("%s 执行语句失败: %s",sql.c_str(), sqlite3_errmsg(_handler));return false;}return true;}void close(){if (_handler)sqlite3_close_v2(_handler);}private:std::string _dbfile;sqlite3* _handler;};class StringHelper{public:static size_t splite(const std::string& str, const std::string& sep, std::vector<std::string>& res){int index = 0, pos = 0;while (index < str.size()){pos = str.find(sep, index);if (pos == std::string::npos){res.push_back(str.substr(index));break;}if (index != pos){res.push_back(str.substr(index, pos - index));}index = pos + sep.size();}return res.size();}};class UUIDHelper{public:static std::string uuid(){std::random_device rd;std::mt19937_64 gernator(rd());//限制数字区间std::uniform_int_distribution<int> distribution(0, 255);//将生成的数字转换为16进制数字字符std::stringstream ss;for (int i = 0; i < 8; i++){ss << std::setw(2) << std::setfill('0') << std::hex << distribution(gernator);if (i == 3 || i == 5 || i == 7)ss << "-";}//定义一个原子类型整形,初始值为1static std::atomic<size_t> seq(1);size_t num = seq.fetch_add(1);for (int i = 7; i >= 0; i--){ss << std::setw(2) << std::setfill('0') << std::hex << ((num >> (i * 8)) & 0xff);if (i == 6)ss << "-";}return ss.str();}};class FileHelper{public:FileHelper(const std::string& filename): _filename(filename){}bool exists()  //判断是否存在{//使用stat获取文件状态,获取失败则表示文件不存在struct stat st;return stat(_filename.c_str(), &st) == 0;//成功返回0,不成功返回-1}size_t size()  //获取文件大小{struct stat st;int ret = stat(_filename.c_str(), &st);if (ret < 0)return 0;return st.st_size;}bool read(std::string& buffer)  //读取文件中所有内容到buffer中 {//获取文件大小,根据文件大小调整buffer的空间size_t fsize = size();buffer.resize(fsize);return read(&buffer[0], 0, fsize);}bool read(char* buffer, size_t offset, size_t len)  //从offset位置读取len个字符{//1.打开文件std::ifstream ifs(_filename, std::ios::binary | std::ios::in);if (ifs.is_open() == false){ELOG("%s 文件打开失败", _filename.c_str());return false;}//2.跳转文件读写位置ifs.seekg(offset, std::ios::beg);//3.读取文件数据ifs.read(buffer, len);if (ifs.good() == false){ELOG("% 文件读取数据失败", _filename.c_str());ifs.close();return false;}//4.关闭文件ifs.close();return true;}bool write(const std::string& buffer)   //将buffer写入文件当中{   return write(buffer.c_str(), 0, buffer.size());}bool write(const char* buffer, size_t offset, size_t len)  //将buffer写入指定位置{//1.打开文件std::fstream ofs(_filename, std::ios::binary | std::ios::out | std::ios::in); //跳转位置需要读权限if (ofs.is_open() == false){ELOG("%s 文件打开失败, 失败原因:%s", _filename.c_str(), strerror(errno));return false;           }//2.跳转到文件指定位置ofs.seekp(offset, std::ios::beg);//3.写入数据ofs.write(buffer, len);if (ofs.good() == false){ELOG("% 文件写入数据失败", _filename.c_str());ofs.close();return false;   }//4.关闭文件ofs.close();return true;}bool renameFile(const std::string& newName)  //修改文件名称{return ::rename(_filename.c_str(), newName.c_str()) == 0;}static bool createFile(const std::string& filename)  //创建文件{std::fstream ofs(filename, std::ios::binary | std::ios::out);if (ofs.is_open() == false){ELOG("%s 文件打开失败", filename.c_str());return false;           }ofs.close();return true;}static bool removeFile(const std::string& filename)  //删除文件{return ::remove(filename.c_str()) == 0;}static bool createDirectory(const std::string& path) //创建目录{//多级路径创建中,需要从第一个父级目录开始创建size_t pos = 0, index = 0;while (index < path.size()){pos = path.find('/', index);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;}index = pos + 1;}return true;}static bool removeDirectory(const std::string& path) //删除目录{std::string cmd = "rm -rf " + path;return system(cmd.c_str()) != -1;}static std::string parentDirectory(const std::string& filename) //获取文件路径{size_t pos = filename.rfind('/');if (pos == std::string::npos){//文件名没有/,返回当前目录return "./";}std::string path = filename.substr(0, pos);return path;}private:std::string _filename;};
}

8.3 消息类型定义&交换机类型定义

在开始正式项目功能模块代码编写之前,我们需要先提前做一件事情,就是将消息类型定义出来。

而消息最终是需要进行持久化存储的,因此涉及到数据的序列化和反序列化,因此消息的类型定义我们使用 protobuf 来进行生成。

因此定义消息类型,其实就是定义一个消息类型的 proto 文件,并生成相关代码。

1.消息所需要素:

  • 1.消息本身要素:
    • 1)消息属性:消息属性中包含有以下内容
      • i. 消息 ID
      • ii. 消息投递模式:非持久化模式/持久化模式
      • iii. 消息的 routing_key
    • 2)消息有效载荷内容
  • 2.消息额外存储所需要素:
    • 1)消息的存储位置
    • 2)消息的长度
    • 3)消息是否有效:注意这里并不使用 bool 类型,而是使用字符的 0/1,因为bool 类型在持久化的时候所占长度不同,会导致,修改文件中消息有效位后消息长度发生变化,因此不用 bool 类型。
       

因为客户端与服务端都会用到交换机的一些相关信息,比如交换机类型,还有就是消息的持久化模式,因此我们将交换机类型的枚举,与消息投递模式的枚举也顺便同时定义到 proto 文件中。

2.交换机类型

  • 1)DIRECT
  • 2)FANOUT
  • 3)TOPIC

3.消息投递模式

  • UNDURABLE:在 RabbitMQ 中,此模式的值为 1,咱们也效仿
  • DURABLE :值为 2.

编写的proto文件内容如下:

8.4 交换机数据管理

从这里开始,这个项目正式开始,我们首先需要创建交换机,队列,绑定,虚拟机等管理类,先从交换机开始逐个实现。

交换机数据管理主要分为下面三个类。

1. 定义交换机数据类

  • 1)交换机名称
  • 2)交换机类型
  • 3)是否持久化标志
  • 4)是否自动删除标志
  • 5)其他参数

2. 定义交换机数据持久化类(数据持久化的 sqlite3 数据库中)

  • 1)创建/删除交换机数据表
  • 2)新增交换机数据
  • 3)移除交换机数据
  • 4)查询所有交换机数据

3. 定义交换机数据管理类

  • 1)声明交换机,并添加管理(存在则 OK,不存在则创建)
  • 2)删除交换机
  • 3)获取指定交换机对象
  • 4)判断交换机是否存在
  • 5)销毁所有交换机数据

该类是对外提供的整体数据管理类,包括数据持久化,以及内存出数据的管理。

声明交换机数据类

// 1. 定义交换机数据类
struct Exchange
{using ptr = std::shared_ptr<Exchange>;Exchange(const std::string& ename, ExchangeType& etype,bool edurable, bool eauto_delete, std::unordered_map<std::string, std::string>& eargs): name(ename), type(etype), durable(edurable), auto_delete(eauto_delete), args(eargs){ }//解析str_args字符串,将结果存储到成员中(反序列化)void setArgs(const std::string& str_args);std::string getArgs();  //将args中的内容进行序列化,返回字符串std::string name;          // 1)交换机名称bitmq::ExchangeType type;  // 2)交换机类型bool durable;              // 3)是否持久化标志bool auto_delete;          // 4)是否自动删除标志std::unordered_map<std::string, std::string> args; // 5)其他参数//args存储键值对,在存储数据库时,会组织成一个格式字符串存储:key=val&key=val&...
};
  • 在其他两个类获取交换机对象时,我们期望使用获取的是智能指针,相较于直接获取对象或者指针,他更加安全高效,所以在内部声明ptr为交换机类的智能指针对象。
  • 其他类型选择使用哈希表来存储,将来存储至数据库时,会根据key和value组成一个格式字符串(key=value&key=value&....)存储,而成员函数setArgs用于将上面的格式字符串转换成key和value并存储至哈希表中,getArgs用于将哈希表中的内容序列化成字符串。

2. 声明交换机数据持久化类(数据持久化的 sqlite3 数据库中)

// 2. 定义交换机数据持久化类(数据持久化的 sqlite3 数据库中)
class ExchangeMapper
{
public:ExchangeMapper(const std::string& dbfile){}void createTable();  // 1)创建交换机数据表void removeTable(); //  2) 删除交换机数据表void insert(Exchange::ptr& exchange);  // 3)新增交换机数据void remove(const std::string& name);  // 4)移除交换机数据using ExchangeMap = std::unordered_map<std::string, Exchange::ptr>;ExchangeMap recovery()  // 5)查询所有交换机数据private:bitmap::SqliteHelper _sql_helper;
};

该类的功能是对交换机的增删查,并完成交换机数据的持久化(将交换机数据写入数据库中),该类并不会暴露给外部使用,而是用于内部。

3. 声明交换机数据管理类

// 3. 定义交换机数据管理类
class ExchangeManager
{
public:ExchangeManager(const std::string& dbfile);// 1)声明交换机,并添加管理(存在则 OK,不存在则创建)void declareExchange(const std::string& name, bitmq::ExchangeType type, bool durable, bool auto_delete, std::unordered_map<std::string, std::string> args);// 2)删除交换机void deleteExchange(const std::string& name);// 3)获取指定交换机对象Exchange::ptr selectExchange(const std::string& name);// 3)判断交换机是否存在bool exists(const std::string& name);// 4)销毁所有交换机数据void clear();private:std::mutex _mutex;ExchangeMapper _mapper;std::unordered_map<std::string, Exchange::ptr> _exchanges;
};

该类是交换机数据管理类,_exchanges中会保存所有交换机名称与交换机对象的映射,并可以通过_mapper对象对交换机进行操作,该类是真正暴露给外部使用的类,将来会有多个线程使用该类,所需需要添加锁来保护线程安全。



整体框架搭建好了,接下来就是细节实现了。

定义交换机数据类

// 1. 定义交换机数据类
struct Exchange
{using ptr = std::shared_ptr<Exchange>;Exchange(){}Exchange(const std::string& ename, ExchangeType& etype,bool edurable, bool eauto_delete, std::unordered_map<std::string, std::string>& eargs): name(ename), type(etype), durable(edurable), auto_delete(eauto_delete), args(eargs){ }//解析str_args字符串,将结果存储到成员中(反序列化)void setArgs(const std::string& str_args){std::vector<std::string> sub_args;size_t ret = bitmq::StringHelper::splite(str_args, "&", sub_args);for (auto& str : sub_args){size_t pos = str.find('=');std::string key = str.substr(0, pos);std::string value = str.substr(pos+1);args.insert({key, value});}}//将args中的内容进行序列化,返回字符串std::string getArgs(){std::string ret;for (auto& [k, v] : args){ret += k + '=' + v;ret += '&';}if (ret.size() != 0)ret.pop_back();return ret;}std::string name;          // 1)交换机名称bitmq::ExchangeType type;  // 2)交换机类型bool durable;              // 3)是否持久化标志bool auto_delete;          // 4)是否自动删除标志std::unordered_map<std::string, std::string> args; // 5)其他参数//args存储键值对,在存储数据库时,会组织成一个格式字符串存储:key=val&key=val
};

Exchange类较为简单,只需实现getArgs和setArgs函数。

  • setArgs:将传递进来的字符串以&进行分割放入字符串数组当中,然后依次去数组中每一个成员,=左边的就的就是key,=右边的就是value。
  • getArgs:将哈希表中的key和value组成格式字符串,以&作为分隔符。

2. 定义交换机数据持久化类(数据持久化的 sqlite3 数据库中)

// 2. 定义交换机数据持久化类(数据持久化的 sqlite3 数据库中)
class ExchangeMapper
{
public:ExchangeMapper(const std::string& dbfile): _sql_helper(dbfile){//创建目录std::string path = bitmq::FileHelper::parentDirectory(dbfile);bitmq::FileHelper::createDirectory(path);assert(_sql_helper.open());createTable();}void createTable()  // 1)创建交换机数据表{#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() //  2) 删除交换机数据表{#define DROP_TABLE "drop table if exists exchange_table;"bool ret = _sql_helper.exec(DROP_TABLE, nullptr, nullptr);if (ret == false){DLOG("删除交换机数据库表失败!!");abort();  //直接异常退出程序}}void insert(Exchange::ptr& exchange)  // 3)新增交换机数据{#define INSERT_SQL "insert into exchange_table values('%s', %d, %d, %d, '%s');"char sql_str[4096] = { 0 };std::string args_str = exchange->getArgs();sprintf(sql_str, INSERT_SQL, exchange->name.c_str(), exchange->type, exchange->durable,exchange->auto_delete, args_str.c_str());bool ret = _sql_helper.exec(sql_str, nullptr, nullptr);if (ret == false){DLOG("新增交换机数据失败!!");}}void remove(const std::string& name)  // 4)移除交换机数据{std::string delete_sql = "delete from exchange_table where name = '";delete_sql += name + "';";bool ret = _sql_helper.exec(delete_sql, nullptr, nullptr);if (ret == false){DLOG("删除交换机数据失败!!");}}using ExchangeMap = std::unordered_map<std::string, Exchange::ptr>;ExchangeMap recovery()  // 5)查询所有交换机数据{ExchangeMap result;std::string sql = "select name, type, durable, auto_delete, args from exchange_table;";_sql_helper.exec(sql, selectAllCallback, &result);return result;}private:static int selectAllCallback(void* arg, int numCol, char** row, char** fields){ExchangeMap* result = (ExchangeMap*)arg;auto exp = std::make_shared<Exchange>();exp->name = row[0];exp->type = (bitmq::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({exp->name, exp});return 0;}private:bitmq::SqliteHelper _sql_helper;
};
  • ExchangeMapper类主要是完成访问数据库的功能,所以所有的成员函数都是围绕sql语句进行的,需要注意的是,增删功能并不需要知道结果,而查需要,在调用sql执行语句时需要传递一个回调函数,每次回调函数都会返回数据库的一行数据。
  • ExchangeMapper的构造函数是一个文件路径,该路径就是交换机数据库所在路径,首先会根据路径创建父目录,然后在父目录下创建数据库文件。
  • recovery函数的主要作用是重启服务器时将数据库中的数据恢复到内存中,所以该函数虽然功能是查询所有交换机数据,但主要是为恢复数据提供服务(其他地方用不到),所以命名为recovery(恢复)

3. 定义交换机数据管理类

// 3. 定义交换机数据管理类
class ExchangeManager
{
public:using ptr = std::shared_ptr<ExchangeManager>;using ExchangeMap = std::unordered_map<std::string, Exchange::ptr>;ExchangeManager(const std::string& dbfile): _mapper(dbfile){//恢复所有数据_exchanges = _mapper.recovery();}// 1)声明交换机,并添加管理(存在则 OK,不存在则创建)void declareExchange(const std::string& name, bitmq::ExchangeType type, bool durable, bool auto_delete, std::unordered_map<std::string, std::string> args){//需要持久化存储才会存储到磁盘中std::unique_lock<std::mutex> lock(_mutex);auto it = _exchanges.find(name);//交换机已经存在if (it != _exchanges.end())return;auto exp = std::make_shared<Exchange>(name, type, durable, auto_delete, args);if (durable == true){_mapper.insert(exp);}_exchanges.insert({name, exp});}// 2)删除交换机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);}// 3)获取指定交换机对象Exchange::ptr selectExchange(const std::string& name){std::unique_lock<std::mutex> lock(_mutex);auto it = _exchanges.find(name);if (it == _exchanges.end())return {};return it->second;}// 3)判断交换机是否存在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;}// 4)销毁所有交换机数据void clear(){std::unique_lock<std::mutex> lock(_mutex);_mapper.removeTable();_exchanges.clear();}// 5)返回交换机的数量size_t size(){std::unique_lock<std::mutex> lock(_mutex);return _exchanges.size();}private:std::mutex _mutex;ExchangeMapper _mapper;ExchangeMap _exchanges;
};
  • ExchangeManager中的_exchanges可以看成是内存中的交换机数据,_mapper是数据库中数据管理者,ExchangeManager的构造函数会将数据库中的数据全部导入_exchanges。
  • 对ExchangeManager中的_mapper和_exchanges进行操作时需要注意添加锁,因为该类将来只会创建一个,并且不止一个线程会访问该类,不加锁就会出现线程安全问题。
  • 声明交换机 / 删除交换机时应该注意该交换机是否持久化了,只有持久化的交换机才需要访问数据库,减少对数据库的访问可以有效提高效率。


单元测试:

在编写完上述代码后,我们可以进行单元测试,测试我们的代码是否能正常执行或者存在小bug,我们使用gtest库来帮我们完成测试任务。

测试插入功能

#include "../../mqserver/mq_exchange.hpp"
#include <gtest/gtest.h>bitmq::ExchangeManager::ptr emp;class ExchangeTest : public testing::Environment
{
public:virtual void SetUp() override{emp = std::make_shared<bitmq::ExchangeManager>("./data/meta.db");}virtual void TearDown() override {//emp->clear();}
};TEST(exchange_test, insert_test)
{std::unordered_map<std::string, std::string> map1;std::unordered_map<std::string, std::string> map2 = {{"k1", "v1"}, {"k2", "v2"}};emp->declareExchange("exchange1", bitmq::ExchangeType::DIRECT, true, false, map1);emp->declareExchange("exchange2", bitmq::ExchangeType::DIRECT, true, false, map1);emp->declareExchange("exchange3", bitmq::ExchangeType::DIRECT, true, false, map1);emp->declareExchange("exchange4", bitmq::ExchangeType::DIRECT, true, false, map1);emp->declareExchange("exchange5", bitmq::ExchangeType::DIRECT, true, false, map2);emp->declareExchange("exchange6", bitmq::ExchangeType::DIRECT, true, false, map2);ASSERT_EQ(emp->size(), 6);
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);testing::AddGlobalTestEnvironment(new ExchangeTest);RUN_ALL_TESTS();return 0;
}

我们声明了六个交换机,前四个的args为空,后两个args不为空。

测试结果:

再来观察一下数据库

可见插入功能没有问题。

测试删除功能

TEST(exchange_test, remove_test)
{emp->deleteExchange("exchange2");bitmq::Exchange::ptr exp = emp->selectExchange("exchange2");ASSERT_EQ(exp.get(), nullptr);ASSERT_EQ(emp->exists("exchange2"), false);
}

这段代码,我们删除名为exchange2的交换机。

执行结果:

执行结果显示没有问题,我们再来看一下数据库

测试查找功能

TEST(exchange_test, select_test)
{bitmq::Exchange::ptr exp1 = emp->selectExchange("exchange3");ASSERT_EQ(exp1->name, "exchange3");ASSERT_EQ(exp1->type, bitmq::ExchangeType::DIRECT);ASSERT_EQ(exp1->durable, true);ASSERT_EQ(exp1->auto_delete, false);ASSERT_EQ(exp1->getArgs(), "");bitmq::Exchange::ptr exp2 = emp->selectExchange("exchange5");ASSERT_EQ(exp2->name, "exchange5");ASSERT_EQ(exp2->type, bitmq::ExchangeType::DIRECT);ASSERT_EQ(exp2->durable, true);ASSERT_EQ(exp2->auto_delete, false);ASSERT_EQ(exp2->getArgs(), "k2=v2&k1=v1");
}

我们查找exchange3和exchange5,并且比对每一个属性是否正确。

运行结果也是没有问题的。

8.5 队列数据管理

当前队列数据的管理,本质上是队列描述信息的管理,描述当前服务器上有哪些队列。

1. 定义队列描述数据类

  • 1)队列名称
  • 2)是否持久化标志
  • 3)是否独占标志
  • 4)是否自动删除标志
  • 5)其他参数

2. 定义队列数据持久化类(数据持久化的 sqlite3 数据库中)

  • 1)创建/删除队列数据表
  • 2)新增队列数据
  • 3)移除队列数据
  • 4)查询所有队列数据

3. 定义队列数据管理类

  • 1)创建队列,并添加管理(存在则 OK,不存在则创建)
  • 2)删除队列
  • 3)获取指定队列
  • 4)获取所有队列
  • 5)判断指定队列是否存在
  • 6)获取队列数量
  • 7)销毁所有队列数据

该类是对外提供的整体数据管理类,包括数据持久化,以及内存出数据的管理。

声明队列描述数据类

// 1. 定义队列描述数据类
struct MsgQueue
{MsgQueue(){}MsgQueue(const std::string& qname, bool qdurable, bool qexclusive, bool qauto_delete,std::unordered_map<std::string, std::string>& qargs): name(qname), durable(qdurable), exclusive(qexclusive), auto_delete(qauto_delete), args(qargs){}//解析str_args字符串,将结果存储到成员中(反序列化)void setArgs(const std::string& str_args);//将args中的内容进行序列化,返回字符串std::string getArgs();std::string name;  // 1)队列名称bool durable;      // 2)是否持久化标志bool exclusive;    // 3)是否独占标志bool auto_delete;  // 4)是否自动删除标志std::unordered_map<std::string, std::string> args;   // 5)其他参数
};

队列和交换机很多地方基本是一样的,这里就不过多赘述了。

声明队列数据持久化类(数据持久化的 sqlite3 数据库中)

// 2. 定义队列数据持久化类(数据持久化的 sqlite3 数据库中)
class MsgQueueMapper
{
public:MsgQueueMapper(const std::string& dbfile): _sql_helper(dbfile){}void createTable();   // 1)创建队列数据表void removeTable();   // 2)删除队列数据表void insert(MsgQueue::ptr& queue);   // 3)新增队列数据void remove(const std::string& name); // 4)移除队列数据using QueueMap = std::unordered_map<std::string, MsgQueue::ptr>;QueueMap recovery();   // 5)查询所有队列数据private:bitmq::SqliteHelper _sql_helper;
};

队列的数据持久化类也和交换机数据持久类基本一样。

3. 声明队列数据管理类

// 3. 定义队列数据管理类
class MsgQueueManager
{
public:using QueueMap = std::unordered_map<std::string, MsgQueue::ptr>;// 1)创建队列,并添加管理(存在则 OK,不存在则创建)void declareQueue(const std::string& name, bool durable, bool exclusive, bool auto_delete, std::unordered_map<std::string, std::string> args);// 2)删除队列void deleteQueue(const std::string& name);// 3)获取指定队列MsgQueue::ptr selectQueue(const std::string& name);// 4)获取所有队列QueueMap getAllQueues();// 5)判断指定队列是否存在bool exists(const std::string& name);// 6)获取队列数量size_t size();// 7)销毁所有队列数据void clear();private:std::mutex _mutex;MsgQueueMapper _mapper;QueueMap _msg_queues;
};


在实现了队列管理类的框架后,我们需要开始实现每个类的细节了,由于队列和交换机的代码基本上一样,这里就不过多赘述了。

定义队列描述数据类

// 1. 定义队列描述数据类
struct MsgQueue
{using ptr = std::shared_ptr<MsgQueue>;MsgQueue(){}MsgQueue(const std::string& qname, bool qdurable, bool qexclusive, bool qauto_delete,std::unordered_map<std::string, std::string>& qargs): name(qname), durable(qdurable), exclusive(qexclusive), auto_delete(qauto_delete), args(qargs){}//解析str_args字符串,将结果存储到成员中(反序列化)void setArgs(const std::string& str_args){std::vector<std::string> sub_args;size_t ret = bitmq::StringHelper::splite(str_args, "&", sub_args);for (auto& str : sub_args){size_t pos = str.find('=');std::string key = str.substr(0, pos);std::string value = str.substr(pos+1);args.insert({key, value});}}//将args中的内容进行序列化,返回字符串std::string getArgs(){std::string ret;for (auto& [k, v] : args){ret += k + '=' + v;ret += '&';}if (ret.size() != 0)ret.pop_back();return ret;}std::string name;  // 1)队列名称bool durable;      // 2)是否持久化标志bool exclusive;    // 3)是否独占标志bool auto_delete;  // 4)是否自动删除标志std::unordered_map<std::string, std::string> args;   // 5)其他参数
};

定义队列数据持久化类(数据持久化的 sqlite3 数据库中)

// 2. 定义队列数据持久化类(数据持久化的 sqlite3 数据库中)
class MsgQueueMapper
{
public:MsgQueueMapper(const std::string& dbfile): _sql_helper(dbfile){std::string path = bitmq::FileHelper::parentDirectory(dbfile);bitmq::FileHelper::createDirectory(path);assert(_sql_helper.open());createTable();}void createTable()   // 1)创建队列数据表{std::string sql = "create table if not exists queue_table(name varchar(32) primary key, durable int, \exclusive int, auto_delete int, args varchar(128));";assert(_sql_helper.exec(sql, nullptr, nullptr));}void removeTable()   // 2)删除队列数据表{std::string sql = "drop table if exists queue_table";assert(_sql_helper.exec(sql, nullptr, nullptr));}void insert(MsgQueue::ptr& queue)   // 3)新增队列数据{std::stringstream sql;sql << "insert into queue_table values(";sql << "'" << queue->name << "', ";sql << queue->durable << ", ";sql << queue->exclusive << ", ";sql << queue->auto_delete << ", ";sql <<  "'" << queue->getArgs() << "');";_sql_helper.exec(sql.str(), nullptr, nullptr);}void remove(const std::string& name) // 4)移除队列数据{std::string sql = "delete from queue_table where name = '";sql += name + "';";_sql_helper.exec(sql, nullptr, nullptr);}using QueueMap = std::unordered_map<std::string, MsgQueue::ptr>;QueueMap recovery()   // 5)查询所有队列数据{std::string sql = "select name, durable, exclusive, auto_delete, args from queue_table";QueueMap result;_sql_helper.exec(sql, selectAllCallback, &result);return result;}
private:static int selectAllCallback(void* arg, int numCol, char** row, char** fields){QueueMap* result = (QueueMap*)arg;auto exp = std::make_shared<MsgQueue>();exp->name = row[0];exp->durable = (bool)std::stoi(row[1]);exp->exclusive = (bool)std::stoi(row[2]);exp->auto_delete = (bool)std::stoi(row[3]);if (row[4])exp->setArgs(row[4]);result->insert({exp->name, exp});return 0;}

定义队列数据管理类

// 3. 定义队列数据管理类
class MsgQueueManager
{
public:using ptr = std::shared_ptr<MsgQueueManager>;using QueueMap = std::unordered_map<std::string, MsgQueue::ptr>;MsgQueueManager(const std::string& dbfile): _mapper(dbfile){//恢复所有数据_msg_queues = _mapper.recovery();}// 1)创建队列,并添加管理(存在则 OK,不存在则创建)void declareQueue(const std::string& name, bool durable, bool exclusive, bool auto_delete, std::unordered_map<std::string, std::string> args){std::unique_lock<std::mutex> lock(_mutex);auto it = _msg_queues.find(name);if (it == _msg_queues.end())return;MsgQueue::ptr mqp = std::make_shared<MsgQueue>();mqp->name = name;mqp->durable = durable;mqp->exclusive = exclusive;mqp->auto_delete = auto_delete;mqp->args = args;if (durable == true)_mapper.insert(mqp);_msg_queues.insert({name, mqp});}// 2)删除队列void deleteQueue(const std::string& name){std::unique_lock<std::mutex> lock(_mutex);auto it = _msg_queues.find(name);if (it == _msg_queues.end())return;if (it->second->durable == true)_mapper.remove(name);_msg_queues.erase(name);}// 3)获取指定队列MsgQueue::ptr selectQueue(const std::string& name){std::unique_lock<std::mutex> lock(_mutex);auto it = _msg_queues.find(name);if (it == _msg_queues.end())return {};return it->second;       }// 4)获取所有队列QueueMap getAllQueues() {std::unique_lock<std::mutex> lock(_mutex);return _msg_queues;}// 5)判断指定队列是否存在bool exists(const std::string& name){std::unique_lock<std::mutex> lock(_mutex);auto it = _msg_queues.find(name);if (it == _msg_queues.end())return false;return true;  }// 6)获取队列数量size_t size(){std::unique_lock<std::mutex> lock(_mutex);return _msg_queues.size();}// 7)销毁所有队列数据void clear(){std::unique_lock<std::mutex> lock(_mutex);_mapper.removeTable();_msg_queues.clear();}private:std::mutex _mutex;MsgQueueMapper _mapper;QueueMap _msg_queues;
};


单元测试:

由于队列和交换机基本相似,所以交换机的测试代码队列也可以使用,所以我们就直接使用交换机的测试代码进行测试。

测试插入功能

#include "../../mqserver/mq_queue.hpp"
#include <gtest/gtest.h>bitmq::MsgQueueManager::ptr mqmp;class QueueTest: public testing::Environment
{
public:virtual void SetUp() override{mqmp = std::make_shared<bitmq::MsgQueueManager>("./data/meta.db");}virtual void TearDown() override {//mqmp->clear();}
};TEST(queue_test, insert_test)
{std::unordered_map<std::string, std::string> map1;std::unordered_map<std::string, std::string> map2 = {{"k1", "v1"}, {"k2", "v2"}};mqmp->declareQueue("queue1", true, false, false, map1);mqmp->declareQueue("queue2", true, false, false, map1);mqmp->declareQueue("queue3", true, false, false, map1);mqmp->declareQueue("queue4", true, false, false, map1);mqmp->declareQueue("queue5", true, false, false, map2);mqmp->declareQueue("queue6", true, false, false, map2);ASSERT_EQ(mqmp->size(), 6);
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);testing::AddGlobalTestEnvironment(new QueueTest);RUN_ALL_TESTS();return 0;
}

运行结果:

数据被成功插入了数据库中。

测试删除功能

TEST(queue_test, remove_test)
{mqmp->deleteQueue("queue2");bitmq::MsgQueue::ptr mqp = mqmp->selectQueue("queue2");ASSERT_EQ(mqp.get(), nullptr);
}

运行结果:

测试查找功能

在直接测试查找功能前我们可以顺便测试一下恢复功能,我们讲插入测试单元函数屏蔽,也就是说队列数据管理类不会通过函数调用获取数据,而是在初始化时从数据库中读取数据,而在上一次测试结束后,数据库中除了queue2被删除,其他队列应该都存在,所以我们可以通过测试其他队列存在,而queue2不存在来判断数据能否成功被恢复。

TEST(queue_test, select_test)
{ASSERT_EQ(mqmp->exists("queue1"), true);ASSERT_EQ(mqmp->exists("queue2"), false);ASSERT_EQ(mqmp->exists("queue3"), true);ASSERT_EQ(mqmp->exists("queue4"), true);ASSERT_EQ(mqmp->exists("queue5"), true);ASSERT_EQ(mqmp->exists("queue6"), true);bitmq::MsgQueue::ptr mqp1 = mqmp->selectQueue("queue3");ASSERT_EQ(mqp1->name, "queue3");ASSERT_EQ(mqp1->durable, true);ASSERT_EQ(mqp1->exclusive, false);ASSERT_EQ(mqp1->auto_delete, false);ASSERT_EQ(mqp1->getArgs(), "");bitmq::MsgQueue::ptr mqp2 = mqmp->selectQueue("queue5");ASSERT_EQ(mqp2->name, "queue5");ASSERT_EQ(mqp2->durable, true);ASSERT_EQ(mqp2->exclusive, false);ASSERT_EQ(mqp2->auto_delete, false);ASSERT_EQ(mqp2->getArgs(), "k1=v1&k2=v2");
}

运行结果:

测试结果没有问题,说明恢复功能是正确的。

8.6 绑定信息(交换机-队列)管理

绑定信息,本质上就是一个交换机关联了哪些队列的描述。

1. 定义绑定信息类

  • 1)交换机名称
  • 2)队列名称
  • 3)binding_key(分发匹配规则-决定了哪些数据能被交换机放入队列)
  • 4)持久化标志

2. 定义绑定信息数据持久化类(数据持久化的 sqlite3 数据库中)

  • 1)创建/删除绑定信息数据表
  • 2)新增绑定信息数据
  • 3)移除指定绑定信息数据
  • 4)移除指定交换机相关绑定信息数据:移除交换机的时候会被调用
  • 5)移除指定队列相关绑定信息数据:移除队列的时候会被调用
  • 6)查询所有绑定信息数据:用于重启服务器时进行历史数据恢复

3. 定义绑定信息数据管理类

  • 1)创建绑定信息,并添加管理(存在则 OK,不存在则创建)
  • 2)解除指定的绑定信息
  • 3)删除指定队列的所有绑定信息
  • 4)删除交换机相关的所有绑定信息
  • 5)获取交换机相关的所有绑定信息:交换机收到消息后,需要分发给自己关联的队列
  • 6)判断指定绑定信息是否存在
  • 7)获取当前绑定信息数量
  • 8)销毁所有绑定信息数据

定义绑定信息类

// 1. 定义绑定信息类
struct Binding
{using ptr = std::shared_ptr<Binding>;Binding(const std::string& ename, const std::string& qname, const std::string& key): exchange_name(ename), msgqueue_name(qname), binding_key(key){}std::string exchange_name;  // 1)交换机名称std::string msgqueue_name;  // 2)队列名称std::string binding_key;    // 3)binding_key(分发匹配规则-决定了哪些数据能被交换机放入队列)bool durable;               // 4)是否需要持久化
};

声明绑定信息数据持久化类(数据持久化的 sqlite3 数据库中)

// 2. 定义绑定信息数据持久化类(数据持久化的 sqlite3 数据库中)
class BindingMapper
{
public://队列和绑定信息是一一对应的using MsgQueueBindingMap = std::unordered_map<std::string, Binding::ptr>;//一个交换机可以绑定多个队列,所以key为交换机,value为队列和绑定信息表using BindingMap = std::unordered_map<std::string, MsgQueueBindingMap>;
public:BindingMapper(const std::string& dbfile): _sql_helper(dbfile){}// 1)创建绑定信息数据表void createTable();// 2)删除绑定信息数据表 void removeTable();// 2)新增绑定信息数据bool insert(Binding::ptr& binding); // 3)移除指定绑定信息数据void remove(const std::string& ename, const std::string& qname);// 4)移除指定交换机相关绑定信息数据:移除交换机的时候会被调用  void removeExchangeBinding(const std::string& ename);// 5)移除指定队列相关绑定信息数据:移除队列的时候会被调用void removeQueueBinding(const std::string& qname);// 6)查询所有绑定信息数据:用于重启服务器时进行历史数据恢复BindingMap recovery();private:SqliteHelper _sql_helper;
};

注意:队列与绑定信息是一一对应的(因为是给某个交换机绑定队列,因此一个交换机可能会有多个队列的绑定信息),所以需要先定义一个队列名与绑定信息的映射关系,这是为了方便通过队列名查找绑定信息

using MsgQueueBindingMap = std::unordered_map<std::string, Binding::ptr>;

然后定义一个交换机名称与队列绑定信息的映射关系,这个map中包含了所有的绑定信息,并且以交换机为单元进行了区分。

using BindingMap = std::unordered_map<std::string, MsgQueueBindingMap>;

声明绑定信息数据管理类

// 3. 定义绑定信息数据管理类
class BindingManager
{
public://队列和绑定信息是一一对应的using MsgQueueBindingMap = std::unordered_map<std::string, Binding::ptr>;//一个交换机可以绑定多个队列,所以key为交换机,value为队列和绑定信息表using BindingMap = std::unordered_map<std::string, MsgQueueBindingMap>;
public:BindingManager(const std::string& dbfile): _mapper(dbfile){}// 1)创建绑定信息,并添加管理(存在则 OK,不存在则创建)bool bind(const std::string& ename, const std::string& qname, const std::string& key);// 2)解除指定的绑定信息void unBind(const std::string& ename, const std::string& qname);// 3)删除交换机相关的所有绑定信息void removeExchangeBindings(const std::string& ename);// 4)删除指定队列的所有绑定信息void removeMsgQueueBindings(const std::string& qname);// 5)获取交换机相关的所有绑定信息:交换机收到消息后,需要分发给自己关联的队列MsgQueueBindingMap getExchangeBindings(const std::string& ename);// 6)获取指定绑定信息Binding::ptr getBinding(const std::string& ename, const std::string& qname);// 7)判断指定绑定信息是否存在bool exists(const std::string& ename, const std::string& qname);// 8)获取当前绑定信息数量size_t size();// 9)销毁所有绑定信息数据void clear();private:std::mutex _mutex;BindingMapper _mapper;BindingMap _bindings;
};

BindingMap的结构图如下:BindMap是一个哈希表,kye值是交换机名称,value是一个哈希表BindingMapper,BindingMapper的key值是队列名称,value是绑定信息。

这样设计的好处在于,当删除指定队列的所有绑定信息时,只需遍历BindingMap,获取value,通过value(BindingMapper),通过哈希表的erase函数就可以完成,不用遍历所有的绑定信息,提高了查找速率。



在看完整体框架后,需要实现具体细节,由于Binding并不需要额外功能,所有需要的内容在声明时就已经确定好了,所以我们直接从绑定信息持久化类来看。

定义绑定信息数据持久化类(数据持久化的 sqlite3 数据库中)

// 2. 定义绑定信息数据持久化类(数据持久化的 sqlite3 数据库中)
class BindingMapper
{
public://队列和绑定信息是一一对应的using MsgQueueBindingMap = std::unordered_map<std::string, Binding::ptr>;//一个交换机可以绑定多个队列,所以key为交换机,value为队列和绑定信息表using BindingMap = std::unordered_map<std::string, MsgQueueBindingMap>;
public:BindingMapper(const std::string& dbfile): _sql_helper(dbfile){std::string path = bitmq::FileHelper::parentDirectory(dbfile);bitmq::FileHelper::createDirectory(path);assert(_sql_helper.open());createTable();}// 1)创建绑定信息数据表void createTable(){std::string sql = "create table if not exists binding_table(exchange_name varchar(32), \msgqueue_name varchar(32),  binding_key varchar(128), durable int);";assert(_sql_helper.exec(sql, nullptr, nullptr));}// 2)删除绑定信息数据表 void removeTable(){std::string sql = "drop table if exists binding_table";assert(_sql_helper.exec(sql, nullptr, nullptr));}// 2)新增绑定信息数据bool insert(Binding::ptr& binding){std::stringstream ss;ss << "insert into binding_table values(";ss << "'" << binding->exchange_name << "', ";ss << "'" << binding->msgqueue_name << "', ";ss << "'" << binding->binding_key << "', ";ss << binding->durable << ");";return _sql_helper.exec(ss.str(), nullptr, nullptr);}// 3)移除指定绑定信息数据void remove(const std::string& ename, const std::string& qname){std::stringstream ss;ss << "delete from binding_table where "; ss << "exchange_name = '" << ename << "' and ";ss << "msgqueue_name = '" << qname << "';";_sql_helper.exec(ss.str(), nullptr, nullptr);}// 4)移除指定交换机相关绑定信息数据:移除交换机的时候会被调用  void removeExchangeBinding(const std::string& ename){std::stringstream ss;ss << "delete from binding_table where "; ss << "exchange_name = '" << ename << "';";_sql_helper.exec(ss.str(), nullptr, nullptr);}// 5)移除指定队列相关绑定信息数据:移除队列的时候会被调用void removeQueueBinding(const std::string& qname){std::stringstream ss;ss << "delete from binding_table where "; ss << "msgqueue_name = '" << qname << "';";_sql_helper.exec(ss.str(), nullptr, nullptr);}// 6)查询所有绑定信息数据:用于重启服务器时进行历史数据恢复BindingMap recovery(){std::string sql = "select exchange_name, msgqueue_name, binding_key, durable from binding_table";BindingMap result;_sql_helper.exec(sql, selectAllCallback, &result);return result;}private:static int selectAllCallback(void* arg, int numCol, char** row, char** fields){BindingMap* result = (BindingMap*)arg;auto bp = std::make_shared<Binding>();bp->exchange_name = row[0];bp->msgqueue_name = row[1];bp->binding_key = row[2];bp->durable = (bool)std::stoi(row[3]);//获取交换机对应的队列绑定映射MsgQueueBindingMap& qmap = (*result)[bp->exchange_name];//向队列绑定映射中他添加信息qmap.insert({bp->msgqueue_name, bp});return 0;}private:SqliteHelper _sql_helper;
};

在selectAllCallback回调函数中使用如下两句话来实现将binding插入对应的表中。

//获取交换机对应的队列绑定映射
MsgQueueBindingMap& qmap = (*result)[bp->exchange_name];
//向队列绑定映射中他添加信息
qmap.insert({bp->msgqueue_name, bp});

上面使用的方法是获取交换机对应的队列绑定映射,然后直接向该映射中插入数据,而不是直接向交换机插入绑定映射(下面这种写法)

MsgQueueBindingMap qmap;
qmap.insert({bp->msgqueue_name, bp});
result->insert({bp->exchange_name, qmap});

这是为了防止交换机相关的绑定信息已经存在,不能直接创建队列映射进行添加,这样会覆盖历史数据,因此得先获取交换机对应得映射对象,再往里面添加数据,但是,如果交换机对应得映射信息为空,则还是无法添加成功,所以需要添加引用,这样可以保证不存在自动创建)

定义绑定信息数据管理类

// 3. 定义绑定信息数据管理类
class BindingManager
{
public://队列和绑定信息是一一对应的using MsgQueueBindingMap = std::unordered_map<std::string, Binding::ptr>;//一个交换机可以绑定多个队列,所以key为交换机,value为队列和绑定信息表using BindingMap = std::unordered_map<std::string, MsgQueueBindingMap>;using ptr = std::shared_ptr<BindingManager>;
public:BindingManager(const std::string& dbfile): _mapper(dbfile){//恢复历史消息_bindings = _mapper.recovery();}// 1)创建绑定信息,并添加管理(存在则 OK,不存在则创建)bool bind(const std::string& ename, const std::string& qname, const std::string& key, bool durable){std::unique_lock<std::mutex> lock(_mutex);auto it = _bindings.find(ename);if (it != _bindings.end() && it->second.find(qname) != it->second.end())return true;Binding::ptr bp = std::make_shared<Binding>(ename, qname, key, durable);if (durable == true){if (_mapper.insert(bp) == false)return false;}auto& qbmp = _bindings[ename];qbmp.insert({qname, bp});return true;}// 2)解除指定的绑定信息void unBind(const std::string& ename, const std::string& qname){std::unique_lock<std::mutex> lock(_mutex);auto it = _bindings.find(ename);if (it == _bindings.end())   //没有交换机相关信息return;if (it->second.find(qname) == it->second.end())  //交换机没有队列相关绑定信息return;if (it->second[qname]->durable == true)_mapper.remove(ename, qname);it->second.erase(qname);}// 3)删除交换机相关的所有绑定信息void removeExchangeBindings(const std::string& ename){std::unique_lock<std::mutex> lock(_mutex);_mapper.removeExchangeBinding(ename);_bindings.erase(ename);}// 4)删除指定队列的所有绑定信息void removeMsgQueueBindings(const std::string& qname){std::unique_lock<std::mutex> lock(_mutex);_mapper.removeQueueBinding(qname);//遍历每个交换机,移除所有指定 队列相关信息for (auto& it : _bindings){it.second.erase(qname);}}// 5)获取交换机相关的所有绑定信息:交换机收到消息后,需要分发给自己关联的队列MsgQueueBindingMap getExchangeBindings(const std::string& ename){std::unique_lock<std::mutex> lock(_mutex);auto it = _bindings.find(ename);if (it == _bindings.end())return {};return it->second;}// 6)获取指定绑定信息Binding::ptr getBinding(const std::string& ename, const std::string& qname){std::unique_lock<std::mutex> lock(_mutex);auto it = _bindings.find(ename);if (it == _bindings.end())return {}; auto qit = it->second.find(qname);if (qit == it->second.end())return {};return qit->second;}// 7)判断指定绑定信息是否存在bool exists(const std::string& ename, const std::string& qname){std::unique_lock<std::mutex> lock(_mutex);auto it = _bindings.find(ename);if (it == _bindings.end())return false; auto qit = it->second.find(qname);if (qit == it->second.end())return false;return true;}// 8)获取当前绑定信息数量size_t size(){std::unique_lock<std::mutex> lock(_mutex);int total_size = 0;for (auto it : _bindings){total_size += it.second.size();}return total_size;}// 9)销毁所有绑定信息数据void clear(){std::unique_lock<std::mutex> lock(_mutex);_mapper.removeTable();_bindings.clear();}private:std::mutex _mutex;BindingMapper _mapper;BindingMap _bindings;
};


单元测试:

测试插入功能

#include "../../mqserver/mq_binding.hpp"
#include <gtest/gtest.h>bitmq::BindingManager::ptr bmp;class BindingTest : public testing::Environment
{
public:virtual void SetUp() override{bmp = std::make_shared<bitmq::BindingManager>("./data/meta.db");}virtual void TearDown() override {//bmp->clear();}
};TEST(binding_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.zhangsan", true);ASSERT_EQ(bmp->size(), 6);
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);testing::AddGlobalTestEnvironment(new BindingTest);RUN_ALL_TESTS();return 0;
}

执行结果:

数据库:

绑定信息插入成功。

测试查找功能

TEST(binding_test, select_test)
{ASSERT_EQ(bmp->exists("exchange1", "queue1"), true);ASSERT_EQ(bmp->exists("exchange1", "queue2"), true);ASSERT_EQ(bmp->exists("exchange1", "queue3"), true);ASSERT_EQ(bmp->exists("exchange2", "queue1"), true);ASSERT_EQ(bmp->exists("exchange2", "queue2"), true);ASSERT_EQ(bmp->exists("exchange2", "queue3"), true);ASSERT_EQ(bmp->exists("exchange2", "queue4"), false);bitmq::Binding::ptr bxp1 = bmp->getBinding("exchange1", "queue1");ASSERT_NE(bxp1.get(), nullptr);ASSERT_EQ(bxp1->exchange_name, "exchange1");ASSERT_EQ(bxp1->msgqueue_name, "queue1");ASSERT_EQ(bxp1->binding_key, "news.music.#");    ASSERT_EQ(bxp1->durable, true);    
}

运行结果:

测试查找指定交换机

TEST(binding_test, select_exchange_test)
{bitmq::BindingManager::MsgQueueBindingMap mqbm = bmp->getExchangeBindings("exchange1");ASSERT_EQ(mqbm.size(), 3);ASSERT_NE(mqbm.find("queue1"), mqbm.end());ASSERT_NE(mqbm.find("queue3"), mqbm.end());
}

运行结果:

测试删除指定队列功能

TEST(binding_test, remove_queue_test)
{bmp->removeMsgQueueBindings("queue1");ASSERT_EQ(bmp->exists("exchange1", "queue1"), false);ASSERT_EQ(bmp->exists("exchange2", "queue1"), false);
}

运行结果:

数据库:

测试删除指定交换机功能

TEST(binding_test, remove_exchange_test)
{bmp->removeExchangeBindings("exchange1");ASSERT_EQ(bmp->exists("exchange1", "queue1"), false);ASSERT_EQ(bmp->exists("exchange1", "queue2"), false);ASSERT_EQ(bmp->exists("exchange1", "queue3"), false);
}

运行结果:

数据库:

测试删除指定绑定信息

TEST(binding_test, remove_single_test)
{ASSERT_EQ(bmp->exists("exchange2", "queue2"), true);bmp->unBind("exchange2", "queue2");ASSERT_EQ(bmp->exists("exchange2", "queue2"), false);ASSERT_EQ(bmp->exists("exchange2", "queue3"), true);
}

运行结果:

数据库:

恢复历史消息功能

经过前面的操作,数据库中只剩下了exchange2和queue3的绑定了,我们屏蔽掉其他操作,只执行下面这段代码,如果执行成功,则可以说明恢复功能没有问题。

TEST(binding_test, recovery_test)
{ASSERT_EQ(bmp->exists("exchange2", "queue3"), true);ASSERT_EQ(bmp->exists("exchange1", "queue1"), false);ASSERT_EQ(bmp->exists("exchange1", "queue2"), false);ASSERT_EQ(bmp->exists("exchange1", "queue3"), false);ASSERT_EQ(bmp->exists("exchange2", "queue1"), false);ASSERT_EQ(bmp->exists("exchange2", "queue2"), false);
}

运行结果:

8.7 队列消息管理

因为消息数据需要在网络中进行传输,因此消息的类型定义使用 protobuf 进行,因为protobuf 中自带了序列化和反序列化功能,因此操作起来会简便一些。
需要特别说明的是,消息的存储并没有使用数据库,因为消息长度通常不定,且有些消息可能会非常庞大,因此并不适合存储在数据库中,因此我们的处理方式(包括RabbitMQ)是直接将消息存储在文件中进行管理而内存中管理的消息只需要记录好自己在文件中的所在位置和长度即可。

为了便于管理,消息管理以队列为单元进行管理,因此每个队列都会有自己独立的数据存储文件



1.消息的要素

  • 1.网络传输的消息要素:
    • 1)消息属性:消息id消息routing_key持久化标志
    • 2)消息的实际内容:数据
  • 2.服务器上的消息管理所需的额外要素(主要是为了持久化管理)
    • 1)消息有效标志:这个字段是需要随着消息的持久化内容一起进行持久化,每一条消息都有可能要进行持久化存储,等到推送给客户端就会删除掉,然而每次删除一条消息就重写一次文件,效率太低下,如果设置了有效标志位,每次只需要将这个有效标志位对应的数据给修改成无效即可。
    • 2)消息的实际存储位置(相对于文件起始位置的偏移量):当要删除某条消息时,需要重写覆盖这条消息在文件的对应位置(将有效标志位置设置为无效),这时候就需要能够找到这条消息。
    • 3)消息的长度:当恢复历史消息以及读取消息内容时,需要解决粘包问题

2. 消息的持久化管理

  • 1)不使用数据库:有些消息庞大,不适合数据库,其次消息的持久化主要是为了备份,而不是为了查询,因此直接使用文件进行存储。
  • 2)以队列为单元进行消息的持久化管理:每个队列都有一个自己的数据文件,当消息文件垃圾回收时,需要重写加载所有有效消息,重新生成新的数据文件。但是,生成新的数据文件后,消息的存储位置就发生了改变,这时候需要更新内存中的数据。
  • 3)数据存储在文件中,必然要有数据格式的要求:通过4字节长度,描述消息实际存储长度可以解决粘包问题。

a. 管理数据

  • 1)队列名
  • 2)队列消息的存储文件名
  • 3)队列消息的临时交换文件名

b. 管理操作

  • 2)队列消息文件创建/删除功能
  • 3)提供队列消息的新增持久化/删除持久化
  • 4)提供持久化内容的垃圾回收(其实就是重新加载出所有有效消息返回,并重新生成新的消息存储文件)

什么时候需要垃圾回收

因为每次删除数据都不是真正的删除,因此文件中的数据会越来越多,但是也不是每次删除都需要回收,当文件中有效消息超过2000条,且其中有效消息比例低于50%时才会进行垃圾回收。

回收思想

加载文件中所有有效消息,先写入到一个临时文件中,然后再去删除源文件,将临时文件名称改为源文件名称,最后返回所有的有效消息。


3. 消息的管理(以队列为单位进行管理)

a. 队列消息管理数据

  • 1)队列名称
  • 2)待推送消息链表(尾插头删模拟队列)
  • 3)持久化消息 hash:垃圾回收后需要更新消息数据,hash O(1)时间即可找到对应消息
  • 4)待确认消息 hash:一条消息被推送给客户端,并不会立即删除,而是等到被确认后才会删除。一条消息被推送给客户端后,取出待推送消息链表,加入到待确认结构中,等确认后再删除。
  • 5)有效消息数量
  • 6)已经持久化消息总量:可以和有效消息数量计算出是否需要垃圾回收
  • 7)持久化管理句柄

b. 队列管理操作

  • 1)新增消息
  • 2)获取队首消息(获取的同时将消息加入待确认队列)
  • 3)移除指定消息
  • 4)获取队列待消费消息数量
  • 5)获取队列待确认消息数量
  • 6)获取持久化消息数量
  • 7)恢复队列历史消息(构造时)。
  • 8)销毁队列所有消息
  • 9)判断队列消息是否为空

4. 消息的总体对外管理

a.管理的成员

  • 互斥锁
  • 每个队列的消息管理句柄:队列名称和队列消息管理句柄的hash表

b.提供的操作

  • 1)初始化新建队列的消息管理句柄,并创建消息存储文件
  • 2)删除队列的消息管理句柄,以及消息存储文件
  • 3)向指定队列新增消息
  • 4)获取指定队列队首消息
  • 5)确认指定队列待确认消息
  • 6)获取指定队列消息数量(待发送消息数量,持久化消息数量,待确认消息数量,总的消息数量)
  • 7)恢复队列历史消息


声明消息的要素类

在8.3时,我们就使用protobuf完成了消息的要素类。

为什么要使用protobuf编写Message类:

因为Message需要经过网络传输,即需要序列化与反序列化工作,而我们自己手写序列化和反序列化较为麻烦,所以使用protobuf可以变得更加简单高效。

Message中的valid字段为什么要设置为string而不是bool:

protobuf中的bool类型经过编码后可能会导致长度不一致的问题,所以我们使用string来保证长度一致,"1"表示有效,"0"表示无效。

声明消息的持久化管理类

#define DATAFILE_SUBFIX ".mqd"
#define TEMPFILE_SUBFIX ".mqd.temp"using MessagePtr = std::shared_ptr<bitmq::Message>;class MessageMapper
{
public:MessageMapper(std::string& basedir, const std::string& qname){ }//1)创建队列消息文件void createMsgFile();//2)删除队列消息文件void removeMsgFile();//3)队列消息的新增持久化void insert(MessagePtr& msg);//4)队列消息的删除持久化void remove(const MessagePtr& msg);//5)提供持久化内容的垃圾回收std::list<MessagePtr> gc();private:    std::string _qname;     // 1)队列名std::string _datafile;  // 2)队列消息的存储文件名std::string _tempfile;  // 3)队列消息的临时交换文件名

该类是数据持久化类,主要功能就是完成向文件中写入数据和删除数据的功能以及进行垃圾回收,垃圾回收的思路是将有效数据先写入临时文件当中,再将源文件删除,最后将临时文件改名为源文件。

声明队列消息管理类

//队列消息管理
class QueueMessage
{
public:QueueMessage(std::string& basedir, const std::string& qname){}// 1)新增消息bool insert(const BasicProperties* bp, const std::string& body);// 2)移除指定消息bool remove(const std::string& msg_id);// 3)获取队首消息(获取的同时将消息加入待确认队列)MessagePtr front();// 4)获取队列待消费消息数量size_t push_count();// 5)获取队列待确认消息数量size_t waitack_count();// 6)获取持久化消息数量size_t durable_count();// 7)总的消息数量size_t total_count();// 8)销毁队列所有消息void clear();private:std::string _qname;   // 1)队列名称size_t _valid_count;  // 2)有效消息数量size_t _total_count;  // 3)已经持久化消息总量MessageMapper _mapper;    // 4)持久化管理句柄std::list<MessagePtr> _msgs;   // 5)待推送消息链表(头插尾删模拟队列)std::unordered_map<std::string, MessagePtr> _durable_msgs;  // 6)持久化消息 hashstd::unordered_map<std::string, MessagePtr> _waitack_msgs;  // 7)待确认消息 hash
};

该类需要完成对消息的管理工作,负责消息的增删查功能。

声明消息总体对外管理类

class MessageManager
{
public:MessageManager(const std::string& basedir) //目录{}//1)初始化新建队列的消息管理句柄,并创建消息存储文件void initQueueMessage(const std::string& qname);// 2)删除队列的消息管理句柄,以及消息存储文件void destroyQueueMessage(const std::string& qname);// 3)向指定队列新增消息bool insert(const std::string& qname, const BasicProperties* bp, const std::string& body, DeliveryMode delivery_mode);// 4)获取指定队列队首消息MessagePtr front(const std::string& qname);// 5)确认指定队列待确认消息void ack(const std::string& qname, const std::string msg_id);// 4)获取队列待消费消息数量size_t getable_count(const std::string& qname);// 5)获取队列待确认消息数量size_t waitack_count(const std::string& qname);// 6)获取持久化消息数量size_t durable_count(const std::string& qname);// 7)总的消息数量size_t total_count(const std::string& qname);private:std::mutex _mutex;std::string _basedir;std::unordered_map<std::string, QueueMessage::ptr> _queue_msgs;
};

MessageManager是总的管理类,他会管理多个队列消息管理类,所以该类的所有成员函数都包含qname字段(队列名称),只有知道是哪个队列才能执行具体操作。



以上是消息管理的框架部分,接下来我们需要完成具体实现。

定义消息持久化类

#define DATAFILE_SUBFIX ".mqd"
#define TEMPFILE_SUBFIX ".mqd.temp"using MessagePtr = std::shared_ptr<bitmq::Message>;
class MessageMapper
{
public:MessageMapper(){}MessageMapper(std::string& basedir, const std::string& qname): _qname(qname){if (basedir.back() != '/')basedir += '/';_datafile = basedir + qname + DATAFILE_SUBFIX;_tempfile = basedir + qname + TEMPFILE_SUBFIX;if (FileHelper(basedir).exists() == false)assert(FileHelper::createDirectory(basedir));createMsgFile();}//1)创建队列消息文件bool createMsgFile(){//已经存在则不需要创建if (FileHelper(_datafile).exists() == true)return true;bool ret = FileHelper::createFile(_datafile);if (ret == false){DLOG("创建队列数据文件 %s 失败", _datafile.c_str());return false;}return true;}//2)删除队列消息文件void removeMsgFile(){FileHelper::removeFile(_datafile);FileHelper::removeFile(_tempfile);}//3)队列消息的新增持久化bool insert(MessagePtr& msg){return insert(_datafile, msg);}//4)队列消息的删除持久化bool remove(const MessagePtr& msg){//1.将msg中的valid修改成'0'(注意这里的valid是string类型)msg->mutable_payload()->set_valid("0");//2.将msg进行序列化std::string body = msg->payload().SerializeAsString();if (body.size() != msg->length()){DLOG("不能修改文件中的数据信息,因为新生成的数据与原数据长度不一致");return false;}//3.将序列化的消息,写入到数据在文件中的指定位置FileHelper helper(_datafile);bool ret = helper.write(body.c_str(), msg->offset(), body.size());if (ret == false){DLOG("向队列数据文件写入数据失败!");return false;}return true;}//5)提供持久化内容的垃圾回收std::list<MessagePtr> gc(){//1.加载出文件中所有的有效数据std::list<MessagePtr> result = load();   DLOG("垃圾回收得到的有效消息数量:%d", result.size());//2.将有效数据,进行序列化存储到临时文件中FileHelper::createFile(_tempfile);for (auto& msg : result){if (insert(_tempfile, msg) == false){DLOG("向临时文件写入消息数据失败!");return result;}}DLOG("垃圾回收后,向临时文件写入数据完毕,临时文件大小:%d", FileHelper(_tempfile).size());//3.删除源文件if (FileHelper::removeFile(_datafile) == false){DLOG("删除源文件失败!");return result;}//4.修改临时文件名,为源文件名称if (FileHelper(_tempfile).renameFile(_datafile) == false){DLOG("修改文件名失败!");return result;}//5.返回新的有效数据return result;}std::list<MessagePtr> load(){std::list<MessagePtr> result;//1.加载出文件中所有的有效数据FileHelper helper(_datafile);size_t offset = 0, msg_size = 0;size_t fsize = helper.size();DLOG("准备开始持久化数据,当前文件大小:%d", FileHelper(_datafile).size());while (offset < fsize){if (helper.read((char*)&msg_size, offset, sizeof(msg_size)) == false){DLOG("读取消息长度失败!!");return {};}offset += sizeof(msg_size);std::string body(msg_size, '\0');if (helper.read(&body[0], offset, msg_size) == false){DLOG("读取消息长度失败!!");return {};}offset += msg_size;//将body反序列化成对象auto nmp = std::make_shared<Message>();nmp->mutable_payload()->ParseFromString(body);//如果该消息无效,则不添加,直接处理下一个if (nmp->payload().valid() == "0")continue;//将有效数据保存至list中result.push_back(nmp);}return result;}private://3)队列消息的新增持久化bool insert(const std::string& filename, MessagePtr& msg){//新增数据都是添加在文件末尾//1.消息的序列化,得到格式化后的消息std::string body = msg->payload().SerializeAsString(); //只需要序列化有效载荷部分//2.获取文件长度FileHelper helper(filename);size_t fsize = helper.size();size_t msize = body.size();//3.先向文件中写入数据长度bool ret = helper.write((char*)&msize, fsize, sizeof(msize));fsize += sizeof(msize);//4.将数据写入文件指定位置ret = helper.write(body.c_str(), fsize, body.size());if (ret == false){DLOG("向队列数据文件写入数据失败!");return false;}//5.更新msg中的实际存储信息msg->set_offset(fsize);msg->set_length(body.size());return true;}private:    std::string _qname;     // 1)队列名std::string _datafile;  // 2)队列消息的存储文件名std::string _tempfile;  // 3)队列消息的临时交换文件名
};
  • 构造函数:构造函数需要传递一个基础目录地址,以及一个队列名称,将来持久化的数据文件将会保存至该目录下,持久化数据文件是以队列名称进行命名的。在构造函数部分会完成目录和文件的创建
  • insert:将传递进来的Message对象写入持久化文件当中,需要注意的是,并不是整个Message对象都需要持久化,只需要持久化其中的Payload部分。还需要注意我们的存储格式,前4个字节存储有效载荷的大小,后面才是真正的有效载荷部分。
  • remove:该函数是要删除文件中的数据,但这个删除并不是真正的删除,而是将对应的数据中的valid标志位设置成0,在垃圾回收时会过滤到无效信息。而想要将valid标志位置0,只能重新构造一个对象,然后将序列化的数据覆盖回源数据。
  • load:将文件中的有效信息读取出来,其实也就是文件中的所有字节流数据反序列化回对象,然后根据对象中的valid标志判断该对象是否是有效信息,如果有就保存起来,没有则丢弃。
  • gc:垃圾回收机制,通过load函数获取所有有效信息,然后将有效信息写入临时文件,再删除源文件,将临时文件重新命名为源文件。

定义队列消息管理类

//队列消息管理
class QueueMessage
{
public:using ptr = std::shared_ptr<QueueMessage>;QueueMessage(std::string& basedir, const std::string& qname): _mapper(basedir, qname), _qname(qname), _valid_count(0), _total_count(0){}void recovery(){std::unique_lock<std::mutex> lock(_mutex);_msgs = _mapper.gc();for (auto& msg : _msgs){_durable_msgs.insert({msg->payload().properties().id(), msg});}_valid_count = _total_count = _durable_msgs.size();}// 1)新增消息bool insert(const BasicProperties* bp, const std::string& body, DeliveryMode delivery_mode){//1.构造对象MessagePtr msg = std::make_shared<Message>();msg->mutable_payload()->set_body(body);if (bp != nullptr){msg->mutable_payload()->mutable_properties()->set_id(bp->id());msg->mutable_payload()->mutable_properties()->set_delivery_mode(bp->delivery_mode());msg->mutable_payload()->mutable_properties()->set_routing_key(bp->routing_key());}else  {msg->mutable_payload()->mutable_properties()->set_id(UUIDHelper::uuid());msg->mutable_payload()->mutable_properties()->set_delivery_mode(delivery_mode);msg->mutable_payload()->mutable_properties()->set_routing_key("");}std::unique_lock<std::mutex> lock(_mutex);//2.判断消息是否需要持久化存储if (msg->payload().properties().delivery_mode() == DeliveryMode::DURABLE){msg->mutable_payload()->set_valid("1");//3.进行持久化存储if (_mapper.insert(msg) == false){DLOG("持久化消息 %s 失败", body.c_str());return false;}_valid_count += 1;_total_count += 1;_durable_msgs.insert({msg->payload().properties().id(), msg});}//4.内存的管理_msgs.push_back(msg);return true;}// 2)移除指定消息(用于收到确认消息后对消息删除)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("没有找到要删除的消息");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;//4.判断是否需要垃圾回收if (_total_count >= 2000 && _valid_count * 1.0 / _total_count <= 0.5){gc();}}//5.删除内存中的信息_waitack_msgs.erase(msg_id);return true;}// 3)获取队首消息(获取的同时将消息加入待确认队列)MessagePtr front(){std::unique_lock<std::mutex> lock(_mutex);//从带推送消息链表中获取一条队首消息if (_msgs.empty()){DLOG("待推送消息队列中没有消息!");return {};}MessagePtr msg = _msgs.front();_msgs.pop_front();if (msg.get() == nullptr)return {};//将该消息添加至待确认hash表中,等到消息得到确认后再删除。_waitack_msgs.insert({msg->payload().properties().id(), msg});return msg;}// 4)获取队列待消费消息数量size_t getable_count(){std::unique_lock<std::mutex> lock(_mutex);return _msgs.size();}// 5)获取队列待确认消息数量size_t waitack_count(){std::unique_lock<std::mutex> lock(_mutex);return _waitack_msgs.size();}// 6)获取持久化消息数量size_t durable_count(){std::unique_lock<std::mutex> lock(_mutex);return _durable_msgs.size();}// 7)总的消息数量size_t total_count(){std::unique_lock<std::mutex> lock(_mutex);return _total_count;  }// 8)销毁队列所有消息void clear(){std::unique_lock<std::mutex> lock(_mutex);_mapper.removeMsgFile();_msgs.clear();_durable_msgs.clear();_waitack_msgs.clear();_valid_count = 0;_total_count = 0;}private:void gc(){//1.进行垃圾回收,获取垃圾回收后,有效消息链表std::list<MessagePtr> msgs = _mapper.gc();for (auto& msg : msgs){auto it = _durable_msgs.find(msg->payload().properties().id());if (it == _durable_msgs.end()){DLOG("垃圾回收后,有一条持久化消息,在内存中没有进行管理!");_durable_msgs.insert({msg->payload().properties().id(), msg});_msgs.push_back(msg); //重新添加到推送链表}//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;   // 1)队列名称size_t _valid_count;  // 2)有效消息数量size_t _total_count;  // 3)已经持久化消息总量MessageMapper _mapper;    // 4)持久化管理句柄std::list<MessagePtr> _msgs;   // 5)待推送消息链表(头插尾删模拟队列)std::unordered_map<std::string, MessagePtr> _durable_msgs;  // 6)持久化消息 hashstd::unordered_map<std::string, MessagePtr> _waitack_msgs;  // 7)待确认消息 hash
};
  • _msgs对象是待推送消息链表,他是负责内存消息的管理,也就是说新增的消息不管是否持久化都会在_msgs中保存一份,将来获取消息时,也是直接从_msgs对象中获取。
  • _durable_msgs是持久化消息hash表,他主要负责文件消息的管理,也就是说它保存了所有持久化的消息,,我们可以通过该结构查看哪些消息被持久化了。
  • _waitack_msgs是待确认消息hash表,当一条消息发送后,就会进入待确认消息hash表中,直到收到了确认消息才会将该消息删除(同时也会删除文件中持久化的消息)
  • insert:三个参数其实就是一条消息的有效载荷部分,在新增消息时如果该消息是要持久化的不要忘记修改_valid_count和_total_count变量,这两个变量是决定是否需要垃圾回收的。
  • front:从待推送消息链表队首获取一条消息,也就是说这条消息被推送给了用户,所以需要将该消息加入待确认消息hash表中,等到消息得到确认后再删除。
  • remove:根据指定的消息id删除对应的消息,如果消息被持久化了,就需要删除持久化信息。

定义队列管理类

class MessageManager
{
public:using ptr = std::shared_ptr<MessageManager>;MessageManager(const std::string& basedir) //目录: _basedir(basedir){//该类在重启时应该将队列管理信息数据从磁盘中读取到内存。//但是需要所有的队列名称//而队列名称获取可以通过mq_queue.hpp中MsgQueueManager的getAll获取//但是这样做会将两个模块耦合在一起,所以在这里不用恢复,而是将该工作放到虚拟机部分完成}void clear(){std::unique_lock<std::mutex> lock(_mutex);for (auto& qmsg : _queue_msgs)qmsg.second->clear();}//1)初始化新建队列的消息管理句柄,并创建消息存储文件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({qname, qmp});}qmp->recovery();}// 2)删除队列的消息管理句柄,以及消息存储文件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(qname);}qmp->clear();}// 3)向指定队列新增消息bool insert(const std::string& qname, const BasicProperties* bp, const std::string& body, DeliveryMode delivery_mode){QueueMessage::ptr qmp;{std::unique_lock<std::mutex> lock(_mutex);auto it = _queue_msgs.find(qname);if (it == _queue_msgs.end()){DLOG("向队列%s新增消息失败:没有找到消息句柄!", qname.c_str());return false;}qmp = it->second;}qmp->insert(bp, body, delivery_mode);return true;}// 4)获取指定队列队首消息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 {};}qmp = it->second;}return qmp->front();}// 5)确认指定队列待确认消息void ack(const std::string& qname, const std::string msg_id){QueueMessage::ptr qmp;{std::unique_lock<std::mutex> lock(_mutex);auto it = _queue_msgs.find(qname);if (it == _queue_msgs.end()){DLOG("确认队列%s中%s消息失败:没有找到消息句柄!", qname.c_str(), msg_id.c_str());return;}qmp = it->second;}qmp->remove(msg_id);return;}// 4)获取队列待消费消息数量size_t getable_count(const std::string& qname){QueueMessage::ptr qmp;{std::unique_lock<std::mutex> lock(_mutex);auto it = _queue_msgs.find(qname);if (it == _queue_msgs.end()){DLOG("获取队列%s中待推送消息失败:没有找到消息句柄!", qname.c_str());return 0;}qmp = it->second;}return qmp->getable_count();}// 5)获取队列待确认消息数量size_t waitack_count(const std::string& qname){QueueMessage::ptr qmp;{std::unique_lock<std::mutex> lock(_mutex);auto it = _queue_msgs.find(qname);if (it == _queue_msgs.end()){DLOG("获取队列%s中待确认消息失败:没有找到消息句柄!", qname.c_str());return 0;}qmp = it->second;}return qmp->waitack_count();}// 6)获取持久化消息数量size_t durable_count(const std::string& qname){QueueMessage::ptr qmp;{std::unique_lock<std::mutex> lock(_mutex);auto it = _queue_msgs.find(qname);if (it == _queue_msgs.end()){DLOG("获取队列%s中持久化u消息失败:没有找到消息句柄!", qname.c_str());return 0;}qmp = it->second;}return qmp->durable_count();}// 7)总的消息数量size_t total_count(const std::string& qname){QueueMessage::ptr qmp;{std::unique_lock<std::mutex> lock(_mutex);auto it = _queue_msgs.find(qname);if (it == _queue_msgs.end()){DLOG("获取队列%s中总消息数量失败:没有找到消息句柄!", qname.c_str());return 0;}qmp = it->second;}return qmp->total_count();}private:std::mutex _mutex;std::string _basedir;std::unordered_map<std::string, QueueMessage::ptr> _queue_msgs;
};

值得说明的是,该类的构造函数理论上来说需要从恢复所有的持久化队列消息管理类,通过QueueMessage中的recovevry函数就可以恢复,但是想要做到这一点就需要队列的名称,也就是说我们需要所有队列的名称才可以恢复数据,而所有队列的名称信息在mq_queue.hpp中的getAllQueues函数中获取。

如果这样做会导致模块与模块间的耦合性较强,不利于封装思想,所以我们并不是在这个地方完成数据恢复功能,而是在虚拟机管理处手动调用initQueueMessage进行恢复。

为什么MessageManager类所有成员函数都是下面的写法二而不是写法一

写法一:

//1)初始化新建队列的消息管理句柄,并创建消息存储文件
void initQueueMessage(const std::string& qname)
{std::unique_lock<std::mutex> lock(_mutex);auto it = _queue_msgs.find(qname);if (it != _queue_msgs.end())return;QueueMessage::ptr qmp = std::make_shared<QueueMessage>(_basedir, qname);_queue_msgs.insert({qname, qmp});qmp->recovery();
}

前面我们写的所有需要加锁的类几乎都是这样写的(即使用锁锁住整个函数,当前函数调用完才会释放锁),而MessageManager类却不能这样写。

写法二:

//1)初始化新建队列的消息管理句柄,并创建消息存储文件
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({qname, qmp});}qmp->recovery();
}

可以看到两者唯一的区别就是qmp->recovery()这句话是否被锁住了,下面这种写法更好的原因在于QueueMessage类内部就有一把锁,已经可以保证线程安全了,如果在将这句话也锁住,会导致锁冲突问题,而recovery的恢复数据的速率较慢,会影响其他线程的操作,所以这句话可以不被锁管理,大大提高了多进程并发效率。



单元测试:

测试插入消息功能

#include "../../mqserver/mq_message.hpp"
#include <gtest/gtest.h>bitmq::MessageManager::ptr msgp;class MessageTest : public testing::Environment
{
public:virtual void SetUp() override{msgp = std::make_shared<bitmq::MessageManager>("./data/message/");msgp->initQueueMessage("queue1");}virtual void TearDown() override {//msgp->clear();}
};TEST(message_test, insert_test)
{bitmq::BasicProperties bp;bp.set_id(bitmq::UUIDHelper::uuid());bp.set_delivery_mode(bitmq::DeliveryMode::DURABLE);bp.set_routing_key("news.music.pop");msgp->insert("queue1", &bp, "Hello World-1", bitmq::DeliveryMode::DURABLE);msgp->insert("queue1", nullptr, "Hello World-2", bitmq::DeliveryMode::DURABLE);msgp->insert("queue1", nullptr, "Hello World-3", bitmq::DeliveryMode::DURABLE);msgp->insert("queue1", nullptr, "Hello World-4", bitmq::DeliveryMode::UNDURABLE);msgp->insert("queue1", nullptr, "Hello World-5", bitmq::DeliveryMode::DURABLE);msgp->insert("queue1", nullptr, "Hello World-6", bitmq::DeliveryMode::DURABLE);ASSERT_EQ(msgp->total_count("queue1"), 5);ASSERT_EQ(msgp->durable_count("queue1"), 5);ASSERT_EQ(msgp->getable_count("queue1"), 6);ASSERT_EQ(msgp->waitack_count("queue1"), 0);
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);testing::AddGlobalTestEnvironment(new MessageTest);RUN_ALL_TESTS();return 0;
}

在该测试中我们插入6条消息,其中5条是持久化的,1条是非持久化的。

执行结果:

执行结果没有问题,我们再来看一下文件大小

大小为356,说明数据被成功写入文件中了。

测试消息读取功能

TEST(message_test, insert_test)
{bitmq::BasicProperties bp;bp.set_id(bitmq::UUIDHelper::uuid());bp.set_delivery_mode(bitmq::DeliveryMode::DURABLE);bp.set_routing_key("news.music.pop");msgp->insert("queue1", &bp, "Hello World-1", bitmq::DeliveryMode::DURABLE);msgp->insert("queue1", nullptr, "Hello World-2", bitmq::DeliveryMode::DURABLE);msgp->insert("queue1", nullptr, "Hello World-3", bitmq::DeliveryMode::DURABLE);msgp->insert("queue1", nullptr, "Hello World-4", bitmq::DeliveryMode::UNDURABLE);msgp->insert("queue1", nullptr, "Hello World-5", bitmq::DeliveryMode::DURABLE);msgp->insert("queue1", nullptr, "Hello World-6", bitmq::DeliveryMode::DURABLE);ASSERT_EQ(msgp->total_count("queue1"), 5);ASSERT_EQ(msgp->durable_count("queue1"), 5);ASSERT_EQ(msgp->getable_count("queue1"), 6);ASSERT_EQ(msgp->waitack_count("queue1"), 0);
}TEST(message_test, select_test)
{bitmq::MessagePtr msg1 = msgp->front("queue1");ASSERT_NE(msg1.get(), nullptr);ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));ASSERT_EQ(msgp->getable_count("queue1"), 5);ASSERT_EQ(msgp->waitack_count("queue1"), 1);bitmq::MessagePtr msg2 = msgp->front("queue1");ASSERT_NE(msg2.get(), nullptr);ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));ASSERT_EQ(msgp->getable_count("queue1"), 4);ASSERT_EQ(msgp->waitack_count("queue1"), 2);bitmq::MessagePtr msg3 = msgp->front("queue1");ASSERT_NE(msg3.get(), nullptr);ASSERT_EQ(msg3->payload().body(), std::string("Hello World-3"));ASSERT_EQ(msgp->getable_count("queue1"), 3);ASSERT_EQ(msgp->waitack_count("queue1"), 3);bitmq::MessagePtr msg4 = msgp->front("queue1");ASSERT_NE(msg4.get(), nullptr);ASSERT_EQ(msg4->payload().body(), std::string("Hello World-4"));ASSERT_EQ(msgp->getable_count("queue1"), 2);ASSERT_EQ(msgp->waitack_count("queue1"), 4);bitmq::MessagePtr msg5 = msgp->front("queue1");ASSERT_NE(msg5.get(), nullptr);ASSERT_EQ(msg5->payload().body(), std::string("Hello World-5"));ASSERT_EQ(msgp->getable_count("queue1"), 1);ASSERT_EQ(msgp->waitack_count("queue1"), 5);bitmq::MessagePtr msg6 = msgp->front("queue1");ASSERT_NE(msg6.get(), nullptr);ASSERT_EQ(msg6->payload().body(), std::string("Hello World-6"));ASSERT_EQ(msgp->getable_count("queue1"), 0);ASSERT_EQ(msgp->waitack_count("queue1"), 6);bitmq::MessagePtr msg7 = msgp->front("queue1");ASSERT_EQ(msg7.get(), nullptr);
}

删除数据文件(上一次执行完后,文件中应该保存5条消息,而再次执行insert_test后,又会新增6条消息,造成结果误差,所以我们就以第一次插入来测试查询功能),重新执行上面的内容。

运行结果:

查询功能没有问题,符合预期结果。

测试历史消息恢复功能

TEST(message_test, recovery_test)
{bitmq::MessagePtr msg1 = msgp->front("queue1");ASSERT_NE(msg1.get(), nullptr);ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));//只持久化了五个数据,重启系统后,可推送消息只有5个。DLOG("msg1的uuid为: %s   body: %s   valid: %s  routing_key: %s", msg1->payload().properties().id().c_str(), msg1->payload().body().c_str(), msg1->payload().valid().c_str(), msg1->payload().properties().routing_key().c_str());ASSERT_EQ(msgp->getable_count("queue1"), 4);ASSERT_EQ(msgp->waitack_count("queue1"), 1);bitmq::MessagePtr msg2 = msgp->front("queue1");ASSERT_NE(msg2.get(), nullptr);ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));DLOG("msg1的uuid为: %s   body: %s   valid: %s  routing_key: %s", msg2->payload().properties().id().c_str(), msg2->payload().body().c_str(), msg2->payload().valid().c_str(), msg2->payload().properties().routing_key().c_str());ASSERT_EQ(msgp->getable_count("queue1"), 3);ASSERT_EQ(msgp->waitack_count("queue1"), 2);bitmq::MessagePtr msg3 = msgp->front("queue1");ASSERT_NE(msg3.get(), nullptr);ASSERT_EQ(msg3->payload().body(), std::string("Hello World-3"));ASSERT_EQ(msgp->getable_count("queue1"), 2);ASSERT_EQ(msgp->waitack_count("queue1"), 3);bitmq::MessagePtr msg4 = msgp->front("queue1");ASSERT_NE(msg4.get(), nullptr);ASSERT_EQ(msg4->payload().body(), std::string("Hello World-5"));ASSERT_EQ(msgp->getable_count("queue1"), 1);ASSERT_EQ(msgp->waitack_count("queue1"), 4);bitmq::MessagePtr msg5 = msgp->front("queue1");ASSERT_NE(msg5.get(), nullptr);ASSERT_EQ(msg5->payload().body(), std::string("Hello World-6"));ASSERT_EQ(msgp->getable_count("queue1"), 0);ASSERT_EQ(msgp->waitack_count("queue1"), 5);bitmq::MessagePtr msg6 = msgp->front("queue1");ASSERT_EQ(msg6.get(), nullptr);
}

前面的代码执行完后,文件中保存了5条消息,那么直接执行上面的代码,会恢复历史数据,我们依次读取消息,判断读取的消息是否完整,数据是否符合预期。

由于恢复历史消息会自动执行一次垃圾回收,所以可以看到会创建临时文件,并向其中写入数据。

经过测试可以发现消息恢复功能也没有问题。

测试删除消息功能

TEST(message_test, remove_test)
{bitmq::MessagePtr msg1 = msgp->front("queue1");ASSERT_NE(msg1.get(), nullptr);ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));ASSERT_EQ(msgp->getable_count("queue1"), 4);ASSERT_EQ(msgp->waitack_count("queue1"), 1);msgp->ack("queue1", msg1->payload().properties().id());ASSERT_EQ(msgp->waitack_count("queue1"), 0);ASSERT_EQ(msgp->durable_count("queue1"), 4);ASSERT_EQ(msgp->total_count("queue1"), 4);
}

执行结果:

我们再来测试一下删除后重启服务器时,该消息会不会真正被删除(重启服务器会进行一次垃圾回收)

TEST(message_test, gc_test)
{ASSERT_EQ(msgp->getable_count("queue1"), 4);ASSERT_EQ(msgp->waitack_count("queue1"), 0);bitmq::MessagePtr msg1 = msgp->front("queue1");ASSERT_NE(msg1.get(), nullptr);ASSERT_EQ(msg1->payload().body(), std::string("Hello World-2"));//只持久化了五个数据,重启系统后,可推送消息只有5个。ASSERT_EQ(msgp->getable_count("queue1"), 3);ASSERT_EQ(msgp->waitack_count("queue1"), 1);
}

执行结果:

执行结果成功,并且源文件和临时文件的大小不同,说明消息被成功删除了。



总结:

消息模块的编写难度上是比较大的,本人在测试时也遇到了不少的问题,大家在遇到困难时不要害怕和焦虑,而是耐下心来一步步的调试找错,这个过程也是很锻炼我们的能力,世上无难事,只怕有心人,下一个模块不见不散。

8.8 虚拟机管理

虚拟机模块是对上述三个数据管理模块的整合,并基于数据之间的关联关系进行联合操作。

1. 定义虚拟机类包含以下成员:

  • 1)交换机数据管理模块句柄
  • 2)队列数据管理模块句柄
  • 3)绑定数据管理模块句柄
  • 4)消息数据管理模块句柄

2. 虚拟机包含操作:

  • 1)提供声明交换机的功能(存在则 OK,不存在则创建)
  • 2)提供删除交换机的功能(删除交换机的同时删除关联绑定信息)
  • 3)提供声明队列的功能(存在则 OK,不存在则创建,创建的同时创建队列关联消息管理对象)
  • 4)提供删除队列的功能(删除队列的同时删除关联绑定信息,删除关联消息管理对象及队列所有消息)
  • 5)提供交换机-队列绑定的功能
  • 6)提供交换机-队列解绑的功能
  • 7)提供获取交换机相关的所有绑定信息功能
  • 8)提供新增消息的功能
  • 9)提供获取指定队列队首消息的功能
  • 10)提供消息确认删除的功能

3. 虚拟机管理操作:

  • 增删查

注意:本项目为了简化只有一个虚拟机,所以虚拟机的增删查功能就不实现了,想要实现也比较简单,类似交换机/队列/绑定,添加一个数据库,然后创建一个虚拟机持久化管理类负责对虚拟机数据进行持久化,再创建一个虚拟机负责对外提供总的虚拟机管理操作。



声明交换机管理类

class VirtualHost
{
public:VirtualHost(const std::string& basedir, const std::string& dbfile);// 1)提供声明交换机的功能(存在则 OK,不存在则创建)bool declareExchange(const std::string& name, bitmq::ExchangeType type, bool durable, bool auto_delete, std::unordered_map<std::string, std::string> args);// 2)提供删除交换机的功能(删除交换机的同时删除关联绑定信息)  void deleteExchange(const std::string& name);// 3)提供声明队列的功能(存在则 OK,不存在则创建,创建的同时创建队列关联消息管理对象)bool declareQueue(const std::string& name, bool durable, bool exclusive, bool auto_delete, std::unordered_map<std::string, std::string> args);// 4)提供删除队列的功能(删除队列的同时删除关联绑定信息,删除关联消息管理对象及队列所有消息)  void deleteQueue(const std::string& name);// 5)提供交换机-队列绑定的功能void Bind(const std::string& ename, const std::string& qname, const std::string& key, bool durable);// 6)提供交换机-队列解绑的功能void unBind(const std::string& ename, const std::string& qname);// 7)提供获取交换机相关的所有绑定信息功能MsgQueueBindingMap exchangeBindings(const std::string& ename);// 8)提供新增消息的功能bool basicPublish(const std::string& qname, const BasicProperties* bp, const std::string& body, DeliveryMode delivery_mode);// 9)提供获取指定队列队首消息的功能    MessagePtr basicConsume(const std::string& qname);// 10)提供消息确认删除的功能bool basicAck(const std::string& ename, const std::string& qname);private:ExchangeManager::ptr _emp;MsgQueueManager::ptr _mqmp;BindingManager::ptr _bmp;MessageManager::ptr _mmp;
};

定义交换机管理类

class VirtualHost
{
public:using ptr = std::shared_ptr<VirtualHost>;VirtualHost(const std::string& hostname, const std::string& basedir, const std::string& dbfile): _host_name(hostname), _emp(std::make_shared<ExchangeManager>(dbfile)), _mqmp(std::make_shared<MsgQueueManager>(dbfile)), _bmp(std::make_shared<BindingManager>(dbfile)), _mmp(std::make_shared<MessageManager>(basedir)){ //交换机/队列/绑定管理类会在初始化时从数据库中恢复数据//但是消息管理类不会进行该操作,所以我们需要手动完成消息的历史消息恢复auto mq = _mqmp->getAllQueues();for (auto& [qname, msgqueue] : mq){_mmp->initQueueMessage(qname);}}// 1)提供声明交换机的功能(存在则 OK,不存在则创建)bool declareExchange(const std::string& name, bitmq::ExchangeType type, bool durable, bool auto_delete, std::unordered_map<std::string, std::string> args){return _emp->declareExchange(name, type, durable, auto_delete, args);}// 2)提供删除交换机的功能(删除交换机的同时删除关联绑定信息)  void deleteExchange(const std::string& name){//1.删除绑定交换机_emp->deleteExchange(name);//2.删除绑定信息_bmp->removeExchangeBindings(name);}// 3)判断交换机是否存在bool existsExchange(const std::string& ename){return _emp->exists(ename);}// 4)提供声明队列的功能(存在则 OK,不存在则创建,创建的同时创建队列关联消息管理对象)bool declareQueue(const std::string& qname, bool durable, bool exclusive, bool auto_delete, std::unordered_map<std::string, std::string> args){//初始化队列消息句柄(消息的存储管理)_mmp->initQueueMessage(qname);//创建队列return _mqmp->declareQueue(qname, durable, exclusive, auto_delete, args);}// 5)提供删除队列的功能(删除队列的同时删除关联绑定信息,删除关联消息管理对象及队列所有消息)  void deleteQueue(const std::string& qname){//删除队列消息管理句柄_mmp->destroyQueueMessage(qname);//删除队列相关的绑定信息_bmp->removeMsgQueueBindings(qname);return _mqmp->deleteQueue(qname);}// 6)判断队列是否存在bool existsQueue(const std::string& qname){return _mqmp->exists(qname);}// 7)提供交换机-队列绑定的功能bool bind(const std::string& ename, const std::string& qname, const std::string& key){//绑定条件:交换机和队列必须存在)Exchange::ptr ep = _emp->selectExchange(ename);if (ep.get() == nullptr){DLOG("进行队列绑定失败,交换机%s不存在", ename.c_str());return false;}MsgQueue::ptr qp = _mqmp->selectQueue(qname);if (qp.get() == nullptr){DLOG("进行队列绑定失败,队列%s不存在", qname.c_str());return false;}//交换机是否需要持久化取决于交换机和队列是否持久化_bmp->bind(ename, qname, key, ep->durable && qp->durable);}// 8)提供交换机-队列解绑的功能void unBind(const std::string& ename, const std::string& qname){return _bmp->unBind(ename, qname);}// 9)判断绑定是否存在bool existsBinding(const std::string& ename, const std::string& qname){return _bmp->exists(ename, qname);}// 10)提供获取交换机相关的所有绑定信息功能MsgQueueBindingMap exchangeBindings(const std::string& ename){return _bmp->getExchangeBindings(ename);}// 11)提供新增消息的功能bool basicPublish(const std::string& qname, const BasicProperties* bp, const std::string& body){MsgQueue::ptr qp = _mqmp->selectQueue(qname);if (qp.get() == nullptr){DLOG("进行队列绑定失败,队列%s不存在", qname.c_str());return false;}return _mmp->insert(qname, bp, body, qp->durable);}// 12)提供获取指定队列队首消息的功能    MessagePtr basicConsume(const std::string& qname){return _mmp->front(qname);}// 13)提供消息确认删除的功能void basicAck(const std::string& qname, const std::string msg_id){return _mmp->ack(qname, msg_id);}// 14)清除void clear(){_emp->clear();_mqmp->clear();_bmp->clear();_mmp->clear();}private:std::string _host_name;ExchangeManager::ptr _emp;MsgQueueManager::ptr _mqmp;BindingManager::ptr _bmp;MessageManager::ptr _mmp;
};

该类写起来还是比较简单的,因为所有的功能前面都已经实现过了,这里只是将他们进行整合。



单元测试:

在前面我们完成了每个模块的单元测试,所以到这里基本上不会出现什么问题了,为了保险起见,我们还是进行简单的测试。

#include "../../mqserver/mq_virtualhost.hpp"
#include <gtest/gtest.h>using empty_map = std::unordered_map<std::string, std::string>;class HostTest : public testing::Test
{
public:virtual void SetUp() override{_host = std::make_shared<bitmq::VirtualHost>("host1", "./data/host1/message", "./data/host1/host1.db");_host->declareExchange("exchange1", bitmq::ExchangeType::DIRECT, true, false,  empty_map());_host->declareExchange("exchange2", bitmq::ExchangeType::DIRECT, true, false,  empty_map());_host->declareExchange("exchange3", bitmq::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", "new.music.#");_host->bind("exchange1", "queue2", "new.music.#");_host->bind("exchange1", "queue3", "new.music.#");_host->bind("exchange2", "queue1", "new.music.#");_host->bind("exchange2", "queue2", "new.music.#");_host->bind("exchange2", "queue3", "new.music.#");_host->bind("exchange3", "queue1", "new.music.#");_host->bind("exchange3", "queue2", "new.music.#");_host->bind("exchange3", "queue3", "new.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");}virtual void TearDown() override {_host->clear();}
public:bitmq::VirtualHost::ptr _host;
};TEST_F(HostTest, init_test)
{ASSERT_EQ(_host->existsExchange("exchange1"), true);ASSERT_EQ(_host->existsExchange("exchange2"), true);ASSERT_EQ(_host->existsExchange("exchange3"), true);ASSERT_EQ(_host->existsQueue("queue1"), true);ASSERT_EQ(_host->existsQueue("queue2"), true);ASSERT_EQ(_host->existsQueue("queue3"), true);ASSERT_EQ(_host->existsBinding("exchange1", "queue1"), true);ASSERT_EQ(_host->existsBinding("exchange1", "queue2"), true);ASSERT_EQ(_host->existsBinding("exchange1", "queue3"), true);ASSERT_EQ(_host->existsBinding("exchange2", "queue1"), true);ASSERT_EQ(_host->existsBinding("exchange2", "queue2"), true);ASSERT_EQ(_host->existsBinding("exchange2", "queue3"), true);ASSERT_EQ(_host->existsBinding("exchange3", "queue1"), true);ASSERT_EQ(_host->existsBinding("exchange3", "queue2"), true);ASSERT_EQ(_host->existsBinding("exchange3", "queue3"), true);bitmq::MessagePtr msg1 = _host->basicConsume("queue1");ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));bitmq::MessagePtr msg2 = _host->basicConsume("queue1");ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));bitmq::MessagePtr msg3 = _host->basicConsume("queue1");ASSERT_EQ(msg3->payload().body(), std::string("Hello World-3"));bitmq::MessagePtr msg4 = _host->basicConsume("queue1");ASSERT_EQ(msg4.get(), nullptr);
}TEST_F(HostTest, remove_exchange)
{_host->deleteExchange("exchange1");ASSERT_EQ(_host->existsBinding("exchange1", "queue1"), false);ASSERT_EQ(_host->existsBinding("exchange1", "queue2"), false);ASSERT_EQ(_host->existsBinding("exchange1", "queue3"), false);
}TEST_F(HostTest, remove_queue)
{_host->deleteQueue("queue1");ASSERT_EQ(_host->existsBinding("exchange1", "queue1"), false);ASSERT_EQ(_host->existsBinding("exchange2", "queue1"), false);ASSERT_EQ(_host->existsBinding("exchange3", "queue1"), false);bitmq::MessagePtr msg1 = _host->basicConsume("queue1");ASSERT_EQ(msg1.get(), nullptr);
}TEST_F(HostTest, ack_message)
{bitmq::MessagePtr msg1 = _host->basicConsume("queue1");ASSERT_EQ(msg1->payload().body(), std::string("Hello World-1"));_host->basicAck(std::string("queue1"), msg1->payload().properties().id());bitmq::MessagePtr msg2 = _host->basicConsume("queue1");ASSERT_EQ(msg2->payload().body(), std::string("Hello World-2"));_host->basicAck(std::string("queue1"), msg2->payload().properties().id());bitmq::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;
}

上面的测试用例包含了:插入查找,删除功能的测试。

测试结果:

8.9 交换机路由管理

客户端将消息发布到指定的交换机,交换机这时候要考虑这条数据该放入到哪些与自己绑定的队列中,而这个考量是通过交换机类型以及匹配规则来决定的:

交换机类型:

  • 1. 广播交换:直接将消息交给所有绑定的队列,无需匹配
  • 2. 直接交换:队列绑定信息中的 binding_key 与消息中的 routing_key 一致则匹配成功,否则失败。
  • 3. 主题交换:按照一定的匹配规则进行匹配

其中广播交换和直接交换,都非常简单,唯一较为难以理解的是主题交换。

在这里我们需要先对 binding_key 和 routing_key 作以了解:

binding_key

是由数字字母下划线构成的, 并且使用 . 分成若干部分,并支持 * 和 # 通配符

例如: news.music.#,这用于表示交换机绑定的当前队列是一个用于发布音乐新闻的队列。

  • 支持 * 和 # 两种通配符, 但是 * # 只能作为 . 切分出来的独立部分, 不能和其他数字字母混用,
    • 比如 a.*.b 是合法的, a.*a.b 是不合法的
    • * 可以匹配任意一个单词(注意是单词不是字母)
    • # 可以匹配零个或者多个任意单词(注意是单词不是字母)
  • 一个单词中不能既出现 * 又出现 #, 也就是,一个单词中只能有一个通配符,且必须独立存在
  • # 通配符两边不能出现其他通配符,因为 # 可以匹配任意多个任意单词,因此连续出现是没有意义的。

routing_key

是由数据、字母和下划线构成, 并且可以使用 . 划分成若干部分。

例如: news.music.pop,这用于表示当前发布的消息是一个流行音乐的新闻.

比如,在进行队列绑定时,某队列的 binding_key 约定为: news.music.#表示这个队列用于发布音乐新闻。而这时候客户端发布了一条消息,其中 routing_key 为: news.music.pop 则可以匹配成功,而如果发布消息的 routing_key 为: news.sport.football,这时候就会匹配失败。

匹配算法

定义一个二维数组来标记每次匹配的结果,通过最终数组末尾位置的结果来查看是否整体匹配成功。

使用 routing_key 中的每个单词,与 binding_key 中的单词进行逐个匹配,根据匹配结果来标记数组内容,最终以数组中的末尾标记来确定是否匹配成功。

该动态规划的核心主要在推导递推公式, 下面我们通过几个示例来推导递推公式。


示例 1:

binding_key = "bbb.ddd"; routing_key = "aaa.ddd"

定义:dp[2][2]

拿routing_key和binding_key每个单词进行比较,如果单词相同,则对应位置置1。

这个例子中虽然结尾标志为1,但是很明显匹配不成功,我们需要理解一个关键点:当一个routing_key单词与binding_key单词匹配成功,则应该继承上一个单词(上一行和上一列)的匹配结果,需要考虑父级单词是否匹配成功,只有父级匹配成功了,本次匹配才有意义。


示例 2:

binding_key = "aaa.ddd"; routing_key = "aaa.ddd"

定义:dp[2][2]

这个例子中,匹配就是成功的,因此我们可以得到第一个公式:

单词匹配成功: dp[i][j] = dp[i - 1][j - 1]

但是,在将思想转换为代码时,我们考虑当 aaa 匹配成功时,从左上继承结果,但是这时候是没有左上位置的,因此对于代码的逻辑就出现了一个例外的点(代码处理额外增加了难度)。

因此,为了便于将思想转换为代码,因此我们的数组大小定义行列分别额外多申请一行一列,并将 dp[0][0]位置置 1

这样初始将 dp[0][0] 位置置 1, 其他数组位置全部置 0; 这样只要单词匹配成功,则从左上位置继承结果。


示例 3: #通配符的特殊

binding_key = "#"; routing_key = "aaa.bbb"

从这个例子中,能看出,当出现#通配符的时候是比较特殊的,如果 bbb 与#匹配成功的时候,从左上继承结果,得到的结果是 0,匹配失败,但是实际结果应该是成功的。

因此,得出结论:当遇到通配符 # 时,不仅从左上继承结果,还可以从上一个单词与#的匹配结果处(左边)继承。

既: dp[i][j] = dp[i - 1][j - 1] | dp[i][j - 1] ;


示例 4: #通配符的特殊

binding_key = "aaa.#"; routing_key = "aaa"

从上例中,看出,当 aaa 与#匹配成功时,从左边和左上继承的结果这时候都是 0,这也是不合理的。

结论,因此当遇到 # 通配符匹配成功时,不仅从 左上,左边继承结果,也可以从上方继承结果。

既: dp[i][j] = dp[i - 1][j - 1] | dp[i][j - 1] | dp[i - 1][j];


示例 4: #通配符的特殊

binding_key = "#.aaa"; routing_key = "aaa";

观察上述例子,当 aaa 匹配成功时,从左上继承匹配结果,这时候继承到的是 0 ,这是有问题的。因此,当 binding_key 中以起始行以#开始时,应该将起始行的第 0 列置为 1,以便于后边的匹配结果继承。



实际案例1:

binding_key = "#.ccc.bbb.*.bbb", routing_key = "aaa.ddd.ccc.bbb.eee.bbb"。

  • 定义状态: dp[i][j]表示 routing_tokens 的前 i 个字符串 能否与 binding_tokens 前 j 个字符串匹配
  • 初始值: dp[0][0] = true

初始状态:

第一轮匹配:

第二轮匹配:

第三轮匹配:

同样的道理, bbb 匹配成功,非统配符匹配成功, 则从左上继承结果

第四轮匹配:

这一轮遇到的是*通配符,而*表示任意一个单词,它与正常的单词匹配成功并没有区别,从左上继承结果。

第五轮匹配:

这一轮中,第一个 routing_key 中的 bbb 匹配成功,但是其继承结果为 0,而第二个bbb 匹配成功时,继承结果为 1


实际案例2:

  • binding_key: aaa.#.bbb.*.ddd
  • routing_key: aaa.ddd.aaa.bbb.eee.ddd

第一轮匹配:

第二轮匹配:

这一轮匹配中, aaa 遇到#时,就用到了从上方继承结果。其他的从左边继承结果。

第三轮匹配:

这一轮只有 bbb 匹配成功,从左上继承到 true

第四轮匹配:

这一轮虽然所有单词与*都可以匹配成功,但是继承结果时,只有 eee 继承到左上 true

第五轮匹配:


总结:

匹配算法思想:

  • 1.对binding key和routing key进行单词分割
  • 2.按照元素个数,定义出对应大小+1的二维数组,并且将dp[0][0]位置置1
  • 3.使用routing key的每个元素,与binding key的每个元素进行比对,在二维数组中进行标记标记规则:
    • 1.当两个单词匹配成功时:从左上方继承结果 dp[i][j] = dp[i -1][j-1];
    • 2.当遇到#通配符匹配成功时:不仅可以从左上方继承结果,还可以从左方继承结果 dp[i][j]=dp[i-1][j-1] | dp[i][j-1];
    • 3.当遇到#通配符匹配成功时:不仅可以从左上,左方继承结果,还可以从上方继承结果 dp[i][j]=dp[i-1][j-1] | dp[i][j-1] | dp[i-1][j];
    • 4.当binding key以#起始时:需要将#对应行的第0列置为1

路由交换模块

功能:判断一个消息中的routing_key与队列的binding_key是否匹配成功。

取决要素有两个:交换机类型,routing_key与binding_key的匹配

因此基于功能需求分析:路由交换模块只需要对传入的数据进行处理即可,因此这个模块要实现的实际上是一个功能类接口(没有成员)

提供的功能:

  • 1)判断routing_key是否合法:必须是a~z,A~Z,0~9,* # . _ 组成
  • 2)判断binding_key是否合法:
    • 1. 必须是a~z,A~Z,0~9,* # . _ 组成
    • 2.  * #是通配符,必须独立存在,*和#不能连续出现
  • 3)进行routing_key与binding_key的路由匹配
    • 1.广播:不管如何都是成功的
    • 2.直接:相等则成功
    • 3.主题:按匹配模式,完成了则成功

声明路由匹配模块

class Router
{
public://判断routing_key是否合法static bool isLegalRoutingKey(const std::string& routing_key);//判断binding_key是否合法static bool isLegalBindingKey(const std::string& binding_key);//路由匹配static bool route(ExchangeType type, const std::string& routing_key, const std::string& binding_key);
};

定义路由匹配模块

class Router
{
public://判断routing_key是否合法static bool isLegalRoutingKey(const std::string& routing_key){//1.routing_key:判断是否包含非法字符for (auto& ch : routing_key){if ((ch >= 'a' && ch <= 'z') ||(ch >= 'A' && ch <= 'Z') ||(ch >= '0' && ch <= '9') ||(ch == '_' || ch == '.')){continue;}return false;}return true;}//判断binding_key是否合法static bool isLegalBindingKey(const std::string& binding_key){//1.routing_key:判断是否包含非法字符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;StringHelper::splite(binding_key, ".", sub_words);for (auto& word : sub_words){if (word.size() > 1 && (word.find("*") != std::string::npos ||word.find("#") != std::string::npos))return false;}//3. #两边不能出现通配符for (int i = 1; i < sub_words.size(); i++){if ((sub_words[i] == "*" && sub_words[i-1] == "#") ||(sub_words[i] == "#" && sub_words[i-1] == "*") ||(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){if (type == ExchangeType::FANOUT){return true;}else if (type == ExchangeType::DIRECT){return routing_key == binding_key;}else{//主题交换://1.将binding_key与routiing_key进行字符串分割,得到各个的单词数组std::vector<std::string> bkeys, rkeys;int n_rkey = StringHelper::splite(routing_key, ".", rkeys);int n_bkey = StringHelper::splite(binding_key, ".", bkeys);//2.定义标记数组,并初始化[0][0]位置为true,其他位置为falsestd::vector<std::vector<bool>> dp(n_bkey + 1, std::vector<bool>(n_rkey + 1, false));dp[0][0] = true; //3.如果binding_key以#起始,则将#对应行的第0列置为1if (bkeys[0] == "#")dp[1][0] = true;//4.使用rouing_key中的每个单词与binding_key中的每个单词进行匹配for (int i = 1; i <= n_bkey; i++){for (int j = 1; j <= n_rkey; 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] == "#"){dp[i][j] = dp[i-1][j-1] | dp[i][j-1] | dp[i-1][j];}}}return dp[n_bkey][n_rkey];}}
};

单元测试:

#include "../../mqserver/mq_route.hpp"
#include <gtest/gtest.h>class BindingTest : public testing::Environment
{
public:virtual void SetUp() override{ }virtual void TearDown() override { }
};TEST(route_test, legal_routing_key)
{std::string rkey1 = "new.music.pop";std::string rkey2 = "new..music.pop";std::string rkey3 = "news.,music.pop";std::string rkey4 = "news.music_123.pop";ASSERT_EQ(bitmq::Router::isLegalRoutingKey(rkey1), true);ASSERT_EQ(bitmq::Router::isLegalRoutingKey(rkey2), true);ASSERT_EQ(bitmq::Router::isLegalRoutingKey(rkey3), false);ASSERT_EQ(bitmq::Router::isLegalRoutingKey(rkey4), true);
}TEST(route_test, legal_binding_key)
{std::string bkey1 = "news.music.pop";std::string bkey2 = "news.#.music.pop";std::string bkey3 = "news.#.*.music.pop";std::string bkey4 = "news.*.#.music_123.pop";std::string bkey5 = "news.#.#.music_123.pop";std::string bkey6 = "news.*.*.music_123.pop";std::string bkey7 = "news.,music_123.pop";ASSERT_EQ(bitmq::Router::isLegalBindingKey(bkey1), true);ASSERT_EQ(bitmq::Router::isLegalBindingKey(bkey2), true);ASSERT_EQ(bitmq::Router::isLegalBindingKey(bkey3), false);ASSERT_EQ(bitmq::Router::isLegalBindingKey(bkey4), false);ASSERT_EQ(bitmq::Router::isLegalBindingKey(bkey5), false);ASSERT_EQ(bitmq::Router::isLegalBindingKey(bkey6), true);ASSERT_EQ(bitmq::Router::isLegalBindingKey(bkey7), false);
}// [测试用例]
// binding_key     routing_key         result
// 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     trueTEST(route_test, legal_route)
{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(bitmq::Router::route(bitmq::ExchangeType::TOPIC, rkeys[i], bkeys[i]), result[i]);}
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);testing::AddGlobalTestEnvironment(new BindingTest);RUN_ALL_TESTS();return 0;
}

测试结果:

8.10 队列消费者 / 订阅者管理

客户端这边每当发起一个订阅请求,意味着服务器这边就多了一个订阅者(处理消息的客户端描述),而这个消费者或者说订阅者它是和队列直接关联的,因为订阅请求中会描述当前用户想要订阅哪一个队列的消息。

而一个信道关闭的时候,或者队列被删除的时候,那么这个信道或队列关联的消费者也就没有存在的意义了,因此也需要将相关的消费者信息给删除掉。

基于以上需求,因此需要对订阅者信息进行管理。

1. 定义消费者信息结构

  • 1)消费者标识
  • 2)订阅的队列名称
  • 3)一个消息的处理回调函数(实现的是当发布一条消息到队列,则选择消费者进行消费,如何消费?对于服务端来说就是调用这个个回调函数进行处理,其内部逻辑就是找到消费者对应的连接,然后将数据发送给消费者对应的客户端)
    • void(const std::string&, const BasicProperties&, const std::string&)
  • 4)是否自动应答标志。(一个消息被消费者消费后,若自动应答,则直接移除待确认消息,否则等待客户端确认)

2. 消费者管理--以队列为单元进行管理-队列消费者管理结构

  • 1)操作:
    • i. 新增消费者:信道提供的服务是订阅队列消息的时候创建
    • ii. 删除消费者:取消订阅 / 信道关闭 / 连接关闭 的时候删除
    • iii. 获取消费者:从队列所有的消费者中按序取出一个消费者进行消息的推送
    • iv. 判断队列消费者是否为空
    • v. 判断指定消费者是否存在
    • vi. 清理队列所有消费者
  • 2)元素
    • i. 消费者管理结构: vector
    • ii. 轮转序号:一个队列可能会有多个消费者,但是一条消息只需要被一个消费者消费即可,因此采用 RR 轮转
    • iii. 互斥锁:保证线程安全
    • iv. 队列名称

消费者的管理以信道为单元还是以队列为单元?

两者各自的好处

  • 1.以信道为单元:一个信道关闭的时候所有的关联的消费者都要删除
  • 2.以队列为单元:一个队列收到了一条消息,就要找到订阅了队列消息的消费者进行消息推送

而明显找到订阅队列的消费者使用场景更多,所以我们以队列为单元管理消费者。


3. 对消费者进行统一管理结构

  • 1)初始化/删除队列的消费者信息结构(创建/删除队列的时候初始化)
  • 2)向指定队列新增消费者(客户端订阅指定队列消息的时候):新增完成的时候返回消费者对象
  • 3)从指定队列移除消费者(客户端取消订阅的时候)
  • 4)移除指定队列的所有消费者(队列被删除时销毁):删除消费者的队列管理单元对象
  • 5)从指定队列获取一个消费者(轮询获取-消费者轮换消费起到负载均衡的作用)
  • 6)判断队列中消费者是否为空
  • 7)判断队列中指定消费者是否存在
  • 8)清理所有消费者

声明消费者信息结构

using ConsumerCallBack = std::function<void(const std::string&,  const BasicProperties* , const std::string&)>;
//1. 定义消费者信息结构
struct Consumer
{
public:using ptr = std::shared_ptr<Consumer>;Consumer(){}Consumer(const std::string &ctag, const std::string& queue_name, bool ack_flag, ConsumerCallBack& cb): tag(ctag), qname(queue_name), callback(cb), auto_ack(ack_flag){}public:std::string tag;   // 1)消费者标识std::string qname;  // 2)订阅的队列名称ConsumerCallBack callback; // 3)一个消息的处理回调函数bool auto_ack;    // 4)是否自动应答标志
};

声明消费者管理类

//2. 消费者管理--以队列为单元进行管理-队列消费者管理结构
class QueueConsumer
{
public:QueueConsumer(const std::string& qname);// 1)新增消费者:信道提供的服务是订阅队列消息的时候创建Consumer::ptr create(const std::string &ctag, const std::string& queue_name, bool ack_flag, ConsumerCallBack& cb);// 2)删除消费者:取消订阅 / 信道关闭 / 连接关闭 的时候删除void remove(const std::string& ctag);// 3)获取消费者:从队列所有的消费者中按序取出一个消费者进行消息的推送Consumer::ptr choose();// 4)判断队列消费者是否为空bool empty();// 5)判断指定消费者是否存在bool exists(const std::string& ctag);// 6)清理队列所有消费者void clear();private:std::string qname;  // 1)队列名称std::vector<Consumer::ptr> _consumers;  // 2)消费者管理结构: vectoruint64_t _rr_seq;   // 3)轮转序号std::mutex _mutex;  // 4)互斥锁:保证线程安全
};

声明对消费者的统一管理类

class ConsumerManager
{
public:ConsumerManager(){}// 1)初始化/删除队列的消费者信息结构(创建/删除队列的时候初始化)void initQueueConsumer(const std::string& qname);// 2)移除指定队列的所有消费者(队列被删除时销毁):删除消费者的队列管理单元对象void destroyQueueConsumer(const std::string& qname);// 3)向指定队列新增消费者(客户端订阅指定队列消息的时候):新增完成的时候返回消费者对象Consumer::ptr create(const std::string &ctag, const std::string& queue_name, bool ack_flag, ConsumerCallBack& cb);// 4)从指定队列移除消费者(客户端取消订阅的时候)void remove(const std::string& ctag, const std::string& queue_name);// 5)从指定队列获取一个消费者(轮询获取-消费者轮换消费起到负载均衡的作用)Consumer::ptr choose(const std::string& queue_name);// 6)判断队列中指定消费者是否存在bool empty(const std::string& queue_name);// 7)判断队列中指定消费者是否存在bool exists(const std::string& ctag, const std::string& queue_name);// 8)清理所有消费者void clear();private:std::mutex _mutex;std::unordered_map<std::string, QueueConsumer::ptr> _qconsumers;
};


定义消费者管理类

//2. 消费者管理--以队列为单元进行管理-队列消费者管理结构
class QueueConsumer
{
public:using ptr = std::shared_ptr<QueueConsumer>;QueueConsumer(){}QueueConsumer(const std::string& qname): _qname(qname), _rr_seq(0){}// 1)新增消费者:信道提供的服务是订阅队列消息的时候创建Consumer::ptr create(const std::string &ctag, const std::string& queue_name, bool ack_flag, const ConsumerCallBack& cb){//1.加锁std::unique_lock<std::mutex> lock(_mutex);//2.判断消费者是否重复for (auto& cmp : _consumers){if (cmp->tag == ctag)return {};}//3.没有重复则新增————构造对象auto consumer = std::make_shared<Consumer>(ctag, queue_name, ack_flag, cb);//4.添加管理后返回对象_consumers.push_back(consumer);return consumer;}// 2)删除消费者:取消订阅 / 信道关闭 / 连接关闭 的时候删除void remove(const std::string& ctag){std::unique_lock<std::mutex> lock(_mutex);for (auto it = _consumers.begin(); it != _consumers.end(); ++it){if ((*it)->tag == ctag){_consumers.erase(it);return;}}}// 3)获取消费者:从队列所有的消费者中按序取出一个消费者进行消息的推送Consumer::ptr choose(){std::unique_lock<std::mutex> lock(_mutex);if (_consumers.size() == 0)return {};//获取轮转下标int idx = _rr_seq % _consumers.size();++_rr_seq;//返回结果return _consumers[idx];}// 4)判断队列消费者是否为空bool empty(){std::unique_lock<std::mutex> lock(_mutex);return _consumers.size() == 0;}// 5)判断指定消费者是否存在bool exists(const std::string& ctag){std::unique_lock<std::mutex> lock(_mutex);for (auto it = _consumers.begin(); it != _consumers.end(); ++it){if ((*it)->tag == ctag){return true;}}return false;}// 6)清理队列所有消费者void clear(){std::unique_lock<std::mutex> lock(_mutex);_consumers.clear();_rr_seq = 0;}private:std::string _qname;  // 1)队列名称std::vector<Consumer::ptr> _consumers;  // 2)消费者管理结构: vectoruint64_t _rr_seq;   // 3)轮转序号std::mutex _mutex;  // 4)互斥锁:保证线程安全
};

整体代码的编写还是比较简单的,这里不过多赘述了。


定义对消费者的统一管理类

class ConsumerManager
{
public:using ptr = std::shared_ptr<ConsumerManager>;ConsumerManager(){}// 1)初始化/删除队列的消费者信息结构(创建/删除队列的时候初始化)void initQueueConsumer(const std::string& qname){std::unique_lock<std::mutex> lock(_mutex);auto it = _qconsumers.find(qname);if (it != _qconsumers.end())return;auto qconsumers = std::make_shared<QueueConsumer>(qname);_qconsumers.insert({qname, qconsumers});}// 2)移除指定队列的所有消费者(队列被删除时销毁):删除消费者的队列管理单元对象void destroyQueueConsumer(const std::string& qname){std::unique_lock<std::mutex> lock(_mutex);auto it = _qconsumers.find(qname);if (it == _qconsumers.end())return;_qconsumers.erase(it);}// 3)向指定队列新增消费者(客户端订阅指定队列消息的时候):新增完成的时候返回消费者对象Consumer::ptr create(const std::string &ctag, const std::string& queue_name, bool ack_flag, const ConsumerCallBack& cb){QueueConsumer::ptr qcp;{std::unique_lock<std::mutex> lock(_mutex);//获取消费者管理单元auto it = _qconsumers.find(queue_name);if (it == _qconsumers.end()){DLOG("没有找到队列%s的消息管理句柄", queue_name.c_str());return {};}qcp = it->second;}//通过句柄完成创建return qcp->create(ctag, queue_name, ack_flag, cb);}// 4)从指定队列移除消费者(客户端取消订阅的时候)void remove(const std::string& ctag, const std::string& queue_name){QueueConsumer::ptr qcp;{std::unique_lock<std::mutex> lock(_mutex);//获取消费者管理单元auto it = _qconsumers.find(queue_name);if (it == _qconsumers.end()){DLOG("没有找到队列%s的消息管理句柄", queue_name.c_str());return ;}qcp = it->second;}//通过句柄完成删除return qcp->remove(ctag);}// 5)从指定队列获取一个消费者(轮询获取-消费者轮换消费起到负载均衡的作用)Consumer::ptr choose(const std::string& queue_name){QueueConsumer::ptr qcp;{std::unique_lock<std::mutex> lock(_mutex);//获取消费者管理单元auto it = _qconsumers.find(queue_name);if (it == _qconsumers.end()){DLOG("没有找到队列%s的消息管理句柄", queue_name.c_str());return {};}qcp = it->second;}//通过句柄完成获取消费者return qcp->choose();}// 6)判断队列中指定消费者是否存在bool empty(const std::string& queue_name){QueueConsumer::ptr qcp;{std::unique_lock<std::mutex> lock(_mutex);//获取消费者管理单元auto it = _qconsumers.find(queue_name);if (it == _qconsumers.end()){DLOG("没有找到队列%s的消息管理句柄", queue_name.c_str());return false;}qcp = it->second;}return qcp->empty();}// 7)判断队列中指定消费者是否存在bool exists(const std::string& ctag, const std::string& queue_name){QueueConsumer::ptr qcp;{std::unique_lock<std::mutex> lock(_mutex);//获取消费者管理单元auto it = _qconsumers.find(queue_name);if (it == _qconsumers.end()){DLOG("没有找到队列%s的消息管理句柄", queue_name.c_str());return false;}qcp = it->second;}//通过句柄完成删除return qcp->exists(ctag);}// 8)清理所有消费者void clear(){std::unique_lock<std::mutex> lock(_mutex);_qconsumers.clear();}private:std::mutex _mutex;std::unordered_map<std::string, QueueConsumer::ptr> _qconsumers;
};

注意:每个成员函数在获取完消费者管理句柄后,通过句柄对执行对应语句是不用被锁管理的,因为每个消费者管理句柄在内部都存在一把锁,已经可以保证线程安全了,为了避免锁冲突和提高效率,所以该语句不被锁管理。



单元测试:

bitmq::ConsumerManager::ptr cmp;class ConsumerTest : public testing::Environment
{
public:virtual void SetUp() override{ cmp = std::make_shared<bitmq::ConsumerManager>();cmp->initQueueConsumer("queue1");}virtual void TearDown() override { }
};void cb(const std::string &ctag, const bitmq::BasicProperties* bp, const std::string& queue_name)
{}TEST(consumer_test, insert_test)
{cmp->create("consumer1", "queue1", true, cb);cmp->create("consumer2", "queue1", true, cb);cmp->create("consumer3", "queue1", true, cb);ASSERT_EQ(cmp->exists("consumer1", "queue1"), true);ASSERT_EQ(cmp->exists("consumer2", "queue1"), true);ASSERT_EQ(cmp->exists("consumer3", "queue1"), true);ASSERT_EQ(cmp->exists("consumer1", "queue2"), false);
}TEST(consumer_test, remove_test)
{cmp->remove("consumer1", "queue1");ASSERT_EQ(cmp->exists("consumer1", "queue1"), false);ASSERT_EQ(cmp->exists("consumer2", "queue1"), true);ASSERT_EQ(cmp->exists("consumer3", "queue1"), true);
}TEST(consumer_test, choose_test)
{bitmq::Consumer::ptr cp1 = cmp->choose("queue1");ASSERT_NE(cp1.get(), nullptr);ASSERT_EQ(cp1->tag, "consumer2");bitmq::Consumer::ptr cp2 = cmp->choose("queue1");ASSERT_NE(cp2.get(), nullptr);ASSERT_EQ(cp2->tag, "consumer3");bitmq::Consumer::ptr cp3 = cmp->choose("queue1");ASSERT_NE(cp3.get(), nullptr);ASSERT_EQ(cp3->tag, "consumer2");
}int main(int argc, char* argv[])
{testing::InitGoogleTest(&argc, argv);testing::AddGlobalTestEnvironment(new ConsumerTest);RUN_ALL_TESTS();return 0;
}

执行结果:

8.11 信道管理模块

在 AMQP 模型中,除了通信连接 Connection 概念外,还有一个 Channel 的概念, Channel 是针对 Connection 连接的一个更细粒度的通信信道,多个 Channel 可以使用同一个通信连接 Connection 进行通信,但是同一个 Connection 的 Channel 之间相互独立。

而信道模块就是再次将上述模块进行整合提供服务的模块,信道的作用就是给用户提供服务。

1. 管理信息:

  • 1)信道 ID:信道的唯一标识
  • 2)信道关联的消费者:用于消费者信道在关闭的时候取消订阅,删除订阅者信息
  • 3)信道关联的连接:用于向客户端发送数据(响应,推送的消息)
  • 4)protobuf 协议处理句柄:网络通信前的协议处理
  • 5)消费者管理句柄:信道关闭/取消订阅的时候,通过句柄删除订阅者信息
  • 6)虚拟机句柄:交换机/队列/绑定/消息数据管理
  • 7)工作线程池句柄(一条消息被发布到队列后,需要将消息推送给订阅了对应队列的消费者,过程由线程池完成)

2. 管理操作:

  • 1)提供声明&删除交换机操作(删除交换机的同时删除交换机关联的绑定信息)
  • 2)提供声明&删除队列操作(删除队列的同时,删除队列关联的绑定信息,消息,消费者信息)
  • 3)提供绑定&解绑队列操作
  • 4)提供订阅&取消订阅队列消息操作
  • 5)提供发布&确认消息操作

3. 信道管理

  • 1)新增一条信道信道
  • 2)删除指定信道
  • 3)获取指定信道

信道类

#include "muduo/net/TcpConnection.h"
#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher.h"
#include "../mqcommon/mq_helper.hpp"
#include "../mqcommon/mq_logger.hpp"
#include "../mqcommon/mq_msg.pb.h"
#include "../mqcommon/mq_proto.pb.h"
#include "../mqcommon/mq_threadpool.hpp"
#include "mq_virtualhost.hpp"
#include "mq_consumer.hpp"using ProtobufCodecPtr = std::shared_ptr<ProtobufCodec>;
class Channel
{
public:Channel(const std::string& id,const VirtualHost::ptr& host, ConsumerManager::ptr& cmp,const ProtobufCodecPtr& codec,const muduo::net::TcpConnectionPtr& conn,const threadpool::ptr& pool){}~Channel();//1)声明交换机void declareExchange();//2)删除交换机void deleteExchange();//3)声明队列void declarQueue();//4)删除队列void deleteQueue();//5)队列的绑定void bind();//6)队列的解绑定void unBind();//7)发布消息void basicPublish();//8)确认消息void basicAck();//9)订阅队列void basicConsume();//10)取消订阅void basicCancel();private:std::string _cid;           // 1)信道 ID:信道的唯一标识Consumer::ptr _consumer;    // 2)信道关联的消费者muduo::net::TcpConnectionPtr _conn;  // 3)信道关联的连接:用于向客户端发送数据(响应,推送的消息)ProtobufCodecPtr _code;      // 4)protobuf 协议处理句柄:网络通信前的协议处理ConsumerManager::ptr _cmp;  // 5)消费者管理句柄:信道关闭/取消订阅的时候,通过句柄删除订阅者信息VirtualHost::ptr _host;     // 6)虚拟机句柄:交换机/队列/绑定/消息数据管理threadpool::ptr _pool;      // 7)线程池
};
  • 可以看到每个成员函数的参数都没有填写,那是因为如果按照之前的参数,那么需要传递的参数太多了,我们期望将这些参数封装成一个对象,传递时只需传递一个对象进去就好了。
  • 更重要的是,上面的所有函数起其实就是给客户端提供服务的,也就是说所有的参数数据都是要经过网络的,而我们不可能将一个请求的所有参数都分开传递,所以对参数的封装工作就比较重要了。而数据在网络中的传输势必会导致粘包问题,所以我们还需要制定网络通信协议。

我们前面编写的基于muduo实现protobuf通信协议的例子中也是这样写的,这样写可以使代码更加整洁直观。

class ProtoServer
{typedef shared_ptr<request::TranslateRequest> TranslateRequestPtr;typedef shared_ptr<request::TranslateResponse> TranslateResponsePtr;typedef shared_ptr<request::CalculatorRequest> CalculatorRequestPtr;typedef shared_ptr<request::CalculatorResponse> CalculatorResponsePtr;
public:void onTranslate(const muduo::net::TcpConnectionPtr& conn, const TranslateRequestPtr& req, muduo::Timestamp rt);void onCalculator(const muduo::net::TcpConnectionPtr& conn, const CalculatorRequestPtr& req, muduo::Timestamp rt);void onConnection(const muduo::net::TcpConnectionPtr& conn);void onUnknownMessage(const muduo::net::TcpConnectionPtr& conn, const MessagePtr& message, muduo::Timestamp);private:muduo::net::EventLoop _baseloop;muduo::net::TcpServer _server;ProtobufDispatcher _dispatcher;ProtobufCodec _codec;
};

也就是说我们需要自己制定协议,将上述所需函数参数进行封装,而这些参数是会经过网络传播的,所以我们采用protobuf来完成封装工作。

关于网络通信协议的设计不放在这一章节讲,请通过目录找到 [9.网络通信协议设计]部分,仔细阅读完后再返回该位置。



在编写好网络通信协议部分的proto文件后,使用protoc生成对应的.h和.cc文件,再让我们的文件包含刚刚生成的.h文件。

我们期望用智能指针来帮我们管理所有的对象,所以更新我们的代码,如下:

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:Channel(const std::string& id,const VirtualHost::ptr& host, ConsumerManager::ptr& cmp,const ProtobufCodecPtr& codec,const muduo::net::TcpConnectionPtr& conn,const threadpool::ptr pool): _cid(id), _conn(conn), _codec(codec), _cmp(cmp), _pool(pool){}~Channel();//1)声明交换机void declareExchange(const declareExchangeRequestPtr& req);//2)删除交换机void deleteExchange(const deleteExchangeRequestPtr& req);//3)声明队列void declarQueue(const declareQueueRequestPtr& req);//4)删除队列void deleteQueue(const deleteQueueRequestPtr& req);//5)队列的绑定void bind(const queueBindRequestPtr& req);//6)队列的解绑定void unBind(const queueUnBindRequestPtr& req);//7)发布消息void basicPublish(const basicPublishRequestPtr req);//8)确认消息void basicAck(const basicAckRequestPtr& req);//9)订阅队列void basicConsume(const basicConsumeRequestPtr& req);//10)取消订阅void basicCancel(const basicCancelRequestPtr& req);private:std::string _cid;           // 1)信道 ID:信道的唯一标识Consumer::ptr _consumer;    // 2)信道关联的消费者muduo::net::TcpConnectionPtr _conn;  // 3)信道关联的连接:用于向客户端发送数据(响应,推送的消息)ProtobufCodecPtr _codec;      // 4)protobuf 协议处理句柄:网络通信前的协议处理ConsumerManager::ptr _cmp;  // 5)消费者管理句柄:信道关闭/取消订阅的时候,通过句柄删除订阅者信息VirtualHost::ptr _host;     // 6)虚拟机句柄:交换机/队列/绑定/消息数据管理threadpool::ptr _pool;      // 7)线程池
};

定义信道类

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:Channel(const std::string& id,const VirtualHost::ptr& host, ConsumerManager::ptr& cmp,const ProtobufCodecPtr& codec,const muduo::net::TcpConnectionPtr& conn,const threadpool::ptr pool): _cid(id), _conn(conn), _codec(codec), _cmp(cmp), _pool(pool){}~Channel(){//删除信道对应的消费者if (_consumer.get() != nullptr){_cmp->remove(_consumer->tag, _consumer->qname);}}//1)声明交换机void declareExchange(const declareExchangeRequestPtr& req){bool ret = _host->declareExchange(req->exchange_name(), req->exchange_type(),req->durable(), req->auto_delete(), req->args());return basicResponse(ret, req->rid(), req->cid());}//2)删除交换机void deleteExchange(const deleteExchangeRequestPtr& req){_host->deleteExchange(req->exchange_name());    return basicResponse(true, req->rid(), req->cid());}//3)声明队列void declarQueue(const declareQueueRequestPtr& req){bool ret = _host->declareQueue(req->queue_name(), req->durable(), req->exclusive(),req->auto_delete(), req->args());if (ret == false)return;//初始化队列的消费者管理句柄_cmp->initQueueConsumer(req->queue_name());return basicResponse(true, req->rid(), req->cid());}//4)删除队列void deleteQueue(const deleteQueueRequestPtr& req){_host->deleteQueue(req->queue_name());_cmp->destroyQueueConsumer(req->queue_name());return basicResponse(true, req->rid(), req->cid());}//5)队列的绑定void queuebind(const queueBindRequestPtr& req){bool ret = _host->bind(req->exchange_name(), req->queue_name(),req->binding_key());return basicResponse(ret, req->rid(), req->cid());}//6)队列的解绑定void queueUnBind(const queueUnBindRequestPtr& req){_host->unBind(req->exchange_name(), req->queue_name());return basicResponse(true, req->rid(), req->cid());}//7)发布消息void basicPublish(const basicPublishRequestPtr req){//1.判断交换机是否存在auto ep = _host->selectExchange(req->exchange_name());if (ep.get() == nullptr)return basicResponse(true, req->rid(), req->cid());//2.进行交换路由,判断消息可以发布到交换机哪个队列中MsgQueueBindingMap mqbm = _host->exchangeBindings(req->exchange_name());BasicProperties* bp = nullptr;std::string routing_key;//判断properties是否为空if (req->has_properties() != false){routing_key = req->properties().routing_key();bp = req->mutable_properties();}for (auto& binding : mqbm){if (Router::route(ep->type, routing_key, binding.second->binding_key)){//消息匹配成功,则将该消息加入队列_host->basicPublish(binding.first, bp, req->body());//向线程池中添加一个消息消费任务(向指定队列的订阅者去推送消息————由线程池完成auto task = std::bind(&Channel::publish, this, binding.first);_pool->push(task);}}}//8)确认消息void basicAck(const basicAckRequestPtr& req){_host->basicAck(req->queue_name(), req->message_id());return basicResponse(true, req->rid(), req->cid());}//9)订阅队列void basicConsume(const basicConsumeRequestPtr& req){//1.判断队列是否存在bool ret = _host->existsQueue(req->queue_name());if (ret == false){DLOG("订阅队列失败,%s队列不存在", req->queue_name().c_str());return;}//2.创建队列消费者auto cb = std::bind(&Channel::callback, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);//在创建了消费者之后,当前的channel角色就是一个消费者//该消费者需要保存起来,以便在取消订阅 / 删除信道后删除对应的消费者(以防资源泄露)_consumer = _cmp->create(req->consumer_tag(), req->queue_name(), req->auto_ack(), cb);}//10)取消订阅void basicCancel(const basicCancelRequestPtr& req){_cmp->remove(req->consumer_tag(), req->queue_name());return basicResponse(true, req->rid(), req->cid());}private:void callback(const std::string& tag,  const BasicProperties* bp, const std::string& body){//针对参数组织处推送消息请求,将消息推送给channel对应的客户端basicConsumeResponse resp;resp.set_cid(_cid);resp.set_body(body);resp.set_consumer_tag(tag);if (bp != nullptr){resp.mutable_properties()->set_id(bp->id());resp.mutable_properties()->set_delivery_mode(bp->delivery_mode());resp.mutable_properties()->set_routing_key(bp->routing_key());}_codec->send(_conn, resp);}void publish(const std::string& qname){//指定队列消费消息//1.从队列中获取一条消息MessagePtr mp = _host->basicConsume(qname);if (mp.get() == nullptr){DLOG("执行消费任务时,%s队列没有消息", qname.c_str());return;}//2.从队列订阅者中取出一个订阅者Consumer::ptr cmp = _cmp->choose(qname);if (mp.get() == nullptr){DLOG("执行消费任务时,%s队列没有订阅者", qname.c_str());return;}//3.调用订阅者对应的消息处理函数,实现消息的推送cmp->callback(cmp->tag, mp->mutable_payload()->mutable_properties(), mp->payload().body());//4.判断如果订阅者是自动确认——不需要等待确认,直接删除消息,否则需要收到确认后再删除if (cmp->auto_ack == true){_host->basicAck(qname, cmp->tag);}}void basicResponse(bool ok, const std::string& rid, const std::string& cid){basicCommonResponse resp;resp.set_rid(rid);resp.set_cid(cid);resp.set_is_ok(ok);_codec->send(_conn, resp);}private:std::string _cid;           // 1)信道 ID:信道的唯一标识Consumer::ptr _consumer;    // 2)信道关联的消费者muduo::net::TcpConnectionPtr _conn;  // 3)信道关联的连接:用于向客户端发送数据(响应,推送的消息)ProtobufCodecPtr _codec;      // 4)protobuf 协议处理句柄:网络通信前的协议处理ConsumerManager::ptr _cmp;  // 5)消费者管理句柄:信道关闭/取消订阅的时候,通过句柄删除订阅者信息VirtualHost::ptr _host;     // 6)虚拟机句柄:交换机/队列/绑定/消息数据管理threadpool::ptr _pool;      // 7)线程池
};
  • 每个请求在执行完后,都应该对请求进行响应,我们统一使用baasicResponse函数进行响应;
  • 发布消息basicPublish函数较为复杂,发布消息首先要找到对应的交换机是否存在,然后与该交换机关联的所有队列的binding_key进行比较,如果能匹配成功则加入对应的队列中。而当队列中有消息时,就应该将消息推送给消费者,我们将这个任务交给线程池处理,所以这里有了一个回调函数publish。
  • 订阅队列basicConsume函数也比较复杂,复杂的地方在于这个回调函数,publish的函数逻辑是获取一个消息,再获取一个消费者,再将消息推送给消费者,而最后一步将消息推送给消费者的任务就是由callback回调函数执行的,该函数在内部会构造Response并将消息发送给消费者。所以我们在订阅队列时需要创建该回调

信道管理类

class ChannelManager
{
public:using ptr = std::shared_ptr<ChannelManager>;ChannelManager(){}bool openChannel(const std::string& id,const VirtualHost::ptr& host, 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()){return false;}auto channel = std::make_shared<Channel>(id, host, cmp, codec, conn, pool);_channels.insert({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 {};}return it->second;}private:std::mutex _mutex;std::unordered_map<std::string, Channel::ptr> _channels;
};


这部分内容无法完成单元测试,因为该部分的代码还包含了网络部分,而这部分我们目前还没有完成,我们先测试一下编译能否通过。

编写makefile:

test:mq_channeltest.cpp ../../mqcommon/mq_msg.pb.cc ../../mqcommon/mq_proto.pb.ccg++ -o $@ $^ -std=c++11 -lgtest -lprotobuf -lsqlite3 -w -g -I../../third/include
.PHONY:clean
clean:rm -rf test

编译成功。

8.12 连接管理模块

向用户提供一个用于实现网络通信的 Connection 对象,从其内部可创建出粒度更轻的Channel 对象,用于与客户端进行网络通信。

1. 成员信息:

  • a. 连接关联的信道管理句柄(实现信道的增删查)
  • b. 连接关联的实际用于通信的 muduo::net::Connection 连接
  • c. protobuf 协议处理的句柄(ProtobufCodec 对象)
  • d. 消费者管理句柄
  • e. 虚拟机句柄
  • f. 异步工作线程池句柄

2. 连接操作:

  • a. 提供创建 Channel 信道的操作
  • b. 提供删除 Channel 信道的操作

3. 连接管理:

  • a. 连接的增删查

连接类

class Connection
{
public:Connection(const VirtualHost::ptr& host, const ConsumerManager::ptr& cmp,const ProtobufCodecPtr& codec,const muduo::net::TcpConnectionPtr& conn,const threadpool::ptr& pool): _conn(conn), _codec(codec), _cmp(cmp), _host(host), _pool(pool), _channels(std::make_shared<ChannelManager>()){}~Connection();void openChannel(const openChannelRequestPtr& req){bool ret = _channels->openChannel(req->rid(), _host, _cmp,_codec, _conn, _pool);return basicResponse(true, req->rid(), req->cid());}void closeChannel(const closeChannelRequestPtr& req){_channels->closeChannel(req->cid());return basicResponse(true, req->rid(), req->cid());}private:void basicResponse(bool ok, const std::string& rid, const std::string& cid){basicCommonResponse resp;resp.set_rid(rid);resp.set_cid(cid);resp.set_is_ok(ok);_codec->send(_conn, resp);}private:VirtualHost::ptr _host;ConsumerManager::ptr _cmp;ProtobufCodecPtr _codec;muduo::net::TcpConnectionPtr _conn;threadpool::ptr _pool;ChannelManager::ptr _channels; //一个连接关闭了,所有的信道也要析构
};

连接类的编写还是比较简单的就是将前面的内容再次整合。越往后代码的编写越简单,而框架的思想越来越明显

连接管理类

class ConnectionManager
{
public:ConnectionManager(){}void newConnection(const VirtualHost::ptr& host, const ConsumerManager::ptr& cmp,const ProtobufCodecPtr& codec,const muduo::net::TcpConnectionPtr& conn,const threadpool::ptr& pool){std::unique_lock<std::mutex> lock(_mutex);auto it = _connections.find(conn);if (it != _connections.end()){return;}auto newConn = std::make_shared<Connection>(host, cmp, codec, conn, pool);_connections.insert({conn, newConn});}void delConnection(const muduo::net::TcpConnectionPtr& conn){std::unique_lock<std::mutex> lock(_mutex);auto it = _connections.find(conn);if (it == _connections.end()){return;}_connections.erase(it);}Connection::ptr getConnection(const muduo::net::TcpConnectionPtr& conn){std::unique_lock<std::mutex> lock(_mutex);std::unique_lock<std::mutex> lock(_mutex);auto it = _connections.find(conn);if (it == _connections.end()){return {};}return it->second;}private:std::mutex _mutex;std::unordered_map<muduo::net::TcpConnectionPtr, Connection::ptr> _connections;
};

同样这里也是无法直接测试的,我们测试一下编译能否通过即可。

编写makefile:

test:mq_connectiontest.cpp ../../mqcommon/mq_msg.pb.cc ../../mqcommon/mq_proto.pb.ccg++ -o $@ $^ -std=c++11 -lgtest -lprotobuf -lsqlite3 -w -g -I../../third/include
.PHONY:clean
clean:rm -rf test

编译:

到这里服务器核心模块的工作就已经完成了,接下来,我们需要完成整个服务器功能。

9. 网络通信协议设计

9.1 需求确认这个章节我们考虑客户
端和服务器之间的通信方式。回顾 MQ 的交互模型:

其中生产者和消费者都是客户端, 它们都需要通过网络和 Broker Server 进行通信。具体通信的过程我们使用 Muduo 库来实现, 使用 TCP 作为通信的底层协议, 同时在这个基础上自定义应用层协议, 完成客户端对服务器功能的远端调用。 我们要实现的远端调用接口包括:

  • 创建 channel
  • 关闭 channel
  • 创建 exchange
  • 删除 exchange
  • 创建 queue
  • 删除 queue
  • 创建 binding
  • 删除 binding
  • 发送 message
  • 订阅 message
  • 发送 ack
  • 返回 message (服务器 -> 客户端)

9.2 设计应用层协议

使用二进制的方式设计应用层协议。 因为 MQMessage 的消息体是使用 Protobuf 进行序列化的,本身是按照二进制存储的,所以不太适合用 json 等文本格式来定义协议。下面我们设计一下应用层协议:请求/响应报文设计

  • len: 4 个字节, 表示整个报文的长度
  • nameLen: 4 个字节, 表示 typeName 数组的长度
  • typeName:是个字节数组, 占 nameLen 个字节, 表示请求/响应报文的类型名,作用是分发不同消息到对应的远端接口调用中
  • protobufData:是个字节数组, 占 len - nameLen - 8 个字节, 表示请求/响应参数数据通过 protobuf 序列化之后的二进制
  • checkSum: 4 个字节, 表示整个消息的校验和, 作用是为了校验请求/响应报文的完整性

示例:

经过协议处理后:

按照 len - nameLen - 8 的长度读取出 protobufData 就可以将读到的二进制数据反序列化成 ExchangeDeclareArguments 对象进行后续处理。后续的请求报文和这里都是类似的。
 

9.3 定义请求/响应参数

因为这里的参数需要进行网络传输以及序列化, 所以我们需要将参数定义在 pb 文件中。

syntax = "proto3";
package bitmq;import "mq_msg.proto";//信道的打开
message openChannelRequest
{string rid = 1;  //请求idstring cid = 2;  //信道id
};//信道的关闭
message closeChannelRequest
{string rid = 1;  //请求idstring cid = 2;  //信道id  
};//交换机的声明
message declareExchangeRequest
{string rid = 1;  //请求idstring cid = 2;  //信道id  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;  //请求idstring cid = 2;  //信道id  string exchange_name = 3;  //交换机名称
};//队列的声明
message declareQueueRequest
{string rid = 1;  //请求idstring cid = 2;  //信道id  string queue_name = 3;  //队列名称bool exclusive = 4;  //是否独占bool durable = 5;  //持久化标志bool auto_delete = 6;  //自动删除map<string, string> args = 7;  //其他参数
};//队列的删除
message deleteQueueRequest
{string rid = 1;  //请求idstring cid = 2;  //信道id  string queue_name = 3;  //队列名称    
};//队列的绑定
message queueBindRequest
{string rid = 1;  //请求idstring cid = 2;  //信道id  string exchange_name = 3;  //交换机名称string queue_name = 4;     //队列名称string binding_key = 5;  
};//队列的解绑定
message queueUnBindRequest
{string rid = 1;  //请求idstring cid = 2;  //信道id  string exchange_name = 3;  //交换机名称string queue_name = 4;     //队列名称
};//消息的发布
message basicPublishRequest
{string rid = 1;  //请求idstring cid = 2;  //信道id  string exchange_name = 3; //交换机名称string body = 4; //消息的内容BasicProperties properties = 5; //消息的属性
};//消息的确认
message basicAckRequest
{string rid = 1;  //请求idstring cid = 2;  //信道idstring queue_name = 3;  //队列名称string message_id = 4;  //消息id
};//队列的订阅
message basicConsumeRequest
{string rid = 1;  //请求idstring cid = 2;  //信道idstring consumer_tag = 3; //消费者标识string queue_name = 4;   //队列名称bool auto_ack = 5;       //自动应答
};//取消订阅
message basicCancelRequest
{string rid = 1;  //请求idstring cid = 2;  //信道idstring consumer_tag = 3;  //消费者标识string queue_name = 4;    //队列名称
};//消息的推送
message basicConsumeResponse
{string cid = 1;  //信道idstring consumer_tag = 2;  //消费者标识string body = 3;  //消息的内容BasicProperties properties = 4; //消息的属性
};//通用响应
message basicCancelResponse
{string rid = 1;  //请求idstring cid = 2;  //信道idbool is_ok = 3;      //请求是否成功
};

10. 服务器模块实现

服务器模块我们借助 Muduo 网络库来实现

  • _server: Muduo 库提供的一个通用 TCP 服务器, 我们可以封装这个服务器进行TCP 通信
  • _baseloop:主事件循环器, 用于响应 IO 事件和定时器事件,主 loop 主要是为了响应监听描述符的 IO 事件
  • _codec: 一个 protobuf 编解码器, 我们在 TCP 服务器上设计了一层应用层协议,这个编解码器主要就是负责实现应用层协议的解析和封装, 下边具体讲解
  • _dispatcher:一个消息分发器, 当 Socket 接收到一个报文消息后, 我们需要按照消息的类型, 即上面提到的 typeName 进行消息分发, 会不不同类型的消息分发相对应的的处理函数中,下边具体讲解
  • _consumer: 服务器中的消费者信息管理句柄。
  • _threadpool: 异步工作线程池,主要用于队列消息的推送工作。
  • _connections: 连接管理句柄,管理当前服务器上的所有已经建立的通信连接。
  • _virtual_host:服务器持有的虚拟主机。 队列、交换机 、绑定、消息等数据都是通过虚拟主机管理

如何编写MQBroker服务器

在前面我们编写过一个基于muduo库的protobuf通信服务器,代码如下:

class ProtoServer
{typedef shared_ptr<request::TranslateRequest> TranslateRequestPtr;typedef shared_ptr<request::TranslateResponse> TranslateResponsePtr;typedef shared_ptr<request::CalculatorRequest> CalculatorRequestPtr;typedef shared_ptr<request::CalculatorResponse> CalculatorResponsePtr;
public:ProtoServer(int port): _server(&_baseloop, muduo::net::InetAddress(port), "ProtoServer", muduo::net::TcpServer::kReusePort), _dispatcher(bind(&ProtoServer::onUnknownMessage, this, placeholders::_1, placeholders::_2, placeholders::_3)), _codec(bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, placeholders::_1, placeholders::_2, placeholders::_3)){//注册业务处理函数_dispatcher.registerMessageCallback<request::TranslateRequest>(std::bind(&ProtoServer::onTranslate, this,placeholders::_1, placeholders::_2, placeholders::_3));_dispatcher.registerMessageCallback<request::CalculatorRequest>(std::bind(&ProtoServer::onCalculator, this,placeholders::_1, placeholders::_2, placeholders::_3));//设置回调函数_server.setConnectionCallback(bind(&ProtoServer::onConnection, this, placeholders::_1));_server.setMessageCallback(bind(&ProtobufCodec::onMessage, &_codec, placeholders::_1, placeholders::_2, placeholders::_3));}void start(){_server.start();_baseloop.loop();}private:string translate(const string& word){static unordered_map<string, string> dict = {{"sun", "太阳"},{"left", "左"},{"right", "右"},{"hello", "你好"}};auto it = dict.find(word);if (it != dict.end())return it->second;else  return "未找到";}void onTranslate(const muduo::net::TcpConnectionPtr& conn, const TranslateRequestPtr& req, muduo::Timestamp rt){//读取请求字符串string word = req->word();//将请求字符串进行翻译string tranStr = translate(word);//构建TranslateResponse响应request::TranslateResponse resp;resp.set_result(tranStr);//发送响应_codec.send(conn, resp);}void onCalculator(const muduo::net::TcpConnectionPtr& conn, const CalculatorRequestPtr& req, muduo::Timestamp rt){int num1 = req->num1();int num2 = req->num2();char op = req->op()[0];request::CalculatorResponse resp;switch(op){case '+':resp.set_result(num1 + num2);resp.set_code(true);break;case '-':resp.set_result(num1 - num2);resp.set_code(true);break;case '*':resp.set_result(num1 * num2);resp.set_code(true);break;case '/':if (num2 == 0){resp.set_code(false);}else {resp.set_result(num1 / num2);resp.set_code(true);}break;                                }_codec.send(conn, resp);}void onConnection(const muduo::net::TcpConnectionPtr& conn){if (conn->connected()){cout << "连接成功!!!" << endl;}else {cout << "断开连接" << endl;}}void onUnknownMessage(const muduo::net::TcpConnectionPtr& conn, const MessagePtr& message, muduo::Timestamp){std::cout << "onUnknownMessage: " << message->GetTypeName() << std::endl;}private:muduo::net::EventLoop _baseloop;muduo::net::TcpServer _server;ProtobufDispatcher _dispatcher;ProtobufCodec _codec;
};

这个例子很好的展示了一个通信服务器的编写。

  • _baseloop负责事件监控,判断有无事件就绪
  • _server对象就是一个Tcp服务器
  • _dispatcher是事件分发器,我们需要在构造函数处注册好请求以及对应的回调函数,当服务器收到一条请求后,事件分发器就会根据不同的请求调用不同的回调函数。

先搭建一下整体框架:

#define DBFILE "/meta.db"
class MQBrokerServer
{
public:typedef std::shared_ptr<google::protobuf::Message> MessagePtr;MQBrokerServer(int port, const std::string& basedir): _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port), "MQBrokerServer", muduo::net::TcpServer::kReusePort), _dispatcher(std::bind(&MQBrokerServer::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))), _virtual_host(std::make_shared<VirtualHost>(basedir, basedir + DBFILE)), _consumer_manager(std::make_shared<ConsumerManager>()), _connection_manager(std::make_shared<ConnectionManager>()), _threadpool(std::make_shared<threadpool>()){//针对历史消息中的所有队列,初始化队列的消费者管理结构auto qm = _virtual_host->allQueues();for (auto& q : qm){_consumer_manager->initQueueConsumer(q.first);}//注册业务处理函数_dispatcher.registerMessageCallback<openChannelRequest>(std::bind(&MQBrokerServer::onOpenChannel, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<closeChannelRequest>(std::bind(&MQBrokerServer::onCloseChannel, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<declareExchangeRequest>(std::bind(&MQBrokerServer::onDeclareExchange, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<deleteExchangeRequest>(std::bind(&MQBrokerServer::onDeleteExchange, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<declareQueueRequest>(std::bind(&MQBrokerServer::onDeclareQueue, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<deleteQueueRequest>(std::bind(&MQBrokerServer::onDeleteQueue, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<queueBindRequest>(std::bind(&MQBrokerServer::onQueueBind, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<queueUnBindRequest>(std::bind(&MQBrokerServer::onQueueUnBind, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<basicPublishRequest>(std::bind(&MQBrokerServer::onBasicPublish, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<basicAckRequest>(std::bind(&MQBrokerServer::onBasicAck, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));  _dispatcher.registerMessageCallback<basicConsumeRequest>(std::bind(&MQBrokerServer::onBasicConsume, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));  _dispatcher.registerMessageCallback<basicCancelRequest>(std::bind(&MQBrokerServer::onBasicCancel, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));             //设置回调函数_server.setConnectionCallback(std::bind(&MQBrokerServer::onConnection, this, std::placeholders::_1));_server.setMessageCallback(std::bind(&ProtobufCodec::onMessage, _codec.get(), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));}void start(){_server.start();_baseloop.loop();}private://打开信道void onOpenChannel(const muduo::net::TcpConnectionPtr& conn, const openChannelRequestPtr& req, muduo::Timestamp);//关闭信道void onCloseChannel(const muduo::net::TcpConnectionPtr& conn, const closeChannelRequestPtr& req, muduo::Timestamp);//声明交换机void onDeclareExchange(const muduo::net::TcpConnectionPtr& conn, const declareExchangeRequestPtr& req, muduo::Timestamp);//删除交换机void onDeleteExchange(const muduo::net::TcpConnectionPtr& conn, const deleteExchangeRequestPtr& req, muduo::Timestamp);//声明队列void onDeclareQueue(const muduo::net::TcpConnectionPtr& conn, const declareQueueRequestPtr& req, muduo::Timestamp);//删除队列void onDeleteQueue(const muduo::net::TcpConnectionPtr& conn, const deleteQueueRequestPtr& req, muduo::Timestamp);//队列绑定void onQueueBind(const muduo::net::TcpConnectionPtr& conn, const queueBindRequestPtr& req, muduo::Timestamp);//队列解绑void onQueueUnBind(const muduo::net::TcpConnectionPtr& conn, const queueUnBindRequestPtr& req, muduo::Timestamp);//消息发布void onBasicPublish(const muduo::net::TcpConnectionPtr& conn, const basicPublishRequestPtr& req, muduo::Timestamp);//消息确认void onBasicAck(const muduo::net::TcpConnectionPtr& conn, const basicAckRequestPtr& req, muduo::Timestamp);//队列消息订阅void onBasicConsume(const muduo::net::TcpConnectionPtr& conn, const basicConsumeRequestPtr& req, muduo::Timestamp);//队列消息取消订阅void onBasicCancel(const muduo::net::TcpConnectionPtr& conn, const basicCancelRequestPtr& req, muduo::Timestamp);void onConnection(const muduo::net::TcpConnectionPtr& conn){if (conn->connected()){DLOG("连接建立成功");_connection_manager->newConnection(_virtual_host, _consumer_manager,_codec, conn, _threadpool);}else {DLOG("连接断开");_connection_manager->delConnection(conn);}}void onUnknownMessage(const muduo::net::TcpConnectionPtr& conn, const MessagePtr& message, muduo::Timestamp){std::cout << "onUnknownMessage: " << message->GetTypeName() << std::endl;}private:muduo::net::EventLoop _baseloop;muduo::net::TcpServer _server;ProtobufDispatcher _dispatcher;ProtobufCodecPtr _codec;VirtualHost::ptr _virtual_host;ConsumerManager::ptr _consumer_manager;ConnectionManager::ptr _connection_manager;threadpool::ptr _threadpool;
};

上面的代码看似复杂,其实就是将用户所需的服务进行了封装,先使用shared_ptr修饰每种请求类型,再根据对应的请求类型注册业务处理回调函数。

接下来完成服务器的整体代码:

#pragma once#include <iostream>
#include <functional>
#include <memory>
#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher.h"
#include "muduo/net/EventLoop.h"
#include "muduo/net/TcpServer.h"
#include "mq_connection.hpp"
#include "mq_consumer.hpp"
#include "mq_virtualhost.hpp"
#include "../mqcommon/mq_threadpool.hpp"
#include "../mqcommon/mq_msg.pb.h"
#include "../mqcommon/mq_proto.pb.h"
#include "../mqcommon/mq_logger.hpp"namespace bitmq
{#define DBFILE "/meta.db"class MQBrokerServer{public:typedef std::shared_ptr<google::protobuf::Message> MessagePtr;MQBrokerServer(int port, const std::string &basedir): _server(&_baseloop, muduo::net::InetAddress("0.0.0.0", port), "Server", muduo::net::TcpServer::kReusePort), _dispatcher(std::bind(&MQBrokerServer::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))), _virtual_host(std::make_shared<VirtualHost>("", basedir, basedir + DBFILE)), _consumer_manager(std::make_shared<ConsumerManager>()), _connection_manager(std::make_shared<ConnectionManager>()), _threadpool(std::make_shared<threadpool>()){//针对历史消息中的所有队列,初始化队列的消费者管理结构auto qm = _virtual_host->allQueues();for (auto& q : qm){_consumer_manager->initQueueConsumer(q.first);}//注册业务处理函数_dispatcher.registerMessageCallback<openChannelRequest>(std::bind(&MQBrokerServer::onOpenChannel, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<closeChannelRequest>(std::bind(&MQBrokerServer::onCloseChannel, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<declareExchangeRequest>(std::bind(&MQBrokerServer::onDeclareExchange, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<deleteExchangeRequest>(std::bind(&MQBrokerServer::onDeleteExchange, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<declareQueueRequest>(std::bind(&MQBrokerServer::onDeclareQueue, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<deleteQueueRequest>(std::bind(&MQBrokerServer::onDeleteQueue, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<queueBindRequest>(std::bind(&MQBrokerServer::onQueueBind, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<queueUnBindRequest>(std::bind(&MQBrokerServer::onQueueUnBind, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<basicPublishRequest>(std::bind(&MQBrokerServer::onBasicPublish, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); _dispatcher.registerMessageCallback<basicAckRequest>(std::bind(&MQBrokerServer::onBasicAck, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));  _dispatcher.registerMessageCallback<basicConsumeRequest>(std::bind(&MQBrokerServer::onBasicConsumer, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));  _dispatcher.registerMessageCallback<basicCancelRequest>(std::bind(&MQBrokerServer::onBasicCancel, this,std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));             //设置回调函数_server.setConnectionCallback(std::bind(&MQBrokerServer::onConnection, this, std::placeholders::_1));_server.setMessageCallback(std::bind(&ProtobufCodec::onMessage, _codec.get(), std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));}void start(){_server.start();_baseloop.loop();}private://打开信道void onOpenChannel(const muduo::net::TcpConnectionPtr& conn, const openChannelRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("打开信道失败,没有找到对应的Connection对象");conn->shutdown();return;}return mconn->openChannel(req);}//关闭信道void onCloseChannel(const muduo::net::TcpConnectionPtr& conn, const closeChannelRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("关闭信道失败,没有找到对应的Connection对象");conn->shutdown();return;}return mconn->closeChannel(req);}//声明交换机void onDeclareExchange(const muduo::net::TcpConnectionPtr& conn, const declareExchangeRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("声明交换机失败,没有找到对应的Connection对象");conn->shutdown();return;}//获取信道Channel::ptr cp = mconn->getChannel(req->cid());if (mconn.get() == nullptr){DLOG("声明交换机时,没有找到对应的信道");conn->shutdown();return;}return cp->declareExchange(req);}//删除交换机void onDeleteExchange(const muduo::net::TcpConnectionPtr& conn, const deleteExchangeRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("删除交换机失败,没有找到对应的Connection对象");conn->shutdown();return;}//获取信道Channel::ptr cp = mconn->getChannel(req->cid());if (mconn.get() == nullptr){DLOG("删除交换机时,没有找到对应的信道");conn->shutdown();return;}return cp->deleteExchange(req);}//声明队列void onDeclareQueue(const muduo::net::TcpConnectionPtr& conn, const declareQueueRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("声明队列失败,没有找到对应的Connection对象");conn->shutdown();return;}//获取信道Channel::ptr cp = mconn->getChannel(req->cid());if (mconn.get() == nullptr){DLOG("声明队列时,没有找到对应的信道");conn->shutdown();return;}return cp->declareQueue(req);}//删除队列void onDeleteQueue(const muduo::net::TcpConnectionPtr& conn, const deleteQueueRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("删除队列失败,没有找到对应的Connection对象");conn->shutdown();return;}//获取信道Channel::ptr cp = mconn->getChannel(req->cid());if (mconn.get() == nullptr){DLOG("删除队列时,没有找到对应的信道");conn->shutdown();return;}return cp->deleteQueue(req);}//队列绑定void onQueueBind(const muduo::net::TcpConnectionPtr& conn, const queueBindRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("绑定队列失败,没有找到对应的Connection对象");conn->shutdown();return;}//获取信道Channel::ptr cp = mconn->getChannel(req->cid());if (mconn.get() == nullptr){DLOG("绑定队列时,没有找到对应的信道");conn->shutdown();return;}return cp->queueBind(req);}//队列解绑void onQueueUnBind(const muduo::net::TcpConnectionPtr& conn, const queueUnBindRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("队列解绑失败,没有找到对应的Connection对象");conn->shutdown();return;}//获取信道Channel::ptr cp = mconn->getChannel(req->cid());if (mconn.get() == nullptr){DLOG("队列解绑时,没有找到对应的信道");conn->shutdown();return;}return cp->queueUnBind(req);}//消息发布void onBasicPublish(const muduo::net::TcpConnectionPtr& conn, const basicPublishRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("消息发布失败,没有找到对应的Connection对象");conn->shutdown();return;}//获取信道Channel::ptr cp = mconn->getChannel(req->cid());if (mconn.get() == nullptr){DLOG("消息发布时,没有找到对应的信道");conn->shutdown();return;}return cp->basicPublish(req);}//消息确认void onBasicAck(const muduo::net::TcpConnectionPtr& conn, const basicAckRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("消息确认失败,没有找到对应的Connection对象");conn->shutdown();return;}//获取信道Channel::ptr cp = mconn->getChannel(req->cid());if (mconn.get() == nullptr){DLOG("消息确认时,没有找到对应的信道");conn->shutdown();return;}return cp->basicAck(req);}//队列消息订阅void onBasicConsumer(const muduo::net::TcpConnectionPtr& conn, const basicConsumeRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("队列订阅消息失败,没有找到对应的Connection对象");conn->shutdown();return;}//获取信道Channel::ptr cp = mconn->getChannel(req->cid());if (mconn.get() == nullptr){DLOG("队列订阅消息时,没有找到对应的信道");conn->shutdown();return;}return cp->basicConsumer(req);}//队列消息取消订阅void onBasicCancel(const muduo::net::TcpConnectionPtr& conn, const basicCancelRequestPtr& req, muduo::Timestamp){//先获取Connection对象Connection::ptr mconn = _connection_manager->getConnection(conn);if (mconn.get() == nullptr){DLOG("队列消息取消订阅失败,没有找到对应的Connection对象");conn->shutdown();return;}//获取信道Channel::ptr cp = mconn->getChannel(req->cid());if (mconn.get() == nullptr){DLOG("队列消息取消订阅时,没有找到对应的信道");conn->shutdown();return;}return cp->basicCancel(req);}void onUnknownMessage(const muduo::net::TcpConnectionPtr&, const MessagePtr& message, muduo::Timestamp){std::cout << "onUnknownMessage: " << message->GetTypeName() << std::endl;}void onConnection(const muduo::net::TcpConnectionPtr& conn){if (conn->connected()){DLOG("连接建立成功");_connection_manager->newConnection(_virtual_host, _consumer_manager,_codec, conn, _threadpool);}else {DLOG("连接断开");_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;};
}

基本上所有函数的逻辑都是一样的,先获取连接,再获取信道,最后根据不同请求执行不同的任务


服务器的测试工作也暂时无法完成,需要等到客户端完成之后再进行统一的测试,所以我们先测试一下能否成功编译即可。

我们编写如下makefile:

.PHONY:myserver
CFLAG=-I../third/include
LFLAG=-L../third/lib -lgtest -lprotobuf -lsqlite3 -lmuduo_net -lmuduo_base -lz -lpthread
test:mq_server.cpp ../mqcommon/mq_msg.pb.cc ../mqcommon/mq_proto.pb.cc ../third/include/muduo/proto/codec.ccg++ -o $@ $^ -std=c++11 $(CFLAG) $(LFLAG) -w
.PHONY:clean
clean:rm -rf test

编译:

可以编译成功,并且启动服务器也没有问题。

服务器模块到这里就已经实现完毕了,接下来我们进入客户端模块的实现。

11. 客户端模块实现

在 RabbitMQ 中,提供服务的是信道,因此在客户端的实现中,弱化了 Client 客户端的概念,也就是说在 RabbitMQ 中并不会向用户展示网络通信的概念出来,而是以一种提供服务的形式来体现。

其实现思想类似于普通的功能接口封装,一个接口实现一个功能,接口内部完成向客户端请求的过程,但是对外并不需要体现出客户端与服务端通信的概念,用户需要什么服务就调用什么接口就行。

基于以上的思想,客户端的实现共分为四大模块:


订阅者模块:

  • 一个并不直接对用户展示的模块,其在客户端体现的作用就是对于角色的描述,表示这是一个消费者。

信道模块:

  • 一个直接面向用户的模块,内部包含多个向外提供的服务接口,用户需要什么服务,调用对应接口即可。
  • 其包含交换机声明/删除,队列声明/删除,绑定/解绑,消息发布/确认,订阅/解除订阅等服务。

连接模块:

  • 这是唯一能体现出网络通信概念的一个模块了,它向用户提供的功能就是用于打开/关闭信道。

异步线程模块:

  • 虽然客户端部分,并不对外体现网络通信的概念,但是本质上内部还是包含有网络通信的,因此既然有网络通信,那么就必须包含有一个网络通信 IO 事件监控线程模块,用于进行客户端连接的 IO 事件监控,以便于在事件出发后进行 IO操作。
  • 其次,在客户端部分存在一个情况就是,当一个信道作为消费者而存在的时候,服务端会向信道推送消息,而用户这边需要对收到的消息进行不同的业务处理,而这个消息的处理需要一个异步的工作线程池来完成。
  • 因此异步线程模块包含两个部分:
    • 客户端连接的 IO 事件监控线程
    • 推送过来的消息异步处理线程

基于以上模块,实现一个客户端的流程也就比较简单了

  • 1. 实例化异步线程对象
  • 2. 实例化连接对象
  • 3. 通过连接对象,创建信道
  • 4. 根据信道获取自己所需服务
  • 5. 关闭信道
  • 6. 关闭连接

11.1 订阅者模块

与服务端,并无太大差别,客户端这边虽然订阅者的存在感微弱了很多,但是还是有的,当进行队列消息订阅的时候,会伴随着一个订阅者对象的创建,(一个信道只有一个订阅者,一个信道只能订阅一个队列)而这个订阅者对象有以下几个作用:

  • 描述当前信道订阅了哪个队列的消息。
  • 描述了收到消息后该如何对这条消息进行处理。
  • 描述收到消息后是否需要进行确认回复。

订阅者信息:

  • 1)订阅者标识
  • 2)订阅队列名
  • 3)是否自动确认标志
  • 4)回调处理函数(收到消息后该如何处理的回调函数对象)

而客户端订阅者所需代码和服务端基本上一样,所以我们直接将服务器的mq_consumer..hpp文件拿过来用即可。

using ConsumerCallBack = std::function<void(const std::string&,  const BasicProperties* , const std::string&)>;
//1. 定义消费者信息结构
struct Consumer
{
public:using ptr = std::shared_ptr<Consumer>;Consumer(){}Consumer(const std::string &ctag, const std::string& queue_name, bool ack_flag, const ConsumerCallBack& cb): tag(ctag), qname(queue_name), callback(cb), auto_ack(ack_flag){DLOG("new Consumer: %s", ctag.c_str());}~Consumer(){DLOG("delete Consumer: %s", tag.c_str());}public:std::string tag;   // 1)消费者标识std::string qname;  // 2)订阅的队列名称ConsumerCallBack callback; // 3)一个消息的处理回调函数bool auto_ack;    // 4)是否自动应答标志
};

11.2 信道管理模块

同样的,客户端也有信道,其功能与服务端几乎一致,或者说不管是客户端的channel 还是服务端的 channel 都是为了用户提供具体服务而存在的,只不过服务端是为客户端的对应请求提供服务,而客户端的接口服务是为了用户具体需要服务,也可以理解是用户通过客户端 channel 的接口调用来向服务端发送对应请求,获取请求的服务。

1. 信道信息:

  • 1)信道 ID
  • 2)信道关联的网络通信连接对象
  • 3)protobuf 协议处理对象
  • 4)信道关联的消费者
  • 5)请求对应的响应信息队列(这里队列使用<请求 ID,响应>hash 表,以便于查找指定的响应)
  • 6)互斥锁&条件变量(大部分的请求都是阻塞操作,发送请求后需要等到响应才能继续,但是 muduo 库的通信是异步的,因此需要我们自己在收到响应后,通过判断是否是等待的指定响应来进行同步)

2. 信道操作:

  • 1)提供创建信道操作
  • 2)提供删除信道操作
  • 3)提供声明交换机操作(强断言-有则 OK,没有则创建)
  • 4)提供删除交换机
  • 5)提供创建队列操作(强断言-有则 OK,没有则创建)
  • 6)提供删除队列操作
  • 7)提供交换机-队列绑定操作
  • 8)提供交换机-队列解除绑定操作
  • 9)提供添加订阅操作
  • 10)提供取消订阅操作
  • 11)提供发布消息操作
  • 12)提供确认消息操作

3. 信道管理:

  • 1)创建信道
  • 2)查询信道
  • 3)删除信道

声明客户端信道类

using ProtobufCodecPtr = std::shared_ptr<ProtobufCodec>;    
//客户端只能收到两种响应类型
using basicConsumeResponsePtr = std::shared_ptr<basicConsumeResponse>;
using basicCommonResponsePtr = std::shared_ptr<basicCommonResponse>;
class Channel
{
public:Channel(const std::string& cid, const muduo::net::TcpConnectionPtr& conn, const ProtobufCodecPtr& codec);bool declareExchange(const std::string& name, ExchangeType type, bool durable, bool auto_delete, const google::protobuf::Map<std::string, std::string>& args);void deleteExchange(const std::string& qname);bool declareQueue(const std::string& name, bool durable, bool exclusive, bool auto_delete, google::protobuf::Map<std::string, std::string> args);void deleteQueue(const std::string& qname);bool queueBind(const std::string& ename, const std::string& qname, const std::string& key);void queueUnBind(const std::string& ename, const std::string& qname);void basicPublish(const std::string& ename, const BasicProperties* bp, const std::string& body);void basicAck(const std::string& msgid);void basicConsume(const std::string& consumer_tag, const std::string& qname,bool auto_ack, const ConsumerCallBack& cb);void basicCancel(const std::string& consumer_tag);public://连接收到基础响应,向响应消息队列中添加数据void putBasicResponse(const basicCommonResponsePtr& resp);//连接收到消息推送后,需要通过信道找到对应的消费者对象,通过回调函数进行消息处理void consume(const basicConsumeResponsePtr& resp);//等待服务器响应basicCommonResponsePtr waitResponse(const std::string& rid);private:std::string _cid;         // 1)信道 IDmuduo::net::TcpConnectionPtr _conn;  // 2)信道关联的网络通信连接对象ProtobufCodec _codec;     // 3)protobuf 协议处理对象Consumer::ptr _consumer;  // 4)信道关联的消费者std::unordered_map<std::string, basicCommonResponsePtr> _basic_resp;  // 5)请求对应的响应信息队列std::mutex _mutex;             //6)互斥锁std::condition_variable _cv;   //7)条件变量
};

定义客户端信道类

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 std::string& cid, const muduo::net::TcpConnectionPtr& conn, const ProtobufCodecPtr& codec): _cid(cid), _conn(conn), _codec(codec){}~Channel(){//信道关闭时,要取消订阅basicCancel();}bool declareExchange(const std::string& ename, ExchangeType type, bool durable, bool auto_delete, google::protobuf::Map<std::string, std::string>& args){std::string rid = UUIDHelper::uuid();//构造一个声明虚拟机的请求对象declareExchangeRequest req;req.set_rid(rid);req.set_cid(_cid);req.set_exchange_name(ename);req.set_exchange_type(type);req.set_durable(durable);req.set_auto_delete(auto_delete);req.mutable_args()->swap(args);//向服务器发送请求_codec->send(_conn, req);//等待服务器的响应basicCommonResponsePtr resp = waitResponse(rid);return resp->is_ok();}void deleteExchange(const std::string& ename){std::string rid = UUIDHelper::uuid();//构造一个删除虚拟机的请求对象deleteExchangeRequest req;req.set_rid(rid);req.set_cid(_cid);req.set_exchange_name(ename);//向服务器发送请求_codec->send(_conn, req);//等待服务器的响应waitResponse(rid);return;}bool declareQueue(const std::string& qname, bool durable, bool exclusive, bool auto_delete, google::protobuf::Map<std::string, std::string> args){std::string rid = UUIDHelper::uuid();//构造一个声明的请求对象declareQueueRequest req;req.set_rid(rid);req.set_cid(_cid);req.set_queue_name(qname);req.set_durable(durable);req.set_exclusive(exclusive);req.set_durable(auto_delete);req.mutable_args()->swap(args);//向服务器发送请求_codec->send(_conn, req);//等待服务器的响应basicCommonResponsePtr resp = waitResponse(rid);return resp->is_ok();}void deleteQueue(const std::string& qname){std::string rid = UUIDHelper::uuid();//构造一个删除队列的请求对象deleteQueueRequest req;req.set_rid(rid);req.set_cid(_cid);req.set_queue_name(req);//向服务器发送请求_codec->send(_conn, 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(_cid);req.set_exchange_name(ename);req.set_queue_name(qname);req.set_binding_key(key);//向服务器发送请求_codec->send(_conn, req);//等待服务器的响应basicCommonResponsePtr resp = waitResponse(rid);return resp->is_ok();}void queueUnBind(const std::string& ename, const std::string& qname){std::string rid = UUIDHelper::uuid();//构造一个队列解绑定的请求对象queueUnBindRequest req;req.set_rid(rid);req.set_cid(_cid);req.set_exchange_name(ename);req.set_queue_name(qname);//向服务器发送请求_codec->send(_conn, req);//等待服务器的响应waitResponse(rid);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(_cid);req.set_exchange_name(ename);req.set_body(body);if (bp){req.mutable_properties()->set_id(bp->id());req.mutable_properties()->set_delivery_mode(bp->delivery_mode());req.mutable_properties()->set_routing_key(bp->routing_key());}//向服务器发送请求_codec->send(_conn, req);//等待服务器的响应waitResponse(rid);return;}void basicAck(const std::string& msgid){if (_consumer.get() != nullptr){DLOG("消息确认时, 找不到消费者信息")return;}std::string rid = UUIDHelper::uuid();//构造一个消息确认的请求对象basicAckRequest req;req.set_rid(rid);req.set_cid(_cid);req.set_queue_name(_consumer->qname);req.set_message_id(msgid);//向服务器发送请求_codec->send(_conn, req);//等待服务器的响应waitResponse(rid);return;}bool basicConsume(const std::string& consumer_tag, const std::string& qname,bool auto_ack, const ConsumerCallBack& cb){if (_consumer.get() != nullptr){DLOG("已经订阅过队列%s, 不能重复订阅")return false;}std::string rid = UUIDHelper::uuid();//构造一个订阅队列的请求对象basicConsumeRequest req;req.set_rid(rid);req.set_cid(_cid);req.set_queue_name(qname);req.set_consumer_tag(consumer_tag);req.set_auto_ack(auto_ack);//向服务器发送请求_codec->send(_conn, req);//等待服务器的响应basicCommonResponsePtr resp = waitResponse(rid);if (resp->is_ok() == false){DLOG("添加订阅失败");return false;}_consumer = std::make_shared<Consumer>(consumer_tag, qname, auto_ack, cb);return true;}void basicCancel(){std::string rid = UUIDHelper::uuid();//构造一个消息确认的请求对象basicCancelRequest req;req.set_rid(rid);req.set_cid(_cid);req.set_queue_name(_consumer->qname);req.set_consumer_tag(_consumer->tag);//向服务器发送请求_codec->send(_conn, req);//等待服务器的响应waitResponse(rid);_consumer.reset(); //重置订阅return;}public://连接收到基础响应,向响应消息队列中添加数据void putBasicResponse(const basicCommonResponsePtr& resp){std::unique_lock<std::mutex> lock(_mutex);_basic_resp.insert({resp->rid(), resp});_cv.notify_all();}//连接收到消息推送后,需要通过信道找到对应的消费者对象,通过回调函数进行消息处理void consume(const basicConsumeResponsePtr& resp){if (_consumer.get() == nullptr){DLOG("消息处理时,未找到订阅者信息!");return;}if (_consumer->tag != resp->consumer_tag()){DLOG("收到的推送消息中消费者标识和当前信道消费者不一致!");return;}//_consumer->cb已经说明了收到消息后如何处理_consumer->callback(resp->consumer_tag(), resp->mutable_properties(), resp->body());}private:basicCommonResponsePtr waitResponse(const std::string& rid){std::unique_lock<std::mutex> lock(_mutex);_cv.wait(lock, [&rid, this](){return _basic_resp.find(rid) != _basic_resp.end();});basicCommonResponsePtr basic_resp = _basic_resp[rid];_basic_resp.erase(rid);return basic_resp;}private:std::string _cid;         // 1)信道 IDmuduo::net::TcpConnectionPtr _conn;  // 2)信道关联的网络通信连接对象ProtobufCodecPtr _codec;     // 3)protobuf 协议处理对象Consumer::ptr _consumer;  // 4)信道关联的消费者std::unordered_map<std::string, basicCommonResponsePtr> _basic_resp;  // 5)请求对应的响应信息队列std::mutex _mutex;             //6)互斥锁std::condition_variable _cv;   //7)条件变量
};
  • 整体来说所有函数的实现都比较简单,都是先构建请求对象,再向服务端发送请求对象,最后等待服务器的的响应。
  • putBasicResponse是给客户端服务器使用的,当客户端接收到了一条基础响应时,服务器就会通知对应的Channel信道调用该函数,该函数执行后,会唤醒条件变量的等待,那么在原客户端发起请求的函数中就会在waitRespoonse函数继续执行——将哈希表中对应的请求信息删除。
  • consume也是给客户端服务器使用的当客户端接收到了一条消息响应时,就会调用该函数,该函数内部会进行简单的判断,然后执行消息处理回调函数。

11.3 异步工作线程实现

客户端这边存在两个异步工作线程:

  • 一个是 muduo 库中客户端连接的异步循环线程 EventLoopThread。
  • 一个是当收到消息后进行异步处理的工作线程池。

这两项都不是以连接为单元进行创建的,而是创建后,可以用以多个连接,多个信道,多个消费者,因此单独进行封装。

#pragma once#include "muduo/net/EventLoopThread.h"
#include "../mqcommon/mq_threadpool.hpp"
#include "../mqcommon/mq_logger.hpp"namespace bitmq
{//异步工作者类class AsyncWorker{public:using ptr = std::shared_ptr<AsyncWorker>;muduo::net::EventLoopThread loopthread;threadpool pool;};
}

11.4 连接管理模块

在客户端这边, RabbitMQ 弱化了客户端的概念,因为用户所需的服务都是通过信道来提供的(不需要创建客户端向服务器发起请求,而是通过创建信道来向服务器发起请求),因此操作思想转换为先创建连接,通过连接创建信道,通过信道提供服务这一流程。

这个模块同样是针对 muduo 库客户端连接的二次封装,向用户提供创建 channel 信道的接口,创建信道后,可以通过信道来获取指定服务。

但是不管怎么说,连接管理模块实际上都是搭建了一个客户端,而这个客户端是之前我们编写过的:基于muduo库的protobuf通信协议程序的客户端。

代码如下:

#include <iostream>
#include <functional>
#include <memory>
#include "request.pb.h"
#include "muduo/proto/codec.h"
#include "muduo/proto/dispatcher_lite.h"
#include "muduo/proto/dispatcher.h"
#include "muduo/net/EventLoopThread.h"
#include "muduo/net/TcpClient.h"
#include "muduo/net/TcpConnection.h"
#include "muduo/base/CountDownLatch.h"
#include "muduo/base/Mutex.h"
using namespace std;class ProtoClient
{
public:typedef shared_ptr<google::protobuf::Message> MessagePtr;typedef shared_ptr<request::TranslateRequest> TranslateRequestPtr;typedef shared_ptr<request::TranslateResponse> TranslateResponsePtr;typedef shared_ptr<request::CalculatorRequest> CalculatorRequestPtr;typedef shared_ptr<request::CalculatorResponse> CalculatorResponsePtr;ProtoClient(const std::string& ip, int port): _latch(1), _client(_loopthread.startLoop(), muduo::net::InetAddress(ip, port), "Client"), _dispatcher(bind(&ProtoClient::onUnknownMessage, this, placeholders::_1, placeholders::_2, placeholders::_3)), _codec(bind(&ProtobufDispatcher::onProtobufMessage, &_dispatcher, placeholders::_1, placeholders::_2, placeholders::_3)){//注册业务回调函数_dispatcher.registerMessageCallback<request::TranslateResponse>(bind(&ProtoClient::translateResp, this, placeholders::_1, placeholders::_2, placeholders::_3));_dispatcher.registerMessageCallback<request::CalculatorResponse>(bind(&ProtoClient::calculatorResp, this, placeholders::_1, placeholders::_2, placeholders::_3));//注册连接以及消息回调_client.setConnectionCallback(std::bind(&ProtoClient::onConnection, this, placeholders::_1));_client.setMessageCallback(bind(&ProtobufCodec::onMessage, &_codec, placeholders::_1, placeholders::_2, placeholders::_3));}void connect(){_client.connect();_latch.wait();}void translate(){std::cout << "请输入你要翻译的单词:";string word;getline(cin, word);request::TranslateRequest req;req.set_word(word);send(req);}void calculator(){std::cout << "请输入操作数1:";int num1 = 0;cin >> num1;getchar();std::cout << "请输入操作符:";string op;getline(cin, op);std::cout << "请输入操作数2:";int num2 = 0;cin >> num2;getchar();request::CalculatorRequest req;req.set_num1(num1);req.set_op(op);req.set_num2(num2);send(req);}private:void translateResp(const muduo::net::TcpConnectionPtr& conn, const TranslateResponsePtr& req, muduo::Timestamp rt){cout << "结果为:" << req->result() << endl;}void calculatorResp(const muduo::net::TcpConnectionPtr& conn, const CalculatorResponsePtr& req, muduo::Timestamp rt){if (req->code() == false){cout << "发生错误" << endl;}else {cout << "结果为:" << req->result() << endl;}}//多态void send(const google::protobuf::Message& msg){if (_conn->connected())_codec.send(_conn, msg);}void onConnection(const muduo::net::TcpConnectionPtr& conn){if (conn->connected()){cout << "连接成功!!!" << endl;_latch.countDown();_conn = conn;}else {cout << "断开连接" << endl;_conn.reset();}}void onUnknownMessage(const muduo::net::TcpConnectionPtr& conn, const MessagePtr& message, muduo::Timestamp){std::cout << "onUnknownMessage: " << message->GetTypeName() << std::endl;conn->shutdown();}private:muduo::CountDownLatch _latch; //实现同步muduo::net::EventLoopThread _loopthread; //异步循环处理线程muduo::net::TcpConnectionPtr _conn; //客户端与服务器的连接muduo::net::TcpClient _client;    //客户端ProtobufDispatcher _dispatcher; //请求分发器ProtobufCodec _codec; //协议处理器
};

那我们可以直接在这个代码的基础上进行修改:

class Connection
{
public: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>;Connection(const std::string& ip, int port, AsyncWorker::ptr& worker): _latch(1), _client(_worker->loopthread.startLoop(), muduo::net::InetAddress(ip, port), "Client"), _worker(worker), _channel_manager(std::make_shared<ChannelManager>()), _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))){//注册业务回调函数_dispatcher.registerMessageCallback<basicConsumeResponse>(std::bind(&Connection::consumeResponse, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<basicCommonResponse>(std::bind(&Connection::basicResponse, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));//注册连接以及消息回调_client.setConnectionCallback(std::bind(&Connection::onConnection, this, std::placeholders::_1));_client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, _codec, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));//连接服务器_client.connect();_latch.wait();}//创建信道Channel::ptr openChannel();//关闭信道void closeChannel(const Channel::ptr& channel);private:void consumeResponse(const muduo::net::TcpConnectionPtr& conn, const basicConsumeResponsePtr& resp, muduo::Timestamp);void basicResponse(const muduo::net::TcpConnectionPtr& conn, const basicCommonResponsePtr& resp, muduo::Timestamp);void onConnection(const muduo::net::TcpConnectionPtr& conn){if (conn->connected()){DLOG("连接建立成功");_latch.countDown();_conn = conn;}else {DLOG("连接断开");_conn.reset();}}void onUnknownMessage(const muduo::net::TcpConnectionPtr& conn, const MessagePtr& message, muduo::Timestamp){std::cout << "onUnknownMessage: " << message->GetTypeName() << std::endl;conn->shutdown();}private:muduo::CountDownLatch _latch; //实现同步muduo::net::TcpConnectionPtr _conn; //客户端与服务器的连接muduo::net::TcpClient _client;    //客户端ProtobufDispatcher _dispatcher; //请求分发器ProtobufCodecPtr _codec; //协议处理器ChannelManager::ptr _channel_manager;AsyncWorker::ptr _worker;
};
  • 客户端只会向外提供两个服务:1.创建信道,2.删除信道。客户端可以通过创建信道获取信道对象,然后通过信道去完成所有的服务(这就是弱化了客户端的思想)
  • 在构造函数时会注册好_dispatcher的响应回调函数,当收到了基础响应时,就会调用basicResponse,而如果是消息响应,就会调用consumeResponse

完成连接管理模块代码:

class Connection
{
public: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>;Connection(const std::string& ip, int port, AsyncWorker::ptr& worker): _latch(1), _client(_worker->loopthread.startLoop(), muduo::net::InetAddress(ip, port), "Client"), _worker(worker), _channel_manager(std::make_shared<ChannelManager>()), _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))){//注册业务回调函数_dispatcher.registerMessageCallback<basicConsumeResponse>(std::bind(&Connection::consumeResponse, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));_dispatcher.registerMessageCallback<basicCommonResponse>(std::bind(&Connection::basicResponse, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));//注册连接以及消息回调_client.setConnectionCallback(std::bind(&Connection::onConnection, this, std::placeholders::_1));_client.setMessageCallback(std::bind(&ProtobufCodec::onMessage, _codec, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));//连接服务器_client.connect();_latch.wait();}//创建信道Channel::ptr openChannel(){Channel::ptr channel = _channel_manager->create(_conn, _codec);bool ret = channel->openChannel();if (ret == false){DLOG("打开信道失败!");return Channel::ptr();}return channel;}//关闭信道void closeChannel(const Channel::ptr& channel){channel->closeChannel();_channel_manager->remove(channel->getCid());}private:void consumeResponse(const muduo::net::TcpConnectionPtr& conn, const basicConsumeResponsePtr& resp, muduo::Timestamp){//1.找到信道Channel::ptr channel = _channel_manager->get(resp->cid());if (channel.get() == nullptr){DLOG("未找到信道信息!");return;}//2.封装异步任务(消息处理)抛入线程池_worker->pool.push([channel, resp](){channel->consume(resp);});}void basicResponse(const muduo::net::TcpConnectionPtr& conn, const basicCommonResponsePtr& resp, muduo::Timestamp){//1.找到信道Channel::ptr channel = _channel_manager->get(resp->cid());if (channel.get() == nullptr){DLOG("未找到信道信息!");return;}//2.将得到的响应对象添加到信道基础响应hash_map中channel->putBasicResponse(resp);}void onConnection(const muduo::net::TcpConnectionPtr& conn){if (conn->connected()){DLOG("连接建立成功");_latch.countDown();_conn = conn;}else {DLOG("连接断开");_conn.reset();}}void onUnknownMessage(const muduo::net::TcpConnectionPtr& conn, const MessagePtr& message, muduo::Timestamp){std::cout << "onUnknownMessage: " << message->GetTypeName() << std::endl;conn->shutdown();}private:muduo::CountDownLatch _latch; //实现同步muduo::net::TcpConnectionPtr _conn; //客户端与服务器的连接muduo::net::TcpClient _client;    //客户端ProtobufDispatcher _dispatcher; //请求分发器ProtobufCodecPtr _codec; //协议处理器ChannelManager::ptr _channel_manager;AsyncWorker::ptr _worker;
};

12. 基于 MQ 的生产者-消费者模型

到这里我们的项目也进入尾声了,在这一章节中,我们会搭建一个发布消息的生产者客户端和一个订阅消息的消费者客户端,然后对项目进行整体测试。

测试思路:

  • 1.必须有一个生产者客户端
    • 1.声明一个交换机 exchange1
    • 2.声明一个队列 queue1,binding_key = queue1
    • 3.声明一个队列 queue2,binding key = news.music.#
    • 4.将两个队列和交换机绑定起来
  • 2.搭建两个消费者客户端,分别各自订阅一个队列的消息测试:
    • 第一次,将交换机类型,定义为广播交换模式:理论结果,两个消费者客户端分别都能拿到消息
    • 第二次,将交换机类型,定义为直接交换模式:routing key=queue1 理论结果,只有queue1能拿到消息
    • 第三次,将交换机类型,定义为主题交换模式:routing key="news.music.pop",理论结果,只有queue2能拿到消息

12.1 实现生产者客户端

#include "mq_connection.hpp"int main()
{//1.实例化异步工作线程对象bitmq::AsyncWorker::ptr awp = std::make_shared<bitmq::AsyncWorker>();//2.实例化连接对象bitmq::Connection::ptr conn = std::make_shared<bitmq::Connection>("127.0.0.1", 8080, awp);//3.通过连接创建信道bitmq::Channel::ptr channel = conn->openChannel();//4.通过信道提供的服务完成所需//  1.声明一个交换机exchange1, 交换机类型为广播模式google::protobuf::Map<std::string, std::string> map;channel->declareExchange("exchange1", bitmq::ExchangeType::FANOUT, true, false, map);//  2.声明一个队列queue1channel->declareQueue("queue1", true, false, false, map);//  3.声明一个队列queue2channel->declareQueue("queue2", true, false, false, map);//  4.绑定queue1-exchange1, binding_key设置为queue1channel->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++){channel->basicPublish("exchange1", nullptr, "Hello World-" + std::to_string(i+1));}//6.关闭信道conn->closeChannel(channel);return 0;
}

12.2 实现消费者客户端

#include "mq_connection.hpp"void cb(bitmq::Channel::ptr& channel, std::string consumer_tag, const bitmq::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: ./consumer_client queue_name" << std::endl;return -1;}//1.实例化异步工作线程对象bitmq::AsyncWorker::ptr awp = std::make_shared<bitmq::AsyncWorker>();//2.实例化连接对象bitmq::Connection::ptr conn = std::make_shared<bitmq::Connection>("127.0.0.1", 8080, awp);//3.通过连接创建信道bitmq::Channel::ptr channel = conn->openChannel();//4.通过信道提供的服务完成所需//  1.声明一个交换机exchange1, 交换机类型为广播模式google::protobuf::Map<std::string, std::string> map;channel->declareExchange("exchange1", bitmq::ExchangeType::FANOUT, true, false, map);//  2.声明一个队列queue1channel->declareQueue("queue1", true, false, false, map);//  3.声明一个队列queue2channel->declareQueue("queue2", true, false, false, map);//  4.绑定queue1-exchange1, binding_key设置为queue1channel->queueBind("exchange1", "queue1", "queue1");//  5.绑定queue2-exchange1, binding_key设置为news.music.#channel->queueBind("exchange1", "queue2", "news.music.#");auto func = std::bind(cb, channel, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);std::string queue_name = argv[1];std::string consumer_name = "consumer";channel->basicConsume(consumer_name, queue_name, false, func);while (1){std::this_thread::sleep_for(std::chrono::seconds(3));}conn->closeChannel(channel);return 0;
}

12.3 测试的时候发现的问题

1.客户端启动程序后,客户端出现Segmentation fault错误

问题:_worker对象还没有初始化就使用


2.服务器启动后,消费客户端连接服务器,信道创建成功后程序崩溃,出现错误Segmentation fault

错误原因:getChannel时,it的判定设置错误,导致获取的是一个空指针。


3.消费客户端连接服务器后,在绑定队列之后,应订阅队列消息,但在订阅队列消息时程序停止不动

问题:服务器在订阅队列完毕后,没有给客户端发送响应,导致客户端阻塞等待


4.生产者生产消息后阻塞

问题:在服务端接收到了消息后,只是将消息jiao'ge了对应的队列,并没有通知生产者结果,导致生产者阻塞。


5.消费者在收到消息后,一直显示未找到消费者信息。

问题:线程池传入对象时,写成了&channel。

12.4 广播模式测试

在解决了上面的问题后,程序终于可以正常运行了,来看一下测试过程。

首先先介绍一下,每个程序对应的角色:

先启动服务器,再启动接收消息客户端,而此时队列中没有消息,所以这两个客户端会阻塞等待。

此时启动消息发布客户端,由于这次测试采用广播发送,所以消费者1和消费者2都可以收到消息。

消费者成功拿到了数据,程序也没有出现异常报错,说明我们的代码暂时没什么问题了。

到这里简单的功能测试就完毕了,接下来我们再测试一下其他模式的测试。

12.5 直接模式测试

主要就是修改消息发布客户端,接收消息客户端只需要修改交换机类型即可。

#include "mq_connection.hpp"int main()
{//1.实例化异步工作线程对象bitmq::AsyncWorker::ptr awp = std::make_shared<bitmq::AsyncWorker>();//2.实例化连接对象bitmq::Connection::ptr conn = std::make_shared<bitmq::Connection>("127.0.0.1", 8080, awp);//3.通过连接创建信道bitmq::Channel::ptr channel = conn->openChannel();//4.通过信道提供的服务完成所需//  1.声明一个交换机exchange1, 交换机类型为广播模式google::protobuf::Map<std::string, std::string> map;channel->declareExchange("exchange1", bitmq::ExchangeType::DIRECT, true, false, map);//  2.声明一个队列queue1channel->declareQueue("queue1", true, false, false, map);//  3.声明一个队列queue2channel->declareQueue("queue2", true, false, false, map);//  4.绑定queue1-exchange1, binding_key设置为queue1channel->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++){DLOG("开始发布消息: %d", i + 1);bitmq::BasicProperties bp;bp.set_id(bitmq::UUIDHelper::uuid());bp.set_delivery_mode(bitmq::DeliveryMode::DURABLE);bp.set_routing_key("queue1");channel->basicPublish("exchange1", &bp, "Hello World-" + std::to_string(i+1));}//6.关闭信道conn->closeChannel(channel);return 0;
}

代码需要简单的修改:将交换机的类型从广播交换改成直接交换,还需要给每条消息添加一个routing_key。

再启动消息发布客户端,预期效果是:只有消费者1能收到消息,消费者2收不到消息。

测试结果符合预期。

12.6 主题模式测试

#include "mq_connection.hpp"int main()
{//1.实例化异步工作线程对象bitmq::AsyncWorker::ptr awp = std::make_shared<bitmq::AsyncWorker>();//2.实例化连接对象bitmq::Connection::ptr conn = std::make_shared<bitmq::Connection>("127.0.0.1", 8080, awp);//3.通过连接创建信道bitmq::Channel::ptr channel = conn->openChannel();//4.通过信道提供的服务完成所需//  1.声明一个交换机exchange1, 交换机类型为广播模式google::protobuf::Map<std::string, std::string> map;channel->declareExchange("exchange1", bitmq::ExchangeType::TOPIC, true, false, map);//  2.声明一个队列queue1channel->declareQueue("queue1", true, false, false, map);//  3.声明一个队列queue2channel->declareQueue("queue2", true, false, false, map);//  4.绑定queue1-exchange1, binding_key设置为queue1channel->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++){DLOG("开始发布消息: %d", i + 1);bitmq::BasicProperties bp;bp.set_id(bitmq::UUIDHelper::uuid());bp.set_delivery_mode(bitmq::DeliveryMode::DURABLE);bp.set_routing_key("news.music.pop");channel->basicPublish("exchange1", &bp, "Hello World-" + std::to_string(i+1));}bitmq::BasicProperties bp;bp.set_id(bitmq::UUIDHelper::uuid());bp.set_delivery_mode(bitmq::DeliveryMode::DURABLE);bp.set_routing_key("news.sport.football");channel->basicPublish("exchange1", &bp, "Hello World-" + std::to_string(11));//6.关闭信道conn->closeChannel(channel);return 0;
}

我们测试十条能发布的还有一条不能发布的,预期结果是消费者1收不到信息,消费者2能收到10条消息。

结果符合预期。

13. 项目总结

首先明确我们所实现的项目:仿 RabbitMQ 实现一个简化版的消息队列组件,其内部实现了消息队列服务器以及客户端的搭建,并支持不同主机间消息的发布与订阅及消息推送功能。

其次项目中所用到的技术:基于 muduo 库实现底层网络通信服务器和客户端的搭建,在应用层基于 protobuf 协议设计应用层协议接口,在数据管理上使用了轻量数据库sqlite 来进行数据的持久化管理,以及基于 AMQP 模型的理解,实现整个消息队列项目技术的整合,并在项目的实现过程中使用 gtest 框架进行单元测试,完成项目的最终实现。

14. 扩展功能

  • 1)虚拟主机管理
  • 2)用户管理/用户认证
  • 3)交换机/队列 的独占模式和自动删除
  • 4)发送方确认(broker 给生产者的确认应答)
  • 5)消息的管理方式
  • 6)管理接口
  • 7)管理页面

15. 项目源码

https://gitee.com/fan-tianle/bitmq

http://www.dinnco.com/news/50298.html

相关文章:

  • 网站建设可行性研究报告 baidu百度seo查询系统
  • 安阳市哪里做网站建设关键词什么意思
  • 东圃做网站的公司关键词首页排名优化
  • 阿里云服务器上传网站网络优化工程师工作内容
  • 制作简历的免费模板网站培训机构优化
  • 慈溪网站建设报价镇江网站
  • 织梦网站排版能调整吗北京seo网站推广
  • 任丘市做网站价格百度推广培训班
  • 中小型网站建设百度热搜榜排名昨日
  • 国防教育网站建设说明书一键制作网站
  • 郑州做网站推广电话网络推广接单平台
  • 做趣味图形的网站郑州seo优化哪家好
  • 怎么建设自己网站会计培训班哪个机构比较好
  • 网站提交链接入口培训班有哪些
  • 申请网站空间常见的网络营销手段
  • 做网站的哪家好学seo如何入门
  • 银行的网站做的真垃圾新网域名注册
  • 卖环保设备做哪个网站好免费网站模板网
  • 网站优化软件有哪些网站优化快速排名软件
  • 政府网站建设人员的组织雅虎搜索引擎首页
  • wordpress运行机制深圳关键词优化平台
  • 网站权重的重要性长沙优化网站推广
  • 拖拽式建站平台医院线上预约
  • wordpress归档页面泉州seo排名扣费
  • python 网站架构百度地图人工电话
  • 网站建设 翻译seo排名赚下载
  • 网站如何做流量内蒙古最新消息
  • 网站升级中 模版友情链接交换平台
  • win7记事本做网站武汉seo公司出 名
  • 做网站市场分析搜索关键词排名推广