仓库代码链接:郑政祺/消息队列
本项目参照RabbitMQ简单模拟实现了一个消息队列。该消息队列提供了虚拟主机、交换机、队列、消息等核心概念,并实现了交换机转发消息的三种模式,旨在实现跨主机/服务器的生产者消费者模型。
知识点:网络编程、网络通信、RPC通信、IO流、序列化、并发编程、SpringBoot、SQLite
消息队列
消息队列本质上就是一个队列,只是在队列这种数据结构的基础上提供了更加丰富的功能,为的是实现生产者消费者模型。
生产者消费者模型的好处:
1.解耦合
当模块A与模块B进行通信的时候,势必要在模块A中添加与模块B通信的代码,当继续添加一个模块C通信的话,那又要在模块A中添加一段代码,此时模块A和模块B、模块C的耦合度很高,如果后续要修改,那么这些模块都需要进行改动代码,并且如果模块A挂了,其他模块立刻就能感知到。
考虑使用生产者消费者模型。此处模块A为生产者,负责生产数据,模块B、模块C为消费者,负责消费模块A产生的数据。引入一个消息队列,即生产者生产的数据先发送到消息队列,其他消费者直接从消息队列中取数据消费即可。如果再添加一个消费者的时候,生产者模块A不需要进行改动,直接让新的消费者添加订阅消息队列即可。这样就实现了解耦合,而且当模块A挂了,其他消费者也不会立马感知到,最多认为此时消费者还没生产数据。
2.削峰
在某一时刻,模块A接收到大量的请求,交给模块B去处理,如果两个模块都是比较轻量的程序可以还可以正常运行,但如果A是入口程序,B是数据库业务(数据库比较脆弱),此时大量的请求就会讲给数据库处理,就很有可能导致数据库崩了。
假设引入消息队列,虽然有很多请求,但是模块A会先将请求放到消息队列中,模块B可以根据自己的节奏来从队列中取消息,这样就削弱请求的峰值(对模块B),保证程序的正常运行。
需求分析
核心概念
虚拟主机:类似数据库中database,用来隔离交换机、队列、消息。
交换机:实现消息的转发。
队列:用来存储消息。
绑定:用来表示交换机和队列之间的关系。一个交换机可以关联多个队列,一个队列也可以关联多个交换机。
消息:用户自己发送的业务数据。
消息队列服务器
消息队列服务器要做的事就是根据用户的请求来创建交换机、队列、绑定,发送消息,返回消息。
消息服务器的核心API:
方法名 | 功能 |
---|---|
exchangeDeclare | 在服务器上创建交换机,如果存在就不创建 |
exchangeDelete | 销毁服务器上的交换机 |
queueDeclare | 在服务器上创建队列,如果存在就不创建 |
queueDelete | 销毁服务器上的队列 |
queueBind | 在服务器上创建绑定关系 |
queueUnbind | 销毁服务器上的绑定关系 |
basicPublish | 发布消息 |
basicConsume | 订阅消息 |
basicAck | 确认消息.当消费者从服务器上消费完了消息可以显示的告诉服务器,以此保证可靠性 |
消费一个消息有两种方式:pull(拉),push(推).如果需要支持pull的方式,需要再提供一个API,用来手动的获取消息。但是在RabbitMQ
中只支持push推的方式,即消费者订阅了某个队列,服务器会自动的将消息推送给消费者。
消息队列客户端
在客户端中也要提供上述的API,虽然名字一样,但代码时完全不同的。当用户调用创建队列的API,此时用户这边就会发起一个网络请求,服务器接收到了这个请求并解析,计算响应,最后将响应返回给客户端,此处所创建的交换机存在于服务器的内存中,而客户端只是完成了一个发送请求,没有真实的创建交换机。而这种交互方式也称作是RPC(远程过程调用协议)。
除了上述这些API,在客户端这边还有一组特别的API。
方法名 | 功能 |
---|---|
创建connection | 与服务器进行TCP连接 |
关闭connection | 关闭服务器的连接 |
创建channel | 创建一个信道channel,即与服务器创建逻辑连接 |
关闭channel | 关闭信道channel,即与服务器断开逻辑上的连接 |
在客户端与服务器交互的过程中,客户端是通过channel与服务器进行逻辑上的连接/断开。创建一个connection,本质上就是与服务器进行TCP通信,而创建一个channel就是复用这个connection,进行逻辑上的通信,即虽然使用channel中的API发送请求,但本质上还是通过connection中的socket发送请求到服务器中。而服务器也无法区分channel,将返回的响应交给connection,再根据某种规则,将响应返回给对应的channel。
复用TCP连接,是为了提高通信的效率。
公共模块
公共模块所要进行的规定一些网络通信中的参数,对数据进行序列化,以便数据的传输。一个对象实例要想在网络中传输,需要将这个对象进行转换,如转成json格式进行传输或者通过二进制传输;将一个对象存储在硬盘中,也要对对象进行转换。
持久化模块
为了防止服务器重启,依旧能恢复到上次的操作,需要对数据进行持久化,服务器可以根据用户指定的参数,是否进行持久化的操作。
对数据进行持久化可以存储到两个位置:1.文件上 2.数据库
由于虚拟主机、交换机、队列、绑定这些数据会经常进行crud,因此我们可以将这些数据存储在数据库中。
对于消息,可能会比较大,而且也不会对消息进行频繁crud,我们可以存储在文件中。
模拟实现
创建核心类
首先创建如下包。
Exchange:
@Data
public class Exchange {
//标识交换机
private String name;
//交换机的类型
//默认为直接交换机
private ExchangeType type = ExchangeType.DIRECT;
//是否可持久化,即要不要存在硬盘上
private boolean durable = false;
//是否自动删除
private boolean autoDelete = false;
//可选参数
private Map<String, Object> arguments = new HashMap<>();
}
交换机的类型有好多种,在这我们只实现3种交换机:direct、fanout、topic。
ExchangeType:
public enum ExchangeType {
DIRECT(0),
FANOUT(1),
TOPIC(2);
private int type;
ExchangeType(int type) {
this.type = type;
}
}
MSGQueue:
@Data
//为了与标准库中Queue区分,这里改叫MSGQueue
public class MSGQueue {
private String name;
//是否独占(只能被一个消费者使用)
private boolean exclusive = false;
//是否可持久化
private boolean durable = false;
//是否自动删除
private boolean autoDelete = false;
//可选参数
private Map<String, Object> arguments = new HashMap<>();
}
Binding:
@Data
public class Binding {
private String exchangeName;
private String queueName;
//用于Topic交换机,跟Message中routingKey进行匹配
private String bindingKey;
}
Message:
@Data
public class Message {
//消息的基本属性
private BasicProperties basicProperties;
//存储数据
private byte[] body;
//位置偏移量,便于在文件中取出数据
//采用[offsetBeg, offsetEnd)区间
//这两个属性是帮助从文件中读取数据的,不需要进行序列化
private long offsetBeg;
private long offsetEnd;
//表示此消息在文件中是否有效(采用逻辑删除)
//0x00 表示无效, 0x01 表示有效
private byte isValid = 0x01;
//通过一个"工厂方法"来创建message对象,随机生成Id
public static Message createMessageWithId(BasicProperties basicProperties, String routingKey, byte[] body){
Message message = new Message();
if(basicProperties != null){
message.setBasicProperties(basicProperties);
}
message.getBasicProperties().setMessageId("M_" + UUID.randomUUID().toString());
message.getBasicProperties().setRoutingKey(routingKey);
message.setBody(body);
//此处 offsetBeg,offsetEnd,isValid 作用在持久化操作
//当进行持久化时,才需要赋值
return message;
}
public String getMessageId() {
return this.basicProperties.getMessageId();
}
public void setMessageId(String messageId) {
this.basicProperties.setMessageId(messageId);
}
public String getRoutingKey() {
return this.basicProperties.getRoutingKey();
}
public void setRoutingKey(String routingKey) {
this.basicProperties.setRoutingKey(routingKey);
}
public boolean isDurable() {
return this.basicProperties.isDurable();
}
public void setDurable(boolean durable) {
this.basicProperties.setDurable(durable);
}
}
在传输消息的时候,都是通过二进制来传输,所以消息本质上就是一个byte[],其他都是辅助字段。
offsetBeg、offsetEnd、isValid字段是用来描述这个消息在文件中是如何存储的,以及该消息是否已经被删除了。
由于在文件中使用类似数组中的删除元素的操作会大大增加我们程序的运行时间,在此我们选择采用逻辑删除的方式,即通过一个标记字段来表示是否删除。
BasicProperties:
@Data
public class BasicProperties {
//为防止重复,使用UUID生成
private String MessageId;
private boolean isDurable = false;
//口令
private String routingKey;
}
后续的操作都是围绕这几个核心概念来增删改查的。
数据库管理
由于MySQL数据库比较重量,在此就选择使用相对轻量的数据库SQLite. MySQL是客户端服务器结构的程序,而SQLite是一个本地的数据库,操作它相当于是直接操作本地文件。
在Java中使用SQLite,也不需要额外的安装,可以直接使用maven,将相关依赖引入,然后进行配置就可以正常使用了。
SQLite的依赖:
<!-- https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc -->
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.42.0.0</version>
</dependency>
application.yml:
spring:
datasource:
url: jdbc:sqlite:./data/meta.db
username:
password:
driver-class-name: org.sqlite.JDBC
SQLite是把数据存储在当前硬盘的某个指定文件中,在这里就存储在了工作路径的data目录中的meta.db文件了。
SQLite也不需要指定用户名和密码,毕竟是自己一个人用的,只有本地主机才能使用。
接下来就是建库和建表。在MySQL中,需要自己手动进行创建database,但在SQLite中,则无需手动建库了,每一个数据库文件就是一个数据库,即当前的meta.db文件就相当于是MySQL中的database。
为了能够即用即创建表,在这考虑部署的时候就自动创建表,即通过mybatis的update注解来建表并添加一些基础的查询、增加、删除操作。
@Mapper
public interface MetaMapper {
//建表操作
@Update("create table if not exists exchange(" +
"name varchar(50) primary key," +
"type int," +
"durable boolean," +
"autoDelete boolean," +
"arguments varchar(1024))")
public void createExchangeTable();
@Update("create table if not exists queue(" +
"name varchar(50) primary key," +
"exclusive boolean," +
"durable boolean," +
"autoDelete boolean," +
"arguments varchar(1024))")
public void createQueueTable();
@Update("create table if not exists binding(" +
"exchangeName varchar(50)," +
"queueName varchar(50)," +
"bindingKey varchar(256))")
public void createBindingTable();
//基础添加、删除、查询操作
@Insert("insert into exchange values(#{name}, #{type}, #{durable}, #{autoDelete}, #{arguments})")
public void insertExchange(Exchange exchange);
@Delete("delete from exchange where name = #{exchangeName}")
public void deleteExchange(String exchangeName);
@Select("select * from exchange")
public List<Exchange> selectAllExchanges();
@Insert("insert into queue values(#{name}, #{exclusive}, #{durable}, #{autoDelete}, #{arguments})")
public void insertQueue(MSGQueue msgQueue);
@Delete("delete from queue where name = #{queueName}")
public void deleteQueue(String queueName);
@Select("select * from queue")
public List<MSGQueue> selectAllQueues();
@Insert("insert into binding values(#{exchangeName}, #{queueName}, #{bindingKey})")
public void insertBinding(Binding binding);
@Delete("delete from binding where exchangeName = #{exchangeName} and queueName = #{queueName}")
public void deleteBinding(String exchangeName, String queueName);
@Select("select * from binding")
public List<Binding> selectAllBindings();
}
由于SQLite中也不支持HashMap这种数据结构,因此使用String来存储。
Mybatis在写入数据的时候,会自动的调用对象的getter方法,而在读出数据的时候,会自动调用setter方法,因此为了能够将arguments这个HashMap类型的数据存入到数据库中,我们需要转成字符串。
在此使用jackson来处理,引入相关依赖即可。
jackson依赖:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.6</version>
</dependency>
getter和setter:
public String getArguments() {
ObjectMapper objectMapper = new ObjectMapper();
try {
objectMapper.writeValueAsString(arguments);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return "{}";
}
public void setArguments(String arguments) {
ObjectMapper objectMapper = new ObjectMapper();
try {
this.arguments = objectMapper.readValue(arguments, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
e.printStackTrace();
}
}
为了使程序的结构更加清晰,程序员更方便的操作数据库,考虑创建一个DataBaseManager类来封装数据库的相关操作。
DataBaseManager:
//封装数据库操作
//DataBaseManager不交给Spring管理,由程序员手动管理
@Slf4j
public class DataBaseManager {
//从Spring上下文中手动获取MetaMapper的Bean
private MetaMapper metaMapper;
//带有业务逻辑的初始化操作
//完成建库建表操作
//如果不存在表就进行创建,并添加默认初始数据
public void init(){
metaMapper = MqBlogApplication.context.getBean(MetaMapper.class);
if(!checkDBExists()){
//创建data目录
File file = new File("./data");
file.mkdirs();
//建表并初始化数据
createTable();
createDefaultData();
log.info("[DataBaseManager] 数据库初始化完成");
}else{
log.info("[DataBaseManager] 数据库已经初始化");
}
}
//删除数据库目录
//删除目录前,需要保证目录是空的
public void deleteDB(){
File file = new File("./data/meta.db");
//删除目录里面的文件
boolean isSuccess = file.delete();
if(isSuccess){
log.info("[DataBaseManager] 删除数据库文件成功");
}else{
log.info("[DataBaseManager] 删除数据库文件失败");
}
//删除目录
file = new File("./data");
isSuccess = file.delete();
if(isSuccess){
log.info("[DataBaseManager] 删除数据库目录成功");
}else {
log.info("[DataBaseManager] 删除数据库目录失败");
}
}
//判断数据库(meta.db)是否存在
private boolean checkDBExists() {
File DBFile = new File("./data/meta.db");
return DBFile.exists();
}
//对 Exchange,Queue,Binding 进行建表
private void createTable() {
metaMapper.createExchangeTable();
metaMapper.createQueueTable();
metaMapper.createBindingTable();
log.info("[DataBaseManager] 创建表完成");
}
//RabbitMQ初始带有一个匿名交换机, 类型为 Direct
private void createDefaultData() {
Exchange exchange = new Exchange();
exchange.setName("");
exchange.setType(ExchangeType.DIRECT);
exchange.setDurable(true);
exchange.setAutoDelete(false);
metaMapper.insertExchange(exchange);
log.info("[DataBaseManager] 初始数据构造完成");
}
//增删查操作
public void insertExchange(Exchange exchange){
metaMapper.insertExchange(exchange);
}
public void deleteExchange(String exchangeName){
metaMapper.deleteExchange(exchangeName);
}
public List<Exchange> selectAllExchanges(){
List<Exchange> exchanges = metaMapper.selectAllExchanges();
return exchanges;
}
public void insertQueue(MSGQueue queue){
metaMapper.insertQueue(queue);
}
public void deleteQueue(String queueName){
metaMapper.deleteQueue(queueName);
}
public List<MSGQueue> selectAllQueues(){
return metaMapper.selectAllQueues();
}
public void insertBinding(Binding binding){
metaMapper.insertBinding(binding);
}
public void deleteBinding(String exchangeName, String queueName){
metaMapper.deleteBinding(exchangeName, queueName);
}
public List<Binding> selectAllBindings(){
return metaMapper.selectAllBindings();
}
}
文件管理
文件管理主要是对消息进行存储,消息是进行序列化存储的数据,不便于读取,因此我们无法区分一条消息的开始和结束。如果对每条消息都单独保存在一个文件中,可能会创建大量的文件。
我们现在做出如下约定:一条消息在硬盘上存储需要先花费4个字节存储消息体的长度,然后再存储消息本体。
在定义消息的时候,我们定义了两个辅助字段:offsetBeg、offsetEnd,分别记录一个消息在文件中存储的开始和结束。
通过上述分析,我们可以知道,需要对message对象进行序列化存储在文件中,而offsetBeg和offsetEnd则不需要,保留原始值即可。需要对Message类实现serializable接口,由于BasicProperties也包含在类中,也需要实现接口,而offsetBeg,offsetEnd使用transient关键字描述,程序就不会再序列化了。
@Data
public class Message implements Serializable {
//消息的基本属性
private BasicProperties basicProperties;
//存储数据
private byte[] body;
//位置偏移量,便于在文件中取出数据
//采用[offsetBeg, offsetEnd)区间
//这两个属性是帮助从文件中读取数据的,不需要进行序列化
private transient long offsetBeg;
private transient long offsetEnd;
//表示此消息在文件中是否有效(采用逻辑删除)
//0x00 表示无效, 0x01 表示有效
private byte isValid = 0x01;
}
@Data
public class BasicProperties implements Serializable {
//为防止重复,使用UUID生成
private String MessageId;
private boolean isDurable = false;
//口令
private String routingKey;
}
BinaryTool:
public class BinaryTool {
//将一个Java对象序列化成一个字节数组
//这个类必须实现了Serializable
public static byte[] toBytes(Object object) throws IOException {
//此处的 ByteArrayOutputStream 可以不用关闭,是一个纯内存的对象,不涉及到文件描述符
try(ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
//此处 writeObject 会将 object 对象进行序列化,生成的二进制字节数据,写入到objectOutputStream中
//objectOutputStream 关联到了 byteArrayOutputStream, 最终就写到了 byteArrayOutputStream
objectOutputStream.writeObject(object);
}
//将 byteArrayOutputStream 中序列化后的二进制数据 转成byte[] 数组
return byteArrayOutputStream.toByteArray();
}
}
public static Object fromBytes(byte[] data) throws IOException, ClassNotFoundException {
try(ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(data)){
try(ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)){
//此处的 readObject 将 data byte[] 转成对象
return objectInputStream.readObject();
}
}
}
}
由于每条消息都是存储在队列上的,因此就按照队列的维度进行存储。即每个队列都是一个目录,在每个目录下都有两个文件,一个是data数据文件,用来存储消息,另一个是state文件,用来描述data数据文件中的情况,为了后面可以进行gc。
此处的data文件是二进制文件,而state文件则是字符型文件,里面用一个数表示消息的总数,再用一个数表示消息的有效个数。因为采用的是逻辑删除的方式,也就是说被删除的消息不会真正意义上删除,当文件中存储很多数据的时候,就会大大影响我们的读取效率,因此我们需要使用GC来真正删除垃圾消息。在这规定:当文件有1000个消息的时候,并且垃圾消息占比50%,我们就进行一次GC。
在这我们也封装一个类来帮助我们处理文件的操作。
MessageFileManager:
@Slf4j
//对硬盘上的message进行管理
//规定每个队列都对应一个目录,
//每个目录下都包含两个文件:queue_data.txt, queue_state.txt,分别用来存储数据和统计数据个数
//
public class MessageFileManager {
public void init() {
//为了接口的统一性
//暂时不处理,后续可自行添加
}
//表示该队列下统计的文件信息
public static class State{
public int totalCount = 0; //总消息数量
public int validCount = 0; //有效消息数量
}
//获取消息所处队列的目录
private String getQueueDir(String queueName){
return "./data/" + queueName;
}
//获取消息所处队列的数据文件路径
private String getQueueDataPath(String queueName){
return getQueueDir(queueName) + "/queue_data.txt";
}
//获取消息所处队列的统计文件路径
private String getQueueStatePath(String queueName){
return getQueueDir(queueName) + "/queue_state.txt";
}
//创建队列对应的目录和文件
public void createQueueFiles(String queueName) throws IOException {
//1.创建队列对应目录
File directory = new File(getQueueDir(queueName));
if(!directory.exists()){
boolean success = directory.mkdirs();
if(!success){
throw new IOException("[MessageFileManager] 创建目录失败" + directory.getAbsoluteFile());
}
}
//2.创建队列对应数据文件
File dataFile = new File(getQueueDataPath(queueName));
if(!dataFile.exists()){
boolean success = dataFile.createNewFile();
if(!success){
throw new IOException("[ManagerFileManager] 创建文件失败" + dataFile.getAbsoluteFile());
}
}
//3.创建队列对应统计文件
File stateFile = new File(getQueueStatePath(queueName));
if(!stateFile.exists()){
boolean success = stateFile.createNewFile();
if(!success){
throw new IOException("[ManagerFileManager] 创建文件失败" + stateFile.getAbsoluteFile());
}
}
//4.初始化队列文件
State state = new State();
writeState(queueName, state);
}
//删除队列对应的目录和文件
public void destroyQueueFiles(String queueName) throws IOException {
//要删除目录需要先删除里面的文件
File dataFile = new File(getQueueDataPath(queueName));
boolean success1 = dataFile.delete();
File stateFile = new File(getQueueStatePath(queueName));
boolean success2 = stateFile.delete();
File directory = new File(getQueueDir(queueName));
boolean success3 = directory.delete();
//确保文件都删除了
if(!success1 || !success2 || !success3){
throw new IOException("[MessageFileManager] 删除队列文件失败" + dataFile.getAbsoluteFile());
}
}
//从文件中读取统计信息
private State readState(String queueName){
//state文件是文本文件,可以使用Scanner来读写
State state = new State();
try(InputStream inputStream = new FileInputStream(getQueueStatePath(queueName))){
Scanner scanner = new Scanner(inputStream);
state.totalCount = scanner.nextInt();
state.validCount = scanner.nextInt();
return state;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
//向文件中写入统计信息
private void writeState(String queueName, State state){
//OutputStream默认情况打开文件,会清空文件
try(OutputStream outputStream = new FileOutputStream(getQueueStatePath(queueName))){
PrintWriter printWriter = new PrintWriter(outputStream);
printWriter.print(state.totalCount + "\t" + state.validCount);
printWriter.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
//判断队列对应的数据文件和统计文件都存在
private boolean checkQueueFilesExist(String name) {
//如果两个文件都存在了,则说明目录也存在,不需要额外判断
File dataFile = new File(getQueueDataPath(name));
File stateFile = new File(getQueueStatePath(name));
if(!dataFile.exists() || !stateFile.exists()){
return false;
}
return true;
}
//将消息写入对应的队列文件
public void sendMessage(MSGQueue queue, Message message) throws IOException, MqException {
//1.判断对应的队列文件是否存在
if(!checkQueueFilesExist(queue.getName())){
throw new MqException("[MessageFileManager] 队列文件不存在 queueName:" + queue.getName());
}
//2.将消息转成二进制
synchronized (queue){
byte[] data = BinaryTool.toBytes(message);
//3.获取到文件末尾位置,设置文件位置的偏移量
File dataFile = new File(getQueueDataPath(queue.getName()));
//每条消息前4个字节用来表示消息的长度
message.setOffsetBeg(dataFile.length() + 4);
message.setOffsetEnd(dataFile.length() + 4 + data.length);
//4.写入消息文件
try(OutputStream outputStream = new FileOutputStream(dataFile, true)){
try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)){
//写入消息的长度(4个字节)
dataOutputStream.writeInt(data.length);
//写入消息本体
dataOutputStream.write(data);
}
}
//5.更新统计文件
State state = readState(queue.getName());
state.totalCount += 1;
state.validCount += 1;
writeState(queue.getName(), state);
}
}
//从硬盘上删除消息(逻辑删除)
//此处传的消息必须为有效消息
public void deleteMessage(MSGQueue queue, Message message) throws IOException, ClassNotFoundException, MqException {
//1.判断队列对应的文件是否存在
if(!checkQueueFilesExist(queue.getName())){
throw new MqException("[MessageFileManager] 队列文件不存在 queueName:" + queue.getName());
}
synchronized (queue){
//2.从文件中获取到消息
byte[] data = new byte[(int)message.getOffsetEnd() - (int)message.getOffsetBeg()];
try(RandomAccessFile randomAccessFile = new RandomAccessFile(getQueueDataPath(queue.getName()), "rw")){
//移动文件光标
randomAccessFile.seek(message.getOffsetBeg());
randomAccessFile.read(data);
//3.将消息反序列化并标记为无效
Message diskMessage = (Message)BinaryTool.fromBytes(data);
diskMessage.setIsValid((byte)0x00);
//4.将消息重新写入文件
randomAccessFile.seek(message.getOffsetBeg());
randomAccessFile.write(BinaryTool.toBytes(diskMessage));
}
//5.更新统计文件
State state = readState(queue.getName());
//逻辑删除,不需要修改totalCount
state.validCount -= 1;
writeState(queue.getName(), state);
}
}
//从队列文件中读取所有 有效消息
//在程序启动时,进行调用,读取文件中的所有有效消息,加载到内存中
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)){
//记录当前文件光标,方便赋值Message中offsetBeg和offsetEnd值
long currentOffset = 0;
while(true){
//1.读取当前消息的长度
//当 readInt 读取到文件末尾的时候,会抛出异常 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) BinaryTool.fromBytes(buffer);
//4.判断当前消息是否为有效消息
if(message.getIsValid() == 0x00){
currentOffset += (4 + messageSize);
continue;
}
//5.计算在文件中偏移量并赋值,将消息添加到链表中
message.setOffsetBeg(currentOffset + 4);
message.setOffsetEnd(currentOffset + 4 + messageSize);
currentOffset += (4 + messageSize);
messages.add(message);
}
} catch (EOFException e){
//文件读取到末尾,并非真的异常
log.info("[MessageFileManager] 文件读取完毕");
}
}
return messages;
}
//获取新的队列数据文件
private String getQueueNewDataPath(String queueName){
return getQueueDir(queueName) + "/new_queued_data.txt";
}
//约定 当文件中总消息数超过 1000 并且 无效消息的个数 超过一半 就进行一次gc
public boolean checkGC(String queueName){
State state = readState(queueName);
if(state.totalCount > 1000 && (double)state.validCount / (double) state.totalCount > 0.5){
return true;
}
return false;
}
//对消息文件进行gc操作
//此处使用复制算法的方式进行
public void gc(MSGQueue queue) throws IOException, MqException, ClassNotFoundException {
//当一个线程进行GC的时候,其他线程不允许进行操作
synchronized (queue){
//由于gc操作比较耗时,在这输出一下gc的时间
//计算gc开始时间
long gcBeg = System.currentTimeMillis();
//1.创建新文件
File newDataFile = new File(getQueueNewDataPath(queue.getName()));
if(newDataFile.exists()){
log.info("[MessageFileManager] 存在复制的目标文件");
newDataFile.delete();
}
if(!newDataFile.createNewFile()){
throw new MqException("[MessageFileManager] 复制文件创建失败,queueName:" + queue.getName());
}
//2.从旧文件中读取消息
//此处读取到的信息都是有效信息
LinkedList<Message> messages = loadAllMessageFromQueue(queue.getName());
//3.将有效消息写入新文件
try(OutputStream outputStream = new FileOutputStream(newDataFile)){
try(DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
for (Message message : messages) {
//不调用sendMessage,防止outputStream频繁关闭影响性能
byte[] buffer = BinaryTool.toBytes(message);
dataOutputStream.writeInt(buffer.length);
dataOutputStream.write(buffer);
}
}
}
//4.让新文件替换就旧文件
File oldDataFile = new File(getQueueDataPath(queue.getName()));
if(!oldDataFile.delete()){
throw new MqException("[MessageFileManager] 旧数据文件删除失败 oldDataFile:" + oldDataFile.getAbsoluteFile());
}
if(!newDataFile.renameTo(oldDataFile)){
throw new MqException("[MessageFileManager] 文件重命名失败 newDataFile:" + newDataFile.getAbsoluteFile());
}
//5.更新统计信息的文件
State state = readState(queue.getName());
state.totalCount = state.validCount;
writeState(queue.getName(), state);
//计算gc结束时间
long gcEnd = System.currentTimeMillis();
log.info("[MessageFileManager] " + queue.getName() + "gc完成, 总耗时: " + (gcEnd - gcBeg) + "ms");
}
}
}
封装硬盘
为了方便后续对数据进行持久化,即不用去关注数据到底是存在数据库还是文件中,在此再封装一个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(MSGQueue queue) throws IOException {
dataBaseManager.insertQueue(queue);
//每个队列都关联一个目录文件(子文件为queue_data.txt、queue_state.txt)
//需要将对应的文件创建
messageFileManager.createQueueFiles(queue.getName());
}
public void deleteQueue(String queueName) throws IOException {
dataBaseManager.deleteQueue(queueName);
//需要将队列对应的文件都删除
messageFileManager.destroyQueueFiles(queueName);
}
public List<MSGQueue> selectAllQueues(){
return dataBaseManager.selectAllQueues();
}
//绑定
public void insertBinding(Binding binding){
dataBaseManager.insertBinding(binding);
}
public void deleteBinding(Binding binding){
dataBaseManager.deleteBinding(binding.getExchangeName(), binding.getQueueName());
}
public List<Binding> selectAllBindings(){
return dataBaseManager.selectAllBindings();
}
//消息
public void insertMessage(MSGQueue queue, Message message) throws IOException, MqException {
messageFileManager.sendMessage(queue, message);
}
public void deleteMessage(MSGQueue queue, Message message) throws IOException, ClassNotFoundException, MqException {
messageFileManager.deleteMessage(queue, message);
//判断是否需要GC
if(messageFileManager.checkGC(queue.getName())){
messageFileManager.gc(queue);
}
}
public LinkedList<Message> loadAllMessageFromQueue(String queueName) throws IOException, MqException, ClassNotFoundException {
return messageFileManager.loadAllMessageFromQueue(queueName);
}
}
内存管理
上述只是完成了持久化操作,但是真正的主战场还是在内存上,硬盘只是用来保证服务器重启后,数据不丢失。
为了方便后续对象的查找,我们可以使用HashMap的数据结构来存储。
交换机:使用HashMap,key 为 exchangeName,value 为 exchange 对象.
队列:使用HashMap,key 为 queueName,value 为 queue 对象.
绑定:使用嵌套HashMap, 第一个 key 为exchangeName ,第一个 value 为 HashMap<String, Binding>
第二个 key 为 queueName,第二个 value 为 binding 对象.
表示的含义是,先查找交换机邦迪的所有队列,然后再找到指定绑定.
消息:使用HashMap,key 为 messageId, value 为 message 对象.
由于我们的消息是依附于队列的,我们还需要使用数据结构来表示这样的关系,即在队列中存储着消息。
队列和消息之间的关系:使用HashMap,key 为 queueName, value 为 LinkedList,里面存储message对象.
待确认的消息:使用嵌套HashMap,第一个 key 为 queueName,第一个 value 为 HashMap<String, Message>
第二个 key 为 messageId,第二个 value 为 message对象.
当消费者从队列中取了一个消息,我们无法保证有没有丢失或读错,但可以通过应答的方式来保证可靠性,即消费者消费完了这个消息后,发送一个确认应答告诉服务器,后续服务器就可以删除了,但在一些对可靠性不高的场景中,手动应答的方式就会比较鸡肋,于是我们在这里提供两种应答方式:1.自动应答 2.手动应答
/*
* 管理内存中的数据
* 由于会在多线程的场景下使用,需要注意线程安全
*/
@Slf4j
public class MemoryDataCenter {
//在内存中需要管理 交换机、队列、绑定、消息
//key: exchangeName, value: exchange对象
private ConcurrentHashMap<String, Exchange> exchangeMap = new ConcurrentHashMap<>();
//key: queueName, value: queue对象
private ConcurrentHashMap<String, MSGQueue> queueMap = new ConcurrentHashMap<>();
//key: exchangeName, value: 该交换机与队列的所有绑定关系映射集合(HashMap)
private ConcurrentHashMap<String, ConcurrentHashMap<String, Binding>> bindingMap = new ConcurrentHashMap<>();
//key: messageId, value: message对象
private ConcurrentHashMap<String, Message> messageMap = new ConcurrentHashMap<>();
//key: queueName, value: 该队列下的消息对象的集合
private ConcurrentHashMap<String, LinkedList<Message>> queueMessageMap = new ConcurrentHashMap<>();
//key: queueName, value: 该队列下未应答的消息映射集合
//第二个key: messageId(UUID表示)
private ConcurrentHashMap<String, ConcurrentHashMap<String, Message>> queueMessageWaitAckMap = new ConcurrentHashMap<>();
//交换机
public void insertExchange(Exchange exchange){
exchangeMap.put(exchange.getName(), exchange);
log.info("[MemoryDataCenter] 交换机插入成功, exchangeName:" + exchange.getName());
}
public void deleteExchange(String exchangeName){
exchangeMap.remove(exchangeName);
log.info("[MemoryDataCenter] 交换机删除成功, exchangeName:" + exchangeName);
}
public Exchange getExchange(String exchangeName){
return exchangeMap.get(exchangeName);
}
//队列
public void insertQueue(MSGQueue queue){
queueMap.put(queue.getName(), queue);
log.info("[MemoryDataCenter] 队列插入成功, queueName:" + queue.getName());
}
public void deleteQueue(String queueName){
queueMap.remove(queueName);
log.info("[MemoryDataCenter] 队列删除成功, queueName:" + queueName);
}
public MSGQueue getQueue(String queueName){
return queueMap.get(queueName);
}
//绑定
public void insertBinding(Binding binding) throws MqException {
// ConcurrentHashMap<String, Binding> queueBindingMap = bindingMap.get(binding.getExchangeName());
ConcurrentHashMap<String, Binding> queueBindingMap = bindingMap.computeIfAbsent(binding.getExchangeName(), k -> new ConcurrentHashMap<>());
//检查嵌套的哈希表是否存在
synchronized (queueBindingMap){
//这样写需要再创建一把锁
// if(queueBindingMap == null){
// queueBindingMap = new ConcurrentHashMap<>();
// bindingMap.put(binding.getExchangeName(), queueBindingMap);
// }
if(queueBindingMap.get(binding.getQueueName()) != null){
throw new MqException("[MemoryDataCenter] 绑定关系已经存在 exchangeName:" + binding.getExchangeName() +
", queueName" + binding.getQueueName());
}
queueBindingMap.put(binding.getQueueName(), binding);
log.info("[MemoryDataCenter] 绑定关系插入成功 exchangeName:"+ binding.getExchangeName() +
", queueName" + binding.getQueueName());
}
}
public void deleteBinding(Binding binding) throws MqException {
ConcurrentHashMap<String, Binding> queueBindingMap = bindingMap.get(binding.getExchangeName());
if(queueBindingMap == null){
throw new MqException("[MemoryDataCenter] 该绑定不存在 exchangeName:" + binding.getExchangeName() +
", queueName" + binding.getQueueName());
}
bindingMap.remove(binding.getExchangeName());
log.info("[MemoryDataCenter] 绑定关系删除成功 exchangeName:"+ binding.getExchangeName() +
", queueName" + binding.getQueueName());
}
//两个版本的获取绑定:
//1.获取队列和交换机之间的唯一绑定
public Binding getBinding(String exchangeName, String queueName) throws MqException {
ConcurrentHashMap<String, Binding> queueBindingMap = bindingMap.get(exchangeName);
if(queueBindingMap == null){
return null;
}
return queueBindingMap.get(queueName);
}
//2.获取当前当前交换机下的所有绑定
public ConcurrentHashMap<String, Binding> getBindings(String exchangeName){
return bindingMap.get(exchangeName);
}
//消息
//向消息中心添加消息
public void addMessage(Message message){
messageMap.put(message.getMessageId(), message);
log.info("[MemoryDataCenter] 消息添加完成 messageId:" + message.getMessageId());
}
public void removeMessage(String messageId){
messageMap.remove(messageId);
log.info("[MemoryDataCenter] 消息删除完成 messageId:" + messageId);
}
public Message getMessage(String messageId){
return messageMap.get(messageId);
}
//将消息发送到指定队列
public void sendMessage(String queueName, Message message){
// LinkedList<Message> messages = queueMessageMap.get(queueName);
LinkedList<Message> messages = queueMessageMap.computeIfAbsent(queueName, k -> new LinkedList<>());
synchronized (messages){
//queueMessageMap.put(queueName, messages);
//将消息添加到队列中
messages.add(message);
}
//将消息添加到消息中心
addMessage(message);
log.info("[MemoryDataCenter] 消息已成功添加到队列 queueName:" + queueName + ", messageId:" + message.getMessageId());
}
//从指定队列中取消息
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 message = messages.remove(0);
log.info("[MemoryDataCenter] 消息已成功从队列中取出 queueName:" + queueName + ", messageId:" + message.getMessageId());
return message;
}
}
//获取指定队列中消息的个数
public int getMessageSize(String queueName){
LinkedList<Message> messages = queueMessageMap.get(queueName);
if(messages == null){
return 0;
}
//LinkedList 线程不安全
synchronized (messages){
return messages.size();
}
}
//添加未被确认消息
public void addMessageWaitAck(String queueName, Message message){
// ConcurrentHashMap<String, Message> messageWaitAck = queueMessageWaitAckMap.get(queueName);
ConcurrentHashMap<String, Message> messageWaitAck = queueMessageWaitAckMap.computeIfAbsent(queueName, k -> new ConcurrentHashMap<>());
synchronized (messageWaitAck){
// if(messageWaitAck == null){
// messageWaitAck = new ConcurrentHashMap<>();
// queueMessageWaitAckMap.put(queueName, messageWaitAck);
// }
messageWaitAck.put(message.getMessageId(), message);
}
log.info("[MemoryDataCenter] 消息进入待确认队列 queueName:" + queueName + ", messageId:" + message.getMessageId());
}
//删除未确认消息(用户确认了消息)
public void deleteMessageWaitAck(String queueName, String messageId){
ConcurrentHashMap<String, Message> messageWaitAck = queueMessageWaitAckMap.get(queueName);
if(messageWaitAck == null){
return;
}
messageWaitAck.remove(messageId);
log.info("[MemoryDataCenter] 消息已从待确认队列中删除 queueName:" + queueName + ", messageId:" + messageId);
}
//从待确认队列中获取指定消息
public Message getMessageWaitAck(String queueName, String messageId){
ConcurrentHashMap<String, Message> messageWaitAck = queueMessageWaitAckMap.get(queueName);
if(messageWaitAck == null){
return null;
}
return messageWaitAck.get(messageId);
}
//从硬盘上恢复数据到内存中
public void recovery(DiskDataCenter diskDataCenter) throws MqException, IOException, ClassNotFoundException {
//1.清空之前的数据
exchangeMap.clear();
queueMap.clear();
bindingMap.clear();
messageMap.clear();
queueMessageMap.clear();
//2.恢复交换机
List<Exchange> exchangeList = diskDataCenter.selectAllExchanges();
for(Exchange exchange : exchangeList){
insertExchange(exchange);
}
//3.恢复队列
List<MSGQueue> queueList = diskDataCenter.selectAllQueues();
for(MSGQueue queue : queueList){
insertQueue(queue);
}
//4.恢复绑定
List<Binding> bindingList = diskDataCenter.selectAllBindings();
for(Binding binding : bindingList){
insertBinding(binding);
}
//5.恢复消息
for(MSGQueue queue : queueList){
LinkedList<Message> messageList = diskDataCenter.loadAllMessageFromQueue(queue.getName());
queueMessageMap.put(queue.getName(), messageList);
for(Message message : messageList){
addMessage(message);
}
}
}
}
在内存中,需要考虑很多多线程的情况。对于add操作,往往会涉及到判空或者判断是否存在,有很大概率出现线程不安全的情况,因此需要加锁,而对于delete、select操作,加锁不一定是很必要的,需要具体问题具体分析。能用线程安全的尽量使用,但也要考虑锁粒度是否会影响效率~
虚拟主机
在虚拟主机中需要实现对内存硬盘的管理,并提供对交换机、队列、绑定、消息管理的核心API,供上层服务器调用(服务器主要处理网络请求,然后调用这边的API实现业务)。使用虚拟主机的目的是为了隔离交换机、队列、绑定、消息,一个服务器上可以有多个虚拟主机,在RabbitMQ中,就提供了对虚拟主机的创建、删除操作,但在本项目中先不实现。
要想实现隔离的效果,我们得知道这些交换机是从属于哪个虚拟主机的。不·难发现,一个虚拟机可以对应多个交换机,而一个交换机只能对应一个虚拟主机,这是一个"一对多"的关系。
方案一:参照数据库中“一对多”的关系来设计,给交换机增加一个属性,表示从属的虚拟主机;
方案二:给交换机增加一个前缀名,即为虚拟主机的名字,然后可以根据前缀知道从属的虚拟主机;
方案三:为每个虚拟主机单独分配数据库和文件,从物理上真正实现隔离。
此处选择的是方案二。
先来对交换机、队列、绑定的增删.
public class VirtualHost {
private String virtualHostName;
//硬盘数据管理
private DiskDataCenter diskDataCenter = new DiskDataCenter();
//内存数据管理
private MemoryDataCenter memoryDataCenter = new MemoryDataCenter();
private Router router = new Router();
private final Object exchangeLocker = new Object();
private final Object queueLocker = new Object();
public VirtualHost(String name){
//需要对数据进行初始化(数据库建库建表操作)
this.virtualHostName = name;
diskDataCenter.init();
//将硬盘上的数据恢复到内存中
try {
memoryDataCenter.recovery(diskDataCenter);
} catch (MqException | IOException | ClassNotFoundException e) {
log.error("[VirtualHost] 恢复内存数据失败");
e.printStackTrace();
}
}
//创建交换
public boolean exchangeDeclare(String exchangeName, ExchangeType exchangeType, boolean durable,
boolean autoDelete, Map<String, Object> arguments) {
//为了进行虚拟主机的隔离,此处采用在交换机的前缀加上虚拟主机名的方式
exchangeName = virtualHostName + "_" + exchangeName;
try{
synchronized (exchangeLocker){
//1.判断交换机是否存在
Exchange exsitsExchange = memoryDataCenter.getExchange(exchangeName);
if(exsitsExchange != null){
throw new MqException("[VirtualHost] 交换机已经存在 exchangeName:" + exchangeName);
}
//2.创建交换机
Exchange exchange = new Exchange();
exchange.setName(exchangeName);
exchange.setType(exchangeType);
exchange.setDurable(durable);
exchange.setAutoDelete(autoDelete);
exchange.setArguments(arguments);
//3.将交换机写入硬盘
if(durable){
diskDataCenter.insertExchange(exchange);
}
//4.将交换机写入内存
memoryDataCenter.insertExchange(exchange);
log.info("[VirtualHost] 交换机创建成功 exchangeName:" + exchangeName);
return true;
}
} catch (Exception e){
log.error("[VirtualHost] 交换机创建失败 exchangeName:" + exchangeName);
e.printStackTrace();
return false;
}
}
//删除交换机
public boolean exchangeDelete(String exchangeName){
exchangeName = virtualHostName + "_" + exchangeName;
try{
synchronized (exchangeLocker){
//1.判断交换机是否存在
Exchange existsExchange = memoryDataCenter.getExchange(exchangeName);
if(existsExchange == null){
throw new MqException("[VirtualHost] 交换机不存在 exchangeName:" + exchangeName);
}
//2.将交换机从硬盘上删除
//此处不要取反,isDurable 表示 是否存在硬盘上
//如果对象存储在硬盘上才要删除,没有存在硬盘上就不用
if(existsExchange.isDurable()){
diskDataCenter.deleteExchange(exchangeName);
}
//3.将交换机从内存中删除
memoryDataCenter.insertExchange(existsExchange);
log.info("[VirtualHost] 交换机删除成功 exchangeName:" + exchangeName);
}
return true;
} catch (Exception e){
log.error("[VirtualHost] 交换机删除失败 exchangeName:" + exchangeName);
e.printStackTrace();
return false;
}
}
//创建队列
public boolean queueDeclare(String queueName, boolean exclusive, boolean durable, boolean autoDelete,
Map<String, Object> arguments){
queueName = virtualHostName + "_" + queueName;
try{
synchronized (queueLocker){
MSGQueue existsQueue = memoryDataCenter.getQueue(queueName);
if(existsQueue != null){
throw new MqException("[VirtualHost] 队列已经存在 queueName:" + queueName);
}
//创建队列
MSGQueue queue = new MSGQueue();
queue.setName(queueName);
queue.setDurable(durable);
queue.setExclusive(exclusive);
queue.setAutoDelete(autoDelete);
queue.setArguments(arguments);
//写入硬盘
if(durable){
diskDataCenter.insertQueue(queue);
}
//写入内存
memoryDataCenter.insertQueue(queue);
log.info("[VirtualHost] 队列创建成功 queueName:" + queueName);
}
return true;
} catch (Exception e){
log.error("[VirtualHost] 队列创建失败 queueName:" + queueName);
e.printStackTrace();
return false;
}
}
//删除队列
public boolean queueDelete(String queueName){
queueName = virtualHostName + "_" + queueName;
try{
synchronized (queueLocker){
MSGQueue existsQueue = memoryDataCenter.getQueue(queueName);
if(existsQueue == null){
throw new MqException("[VirtualHost] 队列不存在 queueName:" + queueName);
}
if(existsQueue.isDurable()){
diskDataCenter.deleteQueue(queueName);
}
memoryDataCenter.deleteQueue(queueName);
log.error("[VirtualHost] 队列删除成功 queueName:" + queueName);
return true;
}
}catch (Exception e){
log.error("[VirtualHost] 队列删除失败 queueName:" + queueName);
e.printStackTrace();
return false;
}
}
//添加绑定关系
public boolean queueBind(String exchangeName, String queueName, String bindingKey){
exchangeName = virtualHostName + "_" + exchangeName;
queueName = virtualHostName + "_" + queueName;
try{
synchronized (exchangeLocker){
synchronized (queueLocker){
//1.验证转发规则
//等下实现~
if(!router.checkBindingKey(bindingKey)){
throw new MqException("[VirtualHost] bindingKey非法 bindingKey:" + bindingKey);
}
//2.判断绑定关系是否已经存在
Binding existsBinding = memoryDataCenter.getBinding(exchangeName, queueName);
if(existsBinding != null){
throw new MqException("[VirtualHost] 绑定已经存在 exchangeName:" + exchangeName + ", queueName:" + queueName);
}
//3.判断交换机和队列是否都存在
Exchange existsExchange = memoryDataCenter.getExchange(exchangeName);
MSGQueue existsQueue = memoryDataCenter.getQueue(queueName);
if(existsExchange == null){
throw new MqException("[VirtualHost] 交换机不存在 exchangeName:" + exchangeName);
}
if(existsQueue == null){
throw new MqException("[VirtualHost] 队列不存在 queueName:" + queueName);
}
//4.创建绑定
Binding binding = new Binding();
binding.setExchangeName(exchangeName);
binding.setQueueName(queueName);
binding.setBindingKey(bindingKey);
//5.将绑定写入硬盘
diskDataCenter.insertBinding(binding);
//6.将绑定写入内存
memoryDataCenter.insertBinding(binding);
log.info("[VirtualHost] 绑定添加成功 exchangeName:" + exchangeName + ", queueName:" + queueName);
}
}
return true;
} catch (Exception e){
log.error("[VirtualHost] 绑定创建失败 exchangeName:" + exchangeName + ", queueName:" + queueName);
e.printStackTrace();
}
return false;
}
//解除绑定
public boolean queueUnbind(String exchangeName, String queueName){
exchangeName = virtualHostName + "_" + exchangeName;
queueName = virtualHostName + "_" + queueName;
try{
synchronized (exchangeLocker){
synchronized (queueLocker){
//检查是否有这个绑定
Binding existsBinding = memoryDataCenter.getBinding(exchangeName, queueName);
if(existsBinding == null){
throw new MqException("[VirtualHost] 绑定不存在 exchangeName:" + exchangeName + ", queueName:" + queueName);
}
//从硬盘中删除绑定关系
diskDataCenter.deleteBinding(existsBinding);
//从内存中删除绑定关系
memoryDataCenter.deleteBinding(existsBinding);
log.info("[VirtualHost] 绑定解除成功 exchangeName:" + exchangeName + ", queueName:" + queueName);
}
}
return true;
} catch (Exception e){
log.error("[VirtualHost] 绑定解除失败 exchangeName:" + exchangeName + ", queueName:" + queueName);
e.printStackTrace();
return false;
}
}
虽然我们在MemoryDataCenter中已经进行过了加锁操作,但是现在又再VirtualHost中又进行了加锁,是否之前的锁就没有意义了呢?
当然不是,因为无法事先知道什么时候、在哪去调用这些API,如果一个其他线程不安全的类去调用MemoryDataCenter中的一些API,就会引发线程安全问题。
在VirtualHost中定义了一把 交换机锁、 一把队列锁,这些锁的力度比较大,当一个线程去操作交换机A,此时另一个线程即使操作的是交换机B,也会进行阻塞,这会大大降低程序效率。不过影响不是很大,因为上述的这些操作都属于是低频操作,消息队列主要还是消息的转发~
上述实现了一些辅助消息交换转发的API,现在要来实现消息队列中最核心的两个API:发布消息 basicPublish 和 订阅消息 basicConsume.
basicPublish发布消息
发布消息是指生产者将一个消息发送到指定交换机上,然后交换机根据自身的类型,进行消息的转发,发送到与之绑定的队列中。
交换机有3种类型(本项目中实现了3种):
1.Dircet 直接交换机,消息中的routinKey就是队列名,直接将消息发送给队列即可.
2.Fanout 扇出交换机,消息会发送到所有与之绑定的队列中,此时消息中的routingKey无效.
3.Topic 主题交换机,消息会发送给所有与之绑定且binding中的bindingKey 与 消息中的 rouingKey 匹配的队列.
首先我们先来实现路由转发的规则,大致包含如下几个API:checkRoutingKey、checkBindingKey、route.
Router:
这个类用来实现路由转发的规则
*/
public class Router {
//规定:
//bindingKey 由 数字,字母,下划线组成,通过.来分隔,允许通配符*,#存在
//形如: 123.abc.*._aa.#
public boolean checkBindingKey(String bindingKey) {
for (int i = 0; i < bindingKey.length(); i++) {
char ch = bindingKey.charAt(i);
if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z')) {
continue;
}
if (ch == '.' || ch == '_' || ch == '*' || ch == '#') {
continue;
}
return false;
}
//这里处理通配符不合法的情况
//在正则表达式中需要对.进行转义 -> \.
//而 \ 也需要进行转义,再添加一个 \
String[] words = bindingKey.split("\\.");
for (int i = 0; i < words.length; i++) {
String word = words[i];
if (word.length() > 1 && (word.contains("*") || word.contains("#"))) {
return false;
}
if (word.equals("#")) {
if (i + 1 < words.length && (words[i + 1].equals("#") || words[i + 1].equals("*"))) {
return false;
}
}
}
return true;
}
//规定:
//routingKey 由 数字,字母,下划线组成,通过.来分隔
//形如: 123.abc._123
public boolean checkRoutingKey(String routingKey) {
for (int i = 0; i < routingKey.length(); i++) {
char ch = routingKey.charAt(i);
if ((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z')) {
continue;
}
if (ch == '.' || ch == '_') {
continue;
}
return false;
}
return true;
}
//进行路由
public boolean route(ExchangeType exchangeType, String routingKey, String bindingKey) {
if (exchangeType == ExchangeType.FANOUT) {
return true;
}
if (exchangeType == ExchangeType.TOPIC) {
if (routeTopic(routingKey, bindingKey)) {
return true;
}
}
return false;
}
//主题交换机的路由
//*可以代表两个.之间的任意的字符串
//#可以代表0个或多个*
private boolean routeTopic(String routingKey, String bindingKey) {
String[] routingTokens = routingKey.split("\\.");
String[] bindingTokens = bindingKey.split("\\.");
int routingIndex = 0;
int bindingIndex = 0;
while(routingIndex < routingTokens.length && bindingIndex < bindingTokens.length){
if(bindingTokens[bindingIndex].equals("#")){
if(bindingIndex == bindingTokens.length - 1){
return true;
}else {
bindingIndex++;
for(int i = routingIndex; i < routingTokens.length; i++){
if(bindingTokens[bindingIndex].equals(routingTokens[routingIndex])){
break;
}
}
if(routingIndex == routingTokens.length){
return false;
}
}
}else{
if(!routingTokens[routingIndex].equals(bindingTokens[bindingIndex])
&& !bindingTokens[bindingIndex].equals("*")){
return false;
}
}
routingIndex++;
bindingIndex++;
}
if(routingIndex == routingTokens.length && bindingIndex == bindingTokens.length){
return true;
}
return false;
}
}
上述routingKey、bindingKey、路由规则 都是源自于AMQP协议
接下来实现消息的发布。
basicPublish:
//向指定交换机发送消息,并根据交换机的转发规则,向队列发送消息
public boolean basicPublish(String exchangeName, String routingKey, BasicProperties basicProperties, byte[] body){
exchangeName = virtualHostName + "_" + exchangeName;
try{
//1.判断交换机是否存在
Exchange exchange = memoryDataCenter.getExchange(exchangeName);
if(exchange == null){
throw new MqException("[VirtualHost] 交换机不存在 exchangeName:" + exchangeName);
}
//2.判断routingKey是否合法
if(!router.checkRoutingKey(routingKey)){
throw new MqException("[VirtualHost] routingKey非法 routingKey:" + routingKey);
}
//3.检查交换机类型
if(exchange.getType() == ExchangeType.DIRECT){
//直接交换机, routingKey为queueName
//4.查找指定队列
String queueName = virtualHostName + "_" + routingKey;
MSGQueue queue = memoryDataCenter.getQueue(queueName);
if(queue == null){
throw new MqException("[VirtualHost] 队列不存在 queueName:" + queueName);
}
//5.构造message对象
Message message = Message.createMessageWithId(basicProperties, routingKey, body);
//6.发送消息到指定队列
sendMessage(queue, message);
}else{
//4.获取到所有绑定关系
ConcurrentHashMap<String, Binding> bindings = memoryDataCenter.getBindings(exchangeName);
for(Map.Entry<String, Binding> entry : bindings.entrySet()){
Binding binding = entry.getValue();
//5.查找指定队列
String queueName = virtualHostName + "_" + binding.getQueueName();
MSGQueue queue = memoryDataCenter.getQueue(queueName);
if(queue == null){
//如果当前不存在,还需要检查其他的队列是否存在
continue;
}
//6.检查路由规则:
// fanout, 则该交换机绑定的所有队列都要转发
// topic, 则需要判断routingKey和bindingKey是否能对上
if(!router.route(exchange.getType(), routingKey, binding.getBindingKey())){
continue;
}
//7.构造message对象
Message message = Message.createMessageWithId(basicProperties, routingKey, body);
//8.发送消息到指定队列
sendMessage(queue, message);
}
log.info("[VirtualHost] 发送消息成功");
}
return true;
} catch (Exception e){
log.error("[VirtualHost] 发送消息失败 exchangeName: " + exchangeName);
e.printStackTrace();
return false;
}
}
basicConsume订阅消息
订阅消息是指消费者绑定一个队列,后续队列中有消息就自动的推送过来。
//订阅消息
//添加一个队列订阅者,当队列收到消息,就把消息推送给订阅者
//consumerTag: 消费者的唯一标识
//autoAck: 是否自动应答
//consumer: 回调函数,并写成了函数式接口,描述了消费者的行为
public boolean basicConsume(String consumerTag, String queueName, boolean autoAck, Consumer consumer){
queueName = virtualHostName + "_" + queueName;
try{
consumerManager.addConsumer(consumerTag, queueName, autoAck, consumer);
log.info("[VirtualHost] 订阅消息成功 queueName:" + consumerTag + ", queueName:" + queueName);
return true;
}catch (Exception e){
log.error("[VirtualHost] 订阅消息失败 consumerTag:" + consumerTag + ", queueName:" + queueName);
e.printStackTrace();
return false;
}
}
接下来实现ConsumerManager中的API:notifyConsumer 提醒消费者消费消息,addConsumer 添加消费者,consumeMessage 消费消息。
首先我们需要描述一下消费者
@Data
//描述消费者的参数
public class ConsumerEnv {
private String consumerTag;
private String queueName;
private boolean autoAck;
//通过这个回调来处理收到的消息
private Consumer consumer;
public ConsumerEnv() {
}
public ConsumerEnv(String consumerTag, String queueName, boolean autoAck, Consumer consumer) {
this.consumerTag = consumerTag;
this.queueName = queueName;
this.autoAck = autoAck;
this.consumer = consumer;
}
}
@FunctionalInterface
public interface Consumer {
//把消息推送给消费者
void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException;
}
在生产者和消费者的交互逻辑大致有两种:
1.生产者先调用basicPublish发布消息到了某个队列,此时还没有消费者订阅这个队列,后续有消费者订阅了,直接消费消息即可
2.消费者先订阅了一个队列,但生产者还没有生产消息到队列中,后续生产者生产完消息后,提醒消费者去队列中消费消息即可
由于一个队列可能有多个消费者订阅,我们需要使用一个数据结构来存储,并且还需要制定一个算法,实现将消息能给发送给多个消费者。此处采用轮询的方式,即类似取队列元素的方式。
需要在MSGQueue中添加一个字段,用来存储这个队列中的订阅者,并提供相应的取出某一个订阅者的API
@Data
public class MSGQueue {
private String name;
//是否独占(只能被一个消费者使用)
private boolean exclusive = false;
//是否可持久化
private boolean durable = false;
//是否自动删除
private boolean autoDelete = false;
//可选参数
private Map<String, Object> arguments = new HashMap<>();
//订阅者
private LinkedList<ConsumerEnv> consumerEnvs = new LinkedList<>();
//轮询序号
private AtomicInteger consumerSeq = new AtomicInteger(0);
public void addConsumerEnv(ConsumerEnv consumerEnv){
consumerEnvs.add(consumerEnv);
}
public ConsumerEnv chooseConsumer(){
if(consumerEnvs.size() == 0){
return null;
}
int index = consumerSeq.get() % consumerEnvs.size();
consumerSeq.getAndIncrement();
return consumerEnvs.get(index);
}
}
程序如何知道有新的消息来呢?通过一个扫描线程扫描队列,然后再将这个消息提交到线程池中,让线程池去执行服务器端的回调函数。
如果使用一个扫描线程,去扫描所有队列的话,可以去遍历存储队列对象的哈希表,理论上行,但是不够优雅,如果对每一个队列都使用一个线程的话,会开辟很多线程,也会影响效率。在这使用一个阻塞队列,当有一个新的消息添加到队列中,会将这个队列名插入到这个阻塞队列中,后续只要根据这个阻塞队列,去取数据然后处理即可。
@Slf4j
/*
* 通过这个类来实现消费消息
* 每当有消息到队列的时候,就将队列名(令牌)添加到令牌队列中,线程池再根据这个队列进行处理
*/
public class ConsumerManager {
//方便操作数据
private VirtualHost parent;
//使用线程池,用来处理回调任务
private ExecutorService workPool = Executors.newFixedThreadPool(4);
//每当队列中来了消息,就触发这个令牌队列,通知可以去消费消息
private BlockingDeque<String> tokenQueue = new LinkedBlockingDeque<>();
//扫描线程,扫描令牌队列中是否有新令牌
private Thread scanThread = null;
public ConsumerManager(VirtualHost p){
this.parent = p;
//创建扫描线程
scanThread = new Thread(() ->{
while(true){
try {
//获取令牌
String tokenQueueName = tokenQueue.take();
//根据令牌找到指定队列
MSGQueue queue = parent.getMemoryDataCenter().getQueue(tokenQueueName);
if(queue == null){
throw new MqException("[ConsumerManager] 令牌队列不存在 queueName:" + tokenQueueName);
}
//从队列中消费消息
synchronized (queue){
consumeMessage(queue);
}
} catch (InterruptedException | MqException e) {
e.printStackTrace();
}
}
});
//设置为后台线程
scanThread.setDaemon(true);
scanThread.start();
}
//通知消费消息
public void notifyConsumer(String queueName) throws InterruptedException {
tokenQueue.put(queueName);
}
//添加订阅者
public void addConsumer(String consumerTag, String queueName, boolean autoAck, Consumer consumer){
MSGQueue queue = parent.getMemoryDataCenter().getQueue(queueName);
try{
if(queue == null){
throw new MqException("[ConsumerManager] 队列不存在 queueName:" + queueName);
}
synchronized (queue){
//添加消费者
queue.addConsumerEnv(new ConsumerEnv(consumerTag, queueName, autoAck, consumer));
//如果有消息就进行消费
int n = parent.getMemoryDataCenter().getMessageSize(queueName);
for(int i = 0; i < n; i++){
//消费
consumeMessage(queue);
}
}
}catch (Exception e){
log.error("[ConsumerManager] 添加订阅者失败");
e.printStackTrace();
}
}
//消费消息
private void consumeMessage(MSGQueue queue){
//1.通过轮询方式,寻找消费者
ConsumerEnv luckDog = queue.chooseConsumer();
if(luckDog == null){
return;
}
//2.获取队列中的消息
Message message = parent.getMemoryDataCenter().pollMessage(queue.getName());
if(message == null){
return;
}
//3.把消息带入到回调方法,交给线程池处理
workPool.submit(() -> {
try{
//1.把消息添加到待确认队列中
parent.getMemoryDataCenter().addMessageWaitAck(queue.getName(), message);
//2.执行回调操作,消费消息
luckDog.getConsumer().handleDelivery(luckDog.getConsumerTag(), message.getBasicProperties(), message.getBody());
//3.如果当前是自动应答.则将消息删除
// 如果是手动应答,则先不处理,让消费者后续调用 basicAck 处理
if(luckDog.isAutoAck()){
//删除消息从两个方面进行,内存和硬盘
//硬盘
if(message.isDurable()){
parent.getDiskDataCenter().deleteMessage(queue, message);
}
//内存
parent.getMemoryDataCenter().deleteMessageWaitAck(queue.getName(), message.getMessageId());
parent.getMemoryDataCenter().removeMessage(message.getMessageId());
log.info("[ConsumerManager] 消息被成功消费 queueName:" + queue.getName());
}
}catch (Exception e){
log.info("[ConsumerManager] 消息消费失败 queueName:" + queue.getName());
e.printStackTrace();
}
});
}
}
最后还差一个basicAck手动应答,即消费者消费完消息,然后告诉服务器。
//手动应答
public boolean basicAck(String queueName, String messageId){
queueName = virtualHostName + "_" + queueName;
try{
MSGQueue queue = memoryDataCenter.getQueue(queueName);
if(queue == null){
throw new MqException("[VirtualHost] 要确认的队列不存在 queueName:" + queueName);
}
Message message = memoryDataCenter.getMessage(messageId);
if(message == null){
throw new MqException("[VirtualHost] 要确认的消息不存在 messageId:" + messageId);
}
if(message.isDurable()){
diskDataCenter.deleteMessage(queue, message);
}
memoryDataCenter.deleteMessageWaitAck(queueName, messageId);
memoryDataCenter.removeMessage(messageId);
log.info("[VirtualHost] 消息确认收到 queueName:" + queueName);
return true;
}catch (Exception e){
log.info("[VirtualHost] 消息确认失败 queueName:" + queueName);
e.printStackTrace();
return false;
}
}
约定应用层协议
在网络通信中,我们需要约定协议,方便对方接收并读取。在这里使用HTTP协议和其他一些不是很妥,于是我们在TCP协议的基础上自定义实现应用层协议。
协议格式:
类型用来描述,这次请求/响应 是干啥的,例如:0x03 表示 这个请求是创建exchange.
由于进行网络通信也是使用的二进制,为了防止出现粘包等问题,使用长度来分隔.
对于请求,载荷中保存的是序列化的方法的参数,对于响应,载荷中保存的是序列化的方法的返回值.
我们自定义的应用层协议是建立在TCP协议的基础上,而每一次TCP协议都为了可靠性会进行三次握手、四次挥手,如果频繁的创建的话,会比较费时,在这我们使用多信道的方式,复用每一次TCP连接,即TCP连接是一直保持的,但是断开是逻辑上的断开。
约定类型:
类型 | 方法 |
---|---|
0x01 | 创建 channel |
0x02 | 关闭 channel |
0x03 | 创建 exchange |
0x04 | 销毁 exchange |
0x05 | 创建 queue |
0x06 | 销毁 queue |
0x07 | 创建 binding |
0x08 | 销毁 binding |
0x09 | 发送 message |
0x0a | 订阅 queue |
0x0b | 确认应答 |
0x0c | 服务器给客户端推送的消息 |
创建载荷类
为了方便后续代码编写,使用两个类来描述网络通信:request,response
Request:
//表示网络通信中的请求对象
//在此约定:
//一个请求由三部分组成:类型+长度+载荷
//在请求中,载荷为调用方法的参数
@Data
public class Request {
private int type;
private int length;
private byte[] payload;
}
Response:
//表示网络通信中的响应对象
//在响应中,载荷为返回值
@Data
public class Response {
private int type;
private int length;
private byte[] payload;
}
此处虽然两个类中的成员都是一样的,但是为了规范,还是分开来写。
对于请求,载荷中的内容是序列化的 参数值,在这也是用类来描述.
由于我们采用的是多信道复用的方式,在一次TCP通信中,可能会产生多个信道正在与消息队列进行交互,就有可能会出现多个信道通过一个socket发送多个请求,然后多个响应一起通过socket发送到客户端这边,如何进行区分?
针对请求和响应进行标识,即添加一个rid,标识这次请求是哪一个,响应又是针对哪个请求的.
针对多信道使用channelId,标识这次是哪个信息正在通信.
通过上述分析,对于每一次请求都需要带上这两个属性,因此可以针对这两个属性进行提取封装到一个类中.
BasicArguments:
//提取参数中公有的参数
@Data
public class BasicArguments implements Serializable {
//请求的唯一标识,用来和响应对上
private String rid;
//为了节省网络通信开销,采用通过逻辑上的方式表示此次连接
private String channelId;
}
@Data
public class BasicReturns implements Serializable {
private String rid;
private String channelId;
private boolean ok;
}
各种Arguments:
//与前面ExchangeDeclare方法中的参数保持一致
@Data
public class ExchangeDeclareArguments extends BasicArguments implements Serializable {
private String exchangeName;
private ExchangeType exchangeType;
private boolean durable;
private boolean autoDelete;
private Map<String, Object> arguments;
}
@Data
public class ExchangeDeleteArguments extends BasicArguments implements Serializable {
private String exchangeName;
}
@Data
public class QueueDeclareArguments extends BasicArguments implements Serializable {
private String queueName;
private boolean durable;
private boolean exclusive;
private boolean autoDelete;
private Map<String, Object> arguments;
}
@Data
public class QueueDeleteArguments extends BasicArguments implements Serializable {
private String queueName;
}
@Data
public class QueueBindArguments extends BasicArguments implements Serializable {
private String exchangeName;
private String queueName;
private String bindingKey;
}
@Data
public class QueueUnbindArguments extends BasicArguments implements Serializable {
private String queueName;
private String exchangeName;
}
@Data
public class BasicPublishArguments extends BasicArguments implements Serializable {
private String exchangeName;
private String routingKey;
private BasicProperties basicProperties;
private byte[] body;
}
@Data
public class BasicConsumeArguments extends BasicArguments implements Serializable {
private String consumeTag;
private String queueName;
private boolean autoAck;
}
@Data
public class BasicAckArguments extends BasicArguments implements Serializable {
private String queueName;
private String messageId;
}
客户端
在客户端中我们需要实现三个类:ConnectionFactory、Connection、Channel.
ConnectionFactory: 这个是连接工厂类,用来创建Connection的。客户端通过这个类创建Connection.
Connection: 连接类,表示一个TCP连接,在这需要将用户的请求发送,以及针对服务器的响应返还给对应信道中的对应请求.
Channel: 信道类,复用TCP连接,表示逻辑上的通信,在这个类中需要提供与服务器对应的API,用户调用这些API,通过connection中的socket,将请求发送给服务器.
ConnectionFactory:
@Data
public class ConnectionFactory {
private String host;
private int port;
public Connection newConnection() throws IOException {
Connection connection = new Connection(host, port);
return connection;
}
}
对于Connection类来说,相对复杂一点。
首先一个连接可以有多个信道,我们需要一个数据结构来管理这些信道。
其次我们还需要一个线程池来处理客户端这边的回调函数,这个回调函数不同于服务器上的回调函数,这个回调函数处理的是用户订阅消息,要对这个消息进行的操作,而服务器上的回调函数处理的是将消息返回给客户端这边,又因为每一次客户端这边对于消息的回调可能比较复杂,会比较耗时,因此就需要使用一个线程池来处理,这样就不会阻塞等待。
@Slf4j
public class Connection {
//持有的与服务器通信的socket
private Socket socket;
//网络通信的输入输出流
private InputStream inputStream;
private OutputStream outputStream;
//方便操作二进制的输入输出流
private DataInputStream dataInputStream;
private DataOutputStream dataOutputStream;
//管理多个channel
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);
// 创建一个扫描线程, 由这个线程负责不停的从 socket 中读取响应数据. 把这个响应数据再交给对应的 channel 负责处理.
Thread t = new Thread(() -> {
try {
while (!socket.isClosed()) {
//读取响应
Response response = readResponse();
//处理相应
dispatchResponse(response);
}
} catch (SocketException e) {
// 连接正常断开的. 此时这个异常直接忽略.
log.info("[Connection] 连接正常断开!");
} catch (IOException | ClassNotFoundException | MqException e) {
log.info("[Connection] 连接异常断开!");
e.printStackTrace();
}
});
t.start();
}
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);
return response;
}
public Channel createChannel() throws IOException {
// 使⽤ UUID ⽣产 channelId,
String channelId = "C_" + UUID.randomUUID().toString();
Channel channel = new Channel(channelId, this);
// 这⾥需要先把 channel 键值对放到 Map 中.
channelMap.put(channelId, channel);
//告诉服务器创建channel
boolean ok = channel.createChannel();
if (!ok) {
// 服务器返回创建 channel 失败!
// 把 channelId 删除掉即可
channelMap.remove(channelId);
return null;
}
return channel;
}
public void close() {
// 关闭 Connection 释放上述资源
try {
callbackPool.shutdownNow();
channelMap.clear();
inputStream.close();
outputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在前面客户端实现的是消息自动推送push的方式,在这使用一个类描述服务器推送的消息.
@Data
public class SubscribeReturns extends BasicReturns implements Serializable {
private String consumerTag;
private BasicProperties basicProperties;
private byte[] body;
}
在connection增加一个方法来处理多个响应.
// 使用这个方法来分别处理, 当前的响应是一个针对控制请求的响应, 还是服务器推送的消息.
private void dispatchResponse(Response response) throws IOException, ClassNotFoundException, MqException {
if (response.getType() == 0xc) {
// 服务器推送来的消息数据
SubscribeReturns subscribeReturns = (SubscribeReturns) BinaryTool.fromBytes(response.getPayload());
// 根据 channelId 找到对应的 channel 对象
//log.info("channelId:" + subscribeReturns.getChannelId());
Channel channel = channelMap.get(subscribeReturns.getChannelId());
if (channel == null) {
throw new MqException("[Connection] 该消息对应的 channel 在客户端中不存在! channelId=" + channel.getChannelId());
}
// 执行该 channel 对象内部的回调.
callbackPool.submit(() -> {
try {
channel.getConsumer().handleDelivery(subscribeReturns.getConsumerTag(), subscribeReturns.getBasicProperties(),
subscribeReturns.getBody());
} catch (MqException | IOException e) {
e.printStackTrace();
}
});
} else {
// 当前响应是针对刚才的控制请求的响应
BasicReturns basicReturns = (BasicReturns) BinaryTool.fromBytes(response.getPayload());
// 把这个结果放到对应的 channel 的 hash 表中.
Channel channel = channelMap.get(basicReturns.getChannelId());
if (channel == null) {
throw new MqException("[Connection] 该消息对应的 channel 在客户端中不存在! channelId=" + channel.getChannelId());
}
channel.putReturns(basicReturns);
}
}
Channel:
在一次逻辑连接中,也会发送多条数据,我们也需要使用一个数据结构进行组织管理。
大致交互如下:在一次逻辑连接中,发送了多个请求到服务器,发完之后会阻塞等待,然后服务器将响应发送给connection,再告诉响应channel,此时多个阻塞等待的线程,就会去查看当前响应是否为对应的rid,如果是就进行处理,不是就继续阻塞等待。
@Data
public class Channel {
private String channelId;
// 当前这个 channel 属于哪个连接.
private Connection connection;
// 用来存储后续客户端收到的服务器的响应.
private ConcurrentHashMap<String, BasicReturns> basicReturnsMap = new ConcurrentHashMap<>();
// 如果当前 Channel 订阅了某个队列, 就需要在此处记录下对应回调是啥. 当该队列的消息返回回来的时候, 调用回调.
// 此处约定一个 Channel 中只能有一个回调.
private Consumer consumer = null;
public Channel(String channelId, Connection connection) {
this.channelId = channelId;
this.connection = connection;
}
// 在这个方法中, 和服务器进行交互, 告知服务器, 此处客户端创建了新的 channel 了.
public boolean createChannel() throws IOException {
// 对于创建 Channel 操作来说, payload 就是一个 basicArguments 对象
BasicArguments basicArguments = new BasicArguments();
basicArguments.setChannelId(channelId);
basicArguments.setRid(generateRid());
byte[] payload = BinaryTool.toBytes(basicArguments);
Request request = new Request();
request.setType(0x1);
request.setLength(payload.length);
request.setPayload(payload);
// 构造出完整请求之后, 就可以发送这个请求了.
connection.writeRequest(request);
// 等待服务器的响应
BasicReturns basicReturns = waitResult(basicArguments.getRid());
return basicReturns.isOk();
}
private String generateRid() {
return "R_" + UUID.randomUUID().toString();
}
// 期望使用这个方法来阻塞等待服务器的响应.
private BasicReturns waitResult(String rid) {
BasicReturns basicReturns = null;
while ((basicReturns = basicReturnsMap.get(rid)) == null) {
// 如果查询结果为 null, 说明包裹还没回来.
// 此时就需要阻塞等待.
synchronized (this) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 读取成功之后, 还需要把这个消息从哈希表中删除掉.
basicReturnsMap.remove(rid);
return basicReturns;
}
public void putReturns(BasicReturns basicReturns) {
basicReturnsMap.put(basicReturns.getRid(), basicReturns);
synchronized (this) {
// 当前也不知道有多少个线程在等待上述的这个响应.
// 把所有的等待的线程都唤醒.
notifyAll();
}
}
// 关闭 channel, 给服务器发送一个 type = 0x2 的请求
public boolean close() throws IOException {
BasicArguments basicArguments = new BasicArguments();
basicArguments.setRid(generateRid());
basicArguments.setChannelId(channelId);
byte[] payload = BinaryTool.toBytes(basicArguments);
Request request = new Request();
request.setType(0x2);
request.setLength(payload.length);
request.setPayload(payload);
connection.writeRequest(request);
BasicReturns basicReturns = waitResult(basicArguments.getRid());
return basicReturns.isOk();
}
// 创建交换机
public boolean exchangeDeclare(String exchangeName, ExchangeType exchangeType, boolean durable, boolean autoDelete,
Map<String, Object> arguments) throws IOException {
ExchangeDeclareArguments exchangeDeclareArguments = new ExchangeDeclareArguments();
exchangeDeclareArguments.setRid(generateRid());
exchangeDeclareArguments.setChannelId(channelId);
exchangeDeclareArguments.setExchangeName(exchangeName);
exchangeDeclareArguments.setExchangeType(exchangeType);
exchangeDeclareArguments.setDurable(durable);
exchangeDeclareArguments.setAutoDelete(autoDelete);
exchangeDeclareArguments.setArguments(arguments);
byte[] payload = BinaryTool.toBytes(exchangeDeclareArguments);
Request request = new Request();
request.setType(0x3);
request.setLength(payload.length);
request.setPayload(payload);
connection.writeRequest(request);
BasicReturns basicReturns = waitResult(exchangeDeclareArguments.getRid());
return basicReturns.isOk();
}
// 删除交换机
public boolean exchangeDelete(String exchangeName) throws IOException {
ExchangeDeleteArguments arguments = new ExchangeDeleteArguments();
arguments.setRid(generateRid());
arguments.setChannelId(channelId);
arguments.setExchangeName(exchangeName);
byte[] payload = BinaryTool.toBytes(arguments);
Request request = new Request();
request.setType(0x4);
request.setLength(payload.length);
request.setPayload(payload);
connection.writeRequest(request);
BasicReturns basicReturns = waitResult(arguments.getRid());
return basicReturns.isOk();
}
// 创建队列
public boolean queueDeclare(String queueName, boolean durable, boolean exclusive, boolean autoDelete,
Map<String, Object> arguments) throws IOException {
QueueDeclareArguments queueDeclareArguments = new QueueDeclareArguments();
queueDeclareArguments.setRid(generateRid());
queueDeclareArguments.setChannelId(channelId);
queueDeclareArguments.setQueueName(queueName);
queueDeclareArguments.setDurable(durable);
queueDeclareArguments.setExclusive(exclusive);
queueDeclareArguments.setAutoDelete(autoDelete);
queueDeclareArguments.setArguments(arguments);
byte[] payload = BinaryTool.toBytes(queueDeclareArguments);
Request request = new Request();
request.setType(0x5);
request.setLength(payload.length);
request.setPayload(payload);
connection.writeRequest(request);
BasicReturns basicReturns = waitResult(queueDeclareArguments.getRid());
return basicReturns.isOk();
}
// 删除队列
public boolean queueDelete(String queueName) throws IOException {
QueueDeleteArguments arguments = new QueueDeleteArguments();
arguments.setRid(generateRid());
arguments.setChannelId(channelId);
arguments.setQueueName(queueName);
byte[] payload = BinaryTool.toBytes(arguments);
Request request = new Request();
request.setType(0x6);
request.setLength(payload.length);
request.setPayload(payload);
connection.writeRequest(request);
BasicReturns basicReturns = waitResult(arguments.getRid());
return basicReturns.isOk();
}
// 创建绑定
public boolean queueBind(String queueName, String exchangeName, String bindingKey) throws IOException {
QueueBindArguments arguments = new QueueBindArguments();
arguments.setRid(generateRid());
arguments.setChannelId(channelId);
arguments.setQueueName(queueName);
arguments.setExchangeName(exchangeName);
arguments.setBindingKey(bindingKey);
byte[] payload = BinaryTool.toBytes(arguments);
Request request = new Request();
request.setType(0x7);
request.setLength(payload.length);
request.setPayload(payload);
connection.writeRequest(request);
BasicReturns basicReturns = waitResult(arguments.getRid());
return basicReturns.isOk();
}
// 解除绑定
public boolean queueUnbind(String queueName, String exchangeName) throws IOException {
QueueUnbindArguments arguments = new QueueUnbindArguments();
arguments.setRid(generateRid());
arguments.setChannelId(channelId);
arguments.setQueueName(queueName);
arguments.setExchangeName(exchangeName);
byte[] payload = BinaryTool.toBytes(arguments);
Request request = new Request();
request.setType(0x8);
request.setLength(payload.length);
request.setPayload(payload);
connection.writeRequest(request);
BasicReturns basicReturns = waitResult(arguments.getRid());
return basicReturns.isOk();
}
// 发送消息
public boolean basicPublish(String exchangeName, String routingKey, BasicProperties basicProperties, byte[] body) throws IOException {
//1.封装请求参数
BasicPublishArguments arguments = new BasicPublishArguments();
arguments.setRid(generateRid());
arguments.setChannelId(this.channelId);
arguments.setExchangeName(exchangeName);
arguments.setRoutingKey(routingKey);
arguments.setBasicProperties(basicProperties);
arguments.setBody(body);
//2.序列化参数
byte[] payload = BinaryTool.toBytes(arguments);
//3.构造请求
Request request = new Request();
request.setType(0x9);
request.setLength(payload.length);
request.setPayload(payload);
//4.发起请求
connection.writeRequest(request);
//5.接收返回值
BasicReturns basicReturns = waitResult(arguments.getRid());
return basicReturns.isOk();
}
// 订阅消息
public boolean basicConsume(String queueName, boolean autoAck, Consumer consumer) throws MqException, IOException {
// 先设置回调.
if (this.consumer != null) {
throw new MqException("该 channel 已经设置过消费消息的回调了, 不能重复设置!");
}
this.consumer = consumer;
BasicConsumeArguments arguments = new BasicConsumeArguments();
arguments.setRid(generateRid());
arguments.setChannelId(channelId);
arguments.setConsumeTag(channelId); // 此处 consumerTag 也使用 channelId 来表示了.
arguments.setQueueName(queueName);
arguments.setAutoAck(autoAck);
byte[] payload = BinaryTool.toBytes(arguments);
Request request = new Request();
request.setType(0xa);
request.setLength(payload.length);
request.setPayload(payload);
connection.writeRequest(request);
BasicReturns basicReturns = waitResult(arguments.getRid());
return basicReturns.isOk();
}
// 确认消息
public boolean basicAck(String queueName, String messageId) throws IOException {
BasicAckArguments arguments = new BasicAckArguments();
arguments.setRid(generateRid());
arguments.setChannelId(channelId);
arguments.setQueueName(queueName);
arguments.setMessageId(messageId);
byte[] payload = BinaryTool.toBytes(arguments);
Request request = new Request();
request.setType(0xb);
request.setLength(payload.length);
request.setPayload(payload);
connection.writeRequest(request);
BasicReturns basicReturns = waitResult(arguments.getRid());
return basicReturns.isOk();
}
}
服务器
在服务器这边要做的事情就是管理好多个socket,并针对每一次请求进行响应。
//这个类来表示 消息队列的服务器
//本质上是一个 TCP服务器
@Slf4j
public class BrokerServer {
//表示当前服务器持有的虚拟主机,在此只实现
private VirtualHost virtualHost = new VirtualHost("default");
//用这个哈希表表示所有会话关系
//key 为 channelId, value 为 socket对象
private ConcurrentHashMap<String, Socket> sessions = new ConcurrentHashMap<>();
//引入一个线程池,处理多个用户的请求
private ExecutorService executorService = null;
//控制服务器是否继续运行
private volatile boolean runnable = true;
//表示服务的socket
private ServerSocket serverSocket = null;
//指定服务器的指定端口
public BrokerServer(int port) throws IOException {
serverSocket = new ServerSocket(port);
}
//启动服务器
public void start() throws IOException {
log.info("[BrokerServer] 服务器启动");
executorService = Executors.newCachedThreadPool();
try{
while(runnable){
Socket clientSocket = serverSocket.accept();
executorService.submit(()->{
processConnection(clientSocket);
});
}
} catch (SocketException e) {
log.info("[BrokerServer] 服务器停止运行");
}
}
//关闭服务器
public void stop() throws IOException {
runnable = false;
//立即结束线程池中所有任务
executorService.shutdown();
serverSocket.close();
}
//处理客户端连接
//在一次连接中,可能会需要处理多次请求和响应,使用while循环
private void processConnection(Socket clientSocket) {
try(InputStream inputStream = clientSocket.getInputStream();
OutputStream outputStream = clientSocket.getOutputStream()) {
//读取二进制的请求
try (DataInputStream dataInputStream = new DataInputStream(inputStream);
DataOutputStream dataOutputStream = new DataOutputStream(outputStream)) {
while (true) {
//1.读取请求并解析
Request request = readRequest(dataInputStream);
//2.根据请求计算响应
Response response = process(request, clientSocket);
//3.把响应写回客户端
writeResponse(dataOutputStream, response);
}
}
} catch (EOFException | SocketException e){
//DataInputStream 读取到末尾就会返回EOFException
log.info("[BrokerServer] connection 关闭 客户端的地址: " + clientSocket.getInetAddress().toString()
+ ":" + clientSocket.getPort());
} catch (IOException | ClassNotFoundException | MqException e) {
log.error("[BrokerServer] connection 出现异常!");
} finally {
try {
clientSocket.close();
//清理当前信道
clearClosedSession(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void clearClosedSession(Socket clientSocket) {
// 这里要做的事情, 主要就是遍历上述 sessions hash 表, 把该被关闭的 socket 对应的键值对, 统统删掉.
List<String> toDeleteChannelId = new ArrayList<>();
for (Map.Entry<String, Socket> entry : sessions.entrySet()) {
if (entry.getValue() == clientSocket) {
//集合类不能在遍历的时候删除
toDeleteChannelId.add(entry.getKey());
}
}
for (String channelId : toDeleteChannelId) {
sessions.remove(channelId);
}
}
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 {
BasicArguments basicArguments = (BasicArguments) BinaryTool.fromBytes(request.getPayload());
log.info("[Request] rid=" + basicArguments.getRid() + ", channelId=" + basicArguments.getChannelId()
+ ", type=" + request.getType() + ", length=" + request.getLength());
//根据约定的type,来区分这次请求操作
boolean ok = true;
if(request.getType() == 0x01){
//创建channel
sessions.put(basicArguments.getChannelId(), clientSocket);
}else if(request.getType() == 0x02){
//删除channel
sessions.remove(basicArguments.getChannelId());
}else if(request.getType() == 0x03){
//创建交换机
ExchangeDeclareArguments arguments = (ExchangeDeclareArguments) basicArguments;
ok = virtualHost.exchangeDeclare(arguments.getExchangeName(), arguments.getExchangeType(),
arguments.isDurable(), arguments.isAutoDelete(), arguments.getArguments());
}else if (request.getType() == 0x4) {
//删除交换机
ExchangeDeleteArguments arguments = (ExchangeDeleteArguments) basicArguments;
ok = virtualHost.exchangeDelete(arguments.getExchangeName());
} else if (request.getType() == 0x5) {
//创建队列
QueueDeclareArguments arguments = (QueueDeclareArguments) basicArguments;
ok = virtualHost.queueDeclare(arguments.getQueueName(), arguments.isDurable(),
arguments.isExclusive(), arguments.isAutoDelete(), arguments.getArguments());
} else if (request.getType() == 0x6) {
//删除队列
QueueDeleteArguments arguments = (QueueDeleteArguments) basicArguments;
ok = virtualHost.queueDelete((arguments.getQueueName()));
} else if (request.getType() == 0x7) {
//创建绑定
QueueBindArguments arguments = (QueueBindArguments) basicArguments;
ok = virtualHost.queueBind(arguments.getQueueName(), arguments.getExchangeName(), arguments.getBindingKey());
} else if (request.getType() == 0x8) {
//解除绑定
QueueUnbindArguments arguments = (QueueUnbindArguments) basicArguments;
ok = virtualHost.queueUnbind(arguments.getQueueName(), arguments.getExchangeName());
}else if (request.getType() == 0x9) {
//发布消息
BasicPublishArguments arguments = (BasicPublishArguments) basicArguments;
ok = virtualHost.basicPublish(arguments.getExchangeName(), arguments.getRoutingKey(),
arguments.getBasicProperties(), arguments.getBody());
}else if(request.getType() == 0xa){
BasicConsumeArguments arguments = (BasicConsumeArguments) basicArguments;
ok = virtualHost.basicConsume(arguments.getConsumeTag(), arguments.getQueueName(), arguments.isAutoAck(),
new Consumer() {
//这个回调函数,是让服务器把收到的消息推送给消费者
@Override
public void handleDelivery(String consumerTag, BasicProperties basicProperties, byte[] body) throws MqException, IOException {
Socket clientSocket = sessions.get(consumerTag);
if (clientSocket == null || clientSocket.isClosed()) {
throw new MqException("[BrokerServer] 订阅消息的客户端已经关闭!");
}
//构造响应的数据
SubscribeReturns subscribeReturns = new SubscribeReturns();
subscribeReturns.setChannelId(consumerTag);
subscribeReturns.setRid(""); // 由于这里只有响应, 没有请求, 不需要去对应. rid 暂时不需要.
subscribeReturns.setOk(true);
subscribeReturns.setConsumerTag(consumerTag);
subscribeReturns.setBasicProperties(basicProperties);
subscribeReturns.setBody(body);
byte[] payload = BinaryTool.toBytes(subscribeReturns);
Response response = new Response();
// 0xc 表示服务器给消费者客户端推送的消息数据.
response.setType(0xc);
// response 的 payload 就是一个 SubScribeReturns
response.setLength(payload.length);
response.setPayload(payload);
//此处dataOutputStream 这个对象不能 close
//如果 把 dataOutputStream 关闭, 就会直接把 clientSocket 里的 outputStream 也关了
//后续无法继续网socket中写数据了
DataOutputStream dataOutputStream = new DataOutputStream(clientSocket.getOutputStream());
writeResponse(dataOutputStream, response);
}
});
}else if (request.getType() == 0xb) {
// 调用 basicAck 确认消息.
BasicAckArguments arguments = (BasicAckArguments) basicArguments;
ok = virtualHost.basicAck(arguments.getQueueName(), arguments.getMessageId());
} else {
// 当前的 type 是非法的.
throw new MqException("[BrokerServer] 未知的 type! type=" + request.getType());
}
// 构造响应
BasicReturns basicReturns = new BasicReturns();
basicReturns.setChannelId(basicArguments.getChannelId());
basicReturns.setRid(basicArguments.getRid());
basicReturns.setOk(ok);
byte[] payload = BinaryTool.toBytes(basicReturns);
Response response = new Response();
response.setType(request.getType());
response.setLength(payload.length);
response.setPayload(payload);
log.info("[Response] rid=" + basicReturns.getRid() + ", channelId=" + basicReturns.getChannelId()
+ ", type=" + response.getType() + ", length=" + response.getLength());
return response;
}
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;
}
}
CS交互逻辑
首先服务器通过start()启动,调用serverSocket的accept方法,就会进行阻塞等待客户端的连接。
然后客户端通过ConnectionFactory创建一个Connection与服务器建立起了通信.
客户端调用Connection的API创建一个channel,后续客户端通过channel中的API进行与服务器的交互.
客户端调用channel中exchangeDeclare,然后通过connection中socket持有的IO流,将请求发送给服务器.
服务器解析并计算响应,然后将响应再返回给connection中的socket.
connection调用channel中的方法,唤醒正在阻塞等待的线程进行处理.
测试
@Test
public void testMessageDirect() throws IOException, MqException, InterruptedException{
Connection connection = connectionFactory.newConnection();
Assertions.assertNotNull(connection);
Channel channel = connection.createChannel();
Assertions.assertNotNull(channel);
boolean ok = channel.exchangeDeclare("testExchange", ExchangeType.DIRECT, true, false, null);
Assertions.assertTrue(ok);
ok = channel.queueDeclare("testQueue", true, false, false, null);
Assertions.assertTrue(ok);
byte[] requestBody = "hello".getBytes();
// DIRECT 模式, routingKey 就是队列名字
// 发送的时候 basicProperties 可以是空着的. 服务器会进⾏构造. 订阅者收到的消息则是带
ok = channel.basicPublish("testExchange", "testQueue", null, requestBody);
Assertions.assertTrue(ok);
ok = channel.basicConsume("testQueue", true, new Consumer() {
@Override
public void handleDelivery(String consumerTag, BasicProperties properties, byte[] body) throws MqException, IOException {
System.out.println("[消费数据] 开始!");
System.out.println("consumerTag=" + consumerTag);
System.out.println("properties=" + properties);
Assertions.assertEquals(requestBody, body);
}
});
Assertions.assertTrue(ok);
// 等待数据消费完.
Thread.sleep(500);
channel.close();
connection.close();
}