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 |
--------------------------------------------------------------------------------