MQ目录
- MQ基本概念
- 什么是MQ?
- MQ的应用场景
- 首先先明白需求
- 持久化分析
- 那么MQ如何设计持久化?
- 可靠性分析
- 高效性分析
- MQ核心概念(装配层)
- 实现MQ组件
- 思维导图
- 创建项目
- 导入数据库
- 下载SqLite。
- 创建组件实体类
- 创建交换机(要加Getter和Setter方法)
- 创建队列(要加Getter和Setter方法)
- 创建绑定(要加Getter和Setter方法)
- 目录结构
- 创建组件持久化
- 创建数据库和表
- 创建dao层
- 创建MyBaits 的xml
- 创建消息实体类(要加Getter和Setter方法)
- 实现消息的持久化
- 实现交换机配对规则
- 实现第一层封装(这是我的理解)
- 对组件进行封装
- 对消息进行分装
- 实现第二层封装(这是我的理解)
- 对硬盘进行封装
- 对内存处理进行封装
- 实现MQ通信
- 创建基本参数(要加Getter和Setter方法)
- 创建交换机参数类(用户操控)
- 创建交换机(要加Getter和Setter方法)
- 删除交换机(要加Getter和Setter方法)
- 创建队列参数类(用户操作)
- 创建队列(要加Getter和Setter方法)
- 删除队列(要加Getter和Setter方法)
- 创建绑定参数类(用户操作)
- 创建绑定(要加Getter和Setter方法)
- 解除绑定(要加Getter和Setter方法)
- 创建消费数据的参数(要加Getter和Setter方法)
- 创建生产数据的参数(要加Getter和Setter方法)
- 创建应答参数(要加Getter和Setter方法)
- 创建请求和响应协议参数
- 请求参数(要加Getter和Setter方法)
- 响应参数(要加Getter和Setter方法)
- 将对象转换二进制数组工具
- 创建消费者对象(要加Getter和Setter方法)
- 实现的消费者接口
- 实现消费者类
- 创建消息发送时协议
- 基本传输协议参数(要加Getter和Setter方法)
- 订阅返回值(要加Getter和Setter方法)
- 创造连接
- 创建连接工程(要加Getter和Setter方法)
- 创建连接
- 创建连接通道(我叫它连接点)
- 实现VirtualHost
- 实现一个消费者
- 实现第三层封装(VirtualHost)(要加Getter和Setter方法)
- 最终:实现服务器(BrokerServer)(也叫第四层封装)
- 测试
- 生产者
- 消费者
- 启动类
- 思维导图
- 我的源码
MQ基本概念
什么是MQ?
MQ是消息队列(Message Queue)的缩写,是一种应用程序对应用程序的通信方法。应用程序通过写和检索出入列队的针对应用程序的数据(消息)来通信,而无需专用连接来链接它们。
市面上有许多成熟的消息队列非常多,例如:
- RabbitMQ(⾮常知名, 功能强⼤, ⼴泛使⽤的消息队列)
- Kafka(高吞吐、可持久化、可水平扩展、支持流数据处理等多种特性而被广泛使用,大量用于大数据领域)
- RocketMQ(具有高性能、高可靠、高实时、分布式特点)
- ActiveMQ (高可用、高性能、可伸缩的企业级面向消息服务的系统)
MQ的应用场景
而MQ可以做什么呢?
- 跨进程通信
指两个或多个进程之间进行数据交换的过程。 跨进程通信的方式有很多,比如管道、消息队列、共享内存、信号量等 - 异步处理
指在执行某个操作时,不需要等待其完成,可以继续执行其他操作的一种处理方式 - 削峰填谷
指在同一刻传入的数据过大使得服务无法处理,需要等待一些时间,将峰值通过时间一批一批的处理
这里的自拟实现我是模仿RabbitMQ 去实现一个消息队列,所以也就是一个RabbitMQ的简化版
通过对RabbitMQ的观察我可以得出以下构图:
可以看出MQ其实主要由两部分构成,一部分客户端,一部分由服务器构成。而客户端的又分为生产客户端(提交消息)和消费客户端(消费消息)。服务器的构建就有些复杂,需要构图:
这里要解释一下图片的内容:
- N–M :这个代表的是多对多的关系,这个关系比较重要(意味着必须要用多线程处理);在图中 我表示的是多个交换机可以通过绑定去去控制多个队列
- N–1 :这个代表多对1的关系。这个也比较重要(也意味着我们需要多线程处理);在图中表示,多个客户端去访问一个服务器。
大部分作者的文章,有很多都是从结构讲MQ,而在我这里而是从逻辑和业务需求去推演结构变化,从而讲MQ。
首先先明白需求
我一直很喜欢一句话:需求决定业务,业务决定技术
消息队列为什么会诞生?
分布式主机在提出的时候,就要面一个问题,这些主机需要通信,而且要保证安全,也就是说:
消息队列(MQ)的诞生是为了解决分布式系统中的通信问题。在分布式系统中,不同的组件之间的通信是一个复杂的问题,因为不同的组件可能运行在不同的物理位置上。消息队列提供了一种简单的方法来解决这个问题,使得组件之间的通信变得更加容易和可靠 。
通信的安全性、可靠性和高效性。
所以根据这些性质我们就可以知道:
- 安全性:数据如何保证安全?----------------->数据的持久化
- 可靠性:传输如何保证可靠性?---------------->自定义应用层和传输层协议
- 高效性:如何保证信息的传输的高效?---------------->在硬件设备中运用内存去处理速度是最快的。
所以综上我们就可以知道如果要实现一个MQ的话需要做什么?
首先是数据的持久化,然后数据的传输协议,再然后使用内存处理数据。
持久化分析
什么是持久化,在Redis那篇文章中有说,但是我在这里要再说一下,其实很简单就是对数据进行永久化的保存等到要用的时候就可以用。也就是碰到那种服务器宕机使得数据消失,但是在此之前我们对数据进行了保存。使得在次启动服务器使得数据会恢复。(这里注意:数据不一定全保证下来,但是至少可以减少损失)
那么MQ如何设计持久化?
首先数据的持久化是保存在数据库中。像Mysql,Oracle等大型数据库,对于数据持久化可以做到,我们要不要用呢?我的回答是--------》不用。也不能用。
为什么?
因为MQ要满足高效化,使用Mysql这样的数据库,虽然能支持,但是对于频繁 I/O 操作的情况下,效率明显是比较低效。但是同样对于数据的存储,并且可以支持频繁 I/O 操作,让数据存储在文件中此时就比较合适了。
但这并不代表我们不需要像mysql这样的数据库,可是Mysql装配太大了。不利于分布式部署,怎么办?市面上也给我们提供了一个较为合理的数据库,名字叫Sqlite,可以适配小型的部署并且快捷的存储。
那么上面我说了不用Mysql的原因,那么现在为什么要用Mysql呢?原因在于,Mysql不是用来存储信息的,而是用来存储虚拟对象的信息。根据我所化的结构图,就可以明白,一个MQ是有多个虚拟主机的,其实也是一个逻辑主机,他没有实体信息所以需要,将虚拟信息保存起来。
那么综上我就可以得到这么一个图:
到此数据持久化的问题就算完成了。具体的代码实现还需要我在下面去书写。
可靠性分析
主要解决数据以什么数据格式传输,由以什么数据格式保存
数据在网络中传输是以二进制字节流的方式,当然也有字符流,但是怎么说字节流利于远程传输,至少对于TCP来说就需要一个字节流传输。所以对于如何在字节流下安全的传输这就需要我们自己去设置一个数据传输形式。也就是定义一个应用层数据包格式和传输层数据包格式。
(应用层和传输层在我的JAVA网络编程(数据是如何传输的)中有说具体是怎么的),所以这里我直接用图做结构图。
首先先说主体数据结构,其实主体数据结构就是一个实体类,我们通过Object自带的方法转换为二进制数组。
那么问题就会出现,实体类很多,如何才能准确拿到想要的数据,而不多拿,导致粘包问题的出现?其实很简单-----------》市面上有两种方法。一个是给数组后面加个分割符,还有一种就是将主体数据的字节长度统计下来,随着字节流一起传出去,然后另一边就读取字节长度,然后通过字节长度读取主体数据。这个就要用DataInputStream和DataOutputStream方法。这个在代码实现中会体现。
到了这里就差不多 了,一个传输数据的包就构建完成。
高效性分析
在传统意义上,我们通常是将数据传入数据库,通过数据库在进行增删改查,可是 I/O 对于频繁的操作就会速度很慢,可以为了将数据通过文件的形式保存,但是又很难做到及时性。那么怎么办?
当然是将消息存储在内存中,通过内存的运算速度可以做到及时性。这样的话,生产者生产的消息可以立刻通过内存的就能分发出去,而数据库里的数据只需要删除就行了。
不需要查找,然后在通过内存发出去。而如何在内存存储,这就需要我们用数据结构了,大部分的数据结构都是通过内存实现的。所以这就天然带有内存的快速性。
为了保证传输效率到达高效性,所以对于虚拟主机也应该通过内存的数据结构保存起来。而虚拟主机的部件从所以就可以得到以下图片(在最上面的图中解释了,交换机和绑定以及队列的关系):
当满足以上的三点要求我们就可以构建属于自己的MQ了,但在构建之前我还需要补充一些MQ的概念。
MQ核心概念(装配层)
- 生产者(Producer):其实就是将消息上传到服务器
- 消费者(Consumer):其实就是从服务器中拿出消息
- 发布(Publish):就是生产者将消息上传给服务器的过程
- 订阅(Subscribe):就是消费者将消息从服务器拿出的过程
- 中间服务器(BrokerServer):核心服务器,负责给生产者和消费者提供发布和订阅以及构建交换机和绑定以及队列的API
- 虚拟机(VirtualHost): 类似于 MySQL 的 “database”, 是⼀个逻辑上的集合. ⼀个 BrokerServer 上可以存在多个 VirtualHost
- 交换机(Exchange):⽣产者把消息先发送到 Broker 的 Exchange 上. 再根据不同的规则, 把消息转发给不同的 Queue
- 队列(Queue):真正⽤来存储消息的部分. 每个消费者决定⾃⼰从哪个 Queue 上读取消息
- 绑定(Bind): Exchange 和 Queue 之间的关联关系. Exchange 和 Queue 可以理解成 “多对多” 关系. 使⽤⼀个关联表就可以把这两个概念联系起来.(不理解的可以看上面的图)
- 消息(Message):生产者和消费者之间传递的内容
- 内存存储(Memory):方便使用,快速调用
- 硬盘存储(Disk):重启数据不丢失
所以综上就可以画出这样的一个图形:
核心的API其实就是我之前所说的是BrokerServer提供给客户端的使用的API:
创建交换机,删除交换机,创建队列,删除队列,创建绑定,解除绑定,发布消息,订阅消息,确认消息。
这里的确认消息是一个应答机制。有点类似TCP在建立连接的情况。
如何通信?
通信的内容还需要一步一步来。我们先搭建服务器。但是这里我会贴上一个要实现的基本的思维导图:
实现MQ组件
其实本来不想写创建项目的,不过做事讲究一个有始有终。所以还是加上了。
思维导图
创建项目
这里我用的SpringBoot 2 系列的版本,java 8 版本,引入Spring Web 和 MyBatis。
创建项目
导入数据库
之前在上面所说,MQ需要一个快捷,轻量级的数据库,市面上提供了一个免费的数据库SqLite
下载SqLite。
下载链接(不用怕,这是官网):https://www.sqlite.org/index.html
下载完毕过后解压后直接安装就行,反正这个数据库比较小。所以安装在哪里都行(现在大部分都是64位主机吧,如果是32位的朋友可以下载x86的)
安装完毕,我们数据库就安装完成。无需做其他事情,当然此时是没有密码的,如果有人想加用户和密码,自己去查资料。反正我这里不用。
在配置中写上:
spring:datasource:url: jdbc:sqlite:./data/meta.dbusername:password:driver-class-name: org.sqlite.JDBCmybatis:mapper-locations: classpath:mybatis/**Mapper.xmlconfiguration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 这个是加入sql的日志信息的,可加可不加
创建组件实体类
这里得提前写一个异常处理的方法:
public class MqException extends Exception {public MqException(String s) {super(s);}
}
这个实体类和数据库的字段一定要一致。我在这里吃了很大的亏,本来都写完了,测试的时候报的那个错误一直是为获取到bean对象,就这个错误我反复检查了2.5天,把我折磨的够呛。我将市面上关于这错误解决办法全部几乎试了一边(总结下来大概7种),我实在受不了了,就找一个大佬帮我看看,大佬一开始也很懵逼,然后仔细一检查就发现我的字段和实体类的成员变量名有几个写的不一致。(这个错误真的不好检查,所以我真心建议,能一个功能一个功能测试最好,否则一旦错误你会直接炸裂的)(说实话我是真没想到,字段和实体类不一致,也能导致扫描不了)
创建交换机(要加Getter和Setter方法)
注意,这里我不加getter方法和setter方法,不是不加。而是我这里如果粘贴进来的话,会使得文章不够精炼。并且这里可以根据自己的喜好来添加这些方法。
一种是直接通过IDEA给出来的自动生成去做,还有一种使用 lombok 的 @Data 方法,去实现。
如果后面需要添加这个方法的我会,做出提醒。
public class Exchange {private String name;//默认交换机类型private ExchangeType exchangeType=ExchangeType.DIRECT;//默认不持久化private boolean is_Persistence=false;//默认不自动删除private boolean is_Auto_Delete=false;//指定交换机的额外参数,转换成 json 格式private Map<String ,Object> exchangeArguments =new HashMap<>();public String getExchangeArguments(){ObjectMapper objectMapper=new ObjectMapper();try {return objectMapper.writeValueAsString(exchangeArguments);} catch (JsonProcessingException e) {e.printStackTrace();}return "{}";}//为了方便讲数据转换成json格式存储,然后通过get方法取出来public void setExchangeArguments(String argumentJson){ObjectMapper objectMapper =new ObjectMapper();try {objectMapper.readValue(argumentJson, new TypeReference<HashMap<String,Object>>() {});} catch (JsonProcessingException e) {e.printStackTrace();}}
ExchangeType 是个表示的是交换机的类型,为什么要分类型呢?其实很简单,需求决定业务,业务决定技术。
所以这里的需求情况分为3种,
- 一个生产者生产的消息由一个消费者去消费,此时我们叫他(DIRECT)
- 一个生产者生产的消息由有个消费者去消费,此时我们叫他(FANOUT)
- 一个生产者生产的消息有锁,必须由带有锁的消费者去消费,此时我们叫他(TOPIC)
- 队列绑定在交换机的时候,会指定一个bindingKey,
- 消息哪里指定一个outingKey
- 当bindingKey与outingKey满足条件就投递到相应的队列中
此时类的类型用枚举就再好不过了。清晰明了
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;}
}
创建队列(要加Getter和Setter方法)
创建队列没有什么好说的。所以很简单,但是因为队列要与客户直接交互所以后期我们还需要在这里添加一些类
public class MqQueue {private String name;//是否持久化,默认否private boolean is_Persistence=false;//是否在自动删除,默认否private boolean is_Auto_Delete=false;//是否属于唯一使用,true就是一个人使用,false就是多个人消息。默认否private boolean is_Unique_use=false;//指定队列的额外参数private Map<String,Object> queueArguments=new HashMap<>();public void setQueueArguments(String argumentsJson) {ObjectMapper objectMapper=new ObjectMapper();try {objectMapper.readValue(argumentsJson,new TypeReference<HashMap<String,Object>>() {});} catch (JsonProcessingException e) {e.printStackTrace();}}public String getQueueArguments(){ObjectMapper objectMapper=new ObjectMapper();try {return objectMapper.writeValueAsString(queueArguments);} catch (JsonProcessingException e) {e.printStackTrace();}return "{}";}
}
创建绑定(要加Getter和Setter方法)
这里要注意在创建绑定那个章节里中,有说我绑定需要添加 bindingKey 属性
public class Binding {private String exchangeName ;private String queueName;private String bindingKey;
}
目录结构
创建组件持久化
这个持久化就是对组件的持久化,也就是将数据存储Disk中,这里我们需要用到MyBaits。并且也要导入sqlite的驱动,所以这里我们需要去中央仓库导入依赖。这里我直接给出(放在pom.xml中)
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc --><dependency><groupId>org.xerial</groupId><artifactId>sqlite-jdbc</artifactId><version>3.41.2.2</version></dependency>
创建数据库和表
创建dao层
其实这个根据方法名也能很轻易明白这些方法是做什么的。
@Mapper
public interface MateMapper {//创建表void createExchangeTable();void createQueueTable();void createBindingTable();//交换机void insertExchange(Exchange exchange);void deleteExchange(String exchangeName);List<Exchange> selectAllExchange();//队列void insertQueue(MqQueue mqQueue);void deleteQueue(String queueName);List<MqQueue> selectAllQueue();//绑定void insertBinding(Binding binding);void deleteBinding(Binding binding);List<Binding> selectAllBinding();
}
注意下面的 xml 文件中,ID与方法名必须一致,一旦不一致就会出现错误信息。扫描不上。的错误,所以id一定要一致。除此之外一定得注意,类的类型,parameterType 必须写对要调用类的位置和具体的类。
创建MyBaits 的xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybati
s.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mydmq.mqserver.dao.MateMapper"><update id="createExchangeTable">create table if not exists exchange(name varchar(50) primary key,exchangeType int,isPersistence boolean,isAutoDelete boolean,exchangeArguments varchar(1024));</update><update id="createQueueTable">create table if not exists queue(name varchar(50) primary key,isPersistence boolean,isAutoDelete boolean,isUniqueuse boolean,queueArguments varchar(1024));</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.mydmq.mqserver.core.model.Exchange">insert into exchange values(#{name},#{exchangeType},#{is_Persistence},#{is_Auto_Delete},#{exchangeArguments});</insert><delete id="deleteExchange" parameterType="java.lang.String">delete from exchange where `name`=#{exchangeName};</delete><select id="selectAllExchange" resultType="com.example.mydmq.mqserver.core.model.Exchange">select * from exchange;</select><!--队列--><insert id="insertQueue" parameterType="com.example.mydmq.mqserver.core.model.MqQueue">insert into queue values(#{name},#{is_Persistence},#{is_Auto_Delete},#{is_Unique_use},#{queueArguments});</insert><delete id="deleteQueue" parameterType="java.lang.String">delete from queue where `name`=#{queueName};</delete><select id="selectAllQueue" resultType="com.example.mydmq.mqserver.core.model.MqQueue">select * from queue;</select><!--绑定--><insert id="insertBinding" parameterType="com.example.mydmq.mqserver.core.model.Binding">insert into binding values(#{exchangeName},#{queueName},#{bindingKey});</insert><delete id="deleteBinding" parameterType="com.example.mydmq.mqserver.core.model.Binding">delete from binding where exchangeName=#{exchangeName} and queueName=#{queueName};</delete><select id="selectAllBinding" resultType="com.example.mydmq.mqserver.core.model.Binding">select * from binding;</select></mapper>
到了这一步组件的持久化就完成了
创建消息实体类(要加Getter和Setter方法)
其实按照结构划分的话,他应该也放在组件实体类中,但是,消息有他的特殊性,他特殊在什么地方呢?很简单特殊在消息是要通过通信管道传输的。意味着他需要序列化,需要定义传输协议,并且还要具备实体类的特点。所以上3点就是其特殊的地方。
序列化如何解决?什么是序列化?
序列化可以将对象转换为字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。序列化是一种将对象的状态(各个属性量)保存起来,然后在适当的时候再获得的过程。在Java中,只有实现了serializable接口的类的对象才能被实例化。我们创建的实体类上要加入serializable才可以。
如何定义传输协议?
这里的传输协议是应用层的协议,并不是之前我们定义的传输层协议。所以要单独定义。
定义规则:
- 要保证在文件中准确读取一个对象的字节流,那么我们就需要对字节流做定义。我们在这里定义一个字节流开头的位置,一个字节流结束的位置。而中间就是我们需要的对象。
- 需要定义一个成员变量,来判定这个消息是否有效,这种判断方式就是为了方便删除。毕竟在文件中删除数据是个大工程(无效数据太多就需要通过复制算法删除)
- 定义消息的属性
- 为了让消息具有区分度,我们还得给它定义一个唯一的id
public class Message implements Serializable {private static final long Serial_VersionUID=1l;private MessageType messageType=new MessageType();private byte[] body;//记录的起始数据开头的位置private transient long offsetBegin;//结束数据结束的位置private transient long offsetEnd;//0x1 表示数据有效,0x2 表示数据无效private byte is_Valid=0x1;//调用属性public String getMessageId(){return messageType.getMessageId();}public void setMessageId(String messageId){messageType.setMessageId(messageId);}public String getOutingKey(){return messageType.getOutingKey();}public void setOutingKey(String outingKey){messageType.setOutingKey(outingKey);}public boolean getMessage_is_Persistence(){return messageType.isMessage_is_Persistence();}public void setMessage_is_Persistence(boolean message_is_persistence){messageType.setMessage_is_Persistence(message_is_persistence);}//MessageId唯一化处理,组合数据public static Message createWithMessageID(String outingKey,MessageType messageType,byte[] body){Message message=new Message();if (messageType!=null){message.setMessageType(messageType);}message.setMessageId("M:"+ UUID.randomUUID());message.messageType.setOutingKey(outingKey);message.body=body;return message;}
}
创建消息属性值(要加Getter和Setter方法)
public class MessageType implements Serializable {private String messageId;private String outingKey;private boolean Message_is_Persistence=false;
}
实现消息的持久化
之前说过,对消息的持久化,需要保存在文件中,而文件的文件操作就需要文件名,存储统计数据有效的文件,和存储数据的文件。
文件名命名规则:
- 每一个队列单独一个文件夹,所以就以队列名命名,每个文件夹下两个文件:存储统计数据有效的文件,存储数据的文件
- 存储统计数据有效的文件:命名为: queue_data.bin(bin是二进制文件)
- 存储数据的文件:命名:queue_stat.txt (txt文本文件)
所以操作的方式各有不同。
- 操作二进制文件:通过字节流:InputStream 和 outputStream 配合DataInputStream 和 DataoutputStream 以及光标读取(RandomAccessFile对象包含一个记录指针,用以标识当前读写处的位置,)
- 操作文本文件通过:字符流:InputStream和outputStream 配合Scanner读取字符
- 文本文件会记录两个数字,一个是统计有效数据的数字,一个是统计无效数据的数字。通过而者之间的比值可以获取一个特定的值。这个特定的值可以触发GC
- GC如何实现?
模拟实现JVM中的复制算法。通过新建的一个文件,将数据文件中的有效数据放入新的文件中。然后删除之前的旧文件,新文件重命名为原来的文件,并且将统计文件中的有效数据和无效数据更新,就完成了对垃圾的回收。
- GC如何实现?
这个比较长。
public class MessageFileManager {static public class Stat{//统计有效数据和无效数据public int totalCount;public int validCount;}//初始化消息文件(先不写)public void initialization(){}//获取指定的消息队列文件的位置private String getQueueDir(String queueName){return "./data/"+queueName;}//创建对应的存储消息的二进制文件private String getQueueDataPath(String queueName){return getQueueDir(queueName)+"/queue_data.bin";}//创建对应队列中的有效消息和无效消息文件private String getQueueStatPath(String queueName){return getQueueDir(queueName)+"/queue_stat.txt";}//先对文本文件操作;private Stat readStatFile(String queueName){Stat stat=new Stat();try (InputStream inputStream=new FileInputStream(getQueueStatPath(queueName))){Scanner scanner=new Scanner(inputStream);stat.totalCount=scanner.nextInt();stat.validCount=scanner.nextInt();} catch (IOException e) {e.printStackTrace();}return null;}private void writeStat(String queueName,Stat stat){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 createQueueFile(String queueName) throws MqException, IOException {File dir=new File(getQueueDir(queueName));if (!dir.exists()){boolean ok=dir.mkdirs();if (!ok){throw new MqException("MessageFileManager :创建文件目录失败! dir="+dir);}}//消息存储文件File queueDataFile=new File(getQueueDataPath(queueName));if (!queueDataFile.exists()){boolean ok=queueDataFile.createNewFile();if (!ok){throw new MqException("MessageFileManager : 创建存储文件失败! queueDataFile="+queueDataFile.getAbsolutePath());}}//统计消息文件File queueStatFile=new File(getQueueStatPath(queueName));if (!queueStatFile.exists()){boolean ok =queueStatFile.createNewFile();if (!ok){throw new MqException("MessageFileManager :创建统计文件失败!queueStatFile="+queueStatFile.getAbsolutePath());}}//给统计文件,设定初始值Stat stat=new Stat();stat.totalCount=0;stat.validCount=0;writeStat(queueName,stat);}/*** 删除队列对应的文件和目录**/public void deleteQueueFile(String queueName) throws MqException {File queueDataFile=new File(getQueueDataPath(queueName));boolean ok1=queueDataFile.delete();File queueSataFile=new File(getQueueStatPath(queueName));boolean ok2=queueSataFile.delete();File Dir=new File(getQueueDir(queueName));boolean ok3= Dir.delete();if (!ok1||!ok2||!ok3){throw new MqException("MessageFileManager :删除消息文件的队列文件和目录失败!Dir="+Dir.getAbsolutePath());}}/*** 检查队列的目录和文件是否存在*/public boolean queueFileExits(String queueName){File queueDataFlie=new File(getQueueDataPath(queueName));if (!queueDataFlie.exists()){return false;}File queueStatFile=new File(getQueueStatPath(queueName));if (!queueStatFile.exists()){return false;}return true;}//插入消息数据public void sendMassage(MqQueue mqQueue, Message message) throws MqException, IOException {if (!queueFileExits(mqQueue.getName())){throw new MqException("MessageFileManager :队列对应的文件不存在 !queueName="+mqQueue.getName());}byte [] messageBinary= BinaryTool.toBytes(message);synchronized (mqQueue){File queueDataFile=new File(getQueueDataPath(mqQueue.getName()));message.setOffsetBegin(queueDataFile.length()+4);message.setOffsetEnd(queueDataFile.length()+4+messageBinary.length);try (OutputStream outputStream=new FileOutputStream(queueDataFile,true)){try (DataOutputStream dataOutputStream=new DataOutputStream(outputStream)){dataOutputStream.writeInt(messageBinary.length);dataOutputStream.write(messageBinary);}} catch (IOException e) {e.printStackTrace();}Stat stat=new Stat();stat.totalCount+=1;stat.validCount+=1;writeStat(mqQueue.getName(),stat);}}//删除消息数据public void deleteMassage(MqQueue mqQueue,Message message) throws IOException, ClassNotFoundException {synchronized (mqQueue){try (RandomAccessFile randomAccessFile=new RandomAccessFile(getQueueDataPath(mqQueue.getName()),"rw")){byte [] bufferSrc=new byte[(int )(message.getOffsetEnd()- message.getOffsetBegin())];randomAccessFile.seek(message.getOffsetBegin());randomAccessFile.read(bufferSrc);Message diskMassage= (Message) BinaryTool.fromBytes(bufferSrc);diskMassage.setIs_Valid((byte) 0x0);randomAccessFile.seek(message.getOffsetBegin());randomAccessFile.write(bufferSrc);}Stat stat=new Stat();if (stat.validCount>0){stat.validCount-=1;}writeStat(mqQueue.getName(),stat);}}//加载消息数据public LinkedList<Message> loadAllMassageFromQueue(String queueName) throws ClassNotFoundException {LinkedList<Message> messages=new LinkedList<>();try (InputStream inputStream=new FileInputStream(getQueueDataPath(queueName))){try (DataInputStream dataInputStream=new DataInputStream(inputStream)){long Offset=0;while (true){int messageSize=dataInputStream.readInt();byte[] buffer=new byte[messageSize];int actualSize=dataInputStream.read(buffer);if (messageSize!=actualSize){throw new MqException("MessageFileManager : 文件格式错误!queueName= "+queueName);}Message message=(Message)BinaryTool.fromBytes(buffer);if (message.getIs_Valid()!=0x1){Offset+=(4+messageSize);continue;}message.setOffsetEnd(Offset+4);message.setOffsetEnd(Offset+4+messageSize);Offset+=(4+messageSize);messages.add(message);}}} catch (IOException | MqException e) {e.printStackTrace();}return messages;}/*** 垃圾回收*///判断是否触发垃圾回收垃圾public boolean judgeGC(String queueName){Stat stat=new Stat();if (stat.totalCount>2000 && (stat.validCount/(double)stat.totalCount)<0.5){return true;}return false;}//创建一个新的文件存储有效数据private String getQueueDataNewPath(String queueName){return getQueueDataPath(queueName)+"/queue_data_new.bin";}//通过复制算法将将垃圾回收public void gc(MqQueue mqQueue) throws MqException, IOException, ClassNotFoundException {synchronized (mqQueue){long gcBegin=System.currentTimeMillis();File queueDataNewFile=new File(getQueueDataNewPath(mqQueue.getName()));if (queueDataNewFile.exists()){throw new MqException("MessageFileManager : 垃圾回收 gc" +"发现复制的队列的新文件存在!queueName="+mqQueue.getName());}boolean ok =queueDataNewFile.createNewFile();if (!ok){throw new MqException("MessageFileManager : 垃圾回收时 新文件创建失败 ");}LinkedList<Message> messages=loadAllMassageFromQueue(mqQueue.getName());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);}}}File queueOldFile=new File(getQueueDataPath(mqQueue.getName()));ok=queueOldFile.delete();if (!ok){throw new MqException("MessageFileManager : 文件删除失败! queueOldFile="+mqQueue.getName());}ok=queueDataNewFile.renameTo(queueOldFile);if (!ok){throw new MqException("MessageFileManager : 重命名失败;queueOldFile="+queueOldFile.getAbsolutePath()+",queueDataNewFile="+queueDataNewFile.getAbsolutePath());}Stat stat=new Stat();stat.validCount= messages.size();stat.totalCount= messages.size();writeStat(mqQueue.getName(),stat);long gcEnd=System.currentTimeMillis();System.out.println("MessageFileManager : gc 执行完毕"+mqQueue.getName()+",time="+(gcEnd-gcBegin)+"ms");}}
}
实现交换机配对规则
这里的匹配规则就是对交换机类型的判断。
- DIRECT:无需去匹配,对应就能发。发送
- FANOUT:只要去筛选没有匹配字段的情况剩下的就是
- TOPIC:要去约定bindingKey和outingKey
- 约定bindingKey和outingKey 必须由大小写字母或数字以及 ’#‘ 、’ * ‘ 组成或者 ’_‘ ,分割符用 ’.‘ ,
- 约定 * 匹配当前字符,# 匹配所有字符,但是遇到bindingKey 与 outingKey字符相等的情况,特殊字符匹配就不适用了。
- 约定没有特殊字符的情况下,必须完整匹配才可以
总结一下就是:
outingKey:
- 数字,字母,下划线组成
- 使用 . 把整个outingKey 分成多个部分,形如:aaa.123.ccc
bindingKey :
- 数字,字母,下划线组成
- 使用 . 把整个bindingKey 分成多个部分,形如:aaa.123.ccc
- 支持两种特殊的符号作为通配符(* 和 # 必须作为被 ‘ . ’ 分割的单独部分)
- ‘ * ’ :*可以匹配任何一个单独的部分,
- #: # 可以配任何多个独立的部分,包括0个部分
这个约定协议是:RabbitMQ 中AMOP协议定义的。
有三种交换机,但是其实仔细观察就能发现,可以分为主要的两种,一种DIRECT(无需匹配)和 特殊(需要字段匹配),而特殊中又分为两种,FANOUT(扇出)和 TOPIC(特定匹配,所以只需要在判断特殊的时候,FANOUT进行分发时进行匹配TOPIC就可以区分这而者的区别。
public class RuleTranslation {public boolean validationOutingKey(String outingKey){if (outingKey.length()==0){return true;}for (int i = 0; i < outingKey.length(); i++) {char ch=outingKey.charAt(i);if (ch>='A'&& ch<='Z'){continue;}if (ch>='a'&& ch<='z'){continue;}if (ch>='0'&& ch<='9'){continue;}if (ch=='_'||ch=='.'){continue;}return false;}return true;}public boolean validationBindingKey(String bindingKey){if (bindingKey.length()==0){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;}//判断 特殊字符是否为单独存在String [] words=bindingKey.split("\\.");for (String word:words){if (word.length()>1&&word.contains("*")||word.contains("#")){return false;}}for (int i = 0; i < words.length; 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;}//判断交换机属性,并对应转发规则public boolean roote(ExchangeType exchangeType, Binding binding, Message message) throws MqException {if (exchangeType==ExchangeType.FANOUT){return true;}else if (exchangeType==ExchangeType.TOPIC){return rooteTopic(binding,message);}else {throw new MqException("RuleTranslation : 交换机非法 ! exchange="+exchangeType );}}private boolean rooteTopic(Binding binding, Message message) {String [] outingKey=message.getOutingKey().split("\\.");String [] bindingKey=binding.getBindingKey().split("\\.");int bindingIndex=0;int outingIndex=0;while (bindingIndex < bindingKey.length&&outingIndex < outingKey.length){if (bindingKey[bindingIndex].equals(("*"))){bindingIndex++;outingIndex++;continue;}else if (bindingKey[bindingIndex].equals("#")){bindingIndex++;if (bindingIndex==bindingKey.length){return true;}outingIndex=findNextChar(outingKey,outingIndex,bindingKey[bindingIndex]);if (outingIndex==-1){return false;}bindingIndex++;outingIndex++;continue;}else {if (!bindingKey[bindingIndex].equals(outingKey[outingIndex])){return false;}outingIndex++;bindingIndex++;}}if (bindingIndex==bindingKey.length&&outingIndex==outingKey.length){return true;}return false;}private int findNextChar(String[] outingKey, int outingIndex, String s) {for (int i = outingIndex; i < outingKey.length; i++) {if (s.equals(outingKey[i])){return i;}}return -1;}
}
实现第一层封装(这是我的理解)
这个是我的理解,实际中没有第几层这个概念,这里是我的理解,要让我们的客户能够调方便的方法,就需要我们将很多看似很乱的方法,将他们封装起来。在MQ中有很多次封装。这次是对消息,组件的统合封装。此时需要耦合起来。
对组件进行封装
在这个版块中其实有点类似于 MVC ,这里就是Service层实现对dao层的调用。这个dao层就是对之前持久化组件的调用接口。
- 首先就是创建数据库
- 创建的表
- 删除数据库
- 再然后就是调用dao 层的方法:增,查,删等操作
public class DataBaseManager {private MateMapper mateMapper;//初始化public void initialization(){mateMapper = MyDmqApplication.context.getBean(MateMapper.class);if (!dbExists()){File dataDir=new File("./data");dataDir.mkdirs();createTable();createDefaultData();System.out.println("DataBaseManager : 数据库初始化完成");}else {System.out.println("DataBaseManager : 数据库已存在");}}//默认交换机private void createDefaultData() {Exchange exchange=new Exchange();exchange.setName("");exchange.setExchangeType(ExchangeType.DIRECT);exchange.setIs_Persistence(true);exchange.setIs_Auto_Delete(false);mateMapper.insertExchange(exchange);System.out.println("DataBaseManager : 交换机初始完成");}private void createTable() {mateMapper.createExchangeTable();mateMapper.createBindingTable();mateMapper.createQueueTable();System.out.println("DataBaseManager : 创建表");}private boolean dbExists() {File file=new File("./data/meta.db");if (file.exists()){return true;}return false;}//删除数据库public void deleteDB(){File file=new File("./data/meta.db");boolean ret=file.delete();if (ret){System.out.println("DataBaseManager : 数据库删除成功");}File dataDir=new File("./data");ret=dataDir.delete();if (ret){System.out.println("DataBaseManager : 数据库目录删除成功");}else {System.out.println("DataBaseManager : 数据库目录删除失败");}}//public void insertExchange(Exchange exchange){mateMapper.insertExchange(exchange);}public void deleteExchange(String exchangeName){mateMapper.deleteExchange(exchangeName);}public List<Exchange> selectAllExchange(){return mateMapper.selectAllExchange();}public void insertQueue(MqQueue mqQueue){mateMapper.insertQueue(mqQueue);}public void deleteQueue(String queueName){mateMapper.deleteQueue(queueName);}public List<MqQueue> selectAllQueue(){return mateMapper.selectAllQueue();}public void insertBinding(Binding binding){mateMapper.insertBinding(binding);}public void deleteBinding(Binding binding){mateMapper.deleteBinding(binding);}public List<Binding> selectAllBinding(){return mateMapper.selectAllBinding();}
}
对消息进行分装
对消息进行封装的话其实在消息的持久化中就完成了,那个就是对消息的第一层封装。这里只是写出来,给读者们一个明确的逻辑链条
- 首先就是创建存储数据文件,以及文件目录
- 在就是对数据增删改查(这里有些复杂的逻辑需要你们回顾消息持久化的那段)
实现第二层封装(这是我的理解)
跟上面的一样其实原本并没有,第几层的概念的。同样的我们要对第一层的封装再一次进行封装,按照我的说法就是二层封装,这次封装相比于上次会比较复杂,首先,在最上方我写到—》持久化是分为组件持久化和消息持久化。这些是对综合而言就是对硬盘的处理;
在这里就需要对内存进行处理,然后对硬盘进行封装。
对硬盘进行封装
对硬盘的封装其实将之间对消息的封装的API和对组件的分装的API进行业务逻辑的封装。譬如:删除消息,就需要对消息值持久化的消息删除,并且还有内存中的;譬如,添加消息,创建交换机,队列,等等。
对内存处理进行封装
对内存进行处理就需要我们,去规划相应的数据结构:那么就会很多要点:
- 为了实现多用户调用,就需要实现多线程,就要考虑线程安全的问题
- 为了合理区分每个交换机的所对应的队列和绑定,那么就需要名字与组件对应。
- 在交换机中还需要考虑多对一的情况
所以综合这些问题就能得出这些结论:
- 使用线程安全的集合类
- 得使用键值对的集合类
- 或许会大量使用集合类的嵌套
综合而言就是 HashMap 的线程安全的集合类,搭配其他的顺序表形成内存对消息和组件的管理。ConcurrentHashMap这个集合类?
综上就可以确定:
- 交换机的名字 : 交换机(对交换机管理)
- ConcurrentHashMap<String, Exchange> exchangeMemoryMap=new ConcurrentHashMap<>();
- 队列的名字:队列(对队列管理)
- ConcurrentHashMap<String, MqQueue> queueMemoryMap=new ConcurrentHashMap<>();
- 交换机的名字:队列的名字:绑定(绑定管理)
- ConcurrentHashMap<String,ConcurrentHashMap<String, Binding>> bindingMemoryMap=new ConcurrentHashMap<>();
- 消息id:消息(消息)
- ConcurrentHashMap<String, Message> messageMemoryMap=new ConcurrentHashMap<>();
- 队列名:消息列表(消息集合)
- ConcurrentHashMap<String, LinkedList> queueMessageMemoryMap=new ConcurrentHashMap<>();
- 队列名:消息id:消息(应答消息)
- ConcurrentHashMap<String,ConcurrentHashMap<String,Message>> messageMemoryWaitACKMap=new ConcurrentHashMap<>();
- 这个应答是为了确认消息是否被消费者确认接收
所以综上就能写出:
/*** 内存中存储交换机信息,队列信息,绑定信息,还有*/
public class MemoryData {/*** 交换机*///将交换机添加到内存中private ConcurrentHashMap<String, Exchange> exchangeMemoryMap=new ConcurrentHashMap<>();public void insertMemoryExchange(Exchange exchange){exchangeMemoryMap.put(exchange.getName(),exchange);System.out.println("MemoryData : 交换机添加成功!exchangeName="+exchange.getName());}public void deleteMemoryExchange(String exchangeName){exchangeMemoryMap.remove(exchangeName);System.out.println("MemoryData : 交换机删除成功!exchangeName="+exchangeName);}public Exchange getMemoryExchange(String exchangeName){System.out.println("MemoryData : 获取交换机成功!exchangeName="+exchangeName);return exchangeMemoryMap.get(exchangeName);}/*** 队列*///将队列添加到内存中private ConcurrentHashMap<String, MqQueue> queueMemoryMap=new ConcurrentHashMap<>();public void insertMemoryQueue(MqQueue mqQueue){queueMemoryMap.put(mqQueue.getName(), mqQueue);System.out.println("MemoryData : 队列添加成功!queueName="+mqQueue.getName());}public void deleteMemoryQueue(String queueName){queueMemoryMap.remove(queueName);System.out.println("MemoryData : 队列删除成功!queueName="+queueName);}public MqQueue getMemoryQueue(String queueName){System.out.println("MemoryData : 获取队列成功!queueName="+queueName);return queueMemoryMap.get(queueName);}/*** 绑定*///将绑定添加到内存中,因为交换级对应多个队列,1对多的关系private ConcurrentHashMap<String,ConcurrentHashMap<String, Binding>> bindingMemoryMap=new ConcurrentHashMap<>();public void insertMemoryBinding(Binding binding) throws MqException {ConcurrentHashMap<String,Binding> queueBindingMap=bindingMemoryMap.computeIfAbsent(binding.getExchangeName(),k->new ConcurrentHashMap<>());//检查绑定是否存在synchronized (queueBindingMap){if (queueBindingMap.get(binding.getQueueName())!=null){throw new MqException("MemoryData : 内存绑定已存在!exchangeName=" +binding.getExchangeName()+",queueName="+binding.getQueueName());}queueBindingMap.put(binding.getQueueName(),binding);}}public void deleteMemoryBinding(Binding binding) throws MqException {ConcurrentHashMap<String ,Binding> queueBindingMap=bindingMemoryMap.get(binding.getExchangeName());if (queueBindingMap==null){throw new MqException("MemoryData : 绑定不存在!exchangeName=" +binding.getExchangeName()+",queueName="+binding.getQueueName());}queueBindingMap.remove(binding.getQueueName());}//获取队列,此时需要考虑两种需求,1,或许指定的绑定,2.获取所有绑定public Binding getOneMemoryBinding(String exchangeName,String queueName) throws MqException {ConcurrentHashMap<String,Binding> queueBindingMap=bindingMemoryMap.get(exchangeName);if (queueBindingMap==null){System.out.println("MemoryData : 绑定不存在,无法获取! exchangeName=," +exchangeName+"queueName"+queueName);return null;}return queueBindingMap.get(queueName);}public ConcurrentHashMap<String,Binding> getAllMemoryBinding(String exchangeName){return bindingMemoryMap.get(exchangeName);}/*** 消息*///将消息存储在内存中private ConcurrentHashMap<String, Message> messageMemoryMap=new ConcurrentHashMap<>();public void insertMemoryMessage(Message message){messageMemoryMap.put(message.getMessageId(),message);System.out.println("MemoryData : 新消息添加成功!messageId="+message.getMessageId());}public void deleteMemoryMessage(String messageId){messageMemoryMap.remove(messageId);System.out.println("MemoryData : 消息删除成功!messageId="+messageId);}public Message getMemoryMessage(String messageId){System.out.println("MemoryData : 获取消息成功!messageId="+messageId);return messageMemoryMap.get(messageId);}/*** 队列将消息统一管理*/private ConcurrentHashMap<String, LinkedList<Message>> queueMessageMemoryMap=new ConcurrentHashMap<>();public void sendMessage(MqQueue mqQueue,Message message){LinkedList<Message> queueMessageMap=queueMessageMemoryMap.computeIfAbsent(mqQueue.getName(),k->new LinkedList<>());synchronized (queueMessageMap){queueMessageMap.add(message);}System.out.println("MemoryData : 消息被投递到消息队列中! messageId="+message.getMessageId());}//获取队列中的信息public Message pollMessage(String queueName){LinkedList<Message> queueMessageMap=queueMessageMemoryMap.get(queueName);if (queueMessageMap==null){return null;}synchronized (queueMessageMap){if (queueMessageMap.size()==0){return null;}Message message=queueMessageMap.remove(0);System.out.println("MemoryData : 从消息队列中取出!messageId="+message.getMessageId());return message;}}//获取队列中的消息个数public int getMessageCount(String queueName){LinkedList<Message> queueMessageMap=queueMessageMemoryMap.get(queueName);if (queueMessageMap==null){return 0;}synchronized (queueMessageMap){return queueMessageMap.size();}}/*** 这个数据集用来保存消息是否被确认了*/private ConcurrentHashMap<String,ConcurrentHashMap<String,Message>> messageMemoryWaitACKMap=new ConcurrentHashMap<>();public void addMessageWaitACK(String queueName,Message message){ConcurrentHashMap<String,Message> messageMap=messageMemoryWaitACKMap.computeIfAbsent(queueName,k->new ConcurrentHashMap<>());messageMap.put(message.getMessageId(),message);System.out.println("MemoryData : 消息进入未确认列表!messageId="+message.getMessageId());}public void deleteMessageWaitACK(String queueName,String messageId){ConcurrentHashMap<String,Message> massageMap=messageMemoryWaitACKMap.get(queueName);if (massageMap==null){return;}massageMap.remove(messageId);System.out.println("MemoryData : 消息已经确认!messageId="+messageId);}public Message getMessageWaitACK(String queueName,String messageId){ConcurrentHashMap<String,Message> messageMap=messageMemoryWaitACKMap.get(queueName);if (messageMap==null){return null;}System.out.println("MemoryData : 获取未确认的消息!messageId="+messageId);return messageMap.get(messageId);}/*** 回复数据*/public void resume(DiskData diskData) throws ClassNotFoundException {exchangeMemoryMap.clear();queueMemoryMap.clear();bindingMemoryMap.clear();messageMemoryMap.clear();queueMessageMemoryMap.clear();//恢复交换机List<Exchange> exchanges=diskData.selectAllExchange();for (Exchange exchange:exchanges){exchangeMemoryMap.put(exchange.getName(),exchange);}//恢复队列List<MqQueue> mqQueues=diskData.selectAllQueue();for (MqQueue mqQueue:mqQueues){queueMemoryMap.put(mqQueue.getName(),mqQueue);}//恢复绑定List<Binding> bindings=diskData.selecteAllBinding();for (Binding binding:bindings){ConcurrentHashMap<String,Binding> bindingMap=bindingMemoryMap.computeIfAbsent(binding.getExchangeName(),k->new ConcurrentHashMap<>());bindingMap.put(binding.getQueueName(),binding);}//恢复消息队列中的数据for (MqQueue mqQueue:mqQueues){LinkedList<Message> messages=diskData.loadAllMassageFromQueue(mqQueue.getName());queueMessageMemoryMap.put(mqQueue.getName(),messages);for (Message message:messages){messageMemoryMap.put(message.getMessageId(),message);}}}
}
到了这里第二层封装就算完成了。也就意味着我们的主体结构就完成了,剩下的就是通信。
实现MQ通信
这里的通信,除了之前所做的对传输协议进行设置外还要考虑一件事情,就是我们需要给用户提交API,可是这个这个API不能直接对服务的组件进行调控,怎么办?这就需要我们针对通信专门去建一些实体类,通过这些类去传入参数,从而达到对第二层的API的应用,从而不会对组件进行影响。并且这里还要涉及传入消息和拿出消息的逻辑。并且需要序列化,而且每个属性的操作需要通过同一个id进行识别,链接用户id。
总结:
- 这里需要建立操作组件方法的实体类,都需要调用Serializable,进行序列化
- 所有的实体类都会对应一个客户,也就说每个客户都有一个单独的链接id
- 每个操作都有一个公共id
那么假设有N个用户,那么就会对应所有的人都有一套这样的参数。
创建基本参数(要加Getter和Setter方法)
(以下的实体类都会继承这个类)
- rid:识别操作的id
- channelid:识别链接用户的id
public class BasicArgument implements Serializable {protected String rid;protected String channelId;
}
创建交换机参数类(用户操控)
这个交换机的操作其实就只有两个—》删除和创建
创建交换机(要加Getter和Setter方法)
这个类调用了上面的基本参数类和序列化的接口
public class ExchangeCreateArgument extends BasicArgument implements Serializable {private String exchangeName;private ExchangeType exchangeType;private boolean is_persistence;private boolean is_auto_delete;private Map<String,Object> arguments;
}
删除交换机(要加Getter和Setter方法)
这个类调用了上面的基本参数类和序列化的接口
public class ExchangeDeleteArgument extends BasicArgument implements Serializable {private String exchangeName;
}
创建队列参数类(用户操作)
这个队列的操作其实就只有两个—》删除和创建
创建队列(要加Getter和Setter方法)
这个类调用了上面的基本参数类和序列化的接口
public class QueueCreateArgument extends BasicArgument implements Serializable {private String queueName;private boolean is_persistence;private boolean is_auto_delete;private boolean is_unique_use;private Map<String,Object> argument;
}
删除队列(要加Getter和Setter方法)
这个类调用了上面的基本参数类和序列化的接口
public class QueueDeleteArgument extends BasicArgument implements Serializable {private String queueName;
}
创建绑定参数类(用户操作)
这个绑定的操作其实就只有两个—》解除和创建
创建绑定(要加Getter和Setter方法)
这个类调用了上面的基本参数类和序列化的接口
public class BindCreateArgument extends BasicArgument implements Serializable {private String queueName;private String exchangeName;private String bindingKey;
}
解除绑定(要加Getter和Setter方法)
这个类调用了上面的基本参数类和序列化的接口
public class BindUnArgument extends BasicArgument implements Serializable {private String queueName;private String exchangeName;
}
创建消费数据的参数(要加Getter和Setter方法)
- consumerTag:其实就是channelId 为了有个好一点区分就做了这样的安排
- queueName:其实每次消费都是对队列进行操作
- autoACK:自动应答,其实就是是否自动确认收到这样的操作
这个参数对应的操纵就是从队列中消费数据
/*** 消费参数*/public class ConsumerArgument extends BasicArgument implements Serializable {private String consumerTag;private String queueName;private boolean autoACK;}
创建生产数据的参数(要加Getter和Setter方法)
创建消息就需要消息的钥匙outingKey,也需要一个消息的类型属性,和一个存储消息数据的二进制数组。
这个参数对应的参数就是将数据放入队列中
/*** 生产参数*/public class PulishArgument extends BasicArgument implements Serializable {private String exchangeName;private String outingKey;private MessageType messageType;private byte[] body;}
创建应答参数(要加Getter和Setter方法)
通过这个参数调用的应答的API
public class ACKArgument extends BasicArgument implements Serializable {private String queueName;private String messageId;}
到了这一步就将操作组件API的参数创建完毕了。接下来就是确定协议了。
创建请求和响应协议参数
我们需要更具不同的请求,去响应不同的响应,所以这里需要做定义。最上面我也画出具体的数据报的格式。这里我在画一下。
请求参数(要加Getter和Setter方法)
可以看到,这里的主体数据我这里用的二进制数组保存,
public class Request {private int type;private int length;private byte[] payload;
}
响应参数(要加Getter和Setter方法)
请求什么格式,相应的响应也应该是什么格式
public class Response {private int type;private int length;private byte[] payload;}
将对象转换二进制数组工具
此时我们需要去调用ByteArrayInputStream 类,去转换。ByteArrayInputStream是Java中的一个类,它继承自InputStream类。它允许您从字节数组中读取数据,而不是从文件中读取数据。这个类通常用于处理二进制数据,例如图像或音频文件。相应的outputStream,也有这个相应的方法。
所以我们需要两个方法:
- 将一个类转换为二进制数组
- 将一个二进制数组转换为一个类
public class BinaryTool {//转换为二进制数组public static byte[] toBytes(Object object) throws IOException {try (ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream()){try (ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream)){objectOutputStream.writeObject(object);}return byteArrayOutputStream.toByteArray();}}public static Object fromBytes(byte[] data) throws IOException, ClassNotFoundException {Object object=null;try (ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(data)){try (ObjectInputStream objectInputStreamn=new ObjectInputStream(byteArrayInputStream)){object=objectInputStreamn.readObject();}}return object;}
}
创建消费者对象(要加Getter和Setter方法)
跟上方一样ConsumerTag其实就是一个channelid的,表示一个用户对象
但是这里得注意的是,Consumer
实现的消费者接口
这里调用了一个@FunctionalInterface注释,这个注释的作用,有点类似c语言中的回调函数
在Java中,使用@FunctionalInterface注解可以明确地表示一个接口是一个函数式接口。这个注解是可选的,但是加上它可以更清楚地表明接口的意图,并且编译器会检查该接口是否只包含一个抽象方法。
在Java中,@FunctionalInterface注解的作用主要有以下几个方面:
-
明确接口的意图:使用@FunctionalInterface注解可以让编译器更加清楚地知道该接口是一个函数式接口,并且可以检查该接口是否只包含一个抽象方法。这有助于提高代码的可读性和可维护性。
-
编译器检查:当一个接口被定义为函数式接口时,编译器会检查该接口是否满足函数式接口的要求。如果不满足,编译器会报错。这有助于避免因错误地定义接口而导致的潜在问题。
-
方便使用Lambda表达式:@FunctionalInterface注解通常与Lambda表达式一起使用。通过将它添加到接口上,我们可以更自然地使用Lambda表达式来实现接口的方法,而不必使用传统的匿名内部类。
@FunctionalInterface
public interface Consumer {void headDelivery(String consumerTag, MessageType messageType, byte[] body) throws MqException, IOException;
}
实现消费者类
public class ConsumerEnv {private String ConsumerTag;private String queueName;private Consumer consumer;private boolean autoAck;public ConsumerEnv(String consumerTag, String queueName, boolean autoAck, Consumer consumer) {this.ConsumerTag=consumerTag;this.queueName=queueName;this.autoAck=autoAck;this.consumer=consumer;}
}
创建消息发送时协议
此时就需要考虑验证是否连接成功的情况。所以成员变量需要对连接参数和操纵参数做一个验证,和传输。本质还是一个参数类,所以同样要进行二进制的装换。
基本传输协议参数(要加Getter和Setter方法)
ok:是否成功传输
/*** 远程通讯返回基本公共信息*/public class BasicReturns implements Serializable {protected String rid;protected String channelId;protected boolean ok;
}
订阅返回值(要加Getter和Setter方法)
这里是考虑到,有请求就有响应,通过订阅消息后,此时线程停止,等待生产者提交消息,然后消费,这就是消费后的返回值。(回显服务器)
public class SubscribeReturns extends BasicReturns implements Serializable {private String comsumerTag;private MessageType messageType;private byte[] body;
创造连接
创造连接需要很多,东西其中就要涉及TCP的Socket的编程。还要考虑输入输出流。在就是接口和URL,此处主要就是客户端代码
创建连接工程(要加Getter和Setter方法)
host:ip地址
port:接口
/*** 创建连接工程*/public class ConnectionFactory {private String host;private int port;public Connection newConnection() throws IOException {Connection connection=new Connection(host,port);return connection;}
}
创建连接
此时创建连接需要考虑几个部分,首先是通过Socket创建连接,但是因为是多用户的存在此时就需要我们考虑多线程的情况。这里最好用线程池是持续检测连接状态。
一个连接该有的功能包括读取响应,发送请求,关闭资源,将请求发送至对应的消费者身上。
其中在这里有几个点比较重要:
- 为了应对多个用户的情况,这里要用多线程去应对多个响应的情况
- 建立连接的时候需要我们将连接id保存起来,用一个hash表装起来。
- 要以此搭建一个响应的服务器需要与客户端那么形成一个回显服务器。
- 因为涉及到输入输出,所以其中会大量使用inputSteam和outputSteam的接口。
public class Connection {private Socket socket=null;private InputStream inputStream;private OutputStream outputStream;private DataInputStream dataInputStream;private DataOutputStream dataOutputStream;private ConcurrentHashMap<String, Channel> channelMap=new ConcurrentHashMap<>();//应答池private ExecutorService callBackPool=null;public Connection(String host, int port) throws IOException {socket=new Socket(host,port);inputStream=socket.getInputStream();outputStream=socket.getOutputStream();dataInputStream=new DataInputStream(inputStream);dataOutputStream=new DataOutputStream(outputStream);callBackPool= Executors.newFixedThreadPool(4);Thread t= new Thread(() -> {try {while (!socket.isClosed()) {Response response = readResponse();dispatchResponse(response);}} catch (SocketException e) {System.out.println("Connection : 连接正常断开! ");} catch (IOException | MqException | ClassNotFoundException e) {System.out.println("Connection : 异常连接断开! ");e.printStackTrace();}});t.start();}//分辨当前的响应是针对控制请求的响应,还是服务器对的消息private void dispatchResponse(Response response) throws IOException, ClassNotFoundException, MqException {//服务器推送的消息数据if (response.getType()==0xc){SubscribeReturns subscribeReturns=(SubscribeReturns) BinaryTool.fromBytes(response.getPayload());Channel channel=channelMap.get(subscribeReturns.getChannelId());if (channel==null){throw new MqException("Connection : 该消息对应的的channel 在客户端不存在!channelId="+channel.getChannelId());}callBackPool.submit(()->{try {channel.getConsumer().headDelivery(subscribeReturns.getComsumerTag(),subscribeReturns.getMessageType(),subscribeReturns.getBody());} catch (MqException | IOException e) {e.printStackTrace();}});}else {BasicReturns basicReturns= (BasicReturns) BinaryTool.fromBytes(response.getPayload());Channel channel=channelMap.get(basicReturns.getChannelId());if (channel==null){throw new MqException("Connection : 该消息对应的的channel 在客户端不存在!channelId="+channel.getChannelId());}channel.putReturns(basicReturns);}}//读取响应public Response readResponse() throws IOException {Response response=new Response();response.setType(dataInputStream.readInt());response.setLength(dataInputStream.readInt());byte[] payload=new byte[response.getLength()];int n=dataInputStream.read(payload);if (n!=response.getLength()){throw new IOException("读取的响应数据,不完整");}response.setPayload(payload);System.out.println("Connection : 收到响应 ! type="+response.getType()+",length"+response.getLength());return response;}//发送请求public void writeRequest(Request request) throws IOException {dataOutputStream.writeInt(request.getType());dataOutputStream.writeInt(request.getLength());dataOutputStream.write(request.getPayload());dataOutputStream.flush();System.out.println("Connection : 收到响应 ! type="+request.getType()+",length"+request.getLength());}//创建连接点public Channel createChannel() throws IOException {String channelId="C-"+ UUID.randomUUID();Channel channel=new Channel(channelId,this);channelMap.put(channelId,channel);boolean ok=channel.createChannel();if (!ok){channelMap.remove(channelId);return null;}return channel;}//关闭资源public void close(){try {callBackPool.shutdownNow();inputStream.close();outputStream.close();channelMap.clear();socket.close();} catch (IOException e) {e.printStackTrace();}}}
创建连接通道(我叫它连接点)
之前再说请求响应协议的时候,为了快速响应,我们定义传输的响应类型,在这里就具体去运用了。这里我们还需对于Rid进行唯一化处理,用UUID。这里的API其实就是对于用户响应后所传过来的数据。然后做相应的处理:
约定:
其中 type 表⽰请求响应不同的功能. 取值如下:
- 0x1 创建 channel
- 0x2 关闭 channel
- 0x3 创建 exchange
- 0x4 销毁 exchange
- 0x5 创建 queue
- 0x6 销毁 queue
- 0x7 创建 binding
- 0x8 销毁 binding
- 0x9 发送 message
- 0xa 订阅 message
- 0xb 返回 ack
- 0xc 服务器给客⼾端推送的消息.,(被订阅的消息) 响应独有的。
对于请求来说,payload 表⽰这次⽅法调⽤的各种参数信息。
对于响应来说,payload 表⽰这次⽅法调⽤的返回值。
诺是客户端中的队列中没有消息,就会阻塞等待,等待生产者生产消息,当生产后放入消息,就会唤醒当前线程。在这里就是唤醒操作
public class Channel {private String channeId;private Connection connection;//用来存储后续客户端收到的服务器的响应private ConcurrentHashMap<String, BasicReturns> basicReturnsMap=new ConcurrentHashMap<>();private Consumer consumer=null;public Channel(String channelId, Connection connection) {this.channeId=channelId;this.connection=connection;}private String OneRid(){return "R-"+ UUID.randomUUID();}public String getChannelId() throws IOException {return channeId;}public Consumer getConsumer() {return consumer;}public void putReturns(BasicReturns basicReturns) {basicReturnsMap.put(basicReturns.getRid(),basicReturns);synchronized (this){notify();}}public boolean createChannel() throws IOException {BasicArgument basicArgument=new BasicArgument();basicArgument.setChannelId(channeId);basicArgument.setRid(OneRid());byte[] payload= BinaryTool.toBytes(basicArgument);Request request=new Request();request.setType(0x1);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);BasicReturns basicReturns=waitResult(basicArgument.getRid());return basicReturns.isOk();}private BasicReturns waitResult(String rid) {BasicReturns basicReturns=null;while ((basicReturns=basicReturnsMap.get(rid))==null){synchronized (this){try {wait();} catch (InterruptedException e) {e.printStackTrace();}}}basicReturnsMap.remove(rid);return basicReturns;}public boolean close() throws IOException {BasicArgument basicArgument=new BasicArgument();basicArgument.setRid(OneRid());basicArgument.setChannelId(channeId);byte[] payload=BinaryTool.toBytes(basicArgument);Request request=new Request();request.setType(0x2);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);BasicReturns basicReturns=waitResult(basicArgument.getRid());return basicReturns.isOk();}public boolean exchangeCreate(String exchangeName, ExchangeType exchangeType, boolean is_persistence,boolean is_auto_delete, Map<String,Object> arguments) throws IOException {ExchangeCreateArgument exchangeCreateArgument=new ExchangeCreateArgument();exchangeCreateArgument.setExchangeName(exchangeName);exchangeCreateArgument.setRid(OneRid());exchangeCreateArgument.setChannelId(channeId);exchangeCreateArgument.setExchangeType(exchangeType);exchangeCreateArgument.setIs_persistence(is_persistence);exchangeCreateArgument.setIs_auto_delete(is_auto_delete);byte[] payload=BinaryTool.toBytes(exchangeCreateArgument);Request request=new Request();request.setType(0x3);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);BasicReturns basicReturns=waitResult(exchangeCreateArgument.getRid());return basicReturns.isOk();}//删除虚拟机public boolean exchangeDelete(String exchangeName) throws IOException {ExchangeDeleteArgument argument=new ExchangeDeleteArgument();argument.setRid(OneRid());argument.setChannelId(channeId);argument.setExchangeName(exchangeName);byte[] payload=BinaryTool.toBytes(argument);Request request=new Request();request.setType(0x4);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);BasicReturns basicReturns=waitResult(argument.getRid());return basicReturns.isOk();}//队列创建public boolean queueCreate(String queueName,boolean is_persistence, boolean is_auto_delete,boolean is_unique_use,Map<String,Object> argument) throws IOException {QueueCreateArgument queueCreateArgument=new QueueCreateArgument();queueCreateArgument.setQueueName(queueName);queueCreateArgument.setIs_persistence(is_persistence);queueCreateArgument.setIs_auto_delete(is_auto_delete);queueCreateArgument.setIs_unique_use(is_unique_use);queueCreateArgument.setArgument(argument);queueCreateArgument.setRid(OneRid());queueCreateArgument.setChannelId(channeId);byte[] payload=BinaryTool.toBytes(queueCreateArgument);Request request=new Request();request.setType(0x5);request.setPayload(payload);request.setLength(payload.length);connection.writeRequest(request);BasicReturns basicReturns=waitResult(queueCreateArgument.getRid());return basicReturns.isOk();}//删除队列public boolean queueDelete(String queueName) throws IOException {QueueDeleteArgument queueDeleteArgument=new QueueDeleteArgument();queueDeleteArgument.setQueueName(queueName);queueDeleteArgument.setRid(OneRid());queueDeleteArgument.setChannelId(channeId);byte[] payload=BinaryTool.toBytes(queueDeleteArgument);Request request=new Request();request.setType(0x6);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);BasicReturns basicReturns=waitResult(queueDeleteArgument.getRid());return basicReturns.isOk();}//创建绑定public boolean binding(String queueName,String exchangeName,String bindingKey) throws IOException {BindCreateArgument bindCreateArgument=new BindCreateArgument();bindCreateArgument.setRid(OneRid());bindCreateArgument.setChannelId(channeId);bindCreateArgument.setQueueName(queueName);bindCreateArgument.setExchangeName(exchangeName);bindCreateArgument.setBindingKey(bindingKey);byte[] payload=BinaryTool.toBytes(bindCreateArgument);Request request=new Request();request.setType(0x7);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);BasicReturns basicReturns=waitResult(bindCreateArgument.getRid());return basicReturns.isOk();}//删除绑定public boolean unBind(String queueName,String exchangeName) throws IOException {BindUnArgument bindUnArgument=new BindUnArgument();bindUnArgument.setChannelId(channeId);bindUnArgument.setRid(OneRid());bindUnArgument.setExchangeName(exchangeName);bindUnArgument.setQueueName(queueName);byte[] payload=BinaryTool.toBytes(bindUnArgument);Request request=new Request();request.setType(0x8);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);BasicReturns basicReturns=waitResult(bindUnArgument.getRid());return basicReturns.isOk();}//发送消息public boolean publishMassage(String exchangeName, String outingKey, MessageType messageType, byte[] body) throws IOException {PulishArgument pulishArgument=new PulishArgument();pulishArgument.setRid(OneRid());pulishArgument.setChannelId(channeId);pulishArgument.setExchangeName(exchangeName);pulishArgument.setMessageType(messageType);pulishArgument.setOutingKey(outingKey);pulishArgument.setBody(body);byte[] payload=BinaryTool.toBytes(pulishArgument);Request request=new Request();request.setType(0x9);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);BasicReturns basicReturns=waitResult(pulishArgument.getRid());return basicReturns.isOk();}//消费消息public boolean consumeMassage(String queueName,boolean autoAck,Consumer consumer) throws MqException, IOException {if (this.consumer!=null){throw new MqException("Channel 已经经历过消费消息的回调,不能重复设置");}this.consumer=consumer;ConsumerArgument consumerArgument=new ConsumerArgument();consumerArgument.setRid(OneRid());consumerArgument.setChannelId(channeId);consumerArgument.setConsumerTag(channeId);consumerArgument.setQueueName(queueName);consumerArgument.setAutoACK(autoAck);byte[] payload=BinaryTool.toBytes(consumerArgument);Request request=new Request();request.setType(0xa);request.setLength(payload.length);request.setPayload(payload);connection.writeRequest(request);BasicReturns basicReturns=waitResult(consumerArgument.getRid());return basicReturns.isOk();}}
到了这里,属于客户端连接和请求响应的代码就完成了。通过这个方法就能拿到服务器的响应。
实现VirtualHost
之前的封装对组件和消息进行了封装,这里就是将客户端和组件进行封装,这里的封装涉及的概念还是以如何调用客户端的API和组件的API对他们进行组装。
实现一个消费者
注意:
这里还有一个需要注意,就是这里的连接点用是用户调用的,那么就要考虑到多个响应同时出现的情况。这个时候就需要我们将这些响应存储起来,一个一个处理。此时用到一个令牌的概念。
为什么呢?
首先模拟一个场景,此时有成千上万个消费者对象 N,而每个消费者都有一个队列,而每个消费者都有一个回调函数。计算机如何才能知道那个队列中有消息,此时就需要一个通知的概念,而这个通知需要我们用到令牌。
也就是说每个消费者都有属于他自己的回调函数和通知令牌,一旦相应的消费者拿到对应的通知,就会开启消费。
此时这个图就是这样的:
此时我们用一个阻塞队列,存储有消息的队列名,每次扫描到消息,就会上传给线程池。每次要考虑线程安全。
public class ConsumerManger {private VirtualHost parent;private ExecutorService workerPool=Executors.newFixedThreadPool(4);private BlockingDeque<String > tokenQueue=new LinkedBlockingDeque<>();private Thread scannerTread=null;public ConsumerManger (VirtualHost parent){this.parent=parent;scannerTread=new Thread(()->{while (true){try {//拿令牌String queueName=tokenQueue.take();//根据令牌找到队列MqQueue queue=parent.getMemoryData().getMemoryQueue(queueName);if (queue==null){throw new MqException("ConsumerManger : 领取令牌后发现,队列不存在! queueName="+queueName);}//从这个队列中消费消息synchronized (queue){consumerMessage(queue);}} catch (InterruptedException | MqException e) {e.printStackTrace();}}});scannerTread.start();}private void consumerMessage(MqQueue queue) {ConsumerEnv luckyDog=queue.chooseConsumer();if (luckyDog==null){//没有消费者return;}Message message=parent.getMemoryData().pollMessage(queue.getName());if (message==null){//没有消息return;}workerPool.submit(()->{try {parent.getMemoryData().addMessageWaitACK(queue.getName(), message);luckyDog.getConsumer().headDelivery(luckyDog.getConsumerTag(), message.getMessageType(),message.getBody());if (luckyDog.isAutoAck()){if (message.getMessage_is_Persistence()){parent.getDiskData().deleteMassage(queue,message);}parent.getMemoryData().deleteMessageWaitACK(queue.getName(),message.getMessageId());parent.getMemoryData().deleteMemoryMessage(message.getMessageId());System.out.println("ConsumerManger : 消息被成功消费! queueName "+queue.getName());}} catch (IOException | ClassNotFoundException | MqException e) {System.out.println("ConsumerManger : 消息消费失败! queueName "+queue.getName());e.printStackTrace();}});}public void notifyConsume(String queueName) throws InterruptedException {tokenQueue.put(queueName);}public void addConsumer(String consumerTag, String queueName, boolean autoAck, Consumer consumer) throws MqException {MqQueue mqQueue=parent.getMemoryData().getMemoryQueue(queueName);if (mqQueue==null){throw new MqException("ConsumerManger : 队列不存在 queueName"+queueName);}ConsumerEnv consumerEnv=new ConsumerEnv(consumerTag,queueName,autoAck,consumer);synchronized (mqQueue){mqQueue.addConsumerEnv(consumerEnv);int n=parent.getMemoryData().getMessageCount(queueName);for (int i = 0; i < n; i++) {//每调用一次就消费一次consumerMessage(mqQueue);}}}
}
实现第三层封装(VirtualHost)(要加Getter和Setter方法)
就需要封装之前除连接之外的所有API,这些API也是用户主要调用的API,其中就包括,
- 交换机的创建与删除(内存和硬盘)
- 队列的创建与删除(内存和硬盘)
- 绑定的创建与删除(内存和硬盘)
- 输入消息(内存和硬盘)
- 消费消息(内存和硬盘)
- 确认应答(内存和硬盘)
- 初始化数据库
- 以及恢复数据的操作
public class VirtualHost {private String virtualHostName;private MemoryData memoryData=new MemoryData();private DiskData diskData=new DiskData();private RuleTranslation ruleTranslation=new RuleTranslation();private ConsumerManger consumerManger=new ConsumerManger(this);private final Object exchangeLoker=new Object();private final Object queueLoker=new Object();//初始化数据public VirtualHost(String name){this.virtualHostName=name;diskData.init();try {memoryData.resume(diskData);} catch (ClassNotFoundException e) {System.out.println("VirtualHost: 恢复内存数据失败");e.printStackTrace();}}//创建交换机public boolean exchangeCreate(String exchangeName, ExchangeType exchangeType, boolean is_Persistence,boolean is_Auto_Delete, Map<String ,Object> exchange_arguments){exchangeName=virtualHostName+exchangeName;try {synchronized (exchangeLoker){Exchange exchangeMemory=memoryData.getMemoryExchange(exchangeName);if (exchangeMemory!=null){System.out.println("VirtualHost : 交换机已存在,交换机名="+exchangeName);return true;}Exchange exchange=new Exchange();exchange.setName(exchangeName);exchange.setExchangeType(exchangeType);exchange.setIs_Persistence(is_Persistence);exchange.setIs_Auto_Delete(is_Auto_Delete);exchange.setExchangeArguments(exchange_arguments);if (is_Persistence){diskData.insertExchange(exchange);}memoryData.insertMemoryExchange(exchange);System.out.println("VirtualHost : 交换机创建完毕="+exchangeName);return true;}}catch (Exception e){System.out.println("VirtualHost : 交换机创建失败="+exchangeName);e.printStackTrace();return false;}}//删除交换机public boolean exchangeDelete(String exchangeName){exchangeName=virtualHostName+exchangeName;try {synchronized (exchangeLoker){Exchange exchangeMemory=memoryData.getMemoryExchange(exchangeName);if (exchangeMemory==null){throw new MqException("VirtualHost : 交换机不存在无法删除!ExchangeName="+exchangeName);}if(exchangeMemory.isIs_Persistence()){diskData.deleteExchange(exchangeName);}System.out.println("VirtualHost: 交换机删除成功!ExchangeName="+exchangeName);return true;}}catch (Exception e){System.out.println("VirtualHost : 交换机删除失败ExchangeName="+exchangeName);e.printStackTrace();return false;}}public boolean queueCreate(String queueName,boolean is_Persistence,boolean is_Auto_Delete,boolean is_Unique_use,Map<String,Object> queueArguments){queueName=virtualHostName+queueName;try {synchronized (queueLoker){MqQueue queueMemory=memoryData.getMemoryQueue(queueName);if (queueMemory!=null){System.out.println("VirtualHost : 队列已存在! queueName"+queueName);return true;}MqQueue queue=new MqQueue();queue.setName(queueName);queue.setIs_Persistence(is_Persistence);queue.setIs_Auto_Delete(is_Auto_Delete);queue.setIs_Unique_use(is_Unique_use);queue.setQueueArguments(queueArguments);if (is_Persistence){diskData.insertQueue(queue);}memoryData.insertMemoryQueue(queue);System.out.println("VirtualHost : 队列创建成功!队列="+queueName);return true;}}catch (Exception e){System.out.println("VirtualHost : 队列创建失败 !队列="+queueName);e.printStackTrace();return false;}}public boolean queueDelete(String queueName){queueName=virtualHostName+queueName;try {synchronized (queueLoker){MqQueue queueMemory=memoryData.getMemoryQueue(queueName);if (queueMemory==null){throw new MqException("VirtualHost : 队列无法删除!队列已存在! queueName="+queueName);}if (queueMemory.isIs_Persistence()){diskData.deleteQueue(queueName);}memoryData.deleteMemoryQueue(queueName);System.out.println("VirtualHost : 队列删除成功 !队列="+queueName);return true;}}catch (Exception e){System.out.println("VirtualHost : 队列删除失败 !队列="+queueName);e.printStackTrace();return false;}}public boolean bindCreate(String exchangeName,String queueName,String bindingKey){exchangeName=virtualHostName+exchangeName;queueName=virtualHostName+queueName;try {synchronized (exchangeLoker){synchronized (queueLoker){Binding bindMemory=memoryData.getOneMemoryBinding(exchangeName,queueName);if (bindMemory!=null){throw new MqException("VirtualHost : 要绑定节点已存在!exchangeName="+exchangeName+",queueName="+queueName);}if (!ruleTranslation.validationBindingKey(bindingKey)){throw new MqException("VirtualHost : bindingKey非法!bindingKey="+bindingKey);}Binding binding=new Binding();binding.setExchangeName(exchangeName);binding.setQueueName(queueName);binding.setBindingKey(bindingKey);MqQueue queue=memoryData.getMemoryQueue(queueName);Exchange exchange=memoryData.getMemoryExchange(exchangeName);if (queue==null){throw new MqException("VirtualHost : 队列不存在!queueName="+queueName);}if (exchange==null){throw new MqException("VirtualHost : 交换机不存在!exchange="+exchangeName);}if (queue.isIs_Persistence()&&exchange.isIs_Persistence()){diskData.insertBinding(binding);}memoryData.insertMemoryBinding(binding);return true;}}} catch (Exception e) {System.out.println("VirtualHost : 绑定创建失败!queueName="+queueName+",exchangeName="+exchangeName);e.printStackTrace();return false;}}public boolean bindDelete(String exchangeName,String queueName){exchangeName=virtualHostName+exchangeName;queueName=virtualHostName+queueName;try {synchronized (exchangeLoker){synchronized (queueLoker){Binding bindingMemory=memoryData.getOneMemoryBinding(exchangeName,queueName);if (bindingMemory==null){throw new MqException("VirtualHost : 解除绑定失败! 绑定不存在! " +"queueName="+queueName+",exchangeName="+exchangeName);}diskData.deleteBinding(bindingMemory);memoryData.deleteMemoryBinding(bindingMemory);System.out.println("解除绑定成功");return true;}}}catch (Exception e){System.out.println("VirtualHost : 绑定删除失败!queueName="+queueName+",exchangeName="+exchangeName);e.printStackTrace();return false;}}/*** 发送消息到指定的交换机和队列**/public boolean publish(String exchangeName, String outingKey, MessageType messageType, byte[] body){try {exchangeName=virtualHostName+exchangeName;//验证钥匙if (!ruleTranslation.validationOutingKey(outingKey)){throw new MqException("VirtualHost : outingKey 非法! outingKey="+outingKey);}//验证交换机Exchange exchange=memoryData.getMemoryExchange(exchangeName);if (exchange==null){throw new MqException("VirtualHost : 交换机不存在 ! exchangeName="+exchangeName);}if (exchange.getExchangeType()==ExchangeType.DIRECT){//此时对于钥匙的要求没有,所以用outing作为队列名字String queueName=virtualHostName+outingKey;Message message=Message.createWithMessageID(outingKey,messageType,body);MqQueue mqQueue=memoryData.getMemoryQueue(queueName);if (mqQueue==null){throw new MqException("VirtualHost : 发送消息失败!队列不存在 queueName="+queueName);}sendMessage(mqQueue,message);}else {//获取绑定,遍历,符合条件的分发数据ConcurrentHashMap<String,Binding> bindingMap=memoryData.getAllMemoryBinding(exchangeName);for (Map.Entry<String,Binding>entry:bindingMap.entrySet()){Binding binding=entry.getValue();MqQueue mqQueue=memoryData.getMemoryQueue(binding.getQueueName());if (mqQueue==null){System.out.println("VirtualHost : publish 发送消息失败!队列不存在 queueName="+binding.getQueueName());continue;}Message message=Message.createWithMessageID(outingKey,messageType,body);if (!ruleTranslation.roote(exchange.getExchangeType(),binding,message)){continue;}sendMessage(mqQueue,message);}}return true;} catch (MqException |InterruptedException| IOException e) {System.out.println("VirtualHost : 发送消息失败 ");e.printStackTrace();return false;}}public boolean consume(String consumerTag, String queueName, boolean autoAck, Consumer consumer){queueName=virtualHostName+queueName;try {consumerManger.addConsumer(consumerTag,queueName,autoAck,consumer);System.out.println("VirtualHost : consume 成功! queueName"+queueName);return true;}catch (Exception e){System.out.println("VirtualHost : consume 失败! queueName"+queueName);e.printStackTrace();return false;}}//手动应答public boolean ACK(String queueName,String messageId){queueName=virtualHostName+queueName;try {Message message=memoryData.getMemoryMessage(messageId);MqQueue mqQueue=memoryData.getMemoryQueue(queueName);if (message==null){throw new MqException("VirtualHost : 要确认的消息不存在!messageId="+messageId);}if (mqQueue==null){throw new MqException("VirtualHost : 要确认消息的队列不存在!queueName="+queueName);}if (message.getMessage_is_Persistence()){diskData.deleteMassage(mqQueue,message);}memoryData.deleteMemoryMessage(messageId);memoryData.deleteMessageWaitACK(queueName,messageId);System.out.println("VirtualHost : 确认消息!queueName="+queueName+",messageId="+messageId);return true;} catch (MqException | ClassNotFoundException | IOException e) {System.out.println("VirtualHost : 确认消息失败!queueName="+queueName+",messageId="+messageId);e.printStackTrace();return false;}}private void sendMessage(MqQueue mqQueue, Message message) throws IOException, MqException, InterruptedException {boolean mode=message.getMessage_is_Persistence();if (mode){diskData.sendMassage(mqQueue,message);}memoryData.sendMessage(mqQueue,message);//插入后取消等待consumerManger.notifyConsume(mqQueue.getName());}}
最终:实现服务器(BrokerServer)(也叫第四层封装)
这次封装也最后一层封装,主要是在第三层的API进行统合,以对客户端进行连接。齐抓药的功能在于,服务器的启动,停止,链接客户端,并根据客户端响应的类型进行处理,并保存回话信息。
到了这里之前写的通信才算大显身手。
这里我在画个图:
public class BrokerServer {//TCPprivate ServerSocket serverSocket=null;//只考虑当前只有一个虚拟主机private final VirtualHost virtualHost=new VirtualHost("default");//此处key,是channelId的编号,用于记录链接点客户端的连接点(也就是回话信息)private final ConcurrentHashMap<String, Socket> sessions=new ConcurrentHashMap<>();//引入线程池,处理链接服务器的客户端private ExecutorService executorService=null;//判断服务器是否继续运行private volatile boolean runnable=true;public BrokerServer(int port) throws IOException {serverSocket=new ServerSocket(port);}public void start(){System.out.println("BrokerServer : 启动");executorService= Executors.newCachedThreadPool();try {while (runnable){Socket clientSocket=serverSocket.accept();executorService.submit(()->{connection(clientSocket);});}} catch (IOException e) {e.printStackTrace();}}private void connection(Socket clientSocket) {try (InputStream inputStream=clientSocket.getInputStream();OutputStream outputStream=clientSocket.getOutputStream()){try (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 (EOFException|SocketException e){System.out.println("BrokerServer: connection 关闭!客户端的地址"+clientSocket.getInetAddress().toString()+":"+clientSocket.getPort());}catch (IOException | ClassNotFoundException | MqException e) {System.out.println("BrokerServer: connection 出现异常");e.printStackTrace();}finally {try {clientSocket.close();clearCloseSession(clientSocket);} catch (IOException e) {e.printStackTrace();}}}//关闭回话对象,并清理private void clearCloseSession(Socket clentSocket) {//遍历哈希表,将关闭的Soket关闭List<String > deleteChannelId=new ArrayList<>();for (Map.Entry<String,Socket> entry: sessions.entrySet()){if (entry.getValue()==clentSocket){deleteChannelId.add(entry.getKey());}}for (String channelId:deleteChannelId){sessions.remove(channelId);System.out.println("BrokerServer : 清理session 完成 被清理的channelId="+channelId);}}private Response process(Request request, Socket clentSocket) throws IOException, ClassNotFoundException, MqException {BasicArgument basicArgument=(BasicArgument) BinaryTool.fromBytes(request.getPayload());System.out.println("Request: rid="+basicArgument.getRid()+",channelId="+basicArgument.getChannelId()+",type:"+request.getType()+",length="+request.getLength());boolean ok=true;if (request.getType()==0x1){//创建链接sessions.put(basicArgument.getChannelId(),clentSocket);System.out.println("BrokerServer: 创建 channel(回话) 完成!channelId="+basicArgument.getChannelId());}else if (request.getType()==0x2){//断开链接sessions.remove(basicArgument.getChannelId());System.out.println("BrokerServer: 销毁 channel 完成!channelId="+basicArgument.getChannelId());}else if (request.getType()==0x3){//创建交换机ExchangeCreateArgument argument= (ExchangeCreateArgument) basicArgument;ok=virtualHost.exchangeCreate(argument.getExchangeName(), argument.getExchangeType(),argument.isIs_persistence(),argument.isIs_auto_delete(),argument.getArguments());System.out.println("BrokerServer: 创建交换机成功 完成!ExchangeName=" + argument.getExchangeName());}else if (request.getType()==0x4){//删除交换机ExchangeDeleteArgument argument= (ExchangeDeleteArgument) basicArgument;ok=virtualHost.exchangeDelete(argument.getExchangeName());System.out.println("BrokerServer: 删除交换机成功 完成!ExchangeName=" + argument.getExchangeName());}else if (request.getType()==0x5){//创建队列QueueCreateArgument argument= (QueueCreateArgument) basicArgument;ok=virtualHost.queueCreate(argument.getQueueName(),argument.isIs_persistence(),argument.isIs_auto_delete(),argument.isIs_unique_use(),argument.getArgument());System.out.println("BrokerServer: 队列创建成功 完成!QueueName=" + argument.getQueueName());}else if (request.getType()==0x6){//删除队列QueueDeleteArgument argument= (QueueDeleteArgument) basicArgument;ok=virtualHost.queueDelete(argument.getQueueName());System.out.println("BrokerServer: 队列删除成功 完成!QueueName=" + argument.getQueueName());}else if (request.getType()==0x7){//创建绑定BindCreateArgument argument= (BindCreateArgument) basicArgument;ok=virtualHost.bindCreate(argument.getExchangeName(), argument.getQueueName(), argument.getBindingKey());System.out.println("BrokerServer: 绑定创建成功!QueueName=" + argument.getQueueName()+",ExchangeName=" + argument.getExchangeName());}else if (request.getType()==0x8){//解除绑定BindUnArgument argument= (BindUnArgument) basicArgument;ok=virtualHost.bindDelete(argument.getExchangeName(), argument.getQueueName());System.out.println("BrokerServer: 绑定删除成功!QueueName=" + argument.getQueueName()+",ExchangeName=" + argument.getExchangeName());}else if (request.getType()==0x9){//生产消息PulishArgument argument= (PulishArgument) basicArgument;ok=virtualHost.publish(argument.getExchangeName(), argument.getOutingKey(), argument.getMessageType(), argument.getBody());System.out.println("BrokerServer: 发送消息成功!");}else if (request.getType()==0xa){//消费消息ConsumerArgument argument= (ConsumerArgument) basicArgument;ok=virtualHost.consume(argument.getConsumerTag(), argument.getQueueName(), argument.isAutoACK(), new Consumer() {@Overridepublic void headDelivery(String consumerTag, MessageType messageType, byte[] body) throws MqException, IOException {/*** 要知道目前拿到的消息,是给那个客户用的。通过 consumerTag 知道* 此时consumerTag 就是channelId ,然后通过这个链接点去找session 中寻找*/Socket clientSocket=sessions.get(consumerTag);if (clientSocket==null||clentSocket.isClosed()){throw new MqException("BrokerServer : 订阅的客户端已经关闭!");}//构造响应数据SubscribeReturns subscribeReturns=new SubscribeReturns();subscribeReturns.setComsumerTag(consumerTag);subscribeReturns.setBody(body);subscribeReturns.setMessageType(messageType);subscribeReturns.setChannelId(consumerTag);subscribeReturns.setRid("");subscribeReturns.setOk(true);byte[] payload=BinaryTool.toBytes(subscribeReturns);Response response=new Response();response.setType(0xc);response.setLength(payload.length);response.setPayload(payload);DataOutputStream dataOutputStream=new DataOutputStream(clientSocket.getOutputStream());writeResponse(dataOutputStream,response);}});System.out.println("BrokerServer: 消息成功消费!");}else if (request.getType()==0xb){//确认消息ACKArgument argument= (ACKArgument) basicArgument;ok=virtualHost.ACK(argument.getQueueName(), argument.getMessageId());System.out.println("BrokerServer: 消费者确认消息!");}else {throw new MqException("BrokerServer: 无法确认操作属性,未知type="+request.getType());}//构造响应BasicReturns basicReturns=new BasicReturns();basicReturns.setChannelId(basicArgument.getChannelId());basicReturns.setRid(basicArgument.getRid());basicReturns.setOk(ok);byte[] payload=BinaryTool.toBytes(basicReturns);Response response=new Response();response.setType(request.getType());response.setLength(payload.length);response.setPayload(payload);System.out.println("BrokerServer: rid="+basicReturns.getRid()+",channlId="+basicReturns.getChannelId()+",type="+response.getType()+",length="+response.getLength());return response;}private void writeResponse(DataOutputStream dataOutputStream, Response response) throws IOException {dataOutputStream.writeInt(response.getType());dataOutputStream.writeInt(response.getLength());dataOutputStream.write(response.getPayload());dataOutputStream.flush();}private Request readRequest(DataInputStream dataInputStream) throws IOException {Request request=new Request();request.setType(dataInputStream.readInt());request.setLength(dataInputStream.readInt());byte[] payload=new byte[request.getLength()];int n =dataInputStream.read(payload);if (n!=request.getLength()){throw new IOException("读取数据格式错误");}request.setPayload(payload);return request;}public void stop() throws IOException {runnable=false;executorService.shutdownNow();serverSocket.close();}}
先小小的总结一下:
测试
对服务器做一次生产者和消费者的模拟
生产者
public class DemoProducer {public static void main(String[] args) throws IOException, InterruptedException {System.out.println("启动生产者");ConnectionFactory factory=new ConnectionFactory();factory.setHost("127.0.0.1");factory.setPort(9090);Connection connection=factory.newConnection();Channel channel=connection.createChannel();channel.exchangeCreate("testExchange", ExchangeType.DIRECT,true,false,null);channel.queueCreate("testQueue",true,false,false,null);byte[] body="hello world".getBytes();boolean ok=channel.publishMassage("testExchange","testQueue",null,body);System.out.println("投递消息完成!ok="+ok);Thread.sleep(1000);channel.close();connection.close();}
}
消费者
public class DomeConsumer {public static void main(String[] args) throws IOException, MqException, InterruptedException {System.out.println("启动消费者");ConnectionFactory factory=new ConnectionFactory();factory.setHost("127.0.0.1");factory.setPort(9090);Connection connection=factory.newConnection();Channel channel=connection.createChannel();channel.exchangeCreate("testExchange", ExchangeType.DIRECT,true,false,null);channel.queueCreate("testQueue",true,false,false,null);channel.consumeMassage("testQueue", true, new Consumer() {@Overridepublic void headDelivery(String consumerTag, MessageType messageType, byte[] body) throws MqException, IOException {System.out.println("消费开始");System.out.println("ConsumerTag="+consumerTag);System.out.println("MessageId="+messageType.getMessageId());System.out.println("OutingKey="+messageType.getOutingKey());String bodyString=new String(body,0, body.length);System.out.println("body="+bodyString);System.out.println("消费数据完毕");}});while (true){Thread.sleep(500);}}
}
启动类
主要是启动服务器(注意此时的端口不要写8080),至于为什么,因为Spring内部自带Tomcat 所以默认端口8080。传入的端口一定不是8080
@SpringBootApplication
//@MapperScan("java.com.example.mydmq.mqserver.dao")
public class MyDmqApplication {public static ConfigurableApplicationContext context;public static void main(String[] args) throws IOException {context= SpringApplication.run(MyDmqApplication.class, args);BrokerServer brokerServer=new BrokerServer(9090);brokerServer.start();}}
思维导图
我的源码
https://gitee.com/bdhejn/Java2/tree/master/MyDMQ