模拟实现一个消息队列中间件(Java)

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,用于 绑定路由规则 中。

  1. 直接交换机(Direct ):routingKey 的值作为队列名字,直接投递到指定队列中。

  2. 扇出交换机(Fanout):routingKey 无意义,直接将消息投递给每个已绑定的队列。

  3. 主题交换机(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 服务器接口

        服务器提供的功能

  1. 创建交换机
  2. 删除交换机
  3. 创建队列
  4. 删除队列
  5. 创建绑定
  6. 解除绑定
  7. 接收生产者的消息(生产消息)
  8. 接收消费者的订阅
  9. 发送消息给消费者(消费消息)

我们可以将这些功能整理成接口,这个接口不被直接使用,仅为了更直观地展示服务器功能

  • 整理接口
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 文件存储

        我们使用文件来存储消息数据,我们进行如下约定:

  1. 在一个总目录下,创建多个队列目录,每个目录代表着一个队列,用队列名命名目录。
  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 小节中)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

配点點.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值