1. 实现背景
1.1 消息队列简介
消息队列(Message Queue)是一种用于 进程间通信 或 分布式系统 中各个组件之间通信的技术。它通过在发送方和接收方之间插入一个“消息队列”,实现消息的异步传递。
当今主流的消息队列有:RabbitMQ,Apache Kafka,Amazon SQS,ActiveMQ 等。
优点:
解耦性:消息队列将生产者和消费者解耦,使得它们可以独立地扩展和维护。
弹性伸缩:可以动态地增加或减少消费者数量,以应对流量的变化。
可靠性:通过消息持久化和确认机制,确保消息不会丢失。
削峰填谷:在高峰期积压消息,在低谷期处理消息,平滑系统负载。
异步通信:允许发送方和接收方异步处理任务,提高系统的响应能力。
1.2 模型结构
消息队列同时满足生产者-消费者模型及客户端-服务器模型,结构如下:
整个模型结构涉及到几个基本概念:
生产者(Producer):消息的发送方,它将消息发送到队列中。
消费者(Consumer):消息的接收方,它从队列中读取并处理消息。
中间件(Broker):负责管理和存储消息的中间件组件,此处为消息队列服务器
订阅(Subscribe):消费者通过订阅中间件的指定队列后,可收到想要的消息
虚拟主机(Virtual Host):消息队列中的逻辑实体,用于隔离和管理资源
交换机(Exchange):负责接收来自生产者的消息,按一定规则转发给队列
队列(Queue):存储消息的容器,消息按照顺序存储并传递。
绑定(Binding):一个交换机 与 一个队列 之间的连接关系。
该模型的工作原理:
生产者将消息发送到消息队列中。
队列按照先进先出的原则存储消息。
消费者从队列中读取消息并进行处理。
处理完毕后,消息可以被删除或者存档。
1.3 项目简介
本项目开发及运行环境:Windows 11,JDK 17,SpringBoot 3.1.3,SpringMVC,MyBatis,SQLite。(结尾附源码)
本项目源码:配点點/mq - Gitee.com
2 搭建项目基础结构
先创建好一个 SpringBoot 项目(引入 MyBatis,SpringWeb 依赖)。
既然消息队列基于客户端-服务器结构,我们首先搭建起这个结构。
2.1 构造服务器
- 创建两个包(server 包与 clients 包) 分别存放服务器与客户端的代码内容。
- 在 server 包底下创建一个服务器类 - BrokerServer 类。
public class BrokerServer {
private ServerSocket serverSocket = null;
// 引入一个哈希表,存储已连接的客户端
private ConcurrentHashMap<String, Socket> sessions = new ConcurrentHashMap<String, Socket>();
// 引入一个线程池,实现同时处理多个客户端的连接和请求
private ExecutorService executorService = null;
// 引入一个布尔变量,控制服务器是否继续运行
private volatile boolean runnable = true;
// 分配端口号
public BrokerServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
// 服务器启动方法
public void start() throws IOException {
System.out.println("服务器启动,等待客户端连接...");
executorService = Executors.newCachedThreadPool();
try {
while (runnable) {
// 阻塞等待连接
Socket clientSocket = serverSocket.accept();
executorService.submit(() -> {
// 处理连接
processConnection(clientSocket);
});
}
} // catch TODO
}
// 服务器关闭方法
public void stop() throws IOException {
// TODO
}
// 处理连接和请求
// 多个连接和请求同时处理,故接下来的业务运行需要考虑线程安全
private void processConnection(Socket clientSocket) {
try {
InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();
DataInputStream dataInputStream = new DataInputStream(inputStream);
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
while (true) {
// 接收并处理请求
// TODO
// 根据请求计算响应
// TODO
// 发送响应
// TODO
}
} // catch TODO
}
}
该类为一个服务器的基础结构,暂时未含任何业务内容。
后续将慢慢填充为一个完整的消息队列服务器,成为项目的核心类。
2.2 构造客户端
- 在clients包下创建两个客户端类 - Producer1类 和 Consumer1类
public class Producer1 {
private String clientTag = "producer1"; // 客户端唯一身份标识
private Socket clientSocket;
private InputStream inputStream;
private OutputStream outputStream;
private DataInputStream dataInputStream;
private DataOutputStream dataOutputStream;
public Producer1(String ip, int port) throws IOException {
// 连接到服务器
clientSocket = new Socket(ip, port);
}
// 客户端启动方法
public void start() {
try {
System.out.println("已连接到服务器");
inputStream = clientSocket.getInputStream();
outputStream = clientSocket.getOutputStream();
dataInputStream = new DataInputStream(inputStream);
dataOutputStream = new DataOutputStream(outputStream);
// 构造请求
// TODO
// 发送请求
// TODO
// 接收响应
// TODO
} // catch TODO
}
// 客户端关闭方法
public void stop() {
// TODO
}
}
public class Consumer1 {
private String clientTag = "consumer1"; // 客户端唯一身份标识
private Socket clientSocket;
private InputStream inputStream;
private OutputStream outputStream;
private DataInputStream dataInputStream;
private DataOutputStream dataOutputStream;
private boolean isReadResponse = false; // 是否接收响应
private Thread responseThread; // 接收响应线程
public Consumer1(String ip, int port) throws IOException {
clientSocket = new Socket(ip, port); // 连接到服务器
}
// 客户端启动方法
public void start() {
try {
System.out.println("已连接到服务器");
inputStream = clientSocket.getInputStream();
outputStream = clientSocket.getOutputStream();
dataInputStream = new DataInputStream(inputStream);
dataOutputStream = new DataOutputStream(outputStream);
responseThread = new Thread(() -> {
while (true) {
while (isReadResponse) {
// 接收响应
// TODO
}
}
});
responseThread.start();
// 构造请求
// TODO
// 发送请求
// TODO
} // catch TODO
}
}
这两个类同样暂未含任何业务内容,后续完善代码后将成为生产者客户端和消费者客户端的角色。
3. 服务器主体内容完善
本项目要模拟实现的消息队列本质上是一个服务器,接下来我们开始逐步完善。
构建若干个实体类,使得 虚拟机,内存,交换机,队列,绑定,绑定路由规则,消息,消费者,等概念在服务器中以实体类的形式存在。
3.1 虚拟机
- 构建虚拟机实体类 - VirtualHost 类
public class VirtualHost {
private String virtualHostName; // 唯一标识
private MemoryData memoryData = new MemoryData(); // 内存
private Router router = new Router(); // 绑定路由规则
// 引入一个顺序表,存储已连接的客户端
private ConcurrentHashMap<String, Socket> sessions = new ConcurrentHashMap<String, Socket>();
public VirtualHost(String virtualHostName) {
this.virtualHostName = virtualHostName;
}
public MemoryData getMemoryData() {
return memoryData;
}
public void setSessions(ConcurrentHashMap<String, Socket> sessions) {
this.sessions = sessions;
}
}
3.2 内存
- 构建内存实体类 - MemoryData 类
public class MemoryData { // 存储交换机 // key 是 exchangeName, value 是 Exchange 对象 private ConcurrentHashMap<String, Exchange> exchangeMap = new ConcurrentHashMap<>(); // 存储队列 // key 是 queueName, value 是 MSGQueue 对象 private ConcurrentHashMap<String, MQueue> queueMap = new ConcurrentHashMap<>(); // 存储绑定关系实体 // 外面的 key 是 exchangeName,里面的 key 是 queueName private ConcurrentHashMap<String, ConcurrentHashMap<String, Binding>> bindingsMap = new ConcurrentHashMap<>(); // 存储消息 // key 是 messageId, value 是 Message 对象 private ConcurrentHashMap<String, Message> messageMap = new ConcurrentHashMap<>(); // 存储每个队列有哪些消息 // key 是 queueName, value 是一个 Message 的链表 private ConcurrentHashMap<String, LinkedList<Message>> queueMessageMap = new ConcurrentHashMap<>(); // 内存操作 // TODO }
这个内存实体类是本项目(模拟实现消息队列)的核心类之一。以实体类的形式,并引入了哈希表,链表等数据结构来存储数据,从而实现了一个存储数据的内存。
(后续,该实体类将实现 交换机,队列,绑定,消息等实体类的增删查 等操作(第五章),以便提供给实现服务器接口时使用。)
3.3 交换机
- 构建交换机实体类 - Exchange 类
public class Exchange { // 唯一身份标识 // 交换机名字在后续使用中以 加上虚拟机名字作为前缀 的形式存在 private String name; // 交换机类型: DIRECT, FANOUT, TOPIC private ExchangeType type = ExchangeType.DIRECT; // Get/Set方法 }
此处涉及到了 交换机的类型:
在生产者给消息队列发送消息时,是把消息直接投递给了消息队列中的交换机,交换机根据自己的类型按照 绑定路由规则 再把消息投递给队列进行存储。生产者在生产消息时会给消息设置一个属性 - routingKey,用于 绑定路由规则 中。
-
直接交换机(Direct ):routingKey 的值作为队列名字,直接投递到指定队列中。
-
扇出交换机(Fanout):routingKey 无意义,直接将消息投递给每个已绑定的队列。
-
主题交换机(Topic):routingKey 参与 绑定路由规则 运算,算出符合条件的 绑定实体,即可将消息投递给这些 绑定实体 下的队列。
- 根据上述内容,我们再构造一个交换机类型的枚举 - ExchangeType 枚举
public enum ExchangeType {
DIRECT(0),
FANOUT(1),
TOPIC(2);
private final int type;
private ExchangeType(int type) {
this.type = type;
}
public int getType() {
return type;
}
}
3.4 队列
- 构造队列实体类 - MQueue 类
public class MQueue {
// 唯一身份标识.
private String name;
// 已被哪些消费者订阅了
private List<ConsumerEnv> consumerEnvList = new ArrayList<>();
// 记录当前取到了第几个消费者. 以实现轮询策略
private AtomicInteger consumerSeq = new AtomicInteger(0);
// 添加一个新的订阅者
public void addConsumerEnv(ConsumerEnv consumerEnv) {
consumerEnvList.add(consumerEnv);
}
// 轮询 处理每个已订阅的消费者
public ConsumerEnv chooseConsumer() {
if (consumerEnvList.size() == 0) {
// 该队列没有人订阅的
return null;
}
// 计算一下当前要取的元素的下标
int index = consumerSeq.get() % consumerEnvList.size();
consumerSeq.getAndIncrement();
return consumerEnvList.get(index);
}
// get/set 方法
}
消费者在订阅消息队列服务器时,具体订阅的对象是消息队列服务器中的队列。
故我们才此处实现的队列实体类中引入订阅者的存储,新增,获取等机制。
3.5 绑定
- 构造绑定实体类 - Binding 类
// 交换机与队列的绑定关系 以实体类的形式存在
public class Binding {
private String exchangeName;
private String queueName;
private String bindingKey; // 用于与 RoutingKey 匹配计算
// get/set 方法
}
这里涉及到的 bindingKey 属性用于后续参与 绑定路由规则 的转发运算,即与 RoutingKey 进行匹配。
3.6 绑定路由规则
- 构造绑定路由规则实体类 - Router 类
// 实现 bindingKey 和 routingKey 的匹配规则
public class Router {
// bindingKey 的构造规则:
// 1. 数字, 字母, 下划线
// 2. 使用 . 分割成若干部分
// 3. 允许存在 * 和 # 作为通配符. 但是通配符只能作为独立的分段.
public boolean checkBindingKey(String bindingKey) {
if (bindingKey.length() == 0) {
// 空字符串, 也是合法情况. 比如在使用 direct / fanout 交换机的时候, bindingKey 是用不上的.
return true;
}
// 检查字符串中不能存在非法字符
for (int i = 0; i < bindingKey.length(); i++) {
char ch = bindingKey.charAt(i);
if (ch >= 'A' && ch <= 'Z') {
continue;
}
if (ch >= 'a' && ch <= 'z') {
continue;
}
if (ch >= '0' && ch <= '9') {
continue;
}
if (ch == '_' || ch == '.' || ch == '*' || ch == '#') {
continue;
}
return false;
}
// 检查 * 或者 # 是否是独立的部分.
// aaa.*.bbb 合法情况; aaa.a*.bbb 非法情况.
String[] words = bindingKey.split("\\.");
for (String word : words) {
// 检查 word 长度 > 1 并且包含了 * 或者 # , 就是非法的格式了.
if (word.length() > 1 && (word.contains("*") || word.contains("#"))) {
return false;
}
}
// 约定一下, 通配符之间的相邻关系(人为(俺)约定的).
// 为啥这么约定? 因为前三种相邻的时候, 实现匹配的逻辑会非常繁琐, 同时功能性提升不大~~
// 1. aaa.#.#.bbb => 非法
// 2. aaa.#.*.bbb => 非法
// 3. aaa.*.#.bbb => 非法
// 4. aaa.*.*.bbb => 合法
for (int i = 0; i < words.length - 1; i++) {
// 连续两个 ##
if (words[i].equals("#") && words[i + 1].equals("#")) {
return false;
}
// # 连着 *
if (words[i].equals("#") && words[i + 1].equals("*")) {
return false;
}
// * 连着 #
if (words[i].equals("*") && words[i + 1].equals("#")) {
return false;
}
}
return true;
}
// routingKey 的构造规则:
// 1. 数字, 字母, 下划线
// 2. 使用 . 分割成若干部分
public boolean checkRoutingKey(String routingKey) {
if (routingKey.length() == 0) {
// 空字符串. 合法的情况. 比如在使用 fanout 交换机的时候, routingKey 用不上, 就可以设为 ""
return true;
}
for (int i = 0; i < routingKey.length(); i++) {
char ch = routingKey.charAt(i);
// 判定该字符是否是大写字母
if (ch >= 'A' && ch <= 'Z') {
continue;
}
// 判定该字母是否是小写字母
if (ch >= 'a' && ch <= 'z') {
continue;
}
// 判定该字母是否是阿拉伯数字
if (ch >= '0' && ch <= '9') {
continue;
}
// 判定是否是 _ 或者 .
if (ch == '_' || ch == '.') {
continue;
}
// 该字符, 不是上述任何一种合法情况, 就直接返回 false
return false;
}
// 把每个字符都检查过, 没有遇到非法情况. 此时直接返回 true
return true;
}
// 这个方法用来判定该消息是否可以转发给这个绑定对应的队列
public boolean route(ExchangeType exchangeType, Binding binding, Message message) throws MqException {
// 根据不同的 exchangeType 使用不同的判定转发规则.
if (exchangeType == ExchangeType.FANOUT) {
// 如果是 FANOUT 类型, 则该交换机上绑定的所有队列都需要转发
return true;
} else if (exchangeType == ExchangeType.TOPIC) {
// 如果是 TOPIC 主题交换机, 规则就要更复杂一些.
return routeTopic(binding, message);
} else {
// 其他情况是不应该存在的.
throw new MqException("[Router] 交换机类型非法! exchangeType=" + exchangeType);
}
}
// [测试用例]
// 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.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
private boolean routeTopic(Binding binding, Message message) {
// 先把这两个 key 进行切分
String[] bindingTokens = binding.getBindingKey().split("\\.");
String[] routingTokens = message.getRoutingKey().split("\\.");
// 引入两个下标, 指向上述两个数组. 初始情况下都为 0
int bindingIndex = 0;
int routingIndex = 0;
// 此处使用 while 更合适, 每次循环, 下标不一定就是 + 1, 不适合使用 for
while (bindingIndex < bindingTokens.length && routingIndex < routingTokens.length) {
if (bindingTokens[bindingIndex].equals("*")) {
// [情况二] 如果遇到 * , 直接进入下一轮. * 可以匹配到任意一个部分!!
bindingIndex++;
routingIndex++;
continue;
} else if (bindingTokens[bindingIndex].equals("#")) {
// 如果遇到 #, 需要先看看有没有下一个位置.
bindingIndex++;
if (bindingIndex == bindingTokens.length) {
// [情况三] 该 # 后面没东西了, 说明此时一定能匹配成功了!
return true;
}
// [情况四] # 后面还有东西, 拿着这个内容, 去 routingKey 中往后找, 找到对应的位置.
// findNextMatch 这个方法用来查找该部分在 routingKey 的位置. 返回该下标. 没找到, 就返回 -1
routingIndex = findNextMatch(routingTokens, routingIndex, bindingTokens[bindingIndex]);
if (routingIndex == -1) {
// 没找到匹配的结果. 匹配失败
return false;
}
// 找到的匹配的情况, 继续往后匹配.
bindingIndex++;
routingIndex++;
} else {
// [情况一] 如果遇到普通字符串, 要求两边的内容是一样的.
if (!bindingTokens[bindingIndex].equals(routingTokens[routingIndex])) {
return false;
}
bindingIndex++;
routingIndex++;
}
}
// [情况五] 判定是否是双方同时到达末尾
// 比如 aaa.bbb.ccc 和 aaa.bbb 是要匹配失败的.
if (bindingIndex == bindingTokens.length && routingIndex == routingTokens.length) {
return true;
}
return false;
}
private int findNextMatch(String[] routingTokens, int routingIndex, String bindingToken) {
for (int i = routingIndex; i < routingTokens.length; i++) {
if (routingTokens[i].equals(bindingToken)) {
return i;
}
}
return -1;
}
}
绑定消息规则 实体类本质上是一个工具类,进行 BingKey 与 RoutingKey 的匹配,具体的匹配机制这里参考了 RabbitMQ ,我们可以直接使用。
3.7 消息
- 构造消息实体类 - Message 类
public class Message extends BasicProperties implements Serializable {
private byte[] body; // 消息内容
// 创建一个工厂方法, 让工厂方法帮我们封装一下创建 Message 对象的过程.
public static Message createMessageWithId(String routingKey, byte[] body) {
Message message = new Message();
// 此处生成的 MessageId 以 M- 作为前缀.
message.messageId = "M-" + UUID.randomUUID();
message.routingKey = routingKey;
message.body = body;
return message;
}
public byte[] getBody() {
return body;
}
public void setBody(byte[] body) {
this.body = body;
}
@Override
public String toString() {
return "Message{" +
"messageId='" + messageId + '\'' +
", routingKey='" + routingKey + '\'' +
", body=" + Arrays.toString(body) +
'}';
}
}
在这个实体类中,我们除了存放真正消息数据以外,还继承了 BasicProperties 类,这个类中我们添加消息的基本属性:
public class BasicProperties implements Serializable {
// 消息的唯一身份标识
// 此处为了保证 id 的唯一性, 使用 UUID 来作为 message id
protected String messageId;
// 用于和 bindingKey 做匹配.
protected String routingKey;
// 实现在硬盘文件中 逻辑删除
// 0x1 表示有效,0x0 表示无效
private byte isValid = 0x1;
// get/set 方法
}
3.8 消费者
为了更好的实现订阅机制,我们需要在服务器端模拟构建 “消费者” 实体。所以此处的消费者实体并非真正的消费者,真正的消费者是前面已经创建的消费者客户端。
- 构造 “消费者” 实体类 - ConsumerEnv 类
// 在服务器端模拟 消费者 对象
public class ConsumerEnv {
private String consumerTag;
public ConsumerEnv(String consumerTag) {
this.consumerTag = consumerTag;
}
// get/set 方法
}
4. 约定服务器与客户端协同工作
接下来我们考虑的问题是,服务器具体可以如何与客户端协同工作,即服务器提供什么样的功能,客户端如何使用这些功能。
4.1 服务器接口
服务器提供的功能:
- 创建交换机
- 删除交换机
- 创建队列
- 删除队列
- 创建绑定
- 解除绑定
- 接收生产者的消息(生产消息)
- 接收消费者的订阅
- 发送消息给消费者(消费消息)
我们可以将这些功能整理成接口,这个接口不被直接使用,仅为了更直观地展示服务器功能
- 整理接口
public interface ServerAPI {
boolean exchangeDeclare(String exchangeName, ExchangeType exchangeType);
boolean exchangeDelete(String exchangeName);
boolean queueDeclare(String queueName);
boolean queueDelete(String queueName);
boolean bind(String queueName, String exchangeName, String bindingKey);
boolean unbind(String queueName, String exchangeName);
boolean produceMessage (String exchangeName, String routingKey, byte[] body);
boolean processSubscribe(String consumerTag, String queueName);
void consumeMessage(String ConsumerTag, Message message, MQueue queue) throws MqException, IOException, ClassNotFoundException;
}
这个接口我们后续再通过虚拟机实体类实现。
4.2 约定应用层协议
接下来,我们约定服务器与客户端的请求与响应的格式。
在本项目中,我们将请求与响应统一成一样的格式,如图所示:
type:请求 / 响应 的内容类型,表示 请求的目的 / 响应的目的
length:请求 / 响应 的有效载体的长度
payload:请求 / 响应 的有效载体
因为在本项目中,我们将 所有的请求与响应 统一成了一套格式,所以我们使用 type 进行类型上的区分,针对 type 的值我们进行如下约定:
type值 | 请求 | 响应 |
0x1 | 创建交换机 | 创建交换机成功 / 失败 |
0x2 | 删除交换机 | 删除交换机成功 / 失败 |
0x3 | 创建队列 | 创建队列成功 / 失败 |
0x4 | 删除队列 | 删除队列成功 / 失败 |
0x5 | 创建绑定 | 创建绑定成功 / 失败 |
0x6 | 解除绑定 | 解除绑定成功 / 失败 |
0x7 | 发送消息(生产消息) | 发送消息(生产消息成功 / 失败 |
0x8 | 订阅队列 | 订阅队列成功 / 失败 |
0xc | / | 发送消息给消费者(消费消息) |
- 构造请求 / 响应的实体类
// 请求
public class Request {
private int type;
private int length;
private byte[] payload;
// get/set 方法
}
// 响应
public class Response {
private int type;
private int length;
private byte[] payload;
// get/set 方法
}
4.3 Payload 类
既然 请求 / 响应 随着 type 的不同有着不同的内容类型,因此有效载体 Payload 也需要有与之对应的不同的种类。
接下来我们引入这些类,命名规则如下:
请求中的 payload 类:
type值 | 请求 | Payload 类 |
0x1 | 创建交换机 | PayloadExchangeDeclare |
0x2 | 删除交换机 | PayloadExchangeDelete |
0x3 | 创建队列 | PayloadQueueDeclare |
0x4 | 删除队列 | PayloadQueueDelete |
0x5 | 创建绑定 | PayloadBind |
0x6 | 解除绑定 | PayloadUnbind |
0x7 | 发送消息(生产消息) | PayloadProduce |
0x8 | 订阅队列 | PayloadSubscribe |
响应中的 payload 类:
type值 | 响应 | Payload 类 |
0x1 | 创建交换机成功 / 失败 | PayloadResponse |
0x2 | 删除交换机成功 / 失败 | PayloadResponse |
0x3 | 创建队列成功 / 失败 | PayloadResponse |
0x4 | 删除队列成功 / 失败 | PayloadResponse |
0x5 | 创建绑定成功 / 失败 | PayloadResponse |
0x6 | 解除绑定成功 / 失败 | PayloadResponse |
0x7 | 发送消息(生产消息成功 / 失败 | PayloadResponse |
0x8 | 订阅队列成功 / 失败 | PayloadResponse |
0xc | 发送消息给消费者(消费消息) | PayloadResponseSubscribe |
约定好命名格式后,我们就可以创建出这些类。在这里就不进行列出,详见源码。
此外,我们创建一个每个 payload 都带有的基础属性的类 - BasicArguments 类,让每个 payload 类都继承它,以少代码冗余。
- 创建 BasicArguments 类
public class BasicArguments implements Serializable {
private String rid;
private String clientTag;
// get/set 方法
}
4.4 填充服务器代码
接下来,我们将初始的服务器框架进行填充。补充具体的 接收请求,根据请求计算响应,发送响应 的部分等。
- 填充服务器类
public class BrokerServer {
private ServerSocket serverSocket = null;
private VirtualHost virtualHost = new VirtualHost("default");
// 引入一个哈希表,存储已连接的客户端
private ConcurrentHashMap<String, Socket> sessions = new ConcurrentHashMap<String, Socket>();
// 引入一个线程池,实现同时处理多个客户端的连接和请求
private ExecutorService executorService = null;
// 引入一个布尔变量,控制服务器是否继续运行
private volatile boolean runnable = true;
// 分配端口号
public BrokerServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动,等待客户端连接...");
executorService = Executors.newCachedThreadPool();
try {
while (runnable) {
// 阻塞等待连接
Socket clientSocket = serverSocket.accept();
executorService.submit(() -> {
// 处理连接
processConnection(clientSocket);
});
}
} catch (SocketException e) {
System.out.println("服务器停止运行");
e.printStackTrace();
}
}
public void stop() throws IOException {
runnable = false;
// 放弃线程池中的所有任务,销毁所有线程
executorService.shutdownNow();
serverSocket.close();
}
// 处理连接和请求
// 多个连接和请求同时处理,故接下来的业务运行需要考虑线程安全
private void processConnection(Socket clientSocket) {
try {
InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream();
DataInputStream dataInputStream = new DataInputStream(inputStream);
DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
while (true) {
// 接收并处理请求
Request request = readRequest(dataInputStream);
// 根据请求计算响应
Response response = process(request, clientSocket);
// 发送响应
writeResponse(dataOutputStream, response);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (MqException e) {
e.printStackTrace();
}
}
private void writeResponse(DataOutputStream dataOutputStream, Response response) throws IOException {
dataOutputStream.writeInt(response.getType());
dataOutputStream.writeInt(response.getLength());
dataOutputStream.write(response.getPayload());
// 刷新缓冲区
dataOutputStream.flush();
}
private Response process(Request request, Socket clientSocket) throws IOException, ClassNotFoundException, MqException {
// 1. 服务器显示收到请求的基本信息
BasicArguments basicArguments = (BasicArguments) BinaryTool.fromBytes(request.getPayload());
// 这句代码容易挂
sessions.put(basicArguments.getClientTag(), clientSocket);
System.out.println(sessions.toString());
virtualHost.setSessions(sessions);
System.out.println("[BrokerServer]: 收到请求:" + "rid = " + basicArguments.getRid() + ", type=" + request.getType() + ", length=" + request.getLength());
// 2. 根据 type 值进行分支
boolean ok = true;
if (request.getType() == 0x1) {
// 创建交换机. 此时 payload 就是 ExchangeDeclareArguments 对象了.
PayloadExchangeDeclare payloadExchangeDeclare = (PayloadExchangeDeclare) BinaryTool.fromBytes(request.getPayload());
ok = virtualHost.exchangeDeclare(payloadExchangeDeclare.getExchangeName(), payloadExchangeDeclare.getExchangeType());
} else if (request.getType() == 0x2) {
PayloadExchangeDelete payloadExchangeDelete = (PayloadExchangeDelete) BinaryTool.fromBytes(request.getPayload());
ok = virtualHost.exchangeDelete(payloadExchangeDelete.getExchangeName());
} else if (request.getType() == 0x3) {
PayloadQueueDeclare payloadQueueDeclare = (PayloadQueueDeclare) BinaryTool.fromBytes(request.getPayload());
ok = virtualHost.queueDeclare(payloadQueueDeclare.getQueueName());
} else if (request.getType() == 0x4) {
PayloadQueueDelete payloadQueueDelete = (PayloadQueueDelete) BinaryTool.fromBytes(request.getPayload());
ok = virtualHost.queueDelete((payloadQueueDelete.getQueueName()));
} else if (request.getType() == 0x5) {
PayloadBind payloadBind = (PayloadBind) BinaryTool.fromBytes(request.getPayload());
ok = virtualHost.bind(payloadBind.getQueueName(), payloadBind.getExchangeName(), payloadBind.getBindingKey());
} else if (request.getType() == 0x6) {
PayloadUnbind payloadUnbind = (PayloadUnbind) BinaryTool.fromBytes(request.getPayload());
ok = virtualHost.unbind(payloadUnbind.getQueueName(), payloadUnbind.getExchangeName());
} else if (request.getType() == 0x7) {
PayloadProduce payloadProduce = (PayloadProduce) BinaryTool.fromBytes(request.getPayload());
ok = virtualHost.produceMessage(payloadProduce.getExchangeName(), payloadProduce.getRoutingKey(), payloadProduce.getBody());
} else if (request.getType() == 0x8) {
PayloadSubscribe payloadSubscribe = (PayloadSubscribe) BinaryTool.fromBytes(request.getPayload());
ok = virtualHost.processSubscribe(payloadSubscribe.getClientTag(), payloadSubscribe.getQueueName());
} else {
// 当前的 type 是非法的.
throw new MqException("[BrokerServer] 未知的 type:type=" + request.getType());
}
// 3. 构造响应
PayloadResponse payloadResponse = new PayloadResponse();
payloadResponse.setRid(basicArguments.getRid());
payloadResponse.setOk(ok);
byte[] payload = BinaryTool.toBytes(payloadResponse);
Response response = new Response();
response.setType(request.getType());
response.setLength(payload.length);
response.setPayload(payload);
System.out.println("[BrokerServer] 返回响应:rid=" + payloadResponse.getRid() + ", type=" + response.getType() + ", length=" + response.getLength() + ", ok = " + payloadResponse.isOk());
return response;
}
private Request readRequest(DataInputStream dataInputStream) throws IOException {
Request request = new Request();
// readInt(), 从基础输入流中读取4个字节并将它们解释为一个int值
request.setType(dataInputStream.readInt());
request.setLength(dataInputStream.readInt());
byte[] payload = new byte[request.getLength()];
// 从基础输入流中读取数据,填充payload
// 返回读取了的字节的个数
// 此处 payload 内容类型有多种可能
int n = dataInputStream.read(payload);
// 格式检查
if (n != request.getLength()) {
throw new IOException("读取请求格式出错!");
}
request.setPayload(payload);
return request;
}
}
在本项目中,我们只实例化出一个虚拟主机。
在代码的根据请求计算响应的部分中,使用到了虚拟主机的一些方法,这些方法是在接下来实现服务器接口所创建的方法。
5. 实现服务器接口
我们已经构造了服务器的接口,接下来,我们使用虚拟机实体类(VirtualHost 类)实现这些接口。
在接下来实现接口的过程中,涉及到的内存与硬盘两大模块的操作,我们在实现接口的同时也按需补充上内存的方法。硬盘部分我们暂时先引入方法的调用,在下一章(第六章)中我们进行具体的补充实现。
5.1 创建交换机
@Override
public boolean exchangeDeclare(String exchangeName, ExchangeType exchangeType) {
// 拿到交换机名字
exchangeName = virtualHostName + exchangeName;
try {
synchronized (exchangeLocker) {
// 1. 判定该交换机是否已经存在
Exchange existsExchange = memoryData.getExchange(exchangeName);
if (existsExchange != null) {
// 该交换机已经存在!
System.out.println("[VirtualHost] 交换机已经存在! exchangeName=" + exchangeName);
return true;
}
// 2. 创建交换机
Exchange exchange = new Exchange();
exchange.setName(exchangeName);
exchange.setType(exchangeType);
// 3.写入硬盘
diskDataCenter.insertExchange(exchange);
// 4.写入内存
memoryData.insertExchange(exchange);
System.out.println("[BrokerServer] 交换机创建完成! exchangeName=" + exchangeName);
}
return true;
} catch (Exception e) {
System.out.println("[VirtualHost] 交换机创建失败! exchangeName=" + exchangeName);
e.printStackTrace();
return false;
}
}
- 相关内存操作:
public void insertExchange(Exchange exchange) {
exchangeMap.put(exchange.getName(), exchange);
System.out.println("[MemoryData] 新交换机添加成功 exchangeName=" + exchange.getName());
}
5.2 删除交换机
@Override
public boolean exchangeDelete(String exchangeName) {
// 拿到交换机名字
exchangeName = virtualHostName + exchangeName;
try {
synchronized (exchangeLocker) {
// 1. 先找到对应的交换机.
Exchange toDelete = memoryData.getExchange(exchangeName);
if (toDelete == null) {
throw new MqException("[VirtualHost] 交换机不存在无法删除!");
}
// 2.从硬盘上删除
diskDataCenter.deleteExchange(exchangeName);
// 3.从内存上删除
memoryData.deleteExchange(exchangeName);
System.out.println("[VirtualHost] 交换机删除成功! exchangeName=" + exchangeName);
}
return true;
} catch (Exception e) {
System.out.println("[VirtualHost] 交换机删除失败! exchangeName=" + exchangeName);
e.printStackTrace();
return false;
}
}
- 相关内存操作:
public void deleteExchange(String exchangeName) {
exchangeMap.remove(exchangeName);
System.out.println("[MemoryDataCenter] 交换机删除成功 exchangeName=" + exchangeName);
}
5.3 创建队列
@Override
public boolean queueDeclare(String queueName) {
// 拿到队列名字
queueName = virtualHostName + queueName;
try {
synchronized (queueLocker) {
// 1. 判定队列是否存在
MQueue existsQueue = memoryData.getQueue(queueName);
if (existsQueue != null) {
System.out.println("[VirtualHost] 队列已经存在! queueName=" + queueName);
return true;
}
// 2. 创建队列对象
MQueue queue = new MQueue();
queue.setName(queueName);
// 3.写入硬盘
diskDataCenter.insertQueue(queue);
// 4.写入内存
memoryData.insertQueue(queue);
System.out.println("[VirtualHost] 队列创建成功! queueName=" + queueName);
}
return true;
} catch (Exception e) {
System.out.println("[VirtualHost] 队列创建失败! queueName=" + queueName);
e.printStackTrace();
return false;
}
}
- 相关内存操作:
public void insertQueue(MQueue queue) {
queueMap.put(queue.getName(), queue);
System.out.println("[MemoryDataCenter] 新队列添加成功! queueName=" + queue.getName());
}
5.4 删除队列
@Override
public boolean queueDelete(String queueName) {
// 拿到队列名字
queueName = virtualHostName + queueName;
try {
synchronized (queueLocker) {
// 1. 根据队列名字, 查询下当前的队列对象
MQueue queue = memoryData.getQueue(queueName);
if (queue == null) {
throw new MqException("[VirtualHost] 队列不存在! 无法删除! queueName=" + queueName);
}
// 2.从硬盘中删除
diskDataCenter.deleteQueue(queueName);
// 3.从内存中删除
memoryData.deleteQueue(queueName);
System.out.println("[VirtualHost] 删除队列成功! queueName=" + queueName);
}
return true;
} catch (Exception e) {
System.out.println("[VirtualHost] 删除队列失败! queueName=" + queueName);
e.printStackTrace();
return false;
}
}
- 相关内存操作:
public void deleteQueue(String queueName) {
queueMap.remove(queueName);
System.out.println("[MemoryDataCenter] 队列删除成功! queueName=" + queueName);
}
5.5 创建绑定
@Override
public boolean bind(String queueName, String exchangeName, String bindingKey) {
queueName = virtualHostName + queueName;
exchangeName = virtualHostName + exchangeName;
try {
synchronized (exchangeLocker) {
synchronized (queueLocker) {
// 1. 判定当前的绑定是否已经存在了.
Binding existsBinding = memoryData.getBinding(exchangeName, queueName);
if (existsBinding != null) {
throw new MqException("[VirtualHost] binding 已经存在! queueName=" + queueName
+ ", exchangeName=" + exchangeName);
}
// 2. 验证 bindingKey 是否合法.
if (!router.checkBindingKey(bindingKey)) {
throw new MqException("[VirtualHost] bindingKey 非法! bindingKey=" + bindingKey);
}
// 3. 创建 Binding 对象
Binding binding = new Binding();
binding.setExchangeName(exchangeName);
binding.setQueueName(queueName);
binding.setBindingKey(bindingKey);
// 4. 检查交换机和队列是否存在
MQueue queue = memoryData.getQueue(queueName);
if (queue == null) {
throw new MqException("[VirtualHost] 队列不存在! queueName=" + queueName);
}
Exchange exchange = memoryData.getExchange(exchangeName);
if (exchange == null) {
throw new MqException("[VirtualHost] 交换机不存在! exchangeName=" + exchangeName);
}
// 5.写入硬盘
diskDataCenter.insertBinding(binding);
// 6.写入内存
memoryData.insertBinding(binding);
}
}
System.out.println("[VirtualHost] 绑定创建成功! exchangeName=" + exchangeName
+ ", queueName=" + queueName);
return true;
} catch (Exception e) {
System.out.println("[VirtualHost] 绑定创建失败! exchangeName=" + exchangeName
+ ", queueName=" + queueName);
e.printStackTrace();
return false;
}
}
- 相关内存操作:
public void insertBinding(Binding binding) throws MqException {
// 先使用 exchangeName 查一下, 对应的哈希表是否存在. 不存在就创建一个.
ConcurrentHashMap<String, Binding> bindingMap = bindingsMap.computeIfAbsent(binding.getExchangeName(), k -> new ConcurrentHashMap<>());
synchronized (bindingMap) {
// 再根据 queueName 查一下. 如果已经存在, 就抛出异常. 不存在才能插入.
if (bindingMap.get(binding.getQueueName()) != null) {
throw new MqException("[MemoryDataCenter] 绑定已经存在! exchangeName=" + binding.getExchangeName() +
", queueName=" + binding.getQueueName());
}
bindingMap.put(binding.getQueueName(), binding);
}
System.out.println("[MemoryDataCenter] 新绑定添加成功! exchangeName=" + binding.getExchangeName()
+ ", queueName=" + binding.getQueueName());
}
5.6 解除绑定
@Override
public boolean unbind(String queueName, String exchangeName) {
queueName = virtualHostName + queueName;
exchangeName = virtualHostName + exchangeName;
try {
synchronized (exchangeLocker) {
synchronized (queueLocker) {
// 1. 获取 binding 看是否已经存在~
Binding binding = memoryData.getBinding(exchangeName, queueName);
if (binding == null) {
throw new MqException("[VirtualHost] 删除绑定失败! 绑定不存在! exchangeName=" + exchangeName + ", queueName=" + queueName);
}
// 2.从硬盘中删除
diskDataCenter.deleteBinding(binding);
// 3.从内存中删除
memoryData.deleteBinding(binding);
System.out.println("[VirtualHost] 删除绑定成功!");
}
}
return true;
} catch (Exception e) {
System.out.println("[VirtualHost] 删除绑定失败!");
e.printStackTrace();
return false;
}
}
- 相关内存操作:
public void deleteBinding(Binding binding) throws MqException {
ConcurrentHashMap<String, Binding> bindingMap = bindingsMap.get(binding.getExchangeName());
if (bindingMap == null) {
// 该交换机没有绑定任何队列. 报错.
throw new MqException("[MemoryDataCenter] 绑定不存在! exchangeName=" + binding.getExchangeName()
+ ", queueName=" + binding.getQueueName());
}
bindingMap.remove(binding.getQueueName());
System.out.println("[MemoryDataCenter] 绑定删除成功! exchangeName=" + binding.getExchangeName()
+ ", queueName=" + binding.getQueueName());
}
5.7 生产消息
@Override
public boolean produceMessage(String exchangeName, String routingKey, byte[] body) {
try {
// 1. 拿到交换机的名字
exchangeName = virtualHostName + exchangeName;
// 2. 检查 routingKey 是否合法
if (!router.checkRoutingKey(routingKey)) {
throw new MqException("[VirtualHost] routingKey 非法! routingKey=" + routingKey);
}
// 3. 查找交换机对象
Exchange exchange = memoryData.getExchange(exchangeName);
if (exchange == null) {
throw new MqException("[VirtualHost] 交换机不存在! exchangeName=" + exchangeName);
}
// 4. 判定交换机的类型
if (exchange.getType() == ExchangeType.DIRECT) {
// 按照直接交换机的方式来转发消息
// 以 routingKey 作为队列的名字, 直接把消息写入指定的队列中.
// 此时, 可以无视绑定关系.
String queueName = virtualHostName + routingKey;
// 5. 构造消息对象
Message message = Message.createMessageWithId(routingKey, body);
// 6. 查找该队列名对应的对象
MQueue queue = memoryData.getQueue(queueName);
if (queue == null) {
throw new MqException("[VirtualHost] 队列不存在! queueName=" + queueName);
}
// 7. 队列存在, 直接给队列中写入消息
sendMessage(queue, message);
} else {
// 按照 fanout 和 topic 的方式来转发.
// 5. 找到该交换机关联的所有绑定, 并遍历这些绑定对象
ConcurrentHashMap<String, Binding> bindingsMap = memoryData.getBindings(exchangeName);
for (Map.Entry<String, Binding> entry : bindingsMap.entrySet()) {
// 1) 获取到绑定对象, 判定对应的队列是否存在
Binding binding = entry.getValue();
MQueue queue = memoryData.getQueue(binding.getQueueName());
if (queue == null) {
// 此处不抛出异常了. 可能此处有多个这样的队列.
// 不因为一个队列的失败, 影响到其他队列的消息的传输.
System.out.println("[VirtualHost] basicPublish 发送消息时, 发现队列不存在! queueName=" + binding.getQueueName());
continue;
}
// 2) 构造消息对象
Message message = Message.createMessageWithId(routingKey, body);
// 3) 判定这个消息是否能转发给该队列.
// 如果是 fanout, 所有绑定的队列都要转发的.
// 如果是 topic, 还需要判定下, bindingKey 和 routingKey 是不是匹配.
if (!router.route(exchange.getType(), binding, message)) {
continue;
}
// 4) 真正转发消息给队列
sendMessage(queue, message);
}
}
return true;
} catch (Exception e) {
System.out.println("[VirtualHost] 消息发送失败!");
e.printStackTrace();
return false;
}
}
private void sendMessage(MQueue queue, Message message) throws IOException, MqException, InterruptedException {
// 写入硬盘
diskDataCenter.sendMessage(queue, message);
// 写入内存
memoryData.sendMessage(queue, message);
// 此处还需要补充一个逻辑, 通知消费者可以消费消息了.
consumerManager.notifyConsume(queue.getName());
}
- 相关内存操作:
// 添加消息
public void addMessage(Message message) {
messageMap.put(message.getMessageId(), message);
System.out.println("[MemoryDataCenter] 新消息添加成功! messageId=" + message.getMessageId());
}
// 投递消息到指定队列
public void sendMessage(MQueue queue, Message message) {
// 把消息放到对应的队列数据结构中.
// 先根据队列的名字, 找到该队列对应的消息链表.
LinkedList<Message> messages = queueMessageMap.computeIfAbsent(queue.getName(), k -> new LinkedList<>());
// 再把数据加到 messages 里面
synchronized (messages) {
messages.add(message);
}
// 在这里把该消息也往消息中心中插入一下. 假设如果 message 已经在消息中心存在, 重复插入也没关系.
// 主要就是相同 messageId, 对应的 message 的内容一定是一样的. (服务器代码不会对 Message 内容做修改 basicProperties 和 body)
addMessage(message);
System.out.println("[MemoryDataCenter] 消息被投递到队列中! messageId=" + message.getMessageId());
}
5.8 订阅
在实现订阅的接口这部分,我们需要引入一个 消费者管理类 。用来协助完成订阅模块和下一小节的消费消息模块
- 构造 消费者管理类 - ConsumerManager 类
public class ConsumerManager {
// 引用所在的虚拟机实例
private VirtualHost virtualHost;
// 存放令牌的队列
private BlockingQueue<String> tokenQueue = new LinkedBlockingQueue<>();
// 扫描线程
private Thread scannerThread = null;
private ExecutorService workerPool = Executors.newFixedThreadPool(4);
public ConsumerManager (VirtualHost virtualHost) {
this.virtualHost = virtualHost;
scannerThread = new Thread(() -> {
while (true) {
try {
// 1. 拿到令牌
String queueName = tokenQueue.take();
// 2. 根据令牌, 找到队列
MQueue queue = virtualHost.getMemoryData().getQueue(queueName);
if (queue == null) {
throw new MqException("[ConsumerManager] 取令牌后发现, 该队列名不存在! queueName=" + queueName);
}
// 3. 从这个队列中消费一个消息.
synchronized (queue) {
preConsume(queue);
}
} catch (InterruptedException | MqException e) {
e.printStackTrace();
}
}
});
// 把线程设为后台线程.
scannerThread.setDaemon(true);
scannerThread.start();
}
// 当生产消息后,这个方法就会被调用,从而触发消费消息的机制
public void notifyConsume(String queueName) throws InterruptedException {
tokenQueue.put(queueName);
}
// 给指定的一个队列新增一个订阅的消费者
public void addConsumer(String consumerTag, String queueName) throws MqException, IOException, ClassNotFoundException {
// 找到对应的队列.
MQueue queue = virtualHost.getMemoryData().getQueue(queueName);
if (queue == null) {
throw new MqException("[ConsumerManager] 队列不存在! queueName=" + queueName);
}
ConsumerEnv consumerEnv = new ConsumerEnv(consumerTag);
synchronized (queue) {
queue.addConsumerEnv(consumerEnv);
// 如果当前队列中已经有了一些消息了, 需要立即就消费掉.
int n = virtualHost.getMemoryData().getMessageCount(queueName);
System.out.println(consumerTag + " 想订阅的队列中有 " + n + " 条消息");
for (int i = 0; i < n; i++) {
// 这个方法调用一次就消费一条消息.
virtualHost.consumeMessage(consumerTag, virtualHost.getMemoryData().pollMessage(queueName), queue);
}
}
}
// 包装消费工作
public void preConsume(MQueue queue) {
// 1. 轮询,拿到消费者
ConsumerEnv luckyDog = queue.chooseConsumer();
if (luckyDog == null) {
// 当前队列没有消费者, 暂时不消费. 等后面有消费者出现再说.
return;
}
// 2. 从队列中取出一个消息
Message message = virtualHost.getMemoryData().pollMessage(queue.getName());
if (message == null) {
// 当前队列中还没有消息, 也不需要消费.
return;
}
// 3. 消费消息
workerPool.submit(() -> {
try {
virtualHost.consumeMessage(luckyDog.getConsumerTag(), message, queue);
// 删除内存中的消息
virtualHost.getMemoryData().removeMessage(message.getMessageId());
System.out.println("[ConsumerManager] 消息被成功消费! queueName=" + queue.getName());
} catch (Exception e) {
e.printStackTrace();
}
});
}
}
- 实现订阅方法
@Override
public boolean processSubscribe(String consumerTag, String queueName) {
// 构造一个 ConsumerEnv 对象, 把这个对应的队列找到, 再把这个 Consumer 对象添加到该队列中.
queueName = virtualHostName + queueName;
try {
consumerManager.addConsumer(consumerTag, queueName);
System.out.println("[VirtualHost] basicConsume 成功! queueName=" + queueName);
return true;
} catch (Exception e) {
System.out.println("[VirtualHost] basicConsume 失败! queueName=" + queueName);
e.printStackTrace();
return false;
}
}
5.9 消费消息
@Override
public void consumeMessage(String clientTag, Message message, MQueue queue) throws MqException, IOException, ClassNotFoundException {
// 先知道当前这个收到的消息, 要发给哪个客户端.
// 此处 consumerTag 其实是 channelId. 根据 channelId 去 sessions 中查询, 就可以得到对应的
// socket 对象了, 从而可以往里面发送数据了
// 1. 根据 channelId 找到 socket 对象
Socket clientSocket = sessions.get(clientTag);
if (clientSocket == null || clientSocket.isClosed()) {
throw new MqException("[BrokerServer] 订阅消息的客户端已经关闭!");
}
// 2. 构造响应数据
PayloadResponseSubscribe payloadResponseSubscribe = new PayloadResponseSubscribe();
payloadResponseSubscribe.setRid("1234567"); // 由于这里只有响应, 没有请求, 不需要去对应. rid 暂时不需要.
payloadResponseSubscribe.setOk(true);
payloadResponseSubscribe.setClientTag(clientTag);
BasicProperties basicProperties = new BasicProperties();
System.out.println(message.toString());
basicProperties.setMessageId(message.getMessageId());
basicProperties.setRoutingKey(message.getRoutingKey());
payloadResponseSubscribe.setBasicProperties(basicProperties);
payloadResponseSubscribe.setBody(message.getBody());
byte[] payload = BinaryTool.toBytes(payloadResponseSubscribe);
Response response = new Response();
// 0xc 表示服务器给消费者客户端推送的消息数据.
response.setType(0xc);
// response 的 payload 就是一个 SubScribeReturns
response.setLength(payload.length);
response.setPayload(payload);
// 3. 把数据写回给客户端.
// 此处的 dataOutputStream 这个对象不能 close
// 如果 把 dataOutputStream 关闭, 就会直接把 clientSocket 里的 outputStream 也关了.
// 此时就无法继续往 socket 中写入后续数据了.
DataOutputStream dataOutputStream = new DataOutputStream(clientSocket.getOutputStream());
writeResponse(dataOutputStream, response);
// 4. 从硬盘中删除数据
diskDataCenter.deleteMessage(queue, message);
}
private void writeResponse(DataOutputStream dataOutputStream, Response response) throws IOException {
dataOutputStream.writeInt(response.getType());
dataOutputStream.writeInt(response.getLength());
dataOutputStream.write(response.getPayload());
// 这个刷新缓冲区也是重要的操作!!
dataOutputStream.flush();
}
- 相关内存操作:
// 从队列中取消息
public Message pollMessage(String queueName) {
// 根据队列名, 查找一下, 对应的队列的消息链表.
LinkedList<Message> messages = queueMessageMap.get(queueName);
if (messages == null) {
return null;
}
synchronized (messages) {
// 如果没找到, 说明队列中没有任何消息.
if (messages.size() == 0) {
return null;
}
// 链表中有元素, 就进行头删.
Message currentMessage = messages.remove(0);
System.out.println("[MemoryDataCenter] 消息从队列中取出! messageId=" + currentMessage.getMessageId());
return currentMessage;
}
}
// 队列中消息的个数
public int getMessageCount(String queueName) {
LinkedList<Message> messages = queueMessageMap.get(queueName);
if (messages == null) {
// 队列中没有消息
return 0;
}
synchronized (messages) {
return messages.size();
}
}
6. 持久化存储
至此,我们模拟构造的 消息队列 的已经可以进行运转,能够完成最基本的使命。但是每次运行完的数据得不到持久化保存,接下来我们硬入硬盘管理数据。
硬盘管理数据主要有两种模式:数据库存储,文件存储。
我们针对运行时内存中所存储的数据进行持久化到硬盘上,有四种核心数据需要我们进行持久化存储:交换机,队列,绑定,消息。对于不同的数据我们用不同的方式进行存储:
数据库存储 | 文件存储 |
交换机 | 消息 |
队列 | |
绑定 |
6.1 数据库存储
在本项目中,我们使用 SQLite 数据库,发挥其轻巧,方便引入的优点。
- 我们从 Java 中央仓库 中导入依赖:
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.41.0.1</version>
</dependency>
在配置好配置文件后(详见源代码中的 .yml 文件),我们创建一个接口,列出所要操作数据库的方法。
- 我们围绕所要存储的交换机,队列,绑定等实体数据,我们可以列出如下方法:
@Mapper
public interface MetaMapper {
// 建表 : 交换机表,队列表,绑定实体表
void createExchangeTable();
void createQueueTable();
void createBindingTable();
// 增删拿
void insertExchange(Exchange exchange);
List<Exchange> selectAllExchanges();
void deleteExchange(String exchangeName);
void insertQueue(MQueue queue);
List<MQueue> selectAllQueues();
void deleteQueue(String queueName);
void insertBinding(Binding binding);
List<Binding> selectAllBindings();
void deleteBinding(Binding binding);
}
- 我们使用MyBatis框架,用 .xml 文件实现这些接口
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mq.server.diskdatamanager.database.MetaMapper">
<update id="createExchangeTable">
create table if not exists exchange (
name varchar(50) primary key,
type int
);
</update>
<update id="createQueueTable">
create table if not exists queue (
name varchar(50) primary key
);
</update>
<update id="createBindingTable">
create table if not exists binding (
exchangeName varchar(50),
queueName varchar(50),
bindingKey varchar(256)
);
</update>
<insert id="insertExchange" parameterType="com.example.mq.server.virtualhost.Exchange">
insert into exchange values(#{name}, #{type});
</insert>
<select id="selectAllExchanges" resultType="com.example.mq.server.virtualhost.Exchange">
select * from exchange;
</select>
<delete id="deleteExchange" parameterType="java.lang.String">
delete from exchange where name = #{exchangeName};
</delete>
<insert id="insertQueue" parameterType="com.example.mq.server.virtualhost.MQueue">
insert into queue values(#{name});
</insert>
<select id="selectAllQueues" resultType="com.example.mq.server.virtualhost.MQueue">
select * from queue;
</select>
<delete id="deleteQueue" parameterType="java.lang.String">
delete from queue where name = #{queueName};
</delete>
<insert id="insertBinding" parameterType="com.example.mq.server.virtualhost.Binding">
insert into binding values(#{exchangeName}, #{queueName}, #{bindingKey});
</insert>
<select id="selectAllBindings" resultType="com.example.mq.server.virtualhost.Binding">
select * from binding;
</select>
<delete id="deleteBinding" parameterType="com.example.mq.server.virtualhost.Binding">
delete from binding where exchangeName = #{exchangeName} and queueName = #{queueName};
</delete>
</mapper>
值得注意的是,我们在上述过程中并没有进行建库操作 ,原因是,SQLite的数据库可以仅仅是一个文件,我们只需要在配置文件中配置好这个文件的路径位置(即数据库路径位置)以及文件名(数据库名),当这个文件还未被创建并且程序走到了建表操作时,MyBatis 会自动为我们创建好这个文件,即数据库。
- 再引入一个 数据库管理类 - DataBaseManager 类,来封装上述接口
// 整合数据库操作
public class DataBaseManager {
private MetaMapper metaMapper;
private String rl = "./diskdata/meta.db";
// 针对数据库进行初始化
// 确保数据库和库中的表存在
public void init() {
// 手动的获取到 MetaMapper
// 从 Spring 中拿到现成的对象
metaMapper = MqApplication.context.getBean(MetaMapper.class);
if (!checkDBExists()) {
// 数据库不存在, 进行建表操作(自动建库)
// 建表
createTable();
// 插入默认数据
createDefaultData();
} else {
// 数据库已经存在
System.out.println("[DataBaseManager] 数据库已检查完毕");
}
}
public void deleteDB() {
// 删文件
File file = new File(rl);
boolean ret = file.delete();
if (ret) {
System.out.println("[DataBaseManager] 删除数据库文件成功");
} else {
System.out.println("[DataBaseManager] 删除数据库文件失败");
}
// 删目录
// 使用 delete 删除目录的时候, 需要保证目录是空的
File dataDir = new File("./data");
ret = dataDir.delete();
if (ret) {
System.out.println("[DataBaseManager] 删除数据库目录成功!");
} else {
System.out.println("[DataBaseManager] 删除数据库目录失败!");
}
}
private boolean checkDBExists() {
File file = new File(rl);
if (file.exists()) {
return true;
}
return false;
}
private void createTable() {
metaMapper.createExchangeTable();
metaMapper.createQueueTable();
metaMapper.createBindingTable();
}
private void createDefaultData() {
// 存在一个默认的匿名交换机,类型是 DIRECT
Exchange exchange = new Exchange();
exchange.setName("");
exchange.setType(ExchangeType.DIRECT);
metaMapper.insertExchange(exchange);
System.out.println("[DataBaseManager] 数据库已完成初始化");
}
// 封装数据库操作
public void insertExchange(Exchange exchange) {
metaMapper.insertExchange(exchange);
}
public List<Exchange> selectAllExchanges() {
return metaMapper.selectAllExchanges();
}
public void deleteExchange(String exchangeName) {
metaMapper.deleteExchange(exchangeName);
}
public void insertQueue(MQueue queue) {
metaMapper.insertQueue(queue);
}
public List<MQueue> selectAllQueues() {
return metaMapper.selectAllQueues();
}
public void deleteQueue(String queueName) {
metaMapper.deleteQueue(queueName);
}
public void insertBinding(Binding binding) {
metaMapper.insertBinding(binding);
}
public List<Binding> selectAllBindings() {
return metaMapper.selectAllBindings();
}
public void deleteBinding(Binding binding) {
metaMapper.deleteBinding(binding);
}
}
这个类在接下里会和文件管理类一起被再次封装成硬盘接口
6.2 文件存储
我们使用文件来存储消息数据,我们进行如下约定:
- 在一个总目录下,创建多个队列目录,每个目录代表着一个队列,用队列名命名目录。
- 每个队列目录下,都有两个文件,分别存储 消息 和 有效消息统计数据。
如图所示:
其中,消息文件内容为二进制数据,有效消息统计文件为文本数据:
消息文件:
而有效消息统计文件的内容十分简单,仅为一行文本数据
格式为:消息总数 /t 有效消息数
例如:1200/t900。所代表的意思是当前队列中一共有1200条消息,其中900条为有效消息,另外300条被逻辑删除。
6.3 代码封装
根据上述的文件存储机制,我们可以创建一个类来实现。
- 构建 文件管理类 - MessageFileManager 类
public class MessageFileManager {
// 构造一个静态内部类, 来表示一个队列的 有效消息统计文件
static public class Stat {
public int totalCount; // 总消息数量
public int validCount; // 有效消息数量
}
private String rl = "./diskdata/messages/";
public void init() {
// TODO
}
// 创建指定队列对应的文件和目录
public void createQueueFiles(String queueName) throws IOException {
// 1. 创建队列目录
File baseDir = new File(getQueueDir(queueName));
if (!baseDir.exists()) {
boolean ok = baseDir.mkdirs(); // 创建目录
if (!ok) {
throw new IOException("创建目录失败! baseDir=" + baseDir.getAbsolutePath());
}
}
// 2. 创建 消息文件
File queueDataFile = new File(getQueueDataPath(queueName));
if (!queueDataFile.exists()) {
boolean ok = queueDataFile.createNewFile();
if (!ok) {
throw new IOException("创建文件失败! queueDataFile=" + queueDataFile.getAbsolutePath());
}
}
// 3. 创建 有效消息统计文件
File queueStatFile = new File(getQueueStatPath(queueName));
if (!queueStatFile.exists()) {
boolean ok = queueStatFile.createNewFile();
if (!ok) {
throw new IOException("创建文件失败! queueStatFile=" + queueStatFile.getAbsolutePath());
}
}
// 4. 给有效消息统计文件 设定初始值 0\t0
Stat stat = new Stat();
stat.totalCount = 0;
stat.validCount = 0;
writeStat(queueName, stat);
}
private String getQueueDir(String queueName) {
return rl + queueName;
}
// 获取 消息文件 路径
private String getQueueDataPath(String queueName) {
return getQueueDir(queueName) + "/queue_data.txt";
}
// 获取 有效消息统计文件 路径
private String getQueueStatPath(String queueName) {
return getQueueDir(queueName) + "/queue_stat.txt";
}
// 写入 有效消息统计文件
private void writeStat(String queueName, Stat stat) {
// 使用 PrintWrite 来写文件.
// OutputStream 打开文件, 默认情况下, 会直接把原文件清空. 此时相当于新的数据覆盖了旧的.
try (OutputStream outputStream = new FileOutputStream(getQueueStatPath(queueName))) {
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.write(stat.totalCount + "\t" + stat.validCount);
printWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
// 删除指定队列的目录和文件.
public void destroyQueueFiles(String queueName) throws IOException {
// 先删除文件,再删除目录
File queueDataFile = new File(getQueueDataPath(queueName));
boolean ok1 = queueDataFile.delete();
File queueStatFile = new File(getQueueStatPath(queueName));
boolean ok2 = queueStatFile.delete();
File baseDir = new File(getQueueDir(queueName));
boolean ok3 = baseDir.delete();
if (!ok1 || !ok2 || !ok3) {
throw new IOException("删除队列目录和文件失败! baseDir=" + baseDir.getAbsolutePath());
}
}
// 存储指定消息到指定队列
public void sendMessage(MQueue queue, Message message) throws MqException, IOException {
// 1. 检查队列的目录及文件是否都存在
if (!checkFilesExits(queue.getName())) {
throw new MqException("[MessageFileManager] 队列对应的文件不存在! queueName=" + queue.getName());
}
// 2. 序列化 message 对象
byte[] messageBinary = BinaryTool.toBytes(message);
// 对队列对象进行加锁
// 如果两个线程往同一个队列中写消息,此时需要阻塞等待;如果往不同队列中写消息,则不需要阻塞等待
synchronized (queue) {
// 3. 填充 offsetBeg 和 offsetEnd
File queueDataFile = new File(getQueueDataPath(queue.getName()));
message.setOffsetBeg(queueDataFile.length() + 4);// .length() 获取文件内容长度,单位字节
message.setOffsetEnd(queueDataFile.length() + 4 + messageBinary.length);
// 4. 写入 消息文件
try (OutputStream outputStream = new FileOutputStream(queueDataFile, true)) {
try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
// 写入消息长度信息,.writeInt() 写四个 4 个字节
dataOutputStream.writeInt(messageBinary.length);
// 写入消息
dataOutputStream.write(messageBinary);
}
}
// 5. 更新 有效消息统计文件
Stat stat = readStat(queue.getName());
stat.totalCount += 1;
stat.validCount += 1;
writeStat(queue.getName(), stat);
}
}
// 检查队列的目录和文件是否存在
public boolean checkFilesExits(String queueName) {
File queueDataFile = new File(getQueueDataPath(queueName));
if (!queueDataFile.exists()) {
return false;
}
File queueStatFile = new File(getQueueStatPath(queueName));
if (!queueStatFile.exists()) {
return false;
}
return true;
}
// 读取 有效消息统计文件
private Stat readStat(String queueName) {
// 由于当前的消息统计文件是文本文件, 可以直接使用 Scanner 来读取文件内容
Stat stat = new Stat();
try (InputStream inputStream = new FileInputStream(getQueueStatPath(queueName))) {
Scanner scanner = new Scanner(inputStream);
stat.totalCount = scanner.nextInt();
stat.validCount = scanner.nextInt();
return stat;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
// 删除指定队列里的指定消息,逻辑删除
public void deleteMessage(MQueue queue, Message message) throws IOException, ClassNotFoundException {
synchronized (queue) {
try (RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()), "rw")) {
// 1. 从文件中读取对应的 Message 数据
byte[] bufferSrc = new byte[(int) (message.getOffsetEnd() - message.getOffsetBeg())];
randomAccessFile.seek(message.getOffsetBeg()); // .seek(long pos) 方法用于将文件指针移动到指定的位置
randomAccessFile.read(bufferSrc);
// 2. 反序列化,拿到 message 对象
Message diskMessage = (Message) BinaryTool.fromBytes(bufferSrc);
// 3. 将 isValid 设置为无效
diskMessage.setIsValid((byte) 0x0);
// 4. 写入文件
byte[] bufferDest = BinaryTool.toBytes(diskMessage);
// 重新调整文件光标
randomAccessFile.seek(message.getOffsetBeg());
randomAccessFile.write(bufferDest);
}
// 5. 更新统计文件
Stat stat = readStat(queue.getName());
if (stat.validCount > 0) {
stat.validCount -= 1;
}
writeStat(queue.getName(), stat);
}
}
// 读取指定队列里的所有消息
// 由于该方法是在程序启动时调用,此时服务器还不能处理请求,因此不涉及多线程操作文件
public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
LinkedList<Message> messages = new LinkedList<>();
try (InputStream inputStream = new FileInputStream(getQueueDataPath(queueName))) {
try (DataInputStream dataInputStream = new DataInputStream(inputStream)) {
// 光标
long currentOffset = 0;
// 一个文件中包含了多条消息,故 循环读取
while (true) {
// 1. 读取当前消息的长度
// readInt 可能会读到文件的末尾(EOF),并抛出 EOFException 异常
int messageSize = dataInputStream.readInt();
// 2. 按照这个长度, 读取消息内容
byte[] buffer = new byte[messageSize];
int actualSize = dataInputStream.read(buffer);
if (messageSize != actualSize) {
// 如果不匹配, 说明文件有问题, 格式错乱了
throw new MqException("[MessageFileManager] 文件格式错误! queueName=" + queueName);
}
// 3. 反序列化,拿到 message 对象
Message message = (Message) BinaryTool.fromBytes(buffer);
// 4. 判定消息是否有效
if (message.getIsValid() != 0x1) {
// 无效数据,仅更新光标即可
currentOffset += (4 + messageSize);
continue;
}
// 有效数据
// 因为 消息所在文件位置记录信息 并没有被序列化存储在文件中,故需要更新此记录信息
message.setOffsetBeg(currentOffset + 4);
message.setOffsetEnd(currentOffset + 4 + messageSize);
// 更新光标
currentOffset += (4 + messageSize);
// 添加进链表
messages.add(message);
}
} catch (EOFException e) {
// 这个 catch 并非真是处理 "异常", 而是处理 "正常" 的业务逻辑. 文件读到末尾, 会被 readInt 抛出该异常.
System.out.println("[MessageFileManager] 加载 Message 数据完成!");
}
}
return messages;
}
// GC
public void gc(MQueue queue) throws MqException, IOException, ClassNotFoundException {
// 上锁
// 进行 gc 的时候, 是针对消息数据文件进行大洗牌. 在这个过程中, 其他线程不能针对该队列的消息文件做任何修改
synchronized (queue) {
// 由于 gc 操作可能比较耗时, 此处统计一下执行消耗的时间.
// 记录当前时刻
long gcBeg = System.currentTimeMillis();
// 1. 创建一个新的文件
File queueDataNewFile = new File(getQueueDataNewPath(queue.getName()));
if (queueDataNewFile.exists()) {
// 正常情况下, 这个文件不应该存在. 如果存在, 就是意外~~ 说明上次 gc 了一半, 程序意外崩溃了.
throw new MqException("[MessageFileManager] gc 时发现该队列的 queue_data_new 已经存在! queueName=" + queue.getName());
}
boolean ok = queueDataNewFile.createNewFile();
if (!ok) {
throw new MqException("[MessageFileManager] 创建文件失败! queueDataNewFile=" + queueDataNewFile.getAbsolutePath());
}
// 2. 从旧的文件中, 读取出所有的有效消息对象了. (这个逻辑直接调用上述方法即可, 不必重新写了)
LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());
// 3. 把有效消息, 写入到新的文件中.
try (OutputStream outputStream = new FileOutputStream(queueDataNewFile)) {
try (DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
for (Message message : messages) {
// 写入
byte[] buffer = BinaryTool.toBytes(message);
dataOutputStream.writeInt(buffer.length);
dataOutputStream.write(buffer);
}
}
}
// 4. 删除旧的数据文件, 并且把新的文件进行重命名
File queueDataOldFile = new File(getQueueDataPath(queue.getName()));
ok = queueDataOldFile.delete();
if (!ok) {
throw new MqException("[MessageFileManager] 删除旧的数据文件失败! queueDataOldFile=" + queueDataOldFile.getAbsolutePath());
}
// 把 queue_data_new.txt => queue_data.txt
ok = queueDataNewFile.renameTo(queueDataOldFile);
if (!ok) {
throw new MqException("[MessageFileManager] 文件重命名失败! queueDataNewFile=" + queueDataNewFile.getAbsolutePath()
+ ", queueDataOldFile=" + queueDataOldFile.getAbsolutePath());
}
// 5. 更新统计文件
Stat stat = readStat(queue.getName());
stat.totalCount = messages.size();
stat.validCount = messages.size();
writeStat(queue.getName(), stat);
// 记录当前时刻
long gcEnd = System.currentTimeMillis();
System.out.println("[MessageFileManager] gc 执行完毕! queueName=" + queue.getName() + ", time="
+ (gcEnd - gcBeg) + "ms");
}
}
private String getQueueDataNewPath(String queueName) {
return getQueueDir(queueName) + "/queue_data_new.txt";
}
// 检查当前是否要针对该队列的消息数据文件进行 GC
public boolean checkGC(String queueName) {
// 判定是否要 GC, 是根据总消息数和有效消息数. 这两个值都是在 消息统计文件 中的
Stat stat = readStat(queueName);
if (stat.totalCount > 2000 && (double)stat.validCount / (double)stat.totalCount < 0.5) {
return true;
}
return false;
}
}
在整个消息存储管理中,我们对于消息的删除采用的是逻辑删除。在程序运转一段时间后会产生无效消息的垃圾数据,因此我们需要引入垃圾回收机制。
在上述代码中,我们的GC采用的是复制算法。当消息总数超过2000并且垃圾文件占比超过50%时就会触发复制算法的垃圾回收机制。
再接着,我们将 文件管理类 连同 数据库管理类 一同封装到硬盘管理类中。
- 构造 硬盘管理类 - DiskDataCenter 类
// 封装硬盘操作
public class DiskDataCenter {
private DataBaseManager dataBaseManager = new DataBaseManager();
private MessageFileManager messageFileManager = new MessageFileManager();
public void init() {
dataBaseManager.init();
messageFileManager.init();
}
// 封装交换机操作
public void insertExchange(Exchange exchange) {
dataBaseManager.insertExchange(exchange);
}
public void deleteExchange(String exchangeName) {
dataBaseManager.deleteExchange(exchangeName);
}
public List<Exchange> selectAllExchanges() {
return dataBaseManager.selectAllExchanges();
}
// 封装队列操作
public void insertQueue(MQueue queue) throws IOException {
dataBaseManager.insertQueue(queue);
// 创建队列:1.把队列对象写到数据库中;2.创建出对应的目录和文件
messageFileManager.createQueueFiles(queue.getName());
}
public void deleteQueue(String queueName) throws IOException {
dataBaseManager.deleteQueue(queueName);
// 删除队列:1.把队列从数据库中删除;2.删除对应的目录和文件
messageFileManager.destroyQueueFiles(queueName);
}
public List<MQueue> selectAllQueues() {
return dataBaseManager.selectAllQueues();
}
// 封装绑定操作
public void insertBinding(Binding binding) {
dataBaseManager.insertBinding(binding);
}
public void deleteBinding(Binding binding) {
dataBaseManager.deleteBinding(binding);
}
public List<Binding> selectAllBindings() {
return dataBaseManager.selectAllBindings();
}
// 封装消息操作
public void sendMessage(MQueue queue, Message message) throws IOException, MqException {
messageFileManager.sendMessage(queue, message);
}
public void deleteMessage(MQueue queue, Message message) throws IOException, ClassNotFoundException, MqException {
messageFileManager.deleteMessage(queue, message);
if (messageFileManager.checkGC(queue.getName())) {
messageFileManager.gc(queue);
}
}
public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
return messageFileManager.loadAllMessageFromQueue(queueName);
}
}
这个类可以直接给实现服务器接口的虚拟机类使用,至此完成硬盘数据管理
(文章中出现的代码皆为部分关键代码,详细请参考源码,本项目的源代码在 1.3 小节中)