├── MiniMQ.iml ├── README.md ├── src ├── main │ ├── resources │ │ ├── config.properties │ │ ├── log4j.properties │ │ └── banner.txt │ └── java │ │ └── com │ │ └── singularityfold │ │ ├── MiniMQApplication.java │ │ ├── util │ │ ├── DateUtil.java │ │ ├── TestUtil.java │ │ ├── BannerUtil.java │ │ ├── KeyUtil.java │ │ └── IpUtil.java │ │ ├── core │ │ ├── BasicMaps.java │ │ ├── MessageQueue.java │ │ ├── WorkerManager.java │ │ └── QueueManager.java │ │ ├── pojo │ │ └── Message.java │ │ ├── netIO │ │ ├── ServerInitializer.java │ │ ├── MQServer.java │ │ └── MessageHandler.java │ │ ├── client │ │ ├── Producer.java │ │ ├── Client.java │ │ └── Consumer.java │ │ └── config │ │ └── Config.java └── test │ └── java │ ├── ClientTest.java │ └── UnitTest.java └── pom.xml /MiniMQ.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # minimq 2 | 一个基于netty实现的简易消息队列,具备通配符模式。 3 |
4 | 在本人的知乎中有关于其实现的详细描述 5 | https://zhuanlan.zhihu.com/p/492371420 6 | -------------------------------------------------------------------------------- /src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | queueNum=3 2 | bindingKeys=American.#, China.*.*, UK.*.* 3 | queueNames=American, China, UK -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log4j.rootLogger=INFO, stdout 2 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 3 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 4 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS} [%p] [%t] (%c{1}\:%L) - %m%n 5 | log4j.logger.com.singularityfold=DEBUG -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/MiniMQApplication.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold; 2 | 3 | import com.singularityfold.netIO.MQServer; 4 | 5 | /** 6 | * @author Mr_Hades 7 | * @date 2022-03-26 13:57 8 | */ 9 | public class MiniMQApplication { 10 | public static void main(String[] args) { 11 | // 启动服务器 12 | MQServer.getInstance().start(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ,--. ,--. ,--. ,--. ,--. ,--. ,-----. 2 | | `.' | `--' ,--,--, `--' | `.' | ' .-. ' 3 | | |'.'| | ,--. | \ ,--. | |'.'| | | | | | 4 | | | | | | | | || | | | | | | | ' '-' '-. 5 | `--' `--' `--' `--''--' `--' `--' `--' `-----'--' 6 | 7 | =================== Version 0.1.0 =================== 8 | ================ Powered by Mr_Hades ================ 9 | ================== SingularityFold® ================== -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/util/DateUtil.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.util; 2 | 3 | import java.text.SimpleDateFormat; 4 | import java.util.Date; 5 | 6 | /** 7 | * 日期工具类,用于返回系统当前日期, 8 | * 格式为yyyy-MM-dd HH:mm:ss 9 | * 10 | * @author Mr_Hades 11 | * @date 2021-09-18 10:22 12 | */ 13 | public class DateUtil { 14 | 15 | public static String getLocalTime() { 16 | SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); //设置日期格式 17 | return df.format(new Date()); // new Date()为获取当前系统时间 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/util/TestUtil.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.util; 2 | 3 | import com.singularityfold.netIO.MessageHandler; 4 | 5 | import java.util.Timer; 6 | import java.util.TimerTask; 7 | 8 | /** 9 | * @author Mr_Hades 10 | * @date 2022-04-17 10:48 11 | */ 12 | public class TestUtil { 13 | public static void getUsersPerSecond() { 14 | // 设置定时器输出每秒客户端的数量 15 | Timer timer = new Timer(); 16 | TimerTask timerTask = new TimerTask() { 17 | @Override 18 | public void run() { 19 | int size = MessageHandler.clients.size(); 20 | if (size != 0) 21 | System.out.println("current channel number: " + size); 22 | } 23 | }; 24 | timer.schedule(timerTask, 1000L, 1000L); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/core/BasicMaps.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.core; 2 | 3 | import com.singularityfold.netIO.MessageHandler; 4 | import io.netty.channel.ChannelId; 5 | import io.netty.channel.group.ChannelGroup; 6 | 7 | import java.util.List; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | /** 11 | * 用来管理消息队列中的基础映射关系 12 | * 1. producer的channelId与自己的键,方便netty的nio线程将消息放到对应的队列中 13 | * 2. 队列的序号与channelId,方便分发线程进行转发到对应的订阅consumer 14 | * 15 | * @author Mr_Hades 16 | * @date 2022-03-26 11:37 17 | */ 18 | public class BasicMaps { 19 | 20 | 21 | /* 22 | 队列的名称与channelId,方便分发线程进行转发到对应的订阅consumer 23 | 写线程有多个,即nio线程组,consumer会动态订阅queue 24 | 读线程有多个,为worker线程组,会从中获取订阅信息 25 | */ 26 | public static ConcurrentHashMap> queueConsumerMap = new ConcurrentHashMap<>(); 27 | 28 | /* 29 | 用于记录和管理所有客户端的channel,可以自动移除已经断开的会话 30 | 此处记录一个对象引用副本 31 | */ 32 | public static ChannelGroup clients = MessageHandler.clients; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/core/MessageQueue.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.core; 2 | 3 | import java.util.ArrayList; 4 | import java.util.concurrent.LinkedBlockingQueue; 5 | 6 | /** 7 | * @author Mr_Hades 8 | * @date 2022-03-26 15:57 9 | */ 10 | public class MessageQueue extends LinkedBlockingQueue { 11 | 12 | // 每个queue的bindingKey,同一个包下才能够访问 13 | private final String bindingKey; 14 | 15 | // 为每个queue起一个具有分辨性的名字,同一个包下才能够访问 16 | private final String name; 17 | 18 | // 在当前queue上工作的所有的worker线程,只允许同包访问 19 | final ArrayList workers = new ArrayList<>(); 20 | 21 | public String getBindingKey() { 22 | return bindingKey; 23 | } 24 | 25 | public String getName() { 26 | return name; 27 | } 28 | 29 | public MessageQueue(String bindingKey, String name) { 30 | super(); 31 | this.bindingKey = bindingKey; 32 | this.name = name; 33 | } 34 | 35 | @Override 36 | public String toString() { 37 | return "MessageQueue{" + 38 | "bindingKey='" + bindingKey + '\'' + 39 | ", name='" + name + '\'' + 40 | ", elements=" + super.toString() + 41 | '}'; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/util/BannerUtil.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.util; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.BufferedReader; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.InputStreamReader; 10 | 11 | /** 12 | * 加载出一个好看的Banner 13 | * 14 | * @author Mr_Hades 15 | * @date 2022-03-26 21:25 16 | */ 17 | public class BannerUtil { 18 | private static Logger log = LoggerFactory.getLogger(BannerUtil.class); 19 | 20 | public static void loadBanner(String filename) { 21 | try { 22 | InputStream inputStream = BannerUtil.class.getClassLoader().getResourceAsStream(filename); 23 | BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); 24 | String line; 25 | System.out.println(); 26 | while ((line = reader.readLine()) != null) { 27 | System.out.println(line); 28 | } 29 | System.out.println(); 30 | log.debug("banner loaded successfully!"); 31 | } catch (IOException e) { 32 | // 忽略输出,不加载banner 33 | log.debug("banner loading failed"); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/pojo/Message.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.pojo; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | 7 | /** 8 | * 定义消息协议格式 9 | * 1. 来自consumer的订阅注册消息,消息格式为: 10 | *

11 |  *      {
12 |  *          type: 0,
13 |  *          extend: ["queue_name1","queue_name1"]  // 订阅的队列名称
14 |  *      }
15 |  *  
16 | * 2. 来自producer的普通消息,消息格式为: 17 | *

18 |  *      {
19 |  *          type: 1,
20 |  *          content: "消息内容",
21 |  *          extend: "com.xhades.top"  // routingKey
22 |  *      }
23 |  *  
24 | 25 | * @author Mr_Hades 26 | * @date 2022-03-25 21:53 27 | */ 28 | @Data 29 | @Builder 30 | @AllArgsConstructor 31 | public class Message { 32 | 33 | /* 34 | 消息类型,分为以下几种 35 | 0: 来自consumer的订阅注册消息 36 | 1: 来自producer的普通消息 37 | */ 38 | private int type; 39 | 40 | /* 41 | 消息正文,可以为null 42 | */ 43 | private String content; 44 | 45 | /* 46 | 若type为0,则extend为来自consumer的queueName,指定与哪个queue相连接 47 | 若type为1,则extend为来自producer的routingKey 48 | */ 49 | private String extend; 50 | 51 | /* 52 | 消息发送的时间 53 | */ 54 | private String datetime; 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/netIO/ServerInitializer.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.netIO; 2 | 3 | import io.netty.channel.ChannelInitializer; 4 | import io.netty.channel.ChannelPipeline; 5 | import io.netty.channel.socket.SocketChannel; 6 | import io.netty.handler.codec.http.HttpObjectAggregator; 7 | import io.netty.handler.codec.http.HttpServerCodec; 8 | import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; 9 | import io.netty.handler.stream.ChunkedWriteHandler; 10 | 11 | /** 12 | * @author Mr_Hades 13 | * @date 2022-03-25 21:17 14 | */ 15 | public class ServerInitializer extends ChannelInitializer { 16 | 17 | protected void initChannel(SocketChannel channel) throws Exception { 18 | ChannelPipeline pipeline = channel.pipeline(); 19 | 20 | //websocket 基于http协议,所需要的http 编解码器 21 | pipeline.addLast(new HttpServerCodec()); 22 | // 对数据流进行分块 23 | pipeline.addLast(new ChunkedWriteHandler()); 24 | //对httpMessage 进行聚合处理,聚合成request或 response 25 | pipeline.addLast(new HttpObjectAggregator(1024 * 64)); 26 | // 简单处理,忽略心跳 27 | // 解析WebSocket帧的结构 28 | pipeline.addLast(new WebSocketServerProtocolHandler("/")); 29 | 30 | //自定义的handler 31 | pipeline.addLast(new MessageHandler()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/client/Producer.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.client; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.singularityfold.pojo.Message; 5 | import com.singularityfold.util.DateUtil; 6 | 7 | import java.net.URI; 8 | 9 | /** 10 | * 封装producer客户端,装饰器模式,仅暴露出必要的接口 11 | * 12 | * @author Mr_Hades 13 | * @date 2022-03-27 11:06 14 | */ 15 | public class Producer { 16 | 17 | private Client client; 18 | private String routingKey; 19 | 20 | public Producer(URI serverUri, String name) { 21 | this.client = new Client(serverUri, name, 1); 22 | try { 23 | client.connectBlocking(); 24 | } catch (InterruptedException e) { 25 | // 26 | } 27 | } 28 | 29 | /** 30 | * 发送一条消息,包含routingKey 31 | * 32 | * @param message 消息,为json字符串格式 33 | * @param routingKey routingKey 34 | */ 35 | public void send(String message, String routingKey) { 36 | if (this.routingKey == null) 37 | this.routingKey = routingKey; 38 | Message packaged = new Message(1, message, routingKey, DateUtil.getLocalTime()); 39 | client.send(JSON.toJSONString(packaged)); 40 | } 41 | 42 | /** 43 | * 根据默认routingKey来进行消息发送 44 | * 45 | * @param massage 消息 46 | */ 47 | public void send(String massage) { 48 | if (routingKey == null){ 49 | System.out.println("Please set a default routing key."); 50 | return; 51 | } 52 | 53 | send(massage, this.routingKey); 54 | } 55 | 56 | /** 57 | * 设置默认的routingKey 58 | * 59 | * @param key routingKey 60 | */ 61 | public void setDefaultRoutingKey(String key) { 62 | this.routingKey = key; 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/test/java/ClientTest.java: -------------------------------------------------------------------------------- 1 | import com.singularityfold.client.Consumer; 2 | import com.singularityfold.client.Producer; 3 | import org.junit.Test; 4 | 5 | import java.io.IOException; 6 | import java.net.URI; 7 | 8 | /** 9 | * @author Mr_Hades 10 | * @date 2022-03-27 10:52 11 | */ 12 | public class ClientTest { 13 | 14 | // String url = "ws://www.xhades.top:2333/"; 15 | String url = "ws://localhost:8888/"; 16 | 17 | @Test 18 | public void TestProducer() throws IOException { 19 | Producer producer1 = new Producer(URI.create(url), "producer_1"); 20 | Producer producer2 = new Producer(URI.create(url), "producer_2"); 21 | Producer producer3 = new Producer(URI.create(url), "producer_3"); 22 | producer1.send("Make America Great Again!", "American.great.again.!"); 23 | producer2.send("China is getting stronger!", "China.daily.com"); 24 | producer2.send("中国建党一百年万岁", "China.xinhua.net"); 25 | producer3.send("The voice from Europe", "UK.Reuters.com"); 26 | System.in.read(); 27 | 28 | } 29 | 30 | @Test 31 | public void TestConsumer() throws Exception { 32 | Consumer consumer1 = new Consumer(URI.create(url), "American"); 33 | Consumer consumer2 = new Consumer(URI.create(url), "China"); 34 | Consumer consumer3 = new Consumer(URI.create(url), "UK"); 35 | consumer1.register("American", true); 36 | consumer1.onMessage((String message) -> System.out.println("American: " + message)); 37 | consumer2.register("China", true); 38 | consumer2.onMessage((String message) -> System.out.println("China: " + message)); 39 | consumer3.register("UK", true); 40 | consumer3.onMessage((String message) -> System.out.println("UK: " + message)); 41 | System.in.read(); 42 | } 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/client/Client.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.client; 2 | 3 | import org.java_websocket.client.WebSocketClient; 4 | import org.java_websocket.handshake.ServerHandshake; 5 | 6 | import java.net.URI; 7 | import java.util.function.Consumer; 8 | 9 | /** 10 | * java的WebSocketClient可以自动发送心跳包ping, 11 | * 而netty的WebSocketServerProtocolHandler可以自动发送心跳包pong 12 | * @author Mr_Hades 13 | * @date 2022-03-27 11:05 14 | */ 15 | public class Client extends WebSocketClient { 16 | 17 | // client的名称 18 | private String name; 19 | 20 | // client 类型,0代表consumer,1代表producer 21 | private int type; 22 | 23 | // 供外部类用来处理接受消息的接口函数 24 | private Consumer onMessageAction; 25 | 26 | // 同一个包内访问 27 | Client(URI serverUri, String name, int type) { 28 | super(serverUri); 29 | this.name = name; 30 | this.type = type; 31 | } 32 | 33 | public void setOnMessageAction(Consumer onMessageAction) { 34 | this.onMessageAction = onMessageAction; 35 | } 36 | 37 | @Override 38 | public void onOpen(ServerHandshake handshakedata) { 39 | System.out.println("Client " + name + " connects successfully!"); 40 | } 41 | 42 | @Override 43 | public void onMessage(String message) { 44 | // System.out.println("Client " + name + " received message: " + message); 45 | if (onMessageAction != null) 46 | onMessageAction.accept(message); 47 | 48 | } 49 | 50 | @Override 51 | public void onClose(int code, String reason, boolean remote) { 52 | System.out.println("Connection closed. code: " + code + ", reason: " + reason + ", remote: " + remote); 53 | } 54 | 55 | @Override 56 | public void onError(Exception ex) { 57 | System.out.println("Connection error: " + ex.getMessage()); 58 | } 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/netIO/MQServer.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.netIO; 2 | 3 | /** 4 | * @author Mr_Hades 5 | * @date 2022-3-25 21:45 6 | */ 7 | 8 | import com.singularityfold.util.BannerUtil; 9 | import io.netty.bootstrap.ServerBootstrap; 10 | import io.netty.channel.Channel; 11 | import io.netty.channel.ChannelFuture; 12 | import io.netty.channel.EventLoopGroup; 13 | import io.netty.channel.nio.NioEventLoopGroup; 14 | import io.netty.channel.socket.nio.NioServerSocketChannel; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | import java.util.Timer; 18 | import java.util.TimerTask; 19 | 20 | @Slf4j(topic = "MQServer") 21 | public class MQServer { 22 | 23 | private static class SingletonWSServer { 24 | static final MQServer instance = new MQServer(); 25 | } 26 | 27 | public static MQServer getInstance() { 28 | return SingletonWSServer.instance; 29 | } 30 | 31 | private EventLoopGroup mainGroup; 32 | private EventLoopGroup subGroup; 33 | private ServerBootstrap server; 34 | private ChannelFuture future; 35 | 36 | public MQServer() { 37 | mainGroup = new NioEventLoopGroup(); 38 | subGroup = new NioEventLoopGroup(); 39 | server = new ServerBootstrap(); 40 | server.group(mainGroup, subGroup) 41 | .channel(NioServerSocketChannel.class) 42 | .childHandler(new ServerInitializer()); 43 | } 44 | 45 | public void start() { 46 | try { 47 | 48 | BannerUtil.loadBanner("banner.txt"); 49 | Class.forName("com.singularityfold.config.Config"); // 初始化配置 50 | 51 | Channel channel = server.bind(8888).sync().channel(); 52 | log.info("Server starts successfully!"); 53 | channel.closeFuture().sync(); 54 | 55 | } catch (Exception e) { 56 | log.error("server error", e); 57 | e.printStackTrace(); 58 | } finally { 59 | mainGroup.shutdownGracefully(); 60 | subGroup.shutdownGracefully(); 61 | } 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/client/Consumer.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.client; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.singularityfold.pojo.Message; 5 | import com.singularityfold.util.DateUtil; 6 | 7 | import java.net.URI; 8 | import java.util.ArrayList; 9 | import java.util.Collections; 10 | import java.util.List; 11 | 12 | /** 13 | * 封装consumer客户端,装饰器模式,仅暴露出必要的接口,方便以后扩展 14 | * 15 | * @author Mr_Hades 16 | * @date 2022-03-27 11:06 17 | */ 18 | public class Consumer { 19 | 20 | private List registerInfo = new ArrayList<>(); 21 | private Client client; 22 | 23 | public Consumer(URI serverUri, String name) { 24 | this.client = new Client(serverUri, name, 0); 25 | try { 26 | client.connectBlocking(); 27 | } catch (InterruptedException e) { 28 | // 29 | } 30 | } 31 | 32 | /** 33 | * 为该Client注册绑定一系列新的queue 34 | * 35 | * @param queueNames queue的名称 36 | * @param append true表示追加绑定,false表示覆盖绑定 37 | */ 38 | public void register(List queueNames, boolean append) { 39 | if (!append) { 40 | registerInfo.clear(); 41 | } 42 | registerInfo.addAll(queueNames); 43 | 44 | Message packaged = new Message(0, null, JSON.toJSONString(queueNames), DateUtil.getLocalTime()); 45 | client.send(JSON.toJSONString(packaged)); 46 | 47 | } 48 | 49 | /** 50 | * 为该Client注册绑定一个新的queue 51 | * 52 | * @param queueName queue的名称 53 | * @param append true表示追加绑定,false表示覆盖绑定 54 | */ 55 | public void register(String queueName, boolean append) { 56 | register(List.of(queueName), append); 57 | } 58 | 59 | 60 | /** 61 | * 函数式编程,自定义消息处理方式,由被调用方提供方法参数 62 | * 63 | * @param action 自定义接口函数 64 | */ 65 | public void onMessage(java.util.function.Consumer action) { 66 | client.setOnMessageAction(action); 67 | } 68 | 69 | /** 70 | * 获取注册绑定信息 71 | * 72 | * @return registerInfo 73 | */ 74 | public List getRegisterInfo() { 75 | return Collections.unmodifiableList(registerInfo); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/config/Config.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.config; 2 | 3 | import com.singularityfold.core.QueueManager; 4 | import com.singularityfold.core.WorkerManager; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.io.InputStream; 8 | import java.util.Properties; 9 | 10 | /** 11 | * @author Mr_Hades 12 | * @date 2022-03-26 13:59 13 | */ 14 | @Slf4j(topic = "Config") 15 | public class Config { 16 | 17 | static { 18 | try { 19 | InputStream inputStream = 20 | Config.class.getClassLoader().getResourceAsStream("config.properties"); 21 | Properties properties = new Properties(); 22 | properties.load(inputStream); 23 | int queueNum = Integer.parseInt((String) properties.get("queueNum")); 24 | String rawKeys = (String) properties.get("bindingKeys"); 25 | String[] bindingKeys = rawKeys.split(",\\s+"); 26 | String[] queueNames = null; 27 | try { 28 | String rawQueueNames = (String) properties.get("queueNames"); 29 | queueNames = rawQueueNames.split(",\\s+"); 30 | } catch (Exception e) { 31 | // 若查找不到QueueNames参数则忽略 32 | log.info("Not found queueNames definition, use default naming strategy."); 33 | } 34 | init(queueNum, bindingKeys, queueNames); 35 | 36 | } catch (Exception e) { 37 | // 若加载异常,则抛出运行时异常 38 | throw new RuntimeException("Can't not find Config.properties or the format is wrong."); 39 | } 40 | } 41 | 42 | 43 | /** 44 | * 对MQ Server进行参数配置 45 | * 46 | * @param queueNum 队列的数量 47 | * @param bindingKeys 每个队列对应的key 48 | */ 49 | private static void init(int queueNum, String[] bindingKeys) { 50 | init(queueNum, bindingKeys, null); 51 | } 52 | 53 | /** 54 | * 对MQ Server进行参数配置 55 | * 56 | * @param queueNum 队列的数量 57 | * @param bindingKeys 每个队列对应的key 58 | * @param queueNames 每个队列对应的名字 59 | */ 60 | private static void init(int queueNum, String[] bindingKeys, String[] queueNames) { 61 | QueueManager.init(queueNum, bindingKeys, queueNames); 62 | WorkerManager.init(queueNum); 63 | if (QueueManager.isInited() && WorkerManager.isInited()) { 64 | log.info("All is ready."); 65 | } 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/util/KeyUtil.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.util; 2 | 3 | /** 4 | * 参考rabbitMQ的匹配规则,对用户的键与队列的键进行匹配 5 | * 6 | * @author Mr_Hades 7 | * @date 2022-03-26 14:28 8 | */ 9 | public class KeyUtil { 10 | 11 | /**
12 |      * 匹配routingKey和bindingKey的工具类
13 |      * 按照以下规则进行匹配:
14 |      *  1. #:匹配一个或多个词
15 |      *  2. *:匹配不多不少恰好1个词
16 |      *  3. |:或的关系,用来匹配多个key
17 |      * 举例:
18 |      *      item.#:能够匹配item.insert.abc 或者 item.insert
19 |      *      item.*:只能匹配item.insert
20 |      * 注:  #符号只能出现在开头或者结尾,不能出现多次
21 |      *      简单处理,此处不对bindingKey的格式进行检查,由用户自行判断
22 |      *
23 | * @return 是否匹配成功 24 | */ 25 | public static boolean routingKeyCompare(String routingKey, String bindingKey) { 26 | String[] keys = bindingKey.split("\\|"); 27 | String[] part1 = routingKey.split("\\."); 28 | for (String key : keys) { 29 | String[] part2 = key.split("\\."); 30 | int len2 = part2.length; 31 | int len1 = part1.length; 32 | // 包含#的bindingKey 33 | if (key.contains("#")) { 34 | int i; 35 | if (part2[0].equals("#")) { 36 | // 从后往前比 37 | for (i = 1; i <= Math.min(len1, len2); i++) { 38 | if (!part2[len2 - i].equals(part1[len1 - i])) 39 | break; 40 | } 41 | if (part2[len2 - i].equals("#")) 42 | return true; // 当前键匹配成功 43 | } else if (part2[len2 - 1].equals("#")) { 44 | // 从前往后比 45 | for (i = 0; i < Math.min(len1, len2); i++) { 46 | if (!part2[i].equals(part1[i])) 47 | break; 48 | } 49 | if (part2[i].equals("#")) 50 | return true; // 当前键匹配成功 51 | } 52 | } else { 53 | boolean flag = true; 54 | if (len1 == len2) { 55 | for (int i = 0; i < len1; i++) { 56 | if (!(part2[i].equals("*") || part1[i].equals(part2[i]))) { 57 | flag = false; 58 | break; // 匹配失败 59 | } 60 | } 61 | if (flag) 62 | return true; 63 | } 64 | } 65 | 66 | } 67 | return false; 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/UnitTest.java: -------------------------------------------------------------------------------- 1 | import com.singularityfold.core.QueueManager; 2 | import com.singularityfold.core.WorkerManager; 3 | import com.singularityfold.util.KeyUtil; 4 | import org.junit.Test; 5 | 6 | /** 7 | * @author Mr_Hades 8 | * @date 2022-03-26 11:04 9 | */ 10 | public class UnitTest { 11 | 12 | @Test 13 | public void testQueue() { 14 | QueueManager.init(4, new String[]{"aaa.bbb.ccc", "aa.cc.df", "bnc"}); 15 | QueueManager.getQueue(0).add( 16 | "hello My guys!" 17 | ); 18 | try { 19 | System.out.println(QueueManager.getQueue(0).take()); 20 | } catch (Exception e) { 21 | 22 | } 23 | } 24 | 25 | @Test 26 | public void testKeyComparator() { 27 | // 模拟邮政管理系统 28 | System.out.println(KeyUtil.routingKeyCompare( 29 | "中国.台湾省.高雄市", "中国.海南省.*|中国.台湾省.*" 30 | )); 31 | System.out.println(KeyUtil.routingKeyCompare( 32 | "中国.湖北省.黄冈市", "中国.#" 33 | )); 34 | System.out.println(KeyUtil.routingKeyCompare( 35 | "中国.黑龙江省.哈尔滨市", "中国.辽宁省.#|中国.吉林省.#" 36 | )); 37 | System.out.println(KeyUtil.routingKeyCompare( 38 | "中国.黑龙江省.哈尔滨市", "#" 39 | )); 40 | } 41 | 42 | @Test 43 | public void testQueueManager() { 44 | QueueManager.init( 45 | 3, 46 | new String[]{"American.#", "China.*.*", "UK.*.*"}, 47 | new String[]{"American", "China", "UK"} 48 | ); 49 | QueueManager.put("Make America Great Again!", "American.great.again.!"); 50 | QueueManager.put("China is getting stronger!", "China.daily.com"); 51 | QueueManager.put("中国建党一百年", "China.xinhua.net"); 52 | QueueManager.put("The voice from Europe", "UK.Reuters.com"); 53 | 54 | for (int i = 0; i < 3; i++) { 55 | System.out.println(QueueManager.getQueue(i)); 56 | } 57 | } 58 | 59 | @Test 60 | public void testWorkerManager() { 61 | int queueNum = 3; 62 | QueueManager.init( 63 | queueNum, 64 | new String[]{"American.#", "China.*.*", "UK.*.*"}, 65 | new String[]{"American", "China", "UK"} 66 | ); 67 | WorkerManager.init(queueNum); 68 | QueueManager.put("Make America Great Again!", "American.great.again.!"); 69 | QueueManager.put("China is getting stronger!", "China.daily.com"); 70 | QueueManager.put("中国建党一百年万岁!", "China.xinhua.net"); 71 | QueueManager.put("The voice from Europe", "UK.Reuters.com"); 72 | 73 | for (int i = 0; i < queueNum; i++) { 74 | System.out.println(QueueManager.getQueue(i)); 75 | } 76 | } 77 | 78 | @Test 79 | public void testColor() { 80 | System.out.println("\033[1;91;40mINFO"); 81 | } 82 | 83 | 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/netIO/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.netIO; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.singularityfold.core.BasicMaps; 5 | import com.singularityfold.core.QueueManager; 6 | import com.singularityfold.pojo.Message; 7 | import io.netty.channel.Channel; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.ChannelId; 10 | import io.netty.channel.SimpleChannelInboundHandler; 11 | import io.netty.channel.group.ChannelGroup; 12 | import io.netty.channel.group.DefaultChannelGroup; 13 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 14 | import io.netty.util.concurrent.GlobalEventExecutor; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | 21 | 22 | /** 23 | * @author Mr_Hades 24 | * @date 2022-03-25 21:45 25 | */ 26 | @Slf4j(topic = "MessageHandler") 27 | public class MessageHandler extends SimpleChannelInboundHandler { 28 | 29 | //用于记录和管理所有客户端的channel,可以自动移除已经断开的会话 30 | public static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); 31 | 32 | protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { 33 | // 获取客户端所传输的消息 34 | String data = msg.text(); 35 | // 获取当前通话channel 36 | Channel channel = ctx.channel(); 37 | clients.add(channel); // 将其纳入管理 38 | Message message; 39 | try { 40 | // 解析出消息类型 41 | message = JSON.parseObject(data, Message.class); 42 | // 来自consumer的订阅注册消息 43 | if (message.getType() == 0) { 44 | 45 | // TODO 完成覆盖绑定的效果 46 | List queueNames = (List) JSON.parseObject(message.getExtend(), List.class); 47 | ConcurrentHashMap> map = BasicMaps.queueConsumerMap; 48 | for (String queueName : queueNames) { 49 | // 该queue此前未被任何consumer注册 50 | if (!map.containsKey(queueName)) { 51 | ArrayList list = new ArrayList<>(); 52 | list.add(channel.id()); 53 | map.put(queueName, list); 54 | } else { 55 | map.get(queueName).add(channel.id()); 56 | } 57 | QueueManager.signal(queueName); 58 | } 59 | 60 | 61 | } 62 | // 来自producer的普通消息 63 | else if (message.getType() == 1) { 64 | String content = message.getContent(); 65 | String routingKey = message.getExtend(); 66 | QueueManager.put(content, routingKey); 67 | 68 | } else { 69 | throw new Exception(); 70 | } 71 | 72 | } catch (Exception e) { 73 | // 消息格式有误 74 | log.debug("{}消息格式有误", data); 75 | channel.writeAndFlush( 76 | new TextWebSocketFrame("消息格式有误") 77 | ).addListener(future -> { 78 | channel.close(); 79 | log.debug("成功移除channel"); 80 | }); 81 | 82 | } 83 | } 84 | 85 | @Override 86 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 87 | log.debug(cause.getMessage()); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/core/WorkerManager.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.core; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import io.netty.channel.Channel; 5 | import io.netty.channel.ChannelId; 6 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * 创建并管理work线程,负责将消息分发给consumer 15 | * 16 | * @author Mr_Hades 17 | * @date 2022-03-26 10:15 18 | */ 19 | @Slf4j(topic = "WorkerManager") 20 | public class WorkerManager { 21 | // 固定数量线程数,方便后续扩展和管理 22 | static private Thread[] threads; 23 | 24 | static private boolean inited = false; 25 | 26 | // 初始化方法,进行线程的创建 27 | public static void init(int threadNum) { 28 | if (inited) 29 | return; 30 | synchronized (WorkerManager.class) { 31 | // double check lock 32 | if (inited) 33 | return; 34 | threads = new Thread[threadNum]; 35 | for (int i = 0; i < threadNum; i++) { 36 | Thread thread = new Thread(new Task(i), "worker-" + i); 37 | // 将thread添加到每个queue的worker线程中 38 | QueueManager.getQueue(i).workers.add(thread); 39 | thread.setDaemon(true); // 设置为当前线程的守护线程,随着主程序的终止而被杀死 40 | thread.start(); 41 | threads[i] = thread; 42 | } 43 | log.info("{} worker threads are started.", threadNum); 44 | inited = true; 45 | } 46 | } 47 | 48 | public static boolean isInited() { 49 | return inited; 50 | } 51 | 52 | /** 53 | * 任务对象,持续取queue中的消息并将其转发到对应的channel中 54 | * 两种情况会阻塞 55 | * 1. queue中无元素 56 | * 2. queue没有consumer来绑定 57 | */ 58 | private static class Task implements Runnable { 59 | 60 | private Logger log = LoggerFactory.getLogger(Task.class); 61 | 62 | private MessageQueue queue; 63 | 64 | @Override 65 | public void run() { 66 | log.debug("worker thread of queue {} is working", queue.getName()); 67 | String message; 68 | while (true) { 69 | try { 70 | message = queue.take(); // 阻塞获取消息 71 | List channelIds; 72 | while ((channelIds = BasicMaps.queueConsumerMap.get(queue.getName())) == null || 73 | channelIds.isEmpty()) { 74 | try { 75 | Thread.sleep(Long.MAX_VALUE); 76 | log.debug("no consumers, sleeping..."); 77 | } catch (InterruptedException e) { 78 | //以防有人捣乱 79 | log.debug("interrupted..."); 80 | } 81 | } 82 | for (ChannelId channelId : channelIds) { 83 | Channel channel = BasicMaps.clients.find(channelId); 84 | channel.writeAndFlush( 85 | new TextWebSocketFrame(JSON.toJSONString(message)) 86 | ); // 发送消息 87 | } 88 | } catch (InterruptedException e) { 89 | // 不做异常处理,继续阻塞获取消息 90 | } 91 | } 92 | } 93 | 94 | 95 | // 绑定的工作队列的序号 96 | public Task(int queueIndex) { 97 | this.queue = QueueManager.getQueue(queueIndex); 98 | } 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/util/IpUtil.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.util; 2 | 3 | 4 | import java.net.*; 5 | import java.util.ArrayList; 6 | import java.util.Enumeration; 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | /** 11 | * 获取本机IP 地址 12 | * 13 | * @author dingwen 14 | * 2021.04.28 11:49 15 | */ 16 | public class IpUtil { 17 | /* 18 | * 获取本机所有网卡信息 得到所有IP信息 19 | * @return Inet4Address> 20 | */ 21 | public static List getLocalIp4AddressFromNetworkInterface() throws SocketException { 22 | List addresses = new ArrayList<>(1); 23 | 24 | // 所有网络接口信息 25 | Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); 26 | if (!networkInterfaces.hasMoreElements()) { 27 | return addresses; 28 | } 29 | while (networkInterfaces.hasMoreElements()) { 30 | NetworkInterface networkInterface = networkInterfaces.nextElement(); 31 | //滤回环网卡、点对点网卡、非活动网卡、虚拟网卡并要求网卡名字是eth或ens开头 32 | // if (!isValidInterface(networkInterface)) { 33 | // continue; 34 | // } 35 | 36 | // 所有网络接口的IP地址信息 37 | Enumeration inetAddresses = networkInterface.getInetAddresses(); 38 | while (inetAddresses.hasMoreElements()) { 39 | InetAddress inetAddress = inetAddresses.nextElement(); 40 | // 判断是否是IPv4,并且内网地址并过滤回环地址. 41 | if (isValidAddress(inetAddress)) { 42 | addresses.add((Inet4Address) inetAddress); 43 | } 44 | } 45 | } 46 | return addresses; 47 | } 48 | 49 | /** 50 | * 过滤回环网卡、点对点网卡、非活动网卡、虚拟网卡并要求网卡名字是eth或ens开头 51 | * 52 | * @param ni 网卡 53 | * @return 如果满足要求则true,否则false 54 | */ 55 | private static boolean isValidInterface(NetworkInterface ni) throws SocketException { 56 | return !ni.isLoopback() && !ni.isPointToPoint() && ni.isUp() && !ni.isVirtual() 57 | && (ni.getName().startsWith("eth") || ni.getName().startsWith("ens")); 58 | } 59 | 60 | /** 61 | * 判断是否是IPv4,并且内网地址并过滤回环地址. 62 | */ 63 | private static boolean isValidAddress(InetAddress address) { 64 | return address instanceof Inet4Address && address.isSiteLocalAddress() && !address.isLoopbackAddress(); 65 | } 66 | 67 | /* 68 | * 通过Socket 唯一确定一个IP 69 | * 当有多个网卡的时候,使用这种方式一般都可以得到想要的IP。甚至不要求外网地址8.8.8.8是可连通的 70 | * @return Inet4Address> 71 | */ 72 | private static Optional getIpBySocket() throws SocketException { 73 | try (final DatagramSocket socket = new DatagramSocket()) { 74 | socket.connect(InetAddress.getByName("8.8.8.8"), 10002); 75 | if (socket.getLocalAddress() instanceof Inet4Address) { 76 | return Optional.of((Inet4Address) socket.getLocalAddress()); 77 | } 78 | } catch (UnknownHostException networkInterfaces) { 79 | throw new RuntimeException(networkInterfaces); 80 | } 81 | return Optional.empty(); 82 | } 83 | 84 | /* 85 | * 获取本地IPv4地址 86 | * @return Inet4Address> 87 | */ 88 | public static Optional getLocalIp4Address() throws SocketException { 89 | final List inet4Addresses = getLocalIp4AddressFromNetworkInterface(); 90 | if (inet4Addresses.size() != 1) { 91 | final Optional ipBySocketOpt = getIpBySocket(); 92 | if (ipBySocketOpt.isPresent()) { 93 | return ipBySocketOpt; 94 | } else { 95 | return inet4Addresses.isEmpty() ? Optional.empty() : Optional.of(inet4Addresses.get(0)); 96 | } 97 | } 98 | return Optional.of(inet4Addresses.get(0)); 99 | } 100 | 101 | public static void main(String[] args) throws SocketException { 102 | System.out.println(IpUtil.getLocalIp4AddressFromNetworkInterface()); 103 | } 104 | 105 | } 106 | 107 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.xhades 8 | MiniMQ 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | org.apache.maven.plugins 14 | maven-compiler-plugin 15 | 3.8.0 16 | 17 | 11 18 | 11 19 | UTF-8 20 | 21 | 22 | 23 | org.apache.maven.plugins 24 | maven-surefire-plugin 25 | 2.20.1 26 | 27 | true 28 | 29 | 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-source-plugin 34 | 3.2.0 35 | 36 | true 37 | 38 | 39 | 40 | 41 | compile 42 | 43 | jar 44 | 45 | 46 | 47 | 48 | 49 | 50 | maven-assembly-plugin 51 | 2.2-beta-5 52 | 53 | 54 | 55 | com.singularityfold.MiniMQApplication 56 | 57 | 58 | 59 | 60 | jar-with-dependencies 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | io.netty 73 | netty-all 74 | 4.1.50.Final 75 | 76 | 77 | org.apache.commons 78 | commons-lang3 79 | 3.4 80 | 81 | 82 | org.slf4j 83 | slf4j-log4j12 84 | 1.7.26 85 | 86 | 87 | com.alibaba 88 | fastjson 89 | 1.2.52 90 | 91 | 92 | org.projectlombok 93 | lombok 94 | 1.18.20 95 | 96 | 97 | junit 98 | junit 99 | 4.13.2 100 | UnitTest 101 | 102 | 103 | org.java-websocket 104 | Java-WebSocket 105 | 1.3.8 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /src/main/java/com/singularityfold/core/QueueManager.java: -------------------------------------------------------------------------------- 1 | package com.singularityfold.core; 2 | 3 | import com.singularityfold.util.KeyUtil; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | import java.util.HashMap; 7 | 8 | /** 9 | * @author Mr_Hades 10 | * @date 2022-03-26 10:37 11 | */ 12 | @Slf4j(topic = "QueueManager") 13 | public class QueueManager { 14 | private static MessageQueue[] queues; 15 | 16 | private static HashMap queueMap; 17 | 18 | private static boolean inited = false; 19 | 20 | /** 21 | * 队列管理器初始化 22 | * 23 | * @param queueNum 队列的数量 24 | * @param bindingKeys 对应于每个queue的bindingKey 25 | * @param queueNames 对应于每个queue的名字,name不可重复 26 | * @throws RuntimeException 如果queueNum和bindingKeys的长度不对应,抛出异常 27 | */ 28 | public static void init(int queueNum, String[] bindingKeys, String[] queueNames) throws RuntimeException { 29 | if (inited) 30 | return; 31 | synchronized (QueueManager.class) { 32 | // double check lock,先做并发控制,方便以后扩展 33 | if (inited) 34 | return; 35 | if (!(bindingKeys.length == queueNum)) { 36 | log.error("The length of bindingKeys not equal to queueNum."); 37 | throw new RuntimeException("The length of bindingKeys not equal to queueNum."); 38 | } 39 | 40 | queues = new MessageQueue[queueNum]; 41 | queueMap = new HashMap<>(); 42 | // 保证名称不能有重复的,如果有的话,将原名做稍微修改 43 | HashMap chosenNames = new HashMap<>(); 44 | for (int i = 0; i < queueNum; i++) { 45 | if (queueNames == null || i >= queueNames.length || queueNames[i] == null) 46 | queues[i] = new MessageQueue(bindingKeys[i], "queue_" + i); 47 | else { 48 | String name = queueNames[i]; 49 | if (chosenNames.containsKey(name)) { 50 | Integer old = chosenNames.get(name); 51 | String newName = name + old; 52 | queues[i] = new MessageQueue(bindingKeys[i], newName); 53 | log.warn("A duplicated queue queueNames {} is modified to {}", name, newName); 54 | chosenNames.put(name, old + 1); 55 | queueMap.put(newName, queues[i]); 56 | } else { 57 | queues[i] = new MessageQueue(bindingKeys[i], name); 58 | chosenNames.put(name, 1); 59 | queueMap.put(name, queues[i]); 60 | } 61 | } 62 | 63 | } 64 | log.info("{} queues are ready.", queueNum); 65 | inited = true; 66 | } 67 | } 68 | 69 | /** 70 | * 队列管理器初始化 71 | * 72 | * @param queueNum 队列的数量 73 | * @param bindingKeys 对应于每个queue的bindingKey 74 | * @throws RuntimeException 如果queueNum和bindingKeys的长度不对应,抛出异常 75 | */ 76 | public static void init(int queueNum, String[] bindingKeys) throws RuntimeException { 77 | init(queueNum, bindingKeys, null); 78 | } 79 | 80 | 81 | // 供外部访问,是否初始化 82 | public static boolean isInited() { 83 | return inited; 84 | } 85 | 86 | // 放入一条消息到消息队列中 87 | public static void put(String message, String routingKey) { 88 | if (!inited) { 89 | // log.error("Please init the QueueManager first."); 90 | throw new RuntimeException("QueueManager not initiated."); 91 | } 92 | for (int i = 0; i < queues.length; i++) { 93 | String bindingKey = queues[i].getBindingKey(); 94 | if (KeyUtil.routingKeyCompare(routingKey, bindingKey)) { 95 | try { 96 | queues[i].put(message); 97 | } catch (InterruptedException e) { 98 | // 忽略打断 99 | } 100 | } 101 | } 102 | } 103 | 104 | public static MessageQueue getQueue(int index) { 105 | if (!inited) { 106 | // log.error("Please init the QueueManager first."); 107 | throw new RuntimeException("QueueManager not initiated."); 108 | } 109 | return queues[index]; 110 | } 111 | 112 | public boolean containsQueue(String name){ 113 | if (!inited) { 114 | // log.error("Please init the QueueManager first."); 115 | throw new RuntimeException("QueueManager not initiated."); 116 | } 117 | return queueMap.containsKey(name); 118 | } 119 | 120 | /** 121 | * 唤醒在某个queue上等待的线程 122 | * 123 | * @param queueName 队列名 124 | */ 125 | public static void signal(String queueName) { 126 | for (Thread worker : queueMap.get(queueName).workers) { 127 | worker.interrupt(); 128 | } 129 | } 130 | 131 | } 132 | --------------------------------------------------------------------------------