├── conf └── xymq-cli.properties ├── doc ├── 系统架构.png ├── 可视化面板.jpg └── 消息持久化模型.png ├── xymq-server ├── src │ ├── main │ │ ├── resources │ │ │ ├── static │ │ │ │ ├── vue2-sfc-loader │ │ │ │ │ └── 说明.txt │ │ │ │ ├── css │ │ │ │ │ ├── fonts │ │ │ │ │ │ ├── element-icons.ttf │ │ │ │ │ │ └── element-icons.woff │ │ │ │ │ └── style.css │ │ │ │ ├── js │ │ │ │ │ └── router.js │ │ │ │ ├── img │ │ │ │ │ └── github.svg │ │ │ │ └── axios │ │ │ │ │ └── axios.min.js │ │ │ ├── application.yml │ │ │ └── templates │ │ │ │ ├── components │ │ │ │ ├── queue.vue │ │ │ │ ├── menu.js │ │ │ │ └── home.vue │ │ │ │ └── index.html │ │ └── java │ │ │ └── com │ │ │ └── xymq_cli │ │ │ ├── interceptor │ │ │ └── AccessInterceptor.java │ │ │ ├── constant │ │ │ ├── MessageConstant.java │ │ │ ├── ServerConstant.java │ │ │ └── Destination.java │ │ │ ├── web │ │ │ ├── domain │ │ │ │ └── QueueVO.java │ │ │ ├── service │ │ │ │ ├── DataChartService.java │ │ │ │ └── impl │ │ │ │ │ └── DataChartServiceImpl.java │ │ │ └── controller │ │ │ │ ├── DataController.java │ │ │ │ └── IndexController.java │ │ │ ├── execution │ │ │ ├── Execution.java │ │ │ ├── ExecutionFactory.java │ │ │ ├── AckExec.java │ │ │ ├── ProducerExec.java │ │ │ └── ConsumerExec.java │ │ │ ├── XymqApplication.java │ │ │ ├── core │ │ │ ├── ServerInitializer.java │ │ │ ├── WebsocketInitializer.java │ │ │ ├── StorageHelper.java │ │ │ ├── ClientManager.java │ │ │ ├── LevelDbStorageHelper.java │ │ │ ├── LevelDb.java │ │ │ └── XymqServer.java │ │ │ ├── util │ │ │ ├── Message2Byte.java │ │ │ └── SnowflakeIdUtils.java │ │ │ ├── listener │ │ │ └── BootListener.java │ │ │ ├── exception │ │ │ ├── XyException.java │ │ │ └── ExceptionEnum.java │ │ │ ├── handler │ │ │ ├── XyTextWebSocketFrameHandler.java │ │ │ └── MessageHandler.java │ │ │ └── config │ │ │ ├── MQServerConfig.java │ │ │ ├── MvcConfig.java │ │ │ └── ThreadConfig.java │ └── test │ │ └── java │ │ └── com │ │ └── xymq_cli │ │ └── DemoApplicationTests.java ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties └── pom.xml ├── xymq-cli ├── src │ └── main │ │ └── java │ │ └── com │ │ └── xymq_cli │ │ ├── listener │ │ ├── MessageListener.java │ │ └── MessageData.java │ │ ├── constant │ │ ├── ClientConstant.java │ │ ├── Destination.java │ │ └── MessageType.java │ │ ├── handler │ │ ├── ProducerHandler.java │ │ ├── SubscriberHandler.java │ │ └── ConsumerHandler.java │ │ ├── client │ │ ├── initializer │ │ │ ├── ProducerlInitializer.java │ │ │ ├── ConsumerlInitializer.java │ │ │ └── SubscriberInitializer.java │ │ ├── Subscriber.java │ │ ├── Consumer.java │ │ └── Producer.java │ │ └── util │ │ └── ResourceUtils.java └── pom.xml ├── .gitignore ├── xymq-common ├── src │ └── main │ │ └── java │ │ └── com │ │ └── xymq_common │ │ ├── protocol │ │ ├── MessageEncoder.java │ │ ├── Protocol.java │ │ ├── MessageDecoder.java │ │ └── MessageUtils.java │ │ └── message │ │ └── Message.java └── pom.xml └── README.md /conf/xymq-cli.properties: -------------------------------------------------------------------------------- 1 | server.port=8686 2 | server.host=127.0.0.1 -------------------------------------------------------------------------------- /doc/系统架构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyx0912/XY-MQ/HEAD/doc/系统架构.png -------------------------------------------------------------------------------- /doc/可视化面板.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyx0912/XY-MQ/HEAD/doc/可视化面板.jpg -------------------------------------------------------------------------------- /doc/消息持久化模型.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyx0912/XY-MQ/HEAD/doc/消息持久化模型.png -------------------------------------------------------------------------------- /xymq-server/src/main/resources/static/vue2-sfc-loader/说明.txt: -------------------------------------------------------------------------------- 1 | https://cdn.jsdelivr.net/npm/vue3-sfc-loader/ 2 | 3 | v0.7.3 -------------------------------------------------------------------------------- /xymq-server/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyx0912/XY-MQ/HEAD/xymq-server/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /xymq-server/src/main/resources/static/css/fonts/element-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyx0912/XY-MQ/HEAD/xymq-server/src/main/resources/static/css/fonts/element-icons.ttf -------------------------------------------------------------------------------- /xymq-server/src/main/resources/static/css/fonts/element-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lyx0912/XY-MQ/HEAD/xymq-server/src/main/resources/static/css/fonts/element-icons.woff -------------------------------------------------------------------------------- /xymq-server/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar 3 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/interceptor/AccessInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.interceptor; 2 | 3 | import org.springframework.web.servlet.HandlerInterceptor; 4 | 5 | /** 6 | * @author 黎勇炫 7 | * @date 2022年07月25日 23:03 8 | */ 9 | public class AccessInterceptor implements HandlerInterceptor { 10 | } 11 | -------------------------------------------------------------------------------- /xymq-server/src/test/java/com/xymq_cli/DemoApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class DemoApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/constant/MessageConstant.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.constant; 2 | 3 | /** 4 | * @author 黎勇炫 5 | * @date 2022年07月10日 12:31 6 | */ 7 | public class MessageConstant { 8 | public static final int PROVIDER = 0; 9 | public static final int CONSUMER = 1; 10 | public static final int ACK =2; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/listener/MessageListener.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.listener; 2 | 3 | import java.util.EventListener; 4 | 5 | /** 6 | * @author 黎勇炫 7 | * @date 2022年07月11日 15:18 8 | */ 9 | public interface MessageListener extends EventListener { 10 | /** 11 | * 获取消息 12 | */ 13 | public void getMessage(MessageData data); 14 | } -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/constant/ServerConstant.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.constant; 2 | 3 | /** 4 | * @author 黎勇炫 5 | * @date 2022年07月12日 16:53 6 | */ 7 | public class ServerConstant { 8 | public static final String OFFLINE_SUBSCRIBER = "offLineSubscriber"; 9 | public static final String OFFLINE_MESSAGE_TOPIC = "offLineTopicMessage"; 10 | } 11 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/constant/ClientConstant.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.constant; 2 | 3 | /** 4 | * @author 黎勇炫 5 | * @date 2022年07月11日 15:00 6 | */ 7 | public class ClientConstant { 8 | 9 | /** 10 | * 服务端口 11 | */ 12 | public static final String SERVER_PORT = "server.port"; 13 | /** 14 | * 服务端地址 15 | */ 16 | public static final String SERVER_HOST = "server.host"; 17 | } 18 | -------------------------------------------------------------------------------- /xymq-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | # 存储设置 2 | leveldb: 3 | folder: leveldb/message.db 4 | charset: utf-8 5 | 6 | # 消息队列服务端口 7 | xymq: 8 | wbsockport: 8687 9 | port: 8686 10 | backLog: 128 11 | storage: 12 | type: levelDb 13 | # thread: 推送消息的线程池配置 14 | # corePoolSize: 75 15 | # maxPoolSize: 200 16 | # queueCapacity: 1000 17 | # keepAliveSeconds: 300 18 | spring: 19 | thymeleaf: 20 | cache: false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | #yml 14 | *.iml 15 | 16 | # Package Files # 17 | #*.jar 18 | *.war 19 | *.nar 20 | *.ear 21 | *.zip 22 | *.tar.gz 23 | *.rar 24 | 25 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 26 | hs_err_pid* 27 | 28 | 29 | target/ 30 | leveldb/ 31 | .idea/ 32 | #rebel 33 | *rebel.xml* -------------------------------------------------------------------------------- /xymq-server/src/main/resources/static/css/style.css: -------------------------------------------------------------------------------- 1 | 2 | .el-aside { 3 | border-right: solid 1px #e6e6e6; 4 | height: 100%; 5 | } 6 | 7 | .el-menu-container { 8 | height: 100%; 9 | } 10 | 11 | .el-header { 12 | padding: 5px 20px; 13 | height: 30px; 14 | background-color: #fcfeff; 15 | color: #333; 16 | text-align: center; 17 | line-height: 30px; 18 | box-shadow: 0 1px 4px rgb(0 21 41 / 8%); 19 | } 20 | .aside-header{ 21 | width: 100%; 22 | height: 60px; 23 | } 24 | .logo-github{ 25 | float: right; 26 | } -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/constant/Destination.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.constant; 2 | 3 | /** 4 | * @author 黎勇炫 5 | * @date 2022年07月10日 13:37 6 | */ 7 | public enum Destination { 8 | QUEUE(0), 9 | TOPIC(1); 10 | 11 | private int destination; 12 | 13 | Destination(int destination) { 14 | this.destination = destination; 15 | } 16 | 17 | public int getDestination() { 18 | return destination; 19 | } 20 | 21 | public void setDestination(int destination) { 22 | this.destination = destination; 23 | } 24 | } -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/constant/Destination.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.constant; 2 | 3 | /** 4 | * @author 黎勇炫 5 | * @date 2022年07月10日 13:37 6 | */ 7 | public enum Destination{ 8 | QUEUE(0), 9 | TOPIC(1); 10 | 11 | private int destination; 12 | 13 | Destination(int destination) { 14 | this.destination = destination; 15 | } 16 | 17 | public int getDestination() { 18 | return destination; 19 | } 20 | 21 | public void setDestination(int destination) { 22 | this.destination = destination; 23 | } 24 | } -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/web/domain/QueueVO.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.web.domain; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author 黎勇炫 7 | * @date 2022年08月09日 18:53 8 | */ 9 | @Data 10 | public class QueueVO { 11 | 12 | /** 13 | * 队列名称 14 | */ 15 | private String queueName; 16 | /** 17 | * 客户端数量 18 | */ 19 | private Integer consumerCount = 0; 20 | /** 21 | * 消息堆积数量 22 | */ 23 | private Long unConsume = 0L; 24 | /** 25 | * 延时消息数量 26 | */ 27 | private Long delayCount = 0L; 28 | } 29 | -------------------------------------------------------------------------------- /xymq-common/src/main/java/com/xymq_common/protocol/MessageEncoder.java: -------------------------------------------------------------------------------- 1 | package com.xymq_common.protocol; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.handler.codec.MessageToByteEncoder; 6 | 7 | /** 8 | * @author 黎勇炫 9 | * @date 2022年07月07日 16:52 10 | */ 11 | public class MessageEncoder extends MessageToByteEncoder { 12 | @Override 13 | protected void encode(ChannelHandlerContext channelHandlerContext, Protocol protocol, ByteBuf byteBuf) throws Exception { 14 | byteBuf.writeInt(protocol.getLen()); 15 | byteBuf.writeBytes(protocol.getContent()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/constant/MessageType.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.constant; 2 | 3 | /** 4 | * @author 黎勇炫 5 | * @date 2022年07月11日 15:57 6 | */ 7 | public enum MessageType { 8 | 9 | 10 | PRIVODER(0,"provider"), //生产者 11 | COMSUMER(1,"comsumer"), //消费者 12 | ACK(2,"ack"); //签收 13 | 14 | 15 | Integer type; 16 | String describe; 17 | 18 | MessageType(Integer type, String describe){ 19 | this.type = type; 20 | this.describe = describe; 21 | } 22 | 23 | public Integer getType(){ 24 | return type; 25 | } 26 | 27 | public String getDescribe(){ 28 | return describe; 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/execution/Execution.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.execution; 2 | 3 | import com.xymq_common.message.Message; 4 | import io.netty.channel.Channel; 5 | 6 | /** 7 | * @author 黎勇炫 8 | * @date 2022年07月10日 12:35 9 | */ 10 | public interface Execution { 11 | /** 12 | * 执行操作(消费、推送和签收) 13 | * @return void 14 | * @author 黎勇炫 15 | * @create 2022/7/10 16 | * @email 1677685900@qq.com 17 | */ 18 | void exec(Message message, Channel channel); 19 | 20 | /** 21 | * 返回当前策略支持的 消息 类型 22 | * @return int 消息类型 23 | * @author 黎勇炫 24 | * @create 2022/7/10 25 | * @email 1677685900@qq.com 26 | */ 27 | int getType(); 28 | } -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/listener/MessageData.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.listener; 2 | 3 | import java.util.EventObject; 4 | 5 | /** 6 | * @author 黎勇炫 7 | * @date 2022年07月11日 15:19 8 | */ 9 | public class MessageData extends EventObject { 10 | /** 11 | * 消息内容 12 | */ 13 | private String message; 14 | 15 | 16 | public MessageData(Object consumer) { 17 | this(consumer,null); 18 | } 19 | 20 | public MessageData(Object consumer,String message){ 21 | super(consumer); 22 | this.message = message; 23 | } 24 | 25 | /** 26 | * 返回消息内容 27 | */ 28 | public String getMessage(){ 29 | return message; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/XymqApplication.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableAsync; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | 8 | 9 | /* 10 | * 启动类 11 | * @author 黎勇炫 12 | * @create 2022/7/9 13 | * @email 1677685900@qq.com 14 | */ 15 | @SpringBootApplication 16 | @EnableScheduling // 定时任务 17 | @EnableAsync // 异步任务 18 | public class XymqApplication { 19 | 20 | public static void main(String[] args) { 21 | SpringApplication.run(XymqApplication.class, args); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /xymq-common/src/main/java/com/xymq_common/protocol/Protocol.java: -------------------------------------------------------------------------------- 1 | package com.xymq_common.protocol; 2 | 3 | /** 4 | * 解决消息内容粘包和拆包问题,将要传输的内容大小写入bytebuf的header中 5 | * @author 黎勇炫 6 | * @date 2022年07月07日 16:51 7 | */ 8 | public class Protocol { 9 | private Integer len; 10 | private byte[] content; 11 | 12 | 13 | public Integer getLen() { 14 | return len; 15 | } 16 | 17 | public void setLen(Integer len) { 18 | this.len = len; 19 | } 20 | 21 | public byte[] getContent() { 22 | return content; 23 | } 24 | 25 | public void setContent(byte[] content) { 26 | this.content = content; 27 | } 28 | 29 | public Protocol(Integer len, byte[] content) { 30 | this.len = len; 31 | this.content = content; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /xymq-common/src/main/java/com/xymq_common/protocol/MessageDecoder.java: -------------------------------------------------------------------------------- 1 | package com.xymq_common.protocol; 2 | 3 | import io.netty.buffer.ByteBuf; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.handler.codec.ReplayingDecoder; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author 黎勇炫 11 | * @date 2022年07月07日 16:53 12 | */ 13 | public class MessageDecoder extends ReplayingDecoder { 14 | @Override 15 | protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List list) throws Exception { 16 | int len = byteBuf.readInt(); 17 | byte[] content = new byte[len]; 18 | byteBuf.readBytes(content); 19 | 20 | Protocol protocol = new Protocol(len,content); 21 | 22 | list.add(protocol); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/web/service/DataChartService.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.web.service; 2 | 3 | import com.xymq_cli.web.domain.QueueVO; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | /** 9 | * 数据图表业务层 10 | * @author 黎勇炫 11 | * @date 2022年08月03日 21:40 12 | */ 13 | public interface DataChartService { 14 | 15 | /** 16 | * 获取队列详细数据 17 | * @return java.util.Map 18 | * @author 黎勇炫 19 | * @create 2022/8/3 20 | * @email 1677685900@qq.com 21 | */ 22 | public Map queueData(); 23 | 24 | /** 25 | * 更新数据 26 | * @return void 27 | * @author 黎勇炫 28 | * @create 2022/8/4 29 | * @email 1677685900@qq.com 30 | */ 31 | public void updateData(); 32 | 33 | public List queueDetail(); 34 | } 35 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/core/ServerInitializer.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.core; 2 | 3 | import com.xymq_cli.handler.MessageHandler; 4 | import com.xymq_common.protocol.MessageDecoder; 5 | import com.xymq_common.protocol.MessageEncoder; 6 | import io.netty.channel.ChannelInitializer; 7 | import io.netty.channel.ChannelPipeline; 8 | import io.netty.channel.socket.SocketChannel; 9 | 10 | /** 11 | * 为netty的channel通道添加编解码器和消息处理器 12 | * @author 黎勇炫 13 | * @date 2022年07月09日 17:32 14 | */ 15 | public class ServerInitializer extends ChannelInitializer { 16 | @Override 17 | protected void initChannel(SocketChannel sc) throws Exception { 18 | ChannelPipeline pipeline = sc.pipeline(); 19 | pipeline.addLast("encoder",new MessageEncoder()) 20 | .addLast("decoder",new MessageDecoder()) 21 | .addLast(new MessageHandler()); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/util/Message2Byte.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.util; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.xymq_common.message.Message; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | 8 | /** 9 | * @author 黎勇炫 10 | * @date 2022年07月09日 17:55 11 | */ 12 | public class Message2Byte { 13 | 14 | /** 15 | * 将消息内容转换成字节数组 16 | * @param message 消息对象 17 | * @return byte[] 18 | * @author 黎勇炫 19 | * @create 2022/7/9 20 | * @email 1677685900@qq.com 21 | */ 22 | public static byte[] change(Message message){ 23 | return JSON.toJSONString(message).getBytes(StandardCharsets.UTF_8); 24 | } 25 | 26 | /** 27 | * 将字节数组转换成message对象 28 | * @param bytes 29 | * @return com.xymq.message.Message 30 | * @author 黎勇炫 31 | * @create 2022/7/9 32 | * @email 1677685900@qq.com 33 | */ 34 | public static Message reverse(byte[] bytes){ 35 | return JSON.parseObject(new String(bytes,StandardCharsets.UTF_8),Message.class); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /xymq-server/src/main/resources/static/js/router.js: -------------------------------------------------------------------------------- 1 | // 2.定义路由,每个路由有两部分,path(路径),component(组件) 2 | const routes = [ 3 | { path: "/", component : () => loadModule('./component/home.vue',options),name:'home' }, 4 | { path: "/news", component : () => loadModule('./component/queue.vue',options),name:'news' } 5 | ]; 6 | 7 | //vue3-sfc-loader v0.7.3 8 | const options = { 9 | moduleCache: { 10 | vue: Vue 11 | }, 12 | async getFile(url) { 13 | const res = await fetch(url); 14 | if ( !res.ok ) 15 | throw Object.assign(new Error(res.statusText + ' ' + url), { res }); 16 | return await res.text(); 17 | }, 18 | addStyle(textContent) { 19 | const style = Object.assign(document.createElement('style'), { textContent }); 20 | const ref = document.head.getElementsByTagName('style')[0] || null; 21 | document.head.insertBefore(style, ref); 22 | }, 23 | } 24 | const { loadModule } = window['vue2-sfc-loader']; 25 | var router = new VueRouter({ 26 | // html5模式 去掉锚点 27 | routes:routes 28 | }) 29 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/listener/BootListener.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.listener; 2 | 3 | import com.xymq_cli.core.LevelDb; 4 | import com.xymq_cli.core.XymqServer; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.context.event.ApplicationReadyEvent; 7 | import org.springframework.context.ApplicationEvent; 8 | import org.springframework.context.ApplicationListener; 9 | import org.springframework.stereotype.Component; 10 | 11 | /** 12 | * 监听springboot启动的生命周期,当springboot完成所有bean的实例化和初始化后就启动netty服务 13 | * @author 黎勇炫 14 | * @date 2022年07月09日 18:08 15 | */ 16 | @Component 17 | public class BootListener implements ApplicationListener { 18 | 19 | @Autowired 20 | private XymqServer xymqServer; 21 | @Autowired 22 | private LevelDb levelDb; 23 | 24 | @Override 25 | public void onApplicationEvent(ApplicationEvent event) { 26 | if(event instanceof ApplicationReadyEvent){ 27 | levelDb.initLevelDb(); 28 | xymqServer.init(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/handler/ProducerHandler.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.handler; 2 | 3 | import com.xymq_common.protocol.Protocol; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.SimpleChannelInboundHandler; 6 | 7 | /** 8 | * @author 黎勇炫 9 | * @date 2022年07月14日 20:26 10 | */ 11 | public class ProducerHandler extends SimpleChannelInboundHandler { 12 | 13 | 14 | 15 | /** 16 | * 客户端连接成功就向服务端注册自己 17 | */ 18 | @Override 19 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 20 | register(); 21 | } 22 | 23 | /** 24 | * 向服务器注册自己 25 | */ 26 | private void register() { 27 | } 28 | 29 | @Override 30 | protected void channelRead0(ChannelHandlerContext channelHandlerContext, Protocol protocol) throws Exception { 31 | 32 | } 33 | 34 | @Override 35 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 36 | System.out.println("关闭与服务端的连接"); 37 | ctx.channel().close(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/exception/XyException.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.exception; 2 | 3 | /** 4 | * @author 黎勇炫 5 | * @date 2022年07月10日 13:01 6 | */ 7 | public class XyException extends RuntimeException{ 8 | /** 9 | * 错误码 10 | */ 11 | private Integer code; 12 | 13 | /** 14 | * 信息 15 | */ 16 | private String message; 17 | 18 | public XyException(Integer code, String message) { 19 | super(message); 20 | this.code = code; 21 | this.message = message; 22 | } 23 | 24 | public XyException(ExceptionEnum exceptionEnum){ 25 | super(exceptionEnum.getMessage()); 26 | this.code = exceptionEnum.getCode(); 27 | this.message = exceptionEnum.getMessage(); 28 | } 29 | 30 | public Integer getCode() { 31 | return code; 32 | } 33 | 34 | public void setCode(Integer code) { 35 | this.code = code; 36 | } 37 | 38 | @Override 39 | public String getMessage() { 40 | return message; 41 | } 42 | 43 | public void setMessage(String message) { 44 | this.message = message; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/exception/ExceptionEnum.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.exception; 2 | 3 | /** 4 | * @author 黎勇炫 5 | * @date 2022年07月10日 13:02 6 | */ 7 | public enum ExceptionEnum { 8 | 9 | /** 10 | * 1开头为处理器执行异常 11 | */ 12 | ECEC_STRETEGY_NOT_FOUNT(101,"找不到执行处理器"), 13 | /** 14 | * 1开头为leveldb异常 15 | */ 16 | LEVELDB_INIT_ERROR(201,"levelDb初始化失败"), 17 | FAILED_TO_STORAGE(202,"消息持久化失败"), 18 | FAILED_TO_RECOVERY_DATA(203,"消息时恢复发生异常"), 19 | FAILED_TO_CLEAN_DATA(204,"数据清理失败"), 20 | FAILED_TO_GET_KEYS(205,"获取keys失败"), 21 | FAILED_TO_CLOSE_DB(206,"levelDb关闭失败"); 22 | 23 | 24 | 25 | 26 | 27 | private Integer code; 28 | private String message; 29 | 30 | ExceptionEnum(Integer code, String message) { 31 | this.code = code; 32 | this.message = message; 33 | } 34 | 35 | public Integer getCode() { 36 | return code; 37 | } 38 | 39 | public void setCode(Integer code) { 40 | this.code = code; 41 | } 42 | 43 | public String getMessage() { 44 | return message; 45 | } 46 | 47 | public void setMessage(String message) { 48 | this.message = message; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/execution/ExecutionFactory.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.execution; 2 | 3 | import com.xymq_cli.exception.ExceptionEnum; 4 | import com.xymq_cli.exception.XyException; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | 12 | /** 13 | * @author 黎勇炫 14 | * @date 2022年07月10日 12:44 15 | */ 16 | @Component 17 | public class ExecutionFactory { 18 | 19 | /** 20 | * 存放不同的策略实现 21 | */ 22 | public static List strategies = new ArrayList<>(); 23 | 24 | /** 25 | * 通过构造器动态传参,自动把所有的策略实现加载进工厂 26 | */ 27 | @Autowired 28 | public ExecutionFactory(List strategies) { 29 | ExecutionFactory.strategies = strategies; 30 | } 31 | 32 | /** 33 | * 根据类型返回登录策略 34 | */ 35 | public static Execution getStrategy(int type) { 36 | for (Execution strategy : strategies) { 37 | if(strategy.getType() == type){ 38 | return strategy; 39 | } 40 | } 41 | throw new XyException(ExceptionEnum.ECEC_STRETEGY_NOT_FOUNT); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/handler/XyTextWebSocketFrameHandler.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.handler; 2 | 3 | import com.xymq_cli.web.service.impl.DataChartServiceImpl; 4 | import io.netty.channel.ChannelHandlerContext; 5 | import io.netty.channel.SimpleChannelInboundHandler; 6 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 7 | 8 | /** 9 | * @author 黎勇炫 10 | * @date 2022年08月04日 17:11 11 | */ 12 | public class XyTextWebSocketFrameHandler extends SimpleChannelInboundHandler { 13 | @Override 14 | protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame textWebSocketFrame) throws Exception { 15 | 16 | } 17 | 18 | /** 19 | * 当有新的连接加入时就将channel添加到指定容器 20 | */ 21 | @Override 22 | public void handlerAdded(ChannelHandlerContext ctx) throws Exception { 23 | DataChartServiceImpl.putChannel(ctx.channel()); 24 | } 25 | 26 | @Override 27 | public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { 28 | 29 | } 30 | 31 | @Override 32 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 33 | System.out.println("发生异常"+cause.getMessage()); 34 | ctx.close(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/client/initializer/ProducerlInitializer.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.client.initializer; 2 | 3 | import com.xymq_cli.handler.ProducerHandler; 4 | import com.xymq_common.protocol.MessageDecoder; 5 | import com.xymq_common.protocol.MessageEncoder; 6 | import io.netty.channel.ChannelInitializer; 7 | import io.netty.channel.ChannelPipeline; 8 | import io.netty.channel.socket.SocketChannel; 9 | 10 | /** 11 | * @author 黎勇炫 12 | * @date 2022年07月14日 20:24 13 | */ 14 | public class ProducerlInitializer extends ChannelInitializer { 15 | 16 | private ProducerHandler producerHandler; 17 | 18 | public ProducerlInitializer(ProducerHandler producerHandler) { 19 | this.producerHandler = producerHandler; 20 | } 21 | 22 | /** 23 | * 该方法主要是为客户端channel设置编解码器以及消息处理器 24 | * @param sc netty通道 25 | * @return void 26 | * @author 黎勇炫 27 | * @create 2022/7/11 28 | * @email 1677685900@qq.com 29 | */ 30 | @Override 31 | protected void initChannel(SocketChannel sc) throws Exception { 32 | ChannelPipeline pipeline = sc.pipeline(); 33 | pipeline.addLast("encoder",new MessageEncoder()) 34 | .addLast("decoder",new MessageDecoder()) 35 | .addLast(this.producerHandler); 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /xymq-common/src/main/java/com/xymq_common/protocol/MessageUtils.java: -------------------------------------------------------------------------------- 1 | package com.xymq_common.protocol; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.xymq_common.message.Message; 5 | 6 | import java.nio.charset.StandardCharsets; 7 | 8 | /** 9 | * @author 黎勇炫 10 | * @date 2022年07月09日 17:55 11 | */ 12 | public class MessageUtils { 13 | 14 | /** 15 | * 将消息内容转换成字节数组 16 | * @param message 消息对象 17 | * @return byte[] 18 | * @author 黎勇炫 19 | * @create 2022/7/9 20 | * @email 1677685900@qq.com 21 | */ 22 | public static byte[] change(Message message){ 23 | return JSON.toJSONString(message).getBytes(StandardCharsets.UTF_8); 24 | } 25 | 26 | /** 27 | * 将字节数组转换成message对象 28 | * @param bytes 29 | * @return com.xymq.message.Message 30 | * @author 黎勇炫 31 | * @create 2022/7/9 32 | * @email 1677685900@qq.com 33 | */ 34 | public static Message reverse(byte[] bytes){ 35 | return JSON.parseObject(new String(bytes,StandardCharsets.UTF_8),Message.class); 36 | } 37 | 38 | public static Protocol message2Protocol(Message message){ 39 | byte[] content = JSON.toJSONString(message).getBytes(StandardCharsets.UTF_8); 40 | Protocol protocol = new Protocol(content.length,content); 41 | return protocol; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/client/initializer/ConsumerlInitializer.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.client.initializer; 2 | 3 | import com.xymq_cli.handler.ConsumerHandler; 4 | import com.xymq_common.protocol.MessageDecoder; 5 | import com.xymq_common.protocol.MessageEncoder; 6 | import io.netty.channel.ChannelInitializer; 7 | import io.netty.channel.ChannelPipeline; 8 | import io.netty.channel.socket.SocketChannel; 9 | 10 | /** 11 | * 消费者初始化器 12 | * @author 黎勇炫 13 | * @date 2022年07月11日 15:40 14 | */ 15 | public class ConsumerlInitializer extends ChannelInitializer { 16 | 17 | private String destination; 18 | private ConsumerHandler consumerHandler; 19 | 20 | public ConsumerlInitializer(ConsumerHandler consumerHandler){ 21 | this.consumerHandler = consumerHandler; 22 | } 23 | 24 | /** 25 | * 该方法主要是为客户端channel设置编解码器以及消息处理器 26 | * @param sc netty通道 27 | * @return void 28 | * @author 黎勇炫 29 | * @create 2022/7/11 30 | * @email 1677685900@qq.com 31 | */ 32 | @Override 33 | protected void initChannel(SocketChannel sc) throws Exception { 34 | ChannelPipeline pipeline = sc.pipeline(); 35 | pipeline.addLast("encoder",new MessageEncoder()) 36 | .addLast("decoder",new MessageDecoder()) 37 | .addLast(consumerHandler); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /xymq-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.xymq 8 | xymq-common 9 | 0.0.1 10 | xymq-common 11 | xymq服务端 12 | 13 | 14 | 8 15 | 8 16 | 4.1.21.Final 17 | 1.2.76 18 | 19 | 20 | 21 | 22 | 23 | io.netty 24 | netty-all 25 | ${netty.version} 26 | compile 27 | 28 | 29 | 30 | com.alibaba 31 | fastjson 32 | ${json.version} 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/web/controller/DataController.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.web.controller; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.xymq_cli.web.service.DataChartService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | /** 11 | * @author 黎勇炫 12 | * @date 2022年08月01日 22:15 13 | */ 14 | @RestController 15 | @RequestMapping("/xy/data") 16 | public class DataController { 17 | 18 | @Autowired 19 | private DataChartService dataChartService; 20 | 21 | /** 22 | * 可视化图表-队列数据概览接口 23 | * @return java.lang.String 24 | * @author 黎勇炫 25 | * @create 2022/8/14 26 | * @email 1677685900@qq.com 27 | */ 28 | @GetMapping("/queue") 29 | public String queueCharts(){ 30 | return JSON.toJSONString(dataChartService.queueData()); 31 | } 32 | 33 | /** 34 | * 获取队列详情列表 35 | * @return java.lang.String 36 | * @author 黎勇炫 37 | * @create 2022/8/14 38 | * @email 1677685900@qq.com 39 | */ 40 | @GetMapping("/list") 41 | public String queueDetail(){ 42 | return JSON.toJSONString(dataChartService.queueDetail()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/config/MQServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.config; 2 | 3 | import com.xymq_cli.core.LevelDbStorageHelper; 4 | import com.xymq_cli.core.StorageHelper; 5 | import com.xymq_cli.util.SnowflakeIdUtils; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | /** 11 | * 消息队列服务端配置类 12 | * @author 黎勇炫 13 | * @date 2022年07月10日 14:21 14 | */ 15 | @Configuration 16 | @ConfigurationProperties(prefix = "xymq.storage") 17 | public class MQServerConfig { 18 | 19 | private String type; 20 | 21 | /** 22 | * id生成 23 | * @return com.xymq.util.SnowflakeIdUtils 24 | * @author 黎勇炫 25 | * @create 2022/7/10 26 | * @email 1677685900@qq.com 27 | */ 28 | @Bean 29 | public SnowflakeIdUtils snowflakeIdUtils(){ 30 | return new SnowflakeIdUtils(9,12); 31 | } 32 | 33 | @Bean 34 | public StorageHelper storageHelper(){ 35 | if(type.equals("levelDb")){ 36 | return new LevelDbStorageHelper(); 37 | } 38 | return null; 39 | } 40 | 41 | public String getType() { 42 | return type; 43 | } 44 | 45 | public void setType(String type) { 46 | this.type = type; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/client/initializer/SubscriberInitializer.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.client.initializer; 2 | 3 | import com.xymq_cli.handler.SubscriberHandler; 4 | import com.xymq_common.protocol.MessageDecoder; 5 | import com.xymq_common.protocol.MessageEncoder; 6 | import io.netty.channel.ChannelInitializer; 7 | import io.netty.channel.ChannelPipeline; 8 | import io.netty.channel.socket.SocketChannel; 9 | 10 | /** 11 | * @author 黎勇炫 12 | * @date 2022年07月17日 16:56 13 | */ 14 | public class SubscriberInitializer extends ChannelInitializer { 15 | 16 | /** 17 | * 订阅者消息处理器 18 | */ 19 | private SubscriberHandler subscriberHandler; 20 | 21 | public SubscriberInitializer(SubscriberHandler subscriberHandler) { 22 | this.subscriberHandler = subscriberHandler; 23 | } 24 | 25 | /** 26 | * 该方法主要是为客户端channel设置编解码器以及消息处理器 27 | * @param sc netty通道 28 | * @return void 29 | * @author 黎勇炫 30 | * @create 2022/7/11 31 | * @email 1677685900@qq.com 32 | */ 33 | @Override 34 | protected void initChannel(SocketChannel sc) throws Exception { 35 | ChannelPipeline pipeline = sc.pipeline(); 36 | pipeline.addLast("encoder",new MessageEncoder()); 37 | pipeline.addLast("decoder",new MessageDecoder()); 38 | pipeline.addLast(subscriberHandler); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/config/MvcConfig.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.config; 2 | 3 | import com.xymq_cli.interceptor.AccessInterceptor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.HandlerInterceptor; 6 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 7 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; 9 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 10 | 11 | /** 12 | * @author 黎勇炫 13 | * @date 2022年07月25日 22:47 14 | */ 15 | @Configuration 16 | public class MvcConfig extends WebMvcConfigurationSupport { 17 | // 放行静态资源 18 | 19 | 20 | @Override 21 | protected void addInterceptors(InterceptorRegistry registry) { 22 | registry.addInterceptor(new AccessInterceptor()).addPathPatterns("/**") 23 | .excludePathPatterns("/","/index","/index.html","/templates/**","/static/**","/**/*.woff","/**/*.ttf","/**/*.svg","/**/*.js"); 24 | } 25 | 26 | @Override 27 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 28 | registry.addResourceHandler("templates/**").addResourceLocations("classpath:/templates/"); 29 | registry.addResourceHandler("static/**").addResourceLocations("classpath:/static/"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/execution/AckExec.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.execution; 2 | 3 | import com.xymq_cli.constant.Destination; 4 | import com.xymq_cli.constant.MessageConstant; 5 | import com.xymq_cli.core.LevelDb; 6 | import com.xymq_common.message.Message; 7 | import io.netty.channel.Channel; 8 | import lombok.Data; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Component; 11 | 12 | /** 13 | * @author 黎勇炫 14 | * @date 2022年07月10日 12:42 15 | */ 16 | @Component 17 | @Data 18 | public class AckExec implements Execution{ 19 | 20 | @Autowired 21 | private LevelDb levelDb; 22 | /** 23 | * 消费成功的队列消息数量 24 | */ 25 | private long queueMessageCount = 0; 26 | 27 | /** 28 | * 执行操作(消费、推送和签收) 29 | * @param message 30 | * @return void 31 | * @author 黎勇炫 32 | * @create 2022/7/10 33 | * @email 1677685900@qq.com 34 | */ 35 | @Override 36 | public void exec(Message message, Channel channel) { 37 | // 消费成功次数++(主题消费不走签收) 38 | queueMessageCount++; 39 | // 直接操作数据库删除消息 40 | levelDb.deleteMessageBean(message.getMessageId()); 41 | } 42 | 43 | /** 44 | * 返回当前策略支持的 消息 类型 45 | * 46 | * @return int 消息类型 47 | * @author 黎勇炫 48 | * @create 2022/7/10 49 | * @email 1677685900@qq.com 50 | */ 51 | @Override 52 | public int getType() { 53 | return MessageConstant.ACK; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /xymq-server/src/main/resources/templates/components/queue.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 59 | 60 | 63 | -------------------------------------------------------------------------------- /xymq-cli/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.xymq 8 | xymq-cli 9 | 1.0 10 | xymq客户端 11 | 12 | 13 | 8 14 | 8 15 | 1.7.30 16 | 17 | 18 | 19 | 20 | 21 | com.xymq 22 | xymq-common 23 | 0.0.1 24 | 25 | 26 | 27 | org.slf4j 28 | slf4j-api 29 | ${slf4j.version} 30 | 31 | 32 | org.slf4j 33 | jul-to-slf4j 34 | ${slf4j.version} 35 | 36 | 37 | org.slf4j 38 | slf4j-nop 39 | ${slf4j.version} 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/core/WebsocketInitializer.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.core; 2 | 3 | import com.xymq_cli.handler.XyTextWebSocketFrameHandler; 4 | import io.netty.channel.ChannelInitializer; 5 | import io.netty.channel.ChannelPipeline; 6 | import io.netty.channel.socket.SocketChannel; 7 | import io.netty.handler.codec.http.HttpClientCodec; 8 | import io.netty.handler.codec.http.HttpObjectAggregator; 9 | import io.netty.handler.codec.http.HttpServerCodec; 10 | import io.netty.handler.codec.http.websocketx.WebSocketClientProtocolHandler; 11 | import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler; 12 | import io.netty.handler.stream.ChunkedWriteHandler; 13 | 14 | /** 15 | * @author 黎勇炫 16 | * @date 2022年08月04日 15:59 17 | */ 18 | public class WebsocketInitializer extends ChannelInitializer { 19 | @Override 20 | protected void initChannel(SocketChannel sc) throws Exception { 21 | ChannelPipeline pipeline = sc.pipeline(); 22 | // 使用http的编解码器 23 | pipeline.addLast(new HttpServerCodec()) 24 | // 以块方式写,添加ChunkedWriteHandler处理器 25 | .addLast(new ChunkedWriteHandler()) 26 | /** 27 | * http在传输过程中是以块传输的,HttpObjectAggregator可以将这些块聚合起来 28 | * 这就是为什么http在发送大量请求时会发起多个请求 29 | */ 30 | .addLast(new HttpObjectAggregator(8192)) 31 | // websocket在传输中是以帧(frame)的形式传输的。请求ws://127.0.0.1:8687/connect 32 | .addLast(new WebSocketServerProtocolHandler("/connect")) 33 | .addLast(new XyTextWebSocketFrameHandler()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/handler/MessageHandler.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.handler; 2 | 3 | import com.xymq_common.message.Message; 4 | import com.xymq_cli.execution.Execution; 5 | import com.xymq_cli.execution.ExecutionFactory; 6 | import com.xymq_cli.util.Message2Byte; 7 | import com.xymq_common.protocol.Protocol; 8 | import io.netty.channel.ChannelHandlerContext; 9 | import io.netty.channel.SimpleChannelInboundHandler; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | /** 14 | * @author 黎勇炫 15 | * @date 2022年07月09日 17:57 16 | */ 17 | public class MessageHandler extends SimpleChannelInboundHandler { 18 | /** 19 | * 日志 20 | */ 21 | private static final Logger log = LoggerFactory.getLogger(MessageHandler.class); 22 | 23 | @Override 24 | protected void channelRead0(ChannelHandlerContext ctx, Protocol protocol) throws Exception { 25 | // 将字节数组转换成消息对象 26 | Message msg = Message2Byte.reverse(protocol.getContent()); 27 | // 判断是消费者还是消息推送者,如果是消费者就将channel保存到容器,如果是推送者就将消息存储到对应的队列中,如果是签收消息就将消息在leveldb中移除 28 | Execution execution = ExecutionFactory.getStrategy(msg.getType()); 29 | execution.exec(msg,ctx.channel()); 30 | } 31 | 32 | /** 33 | * 出现异常就关闭客户端连接 34 | * @param ctx channelhandler上下文 35 | * @param cause 36 | * @return void 37 | * @author 黎勇炫 38 | * @create 2022/7/10 39 | * @email 1677685900@qq.com 40 | */ 41 | @Override 42 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 43 | log.error("客户端{}断开连接---原因:{}",ctx.channel().remoteAddress(),cause.getMessage()); 44 | ctx.channel().close(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/core/StorageHelper.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.core; 2 | 3 | import com.xymq_common.message.Message; 4 | 5 | import java.util.concurrent.ConcurrentHashMap; 6 | import java.util.concurrent.DelayQueue; 7 | import java.util.concurrent.LinkedBlockingDeque; 8 | 9 | /** 10 | * @author 黎勇炫 11 | * @date 2022年07月10日 14:43 12 | */ 13 | public interface StorageHelper { 14 | /** 15 | * 存储消息到队列消息容器 16 | * @param queueContainer 队列消息容器 17 | * @param message 消息对象 18 | * @return void 19 | * @author 黎勇炫 20 | * @create 2022/7/10 21 | * @email 1677685900@qq.com 22 | */ 23 | public void storeQueueMessage(ConcurrentHashMap> queueContainer, Message message); 24 | 25 | /** 26 | * 存储消息到主题消息容器 27 | * @param topicContainer 主题消息容器 28 | * @param message 消息对象 29 | * @return void 30 | * @author 黎勇炫 31 | * @create 2022/7/10 32 | * @email 1677685900@qq.com 33 | */ 34 | public void storeTopicMessage(ConcurrentHashMap> topicContainer, Message message); 35 | 36 | /** 37 | * 存储消息到延时队列容器 38 | * @param delayQueueMap 延时队列容器 39 | * @param message 消息对象 40 | * @return void 41 | * @author 黎勇炫 42 | * @create 2022/7/10 43 | * @email 1677685900@qq.com 44 | */ 45 | public void storeDelayMessage(ConcurrentHashMap> delayQueueMap, Message message); 46 | 47 | /** 48 | * 存储消息到延时主题消息容器 49 | * @param delayTopicMap 主题消息容器 50 | * @param message 消息对象 51 | * @return void 52 | * @author 黎勇炫 53 | * @create 2022/7/10 54 | * @email 1677685900@qq.com 55 | */ 56 | public void storeDelayTopicMessage(ConcurrentHashMap> delayTopicMap, Message message); 57 | } 58 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/util/ResourceUtils.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.util; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import java.io.*; 7 | import java.util.Map; 8 | import java.util.PropertyResourceBundle; 9 | import java.util.ResourceBundle; 10 | 11 | 12 | /** 13 | * @author 黎勇炫 14 | * @date 2022年07月11日 14:33 15 | */ 16 | public class ResourceUtils { 17 | private static final Logger logger = LoggerFactory.getLogger(ResourceUtils.class); 18 | 19 | /** 20 | * 属性文件全名 21 | */ 22 | private static final String PFILE =System.getProperty("user.dir")+File.separator+"conf"+File.separator+"xymq-cli.properties"; 23 | /** 24 | * 对应于属性文件的文件对象变量 25 | */ 26 | private static File m_file = null; 27 | /** 28 | * 属性文件的最后修改日期 29 | */ 30 | private static long m_lastModifiedTime = 0; 31 | private static ResourceBundle rb; 32 | 33 | private static BufferedInputStream inputStream; 34 | static { 35 | try { 36 | m_file = new File(PFILE); 37 | m_lastModifiedTime = m_file.lastModified(); 38 | inputStream = new BufferedInputStream(new FileInputStream(PFILE)); 39 | rb = new PropertyResourceBundle(inputStream); 40 | inputStream.close(); 41 | } catch (FileNotFoundException e ) { 42 | logger.error("配置文件xymq-cli.properties不存在"); 43 | e.printStackTrace(); 44 | } catch (IOException e) { 45 | logger.error("系统启动失败"); 46 | e.printStackTrace(); 47 | } 48 | } 49 | 50 | public static String getKey(String key){ 51 | long newTime = m_file.lastModified(); 52 | if(newTime > m_lastModifiedTime){ 53 | // Get rid of the old properties 54 | try { 55 | m_lastModifiedTime = m_file.lastModified(); 56 | inputStream = new BufferedInputStream(new FileInputStream(PFILE)); 57 | rb = new PropertyResourceBundle(inputStream); 58 | inputStream.close(); 59 | } catch (FileNotFoundException e) { 60 | logger.error("配置文件xymq-cli.properties不存在"); 61 | e.printStackTrace(); 62 | } catch (IOException e) { 63 | logger.error("系统启动失败"); 64 | e.printStackTrace(); 65 | } 66 | } 67 | String result=rb.getString(key); 68 | 69 | return result; 70 | } 71 | 72 | } -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/handler/SubscriberHandler.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.handler; 2 | 3 | import com.xymq_cli.constant.Destination; 4 | import com.xymq_cli.constant.MessageType; 5 | import com.xymq_cli.listener.MessageData; 6 | import com.xymq_cli.listener.MessageListener; 7 | import com.xymq_common.message.Message; 8 | import com.xymq_common.protocol.MessageUtils; 9 | import com.xymq_common.protocol.Protocol; 10 | import io.netty.channel.Channel; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import io.netty.channel.SimpleChannelInboundHandler; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import java.io.File; 17 | import java.util.Map; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | /** 21 | * 订阅者的消息处理器 22 | * @author 黎勇炫 23 | * @date 2022年07月17日 16:59 24 | */ 25 | public class SubscriberHandler extends SimpleChannelInboundHandler { 26 | 27 | /** 28 | * 订阅的主题 29 | */ 30 | private String destination; 31 | /** 32 | * 订阅者唯一标识 33 | */ 34 | private long subscriberId; 35 | /** 36 | * 监听器 37 | */ 38 | private MessageListener messageListener; 39 | 40 | /** 41 | * 日志信息 42 | */ 43 | private Logger logger = LoggerFactory.getLogger(SubscriberHandler.class); 44 | 45 | public SubscriberHandler(String destination, long subscriberId) { 46 | this.destination = destination; 47 | this.subscriberId = subscriberId; 48 | } 49 | 50 | @Override 51 | protected void channelRead0(ChannelHandlerContext ctx, Protocol protocol) throws Exception { 52 | // 将字节数组转换成消息对象 53 | Message message = MessageUtils.reverse(protocol.getContent()); 54 | if(message != null){ 55 | execListener(new MessageData(this,message.getContent())); 56 | } 57 | } 58 | 59 | @Override 60 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 61 | // 向服务端注册该订阅者 62 | retister(ctx.channel()); 63 | } 64 | 65 | /** 66 | * 构建一个消息对象,向服务端注册自己 67 | * @return void 68 | * @author 黎勇炫 69 | * @create 2022/7/17 70 | * @email 1677685900@qq.com 71 | */ 72 | private void retister(Channel channel) { 73 | Message message = new Message(subscriberId, MessageType.COMSUMER.getType(), null, this.destination, Destination.TOPIC.getDestination(), false,0, TimeUnit.MILLISECONDS); 74 | channel.writeAndFlush(MessageUtils.message2Protocol(message)); 75 | } 76 | 77 | /** 78 | * 消息监听器 79 | */ 80 | public void createListener(MessageListener messageListener){ 81 | this.messageListener = messageListener; 82 | } 83 | 84 | /** 85 | * 执行监听事件 86 | */ 87 | private void execListener(MessageData data) { 88 | this.messageListener.getMessage(data); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /xymq-server/src/main/resources/templates/components/menu.js: -------------------------------------------------------------------------------- 1 | var menu = Vue.extend({ 2 | props:{ 3 | index:['index'] 4 | }, 5 | template: ' \n' + 6 | ' \n' + 7 | ' \n' + 14 | '
\n' + 15 | ' \n' + 16 | ' \n' + 17 | ' 容器概览\n' + 18 | ' \n' + 19 | ' \n' + 20 | ' \n' + 21 | ' 队列详情\n' + 22 | ' \n' + 23 | ' \n' + 24 | ' \n' + 25 | ' 主题详情\n' + 26 | ' \n' + 27 | ' \n' + 28 | ' \n' + 29 | ' 使用Demo\n' + 30 | ' \n' + 31 | '
\n' + 32 | '
\n' + 33 | ' \n' + 34 | ' \n' + 35 | ' \n' + 36 | ' \n' + 37 | ' \n' + 38 | ' \n' + 39 | ' \t\t\t \t\n' + 40 | '\t\t\t \t\n' + 41 | '\t\t\t\t\t \n' + 42 | '\t\t\t\t\t \n' + 43 | '\t\t\t\t\t \t\n' + 44 | '\t\t\t\t\t \t\n' + 45 | '\t\t\t\t\t \n' + 46 | '\t\t\t\t\t \n' + 47 | '\t\t\t \t\n' + 48 | ' \n' + 49 | '
' 50 | }); -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/config/ThreadConfig.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.scheduling.TaskScheduler; 7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 8 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 9 | 10 | import java.util.concurrent.ThreadPoolExecutor; 11 | 12 | /** 13 | * @author 黎勇炫 14 | * @date 2022年07月10日 12:25 15 | */ 16 | @Configuration 17 | @ConfigurationProperties(prefix = "xymq.thread") 18 | public class ThreadConfig { 19 | /** 20 | * 核心线程池大小 21 | */ 22 | private int corePoolSize = 65; 23 | 24 | /** 25 | * 最大可创建的线程数 26 | */ 27 | private int maxPoolSize = 200; 28 | 29 | /** 30 | * 队列最大长度 31 | */ 32 | private int queueCapacity = 1000; 33 | 34 | /** 35 | * 线程池维护线程所允许的空闲时间 36 | */ 37 | private int keepAliveSeconds = 300; 38 | 39 | @Bean(name = "taskExecutor") 40 | public ThreadPoolTaskExecutor threadPoolTaskExecutor() 41 | { 42 | ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); 43 | executor.setMaxPoolSize(maxPoolSize); 44 | executor.setCorePoolSize(corePoolSize); 45 | executor.setQueueCapacity(queueCapacity); 46 | executor.setKeepAliveSeconds(keepAliveSeconds); 47 | // 线程池对拒绝任务(无线程可用)的处理策略 48 | executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); 49 | return executor; 50 | } 51 | 52 | /** 53 | * springboot定时任务线程 54 | */ 55 | @Bean 56 | public TaskScheduler taskScheduler() { 57 | ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); 58 | taskScheduler.setPoolSize(10); 59 | return taskScheduler; 60 | } 61 | 62 | public int getCorePoolSize() { 63 | return corePoolSize; 64 | } 65 | 66 | public void setCorePoolSize(int corePoolSize) { 67 | this.corePoolSize = corePoolSize; 68 | } 69 | 70 | public int getMaxPoolSize() { 71 | return maxPoolSize; 72 | } 73 | 74 | public void setMaxPoolSize(int maxPoolSize) { 75 | this.maxPoolSize = maxPoolSize; 76 | } 77 | 78 | public int getQueueCapacity() { 79 | return queueCapacity; 80 | } 81 | 82 | public void setQueueCapacity(int queueCapacity) { 83 | this.queueCapacity = queueCapacity; 84 | } 85 | 86 | public int getKeepAliveSeconds() { 87 | return keepAliveSeconds; 88 | } 89 | 90 | public void setKeepAliveSeconds(int keepAliveSeconds) { 91 | this.keepAliveSeconds = keepAliveSeconds; 92 | } 93 | } 94 | 95 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/core/ClientManager.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.core; 2 | 3 | import com.xymq_common.message.Message; 4 | import io.netty.channel.Channel; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.util.CollectionUtils; 9 | 10 | import java.nio.channels.SocketChannel; 11 | import java.util.*; 12 | import java.util.concurrent.CompletableFuture; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | 15 | /** 16 | * 管理客户端-及时清理下线的客户端 17 | * @author 黎勇炫 18 | * @date 2022年07月19日 15:22 19 | */ 20 | @Component 21 | public class ClientManager { 22 | 23 | @Autowired 24 | private ThreadPoolTaskExecutor taskExecutor; 25 | @Autowired 26 | private LevelDb levelDb; 27 | 28 | /** 29 | * 存储离线的消费者和未消费的信息数据 30 | */ 31 | public void storeOfflineData(ConcurrentHashMap> offLineTopicMessage,ConcurrentHashMap> offLineSubscriber){ 32 | CompletableFuture.runAsync(()->{ 33 | if (CollectionUtils.isEmpty(offLineSubscriber)) { 34 | levelDb.storeOffLineMessage(offLineTopicMessage); 35 | } 36 | if (null != offLineSubscriber) { 37 | ConcurrentHashMap> cpOffLineSubscriber = new ConcurrentHashMap<>(); 38 | cpOffLineSubscriber.putAll(offLineSubscriber); 39 | // for (ConcurrentHashMap.Entry> stringHashMapEntry : cpOffLineSubscriber.entrySet()) { 40 | // for (ConcurrentHashMap.Entry longSocketChannelEntry : stringHashMapEntry.getValue().entrySet()) { 41 | // longSocketChannelEntry.setValue(SocketChannel.open()); 42 | // } 43 | // } 44 | if(!CollectionUtils.isEmpty(cpOffLineSubscriber)){ 45 | levelDb.storeOffLineSubscriber(cpOffLineSubscriber); 46 | } 47 | } 48 | },taskExecutor); 49 | } 50 | 51 | /** 52 | * 向消费这发送心跳包,如果消费者离线,就把消费者从消费队列中剔除,放入离线队列中 53 | */ 54 | public void clean(ConcurrentHashMap> consumerContainer) { 55 | CompletableFuture.runAsync(()->{ 56 | for (Map.Entry> entry : consumerContainer.entrySet()) { 57 | List list = entry.getValue(); 58 | Iterator iterator = list.iterator(); 59 | while (iterator.hasNext()) { 60 | Channel client = (Channel) iterator.next(); 61 | if (!client.isActive()) { 62 | System.out.println("清理离线客户端"); 63 | iterator.remove(); 64 | } 65 | } 66 | } 67 | },taskExecutor); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XY-MQ 2 | 一款基于Netty+SpringBoot+LevelDB实现的拥有高性能、高并发、高可靠性、轻量级的消息队列,支持 队列消息、主题消息、延时消息、消息持久化、消息插队。简单易用,上手快。延时消息可以设置到毫秒。采用高效的kv数据库Leveldb进行数据持久化,JDK原生阻塞队列作为内存级容器,实现同步存储、异步分发,每秒可生产和消费50000+条消息。 3 | 4 | ### 系统架构 5 | 6 | ![image-20220722143333878](https://github.com/Lyx0912/XY-MQ/blob/main/doc/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84.png) 7 | 8 | 持久化模型 9 | 10 | ![image-20220722143333879](https://github.com/Lyx0912/XY-MQ/blob/main/doc/%E6%B6%88%E6%81%AF%E6%8C%81%E4%B9%85%E5%8C%96%E6%A8%A1%E5%9E%8B.png) 11 | 12 | 可视化面板 13 | 14 | ![image-20220722143333880](https://github.com/Lyx0912/XY-MQ/blob/main/doc/%E5%8F%AF%E8%A7%86%E5%8C%96%E9%9D%A2%E6%9D%BF.jpg) 15 | 16 | 17 | 18 | ### 优点 19 | 20 | 1.高性能,基于netty实现数据通信+高效的kv数据库Leveldb存储,快的飞起! 21 | 22 | 2.支持多种消息类型,例如队列消息、主题消息、延时消息,还可以设置优先级。 23 | 24 | 3.系统体积小,整个系统才不到3M。 25 | 26 | 4.支持高并发。 27 | 28 | ### 待优化 29 | 30 | 1.延迟消息BUG:延时消息基于jdk自带的delayQueue实现,系统宕机重启后服务端读取leveldb中的消息后将消息重新放回延时队列,会重新设置到期时间。例如:设置一条消息5分钟后推送,中途系统宕机,系统重启后会从当前时间开始重新计时5分钟。 31 | 32 | 2.异步推送下消息乱序:原先设想是每条队列来消息时,就会交给线程池专门用一条线程负责推送这条队列的消息,直到消息推送完毕。在推送时有新的消息进入容器可能会出现多个线程推送同一个队列的情况,造成消息乱序。而且这种推送模式在队列太多的情况下反而会影响性能,后面考虑使用单线程推送。 33 | 34 | 3.可视化界面:需要一个Web端可视化界面监控系统的运行情况。 35 | 36 | 37 | 38 | ### 使用Demo 39 | 40 | ##### 生产者 41 | 42 | `` 43 | 44 | ```java 45 | /** 46 | * 生产者实例 47 | */ 48 | public static void main(String[] args) { 49 | // 创建生产者 50 | Producer producer = new Producer(); 51 | // 推送普通的队列消息 52 | producer.sendMsg("你好,我是队列消息","queue"); 53 | // 推送主题消息 54 | producer.publish("你好,我是主题消息","topic"); 55 | // 推送延迟消息,设置延迟数和单位,消息会在5分钟后推送给消费者 56 | producer.sendDelayMessage("你好,我是延时队列消息","queueDelayM",5,TimeUnit.SECONDS); 57 | // 推送延迟主题消息 58 | producer.sendDelayMessage("你好,我是延时主题消息","queueDelayT",5,TimeUnit.SECONDS); 59 | // 设置优先级,消息会插入到队列头 60 | producer.sendPriorityMessage("你好,我是队列消息","queue"); 61 | } 62 | ``` 63 | 64 | ##### 消费者 65 | 66 | `` 67 | 68 | ```java 69 | /** 70 | * 消费者示例 71 | */ 72 | public static void main(String[] args) { 73 | // 指定‘queue’队列 74 | Consumer consumer = new Consumer("queue"); 75 | // 构建监听器 76 | consumer.createListener(new MessageListener() { 77 | @Override 78 | public void getMessage(MessageData data) { 79 | // 监听到消息会进入MessageListener监听器中 80 | System.out.println(data.getMessage()); 81 | } 82 | }).run(); 83 | // 关闭消费者 84 | consumer.close(); 85 | } 86 | ``` 87 | 88 | ##### 订阅者 89 | 90 | `` 91 | 92 | ```java 93 | /** 94 | * 订阅者消费示例 95 | */ 96 | public static void main(String[] args) { 97 | // 构建订阅者订阅'topic'主题,订阅者编号为1 98 | Subscriber subscriber = new Subscriber("topic",1); 99 | subscriber.createListener(new MessageListener() { 100 | @Override 101 | public void getMessage(MessageData data) { 102 | System.out.println(data.getMessage()); 103 | } 104 | }).run(); 105 | 106 | // 关闭订阅者 107 | subscriber.close(); 108 | } 109 | ``` 110 | 111 | 112 | 113 | ### 114 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/web/controller/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.web.controller; 2 | 3 | import org.apache.commons.io.FilenameUtils; 4 | import org.apache.commons.io.IOUtils; 5 | import org.springframework.core.io.ClassPathResource; 6 | import org.springframework.http.HttpHeaders; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.ModelMap; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.RequestMapping; 14 | 15 | import javax.servlet.http.HttpServletRequest; 16 | import javax.servlet.http.HttpServletResponse; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.io.UnsupportedEncodingException; 20 | import java.net.URLEncoder; 21 | 22 | /** 23 | * 可视化界面主页 24 | * @author 黎勇炫 25 | * @date 2022年07月25日 21:46 26 | */ 27 | @Controller 28 | public class IndexController { 29 | 30 | @RequestMapping({"/index","/"}) 31 | public String index(){ 32 | return "index"; 33 | } 34 | 35 | /** 36 | * vue组件加载 37 | * @return 38 | */ 39 | @RequestMapping("/component/{name}") 40 | public ResponseEntity component(@PathVariable String name,HttpServletRequest request, HttpServletResponse response){//throws Exception{ 41 | ClassPathResource classPathResource = new ClassPathResource("templates/components/"+name); 42 | if (classPathResource.exists()) { 43 | try (InputStream inputStream = classPathResource.getInputStream()){ 44 | byte[] bytes = IOUtils.toByteArray(inputStream); 45 | return downloadResponse(bytes, FilenameUtils.getName(name),request); 46 | } catch (IOException e) { 47 | e.printStackTrace(); 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | public static ResponseEntity downloadResponse(byte[] body, String fileName, HttpServletRequest request) { 55 | 56 | String header = request.getHeader("User-Agent").toUpperCase(); 57 | HttpStatus status = HttpStatus.CREATED; 58 | try { 59 | //一般来说下载文件是使用201状态码的,但是IE浏览器不支持 60 | if (header.contains("MSIE") || header.contains("TRIDENT") || header.contains("EDGE")) { 61 | fileName = URLEncoder.encode(fileName, "UTF-8"); 62 | // IE下载文件名空格变+号问题 63 | fileName = fileName.replace("+", "%20"); 64 | status = HttpStatus.OK; 65 | } 66 | } catch (UnsupportedEncodingException e) { 67 | e.printStackTrace(); 68 | } 69 | 70 | HttpHeaders headers = new HttpHeaders(); 71 | headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); 72 | headers.setContentDispositionFormData("attachment", fileName); 73 | headers.setContentLength(body.length); 74 | 75 | 76 | return new ResponseEntity(body, headers, status); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/execution/ProducerExec.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.execution; 2 | 3 | import com.xymq_cli.constant.Destination; 4 | import com.xymq_cli.constant.MessageConstant; 5 | import com.xymq_cli.core.StorageHelper; 6 | import com.xymq_cli.core.XymqServer; 7 | import com.xymq_common.message.Message; 8 | import com.xymq_cli.util.SnowflakeIdUtils; 9 | import io.netty.channel.Channel; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Component; 12 | 13 | /** 14 | * 生产者消息处理器 15 | * @author 黎勇炫 16 | * @date 2022年07月10日 12:39 17 | */ 18 | @Component 19 | public class ProducerExec implements Execution{ 20 | 21 | @Autowired 22 | private SnowflakeIdUtils snowflakeIdUtils; 23 | @Autowired 24 | private XymqServer xymqServer; 25 | @Autowired 26 | private StorageHelper storageHelper; 27 | private long queuetTotalCount = 0; 28 | private long topicTotalCount = 0; 29 | 30 | /** 31 | * 处理来自生产者推送的消息,根据消息对象的不同情况将消息存储到不同的消息容器中,每当有对应的队列或主题有新消息推入就次数加一 32 | * @return void 33 | * @author 黎勇炫 34 | * @create 2022/7/10 35 | * @email 1677685900@qq.com 36 | */ 37 | @Override 38 | public void exec(Message message, Channel channel) { 39 | // 先判断是队列消息还是主题消息,然后放入消息容器 40 | message.setMessageId(snowflakeIdUtils.nextId()); 41 | if (message.getDestinationType() == Destination.QUEUE.getDestination()) { 42 | queuetTotalCount++; 43 | execQueueMessage(message); 44 | } else { 45 | topicTotalCount++; 46 | execTopicMessage(message); 47 | } 48 | } 49 | 50 | /** 51 | * 处理队列消息 52 | * @param message 53 | * @return void 54 | * @author 黎勇炫 55 | * @create 2022/7/10 56 | * @email 1677685900@qq.com 57 | */ 58 | private void execQueueMessage(Message message) { 59 | if (message.getDelay() == 0) { 60 | storageHelper.storeQueueMessage(xymqServer.getQueueContainer(), message); 61 | } else { 62 | storageHelper.storeDelayMessage(xymqServer.getDelayQueueContainer(), message); 63 | } 64 | } 65 | 66 | /** 67 | * 处理主题消息 68 | * @param message 消息对象 69 | * @return void 70 | * @author 黎勇炫 71 | * @create 2022/7/10 72 | * @email 1677685900@qq.com 73 | */ 74 | private void execTopicMessage(Message message) { 75 | if (xymqServer.getSubscriberContainer().containsKey(message.getDestination())) { 76 | if(message.getDelay() == 0){ 77 | storageHelper.storeTopicMessage(xymqServer.getTopicContainer(), message); 78 | }else { 79 | storageHelper.storeDelayTopicMessage(xymqServer.getDelayTopicContainer(), message); 80 | } 81 | } 82 | } 83 | 84 | public long getQueuetTotalCount() { 85 | return queuetTotalCount; 86 | } 87 | 88 | public long getTopicTotalCount() { 89 | return topicTotalCount; 90 | } 91 | 92 | 93 | /** 94 | * 返回当前策略支持的 消息 类型 95 | * 96 | * @return int 消息类型 97 | * @author 黎勇炫 98 | * @create 2022/7/10 99 | * @email 1677685900@qq.com 100 | */ 101 | @Override 102 | public int getType() { 103 | return MessageConstant.PROVIDER; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /xymq-server/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 33 |
34 | 35 | 36 | 容器概览 37 | 38 | 39 | 40 | 使用Demo 41 | 42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
56 |
57 | 58 | 59 | 84 | 99 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/client/Subscriber.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.client; 2 | 3 | import com.xymq_cli.client.initializer.SubscriberInitializer; 4 | import com.xymq_cli.handler.ConsumerHandler; 5 | import com.xymq_cli.handler.SubscriberHandler; 6 | import com.xymq_cli.listener.MessageData; 7 | import com.xymq_cli.listener.MessageListener; 8 | import com.xymq_cli.util.ResourceUtils; 9 | import io.netty.bootstrap.Bootstrap; 10 | import io.netty.channel.Channel; 11 | import io.netty.channel.ChannelFuture; 12 | import io.netty.channel.nio.NioEventLoopGroup; 13 | import io.netty.channel.socket.nio.NioSocketChannel; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | /** 18 | * 主题消息订阅者 19 | * @author 黎勇炫 20 | * @date 2022年07月17日 14:46 21 | */ 22 | public class Subscriber { 23 | 24 | /** 25 | * 服务器端地址 26 | */ 27 | private static String host; 28 | /** 29 | * 服务器端口 30 | */ 31 | private static int port; 32 | /** 33 | * 消息处理器 34 | */ 35 | private SubscriberHandler subscriberHandler; 36 | /** 37 | * 当前消费者的通道 38 | */ 39 | private Channel channel; 40 | /** 41 | * 日志信息 42 | */ 43 | private Logger logger = LoggerFactory.getLogger(Subscriber.class); 44 | 45 | static{ 46 | host = ResourceUtils.getKey("server.host"); 47 | port = Integer.parseInt(ResourceUtils.getKey("server.port")); 48 | } 49 | 50 | public Subscriber(String destination,long subscriberId) { 51 | // 将订阅者唯一标识作和目的地为参数传递给处理器 52 | this.subscriberHandler = new SubscriberHandler(destination,subscriberId); 53 | } 54 | 55 | public void run(){ 56 | // 订阅者工作组 57 | NioEventLoopGroup clientGroupS = new NioEventLoopGroup(); 58 | try { 59 | Bootstrap bootstrap = new Bootstrap(); 60 | bootstrap.group(clientGroupS) 61 | .channel(NioSocketChannel.class) 62 | .handler(new SubscriberInitializer(subscriberHandler)); 63 | 64 | // 连接服务器 65 | ChannelFuture sync = bootstrap.connect(host, port).sync(); 66 | this.channel = sync.channel(); 67 | // 监听关闭连接 68 | sync.channel().closeFuture().sync(); 69 | } catch (InterruptedException e) { 70 | logger.error("订阅者启动失败:{}",e.getMessage()); 71 | e.printStackTrace(); 72 | }finally { 73 | clientGroupS.shutdownGracefully(); 74 | } 75 | } 76 | 77 | /** 78 | * 关闭通道 79 | * @return void 80 | * @author 黎勇炫 81 | * @create 2022/7/12 82 | * @email 1677685900@qq.com 83 | */ 84 | public void close(){ 85 | this.channel.close(); 86 | } 87 | 88 | /** 89 | * 设置监听器 90 | * @param listener 监听器 91 | * @return com.xymq_cli.client.Consumer 92 | * @author 黎勇炫 93 | * @create 2022/7/12 94 | * @email 1677685900@qq.com 95 | */ 96 | public Subscriber createListener(MessageListener listener){ 97 | this.subscriberHandler.createListener(listener); 98 | return this; 99 | } 100 | 101 | /** 102 | * 订阅者消费示例 103 | */ 104 | public static void main(String[] args) { 105 | // 构建订阅者订阅'topic'主题,订阅者编号为1 106 | Subscriber subscriber = new Subscriber("topic",2); 107 | subscriber.createListener(new MessageListener() { 108 | @Override 109 | public void getMessage(MessageData data) { 110 | System.out.println(data.getMessage()); 111 | } 112 | }).run(); 113 | 114 | // 关闭订阅者 115 | subscriber.close(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /xymq-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.6.0 9 | 10 | 11 | 12 | com.xymq 13 | xymq-server 14 | 0.0.1-SNAPSHOT 15 | xymq-server 16 | xymq服务端 17 | 18 | 19 | 1.8 20 | 0.12 21 | 3.19.4 22 | 2.10.0 23 | 24 | 25 | 26 | 27 | 28 | 29 | commons-io 30 | commons-io 31 | ${commonio.version} 32 | 33 | 34 | 35 | com.xymq 36 | xymq-common 37 | 0.0.1 38 | 39 | 40 | 41 | org.iq80.leveldb 42 | leveldb 43 | ${leveldb.version} 44 | 45 | 46 | 47 | 48 | org.iq80.leveldb 49 | leveldb-api 50 | ${leveldb.version} 51 | 52 | 53 | 54 | com.google.protobuf 55 | protobuf-java 56 | ${protobuf.version} 57 | 58 | 59 | 60 | org.springframework.boot 61 | spring-boot-starter-web 62 | 63 | 64 | 65 | org.springframework.boot 66 | spring-boot-starter-thymeleaf 67 | 68 | 69 | 70 | org.projectlombok 71 | lombok 72 | true 73 | 74 | 75 | org.springframework.boot 76 | spring-boot-starter-test 77 | test 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | 89 | org.projectlombok 90 | lombok 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /xymq-server/src/main/resources/static/img/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/util/SnowflakeIdUtils.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.util; 2 | 3 | public class SnowflakeIdUtils { 4 | private final long twepoch = 1420041600000L; 5 | 6 | /** 机器id所占的位数 */ 7 | private final long workerIdBits = 5L; 8 | 9 | /** 数据标识id所占的位数 */ 10 | private final long datacenterIdBits = 5L; 11 | 12 | /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */ 13 | private final long maxWorkerId = -1L ^ (-1L << workerIdBits); 14 | 15 | /** 支持的最大数据标识id,结果是31 */ 16 | private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); 17 | 18 | /** 序列在id中占的位数 */ 19 | private final long sequenceBits = 12L; 20 | 21 | /** 机器ID向左移12位 */ 22 | private final long workerIdShift = sequenceBits; 23 | 24 | /** 数据标识id向左移17位(12+5) */ 25 | private final long datacenterIdShift = sequenceBits + workerIdBits; 26 | 27 | /** 时间截向左移22位(5+5+12) */ 28 | private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; 29 | 30 | /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */ 31 | private final long sequenceMask = -1L ^ (-1L << sequenceBits); 32 | 33 | /** 工作机器ID(0~31) */ 34 | private long workerId; 35 | 36 | /** 数据中心ID(0~31) */ 37 | private long datacenterId; 38 | 39 | /** 毫秒内序列(0~4095) */ 40 | private long sequence = 0L; 41 | 42 | /** 上次生成ID的时间截 */ 43 | private long lastTimestamp = -1L; 44 | 45 | //==============================Constructors===================================== 46 | /** 47 | * 构造函数 48 | * @param workerId 工作ID (0~31) 49 | * @param datacenterId 数据中心ID (0~31) 50 | */ 51 | public SnowflakeIdUtils(long workerId, long datacenterId) { 52 | if (workerId > maxWorkerId || workerId < 0) { 53 | throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); 54 | } 55 | if (datacenterId > maxDatacenterId || datacenterId < 0) { 56 | throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); 57 | } 58 | this.workerId = workerId; 59 | this.datacenterId = datacenterId; 60 | } 61 | 62 | // ==============================Methods========================================== 63 | /** 64 | * 获得下一个ID (该方法是线程安全的) 65 | * @return SnowflakeId 66 | */ 67 | public synchronized long nextId() { 68 | long timestamp = timeGen(); 69 | 70 | //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 71 | if (timestamp < lastTimestamp) { 72 | throw new RuntimeException( 73 | String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); 74 | } 75 | 76 | //如果是同一时间生成的,则进行毫秒内序列 77 | if (lastTimestamp == timestamp) { 78 | sequence = (sequence + 1) & sequenceMask; 79 | //毫秒内序列溢出 80 | if (sequence == 0) { 81 | //阻塞到下一个毫秒,获得新的时间戳 82 | timestamp = tilNextMillis(lastTimestamp); 83 | } 84 | } 85 | //时间戳改变,毫秒内序列重置 86 | else { 87 | sequence = 0L; 88 | } 89 | 90 | //上次生成ID的时间截 91 | lastTimestamp = timestamp; 92 | 93 | //移位并通过或运算拼到一起组成64位的ID 94 | return ((timestamp - twepoch) << timestampLeftShift) // 95 | | (datacenterId << datacenterIdShift) // 96 | | (workerId << workerIdShift) // 97 | | sequence; 98 | } 99 | 100 | /** 101 | * 阻塞到下一个毫秒,直到获得新的时间戳 102 | * @param lastTimestamp 上次生成ID的时间截 103 | * @return 当前时间戳 104 | */ 105 | protected long tilNextMillis(long lastTimestamp) { 106 | long timestamp = timeGen(); 107 | while (timestamp <= lastTimestamp) { 108 | timestamp = timeGen(); 109 | } 110 | return timestamp; 111 | } 112 | 113 | /** 114 | * 返回以毫秒为单位的当前时间 115 | * @return 当前时间(毫秒) 116 | */ 117 | protected long timeGen() { 118 | return System.currentTimeMillis(); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/client/Consumer.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.client; 2 | 3 | import com.xymq_cli.client.initializer.ConsumerlInitializer; 4 | import com.xymq_cli.handler.ConsumerHandler; 5 | import com.xymq_cli.listener.MessageData; 6 | import com.xymq_cli.listener.MessageListener; 7 | import com.xymq_cli.util.ResourceUtils; 8 | import io.netty.bootstrap.Bootstrap; 9 | import io.netty.channel.Channel; 10 | import io.netty.channel.ChannelFuture; 11 | import io.netty.channel.nio.NioEventLoopGroup; 12 | import io.netty.channel.socket.nio.NioSocketChannel; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | /** 17 | * @author 黎勇炫 18 | * @date 2022年07月11日 15:08 19 | */ 20 | public class Consumer { 21 | /** 22 | * 服务器端地址 23 | */ 24 | private static String host; 25 | /** 26 | * 服务器端口 27 | */ 28 | private static int port; 29 | /** 30 | * 消息处理器 31 | */ 32 | private ConsumerHandler consumerHandler; 33 | /** 34 | * 当前消费者的通道 35 | */ 36 | private Channel channel; 37 | /** 38 | * 日志信息 39 | */ 40 | private Logger logger = LoggerFactory.getLogger(Consumer.class); 41 | 42 | static{ 43 | host = ResourceUtils.getKey("server.host"); 44 | port = Integer.parseInt(ResourceUtils.getKey("server.port")); 45 | } 46 | 47 | /** 48 | * @param destination 队列/主题名 49 | * @return 50 | * @author 黎勇炫 51 | * @create 2022/7/11 52 | * @email 1677685900@qq.com 53 | */ 54 | public Consumer(String destination) { 55 | consumerHandler = new ConsumerHandler(destination); 56 | } 57 | 58 | private void run() { 59 | 60 | // 检测消息监听器是否已经启动 61 | if(this.consumerHandler.getMessageListener() == null){ 62 | throw new NullPointerException("未设置消息监听器"); 63 | } 64 | NioEventLoopGroup clientGroupC = new NioEventLoopGroup(); 65 | 66 | try{ 67 | Bootstrap bootstrap = new Bootstrap(); 68 | bootstrap.group(clientGroupC) 69 | .channel(NioSocketChannel.class) 70 | .handler(new ConsumerlInitializer(consumerHandler)); 71 | 72 | // 连接服务器 73 | ChannelFuture sync = bootstrap.connect(host, port).sync(); 74 | this.channel = sync.channel(); 75 | // 监听关闭连接 76 | sync.channel().closeFuture().sync(); 77 | } catch (InterruptedException e) { 78 | e.printStackTrace(); 79 | } finally { 80 | // 关闭工作组 81 | clientGroupC.shutdownGracefully(); 82 | } 83 | } 84 | 85 | /** 86 | * 设置自动签名 87 | * @param autoAcknowledge 设置自动签名 88 | * @return void 89 | * @author 黎勇炫 90 | * @create 2022/7/12 91 | * @email 1677685900@qq.com 92 | */ 93 | public void setAutoAcknowledge(boolean autoAcknowledge){ 94 | this.consumerHandler.setAutoAcknowledge(autoAcknowledge); 95 | } 96 | 97 | /** 98 | * 设置监听器 99 | * @param listener 监听器 100 | * @return com.xymq_cli.client.Consumer 101 | * @author 黎勇炫 102 | * @create 2022/7/12 103 | * @email 1677685900@qq.com 104 | */ 105 | public Consumer createListener(MessageListener listener){ 106 | this.consumerHandler.createListener(listener); 107 | return this; 108 | } 109 | 110 | /** 111 | * 关闭通道 112 | * @return void 113 | * @author 黎勇炫 114 | * @create 2022/7/12 115 | * @email 1677685900@qq.com 116 | */ 117 | public void close(){ 118 | this.channel.close(); 119 | } 120 | 121 | 122 | /** 123 | * 消费者示例 124 | */ 125 | public static void main(String[] args) { 126 | // 指定‘queue’队列 127 | Consumer consumer = new Consumer("queue"); 128 | // 构建监听器 129 | consumer.createListener(new MessageListener() { 130 | @Override 131 | public void getMessage(MessageData data) { 132 | // 监听到消息会进入MessageListener今天器中 133 | System.out.println(data.getMessage()); 134 | } 135 | }).run(); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /xymq-common/src/main/java/com/xymq_common/message/Message.java: -------------------------------------------------------------------------------- 1 | package com.xymq_common.message; 2 | 3 | /** 4 | * @author 黎勇炫 5 | * @date 2022年07月09日 17:11 6 | */ 7 | import java.util.concurrent.Delayed; 8 | import java.util.concurrent.TimeUnit; 9 | 10 | /** 11 | * 封装消息的实体类 12 | * @author 黎勇炫 13 | * @date 2022年07月09日 17:11 14 | */ 15 | public class Message implements Delayed { 16 | /** 17 | * 消息唯一ID,避免防止消息重复 18 | */ 19 | private Long messageId; 20 | /** 21 | * 消息类型(0生产者,1消费者,2签收) 22 | */ 23 | private int type; 24 | /** 25 | * 消息内容 26 | */ 27 | private String content; 28 | /** 29 | * 消息的目的地 30 | */ 31 | private String destination; 32 | /** 33 | * 消息的目的地(队列或频道) 34 | */ 35 | private int destinationType; 36 | /** 37 | * 是否插队消息 38 | */ 39 | private boolean isTopPriority = false; 40 | /** 41 | * 到期时间 42 | */ 43 | private long expire; 44 | /** 45 | * 时间单位 46 | */ 47 | private TimeUnit timeUnit; 48 | /** 49 | * 延迟时间 50 | */ 51 | private long delay; 52 | 53 | public Message(Long messageId, int type, String content, String destination, int destinationType, boolean isTopPriority,long delay,TimeUnit timeUnit) { 54 | this.messageId = messageId; 55 | this.type = type; 56 | this.content = content; 57 | this.destination = destination; 58 | this.destinationType = destinationType; 59 | this.isTopPriority = isTopPriority; 60 | this.timeUnit=timeUnit; 61 | this.delay=delay; 62 | if(0 != delay){ 63 | delay = timeUnit.toMillis(delay); 64 | expire = System.currentTimeMillis()+delay; 65 | } 66 | } 67 | 68 | public boolean getIsTopPriority(){ 69 | return this.isTopPriority; 70 | } 71 | 72 | public Long getMessageId() { 73 | return messageId; 74 | } 75 | 76 | public void setMessageId(Long messageId) { 77 | this.messageId = messageId; 78 | } 79 | 80 | public int getType() { 81 | return type; 82 | } 83 | 84 | public void setType(int type) { 85 | this.type = type; 86 | } 87 | 88 | public String getContent() { 89 | return content; 90 | } 91 | 92 | public void setContent(String content) { 93 | this.content = content; 94 | } 95 | 96 | public String getDestination() { 97 | return destination; 98 | } 99 | 100 | public void setDestination(String destination) { 101 | this.destination = destination; 102 | } 103 | 104 | public int getDestinationType() { 105 | return destinationType; 106 | } 107 | 108 | public void setDestinationType(int destinationType) { 109 | this.destinationType = destinationType; 110 | } 111 | 112 | public boolean isTopPriority() { 113 | return isTopPriority; 114 | } 115 | 116 | public void setTopPriority(boolean topPriority) { 117 | isTopPriority = topPriority; 118 | } 119 | 120 | public TimeUnit getTimeUnit() { 121 | return timeUnit; 122 | } 123 | 124 | public void setTimeUnit(TimeUnit timeUnit) { 125 | this.timeUnit = timeUnit; 126 | } 127 | 128 | public Long getDelay() { 129 | return delay; 130 | } 131 | 132 | public void setDelay(long delay) { 133 | this.delay = delay; 134 | } 135 | 136 | @Override 137 | public long getDelay(TimeUnit unit) { 138 | return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS); 139 | } 140 | 141 | @Override 142 | public int compareTo(Delayed o) { 143 | return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS)); 144 | } 145 | 146 | @Override 147 | public String toString() { 148 | return "Message{" + 149 | "messageId=" + messageId + 150 | ", type=" + type + 151 | ", content='" + content + '\'' + 152 | ", destination='" + destination + '\'' + 153 | ", destinationType=" + destinationType + 154 | ", isTopPriority=" + isTopPriority + 155 | ", expire=" + expire + 156 | ", timeUnit=" + timeUnit + 157 | ", delay=" + delay + 158 | '}'; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/execution/ConsumerExec.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.execution; 2 | 3 | import com.xymq_cli.constant.Destination; 4 | import com.xymq_cli.constant.MessageConstant; 5 | import com.xymq_cli.core.XymqServer; 6 | import com.xymq_common.message.Message; 7 | import io.netty.channel.Channel; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.ArrayList; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | 16 | /** 17 | * @author 黎勇炫 18 | * @date 2022年07月10日 12:41 19 | */ 20 | @Component 21 | public class ConsumerExec implements Execution{ 22 | 23 | @Autowired 24 | private XymqServer xymqServer; 25 | 26 | /** 27 | * 执行操作(消费、推送和签收) 28 | * @param message 29 | * @return void 30 | * @author 黎勇炫 31 | * @create 2022/7/10 32 | * @email 1677685900@qq.com 33 | */ 34 | @Override 35 | public void exec(Message message, Channel channel) { 36 | //如果是消费者,判断消费者是消费队列消息还是订阅主题 37 | if (message.getDestinationType() == Destination.QUEUE.getDestination()) { 38 | addConsumer(message, channel); 39 | } else { 40 | /** 41 | * 如果是主题订阅者,就需要先查看是否是之前离线的订阅者(判断标准是订阅者编号) 42 | * 如果是之前离线的订阅者并且还有未接受到的消息就将之前的消息先推送 43 | */ 44 | ConcurrentHashMap> subscriberContainer = xymqServer.getSubscriberContainer(); 45 | ConcurrentHashMap> offLineSubscriber = xymqServer.getOffLineSubscriber(); 46 | // 如果离线订阅者容器存在这个主题 47 | if (offLineSubscriber.containsKey(message.getDestination())) { 48 | // 获取这个容器 49 | HashMap offLineSubscribers = offLineSubscriber.get(message.getDestination()); 50 | // 离线订阅者容器中是否存在指定的订阅者(messageId)实际上就是订阅者编号 51 | if (offLineSubscribers.containsKey(message.getMessageId())) { 52 | // 将新channel覆盖原来断开连接的channel 53 | offLineSubscribers.put(message.getMessageId(), channel); 54 | } else { 55 | // 如果不是离线订阅者就直接存入订阅者容器 56 | putSubscriber(message, channel, subscriberContainer); 57 | } 58 | } else { 59 | putSubscriber(message, channel, subscriberContainer); 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * 将订阅者channel存入容器 66 | * @param message 消息对象(订阅者中messageId就是订阅者编号) 67 | * @param channel 通道 68 | * @param subscriberContainer 订阅者容器 69 | * @return void 70 | * @author 黎勇炫 71 | * @create 2022/7/10 72 | * @email 1677685900@qq.com 73 | */ 74 | private void putSubscriber(Message message, Channel channel, ConcurrentHashMap> subscriberContainer) { 75 | // 如果容器中已经存在该目的地了就直接存入 76 | if (subscriberContainer.containsKey(message.getDestination())) { 77 | subscriberContainer.get(message.getDestination()).put(message.getMessageId(), channel); 78 | } else { 79 | // 如果容器中不存在该目的地就新建一个容器在存入 80 | subscriberContainer.put(message.getDestination(), new HashMap<>()); 81 | subscriberContainer.get(message.getDestination()).put(message.getMessageId(), channel); 82 | } 83 | } 84 | 85 | /** 86 | * 87 | * @param message 消息对象 88 | * @param channel 客户端的通道 89 | * @return void 90 | * @author 黎勇炫 91 | * @create 2022/7/10 92 | * @email 1677685900@qq.com 93 | */ 94 | private void addConsumer(Message message, Channel channel) { 95 | ConcurrentHashMap> consumerContainer = xymqServer.getConsumerContainer(); 96 | // 如果存在这个容器就直接将channel存入容器,否则就新建一个容器 97 | if (consumerContainer.containsKey(message.getDestination())) { 98 | consumerContainer.get(message.getDestination()).add(channel); 99 | } else { 100 | // 新建容器后存入 101 | consumerContainer.put(message.getDestination(), new ArrayList()); 102 | consumerContainer.get(message.getDestination()).add(channel); 103 | } 104 | } 105 | 106 | /** 107 | * 返回当前策略支持的 消息 类型 108 | * 109 | * @return int 消息类型 110 | * @author 黎勇炫 111 | * @create 2022/7/10 112 | * @email 1677685900@qq.com 113 | */ 114 | @Override 115 | public int getType() { 116 | return MessageConstant.CONSUMER; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/handler/ConsumerHandler.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.handler; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.xymq_cli.constant.Destination; 5 | import com.xymq_cli.constant.MessageType; 6 | import com.xymq_cli.listener.MessageData; 7 | import com.xymq_cli.listener.MessageListener; 8 | import com.xymq_common.message.Message; 9 | import com.xymq_common.protocol.MessageUtils; 10 | import com.xymq_common.protocol.Protocol; 11 | import io.netty.channel.Channel; 12 | import io.netty.channel.ChannelHandlerContext; 13 | import io.netty.channel.SimpleChannelInboundHandler; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import java.nio.charset.StandardCharsets; 18 | import java.util.concurrent.TimeUnit; 19 | 20 | /** 21 | * @author 黎勇炫 22 | * @date 2022年07月11日 15:45 23 | */ 24 | public class ConsumerHandler extends SimpleChannelInboundHandler { 25 | 26 | /** 27 | * 要消费的队列满 28 | */ 29 | private String destination; 30 | /** 31 | * 监听器 32 | */ 33 | private MessageListener messageListener; 34 | /** 35 | * 自动签名(默认开启) 36 | */ 37 | private boolean isAutoAcknowledge = true; 38 | 39 | /** 40 | * 日志信息 41 | */ 42 | private Logger logger = LoggerFactory.getLogger(ConsumerHandler.class); 43 | 44 | public ConsumerHandler(String destination){ 45 | this.destination = destination; 46 | } 47 | 48 | /** 49 | * 消息监听通道,加入有消息就会调用这个方法 50 | * @param ctx 51 | * @param protocol 52 | * @return void 53 | * @author 黎勇炫 54 | * @create 2022/7/11 55 | * @email 1677685900@qq.com 56 | */ 57 | @Override 58 | protected void channelRead0(ChannelHandlerContext ctx, Protocol protocol) throws Exception { 59 | // 将字节数组转换成消息对象 60 | Message message = MessageUtils.reverse(protocol.getContent()); 61 | if(message != null){ 62 | execListener(new MessageData(this,message.getContent())); 63 | if(isAutoAcknowledge){ 64 | ack(ctx.channel(), message.getMessageId(), message.getDestination(),message.getDestinationType()); 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * 向服务端签收指定的消息 71 | * @param channel 72 | * @param messageId 73 | * @param destination 74 | * @return void 75 | * @author 黎勇炫 76 | * @create 2022/7/11 77 | * @email 1677685900@qq.com 78 | */ 79 | private void ack(Channel channel, Long messageId, String destination,int destinationType) { 80 | Message message = new Message(messageId, MessageType.ACK.getType(), null,destination, destinationType,false,0,null); 81 | byte[] content = JSON.toJSONString(message).getBytes(StandardCharsets.UTF_8); 82 | Protocol protocol = new Protocol(content.length,content); 83 | channel.writeAndFlush(protocol); 84 | } 85 | 86 | /** 87 | * 客户端一启动成功就向服务端注册这个通道,告诉服务端这是消费者 88 | * @param ctx 89 | * @return void 90 | * @author 黎勇炫 91 | * @create 2022/7/11 92 | * @email 1677685900@qq.com 93 | */ 94 | @Override 95 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 96 | // 注册 97 | register(ctx.channel()); 98 | } 99 | 100 | /** 101 | * 向服务端注册消费者 102 | * @param channel 103 | * @return void 104 | * @author 黎勇炫 105 | * @create 2022/7/11 106 | * @email 1677685900@qq.com 107 | */ 108 | private void register(Channel channel) { 109 | Message message = new Message(null, MessageType.COMSUMER.getType(), null, this.destination, Destination.QUEUE.getDestination(), false,0, TimeUnit.MILLISECONDS); 110 | byte[] content = JSON.toJSONString(message).getBytes(StandardCharsets.UTF_8); 111 | Protocol protocol = new Protocol(content.length,content); 112 | channel.writeAndFlush(protocol); 113 | } 114 | 115 | /** 116 | * 消息监听器 117 | */ 118 | public void createListener(MessageListener messageListener){ 119 | this.messageListener = messageListener; 120 | } 121 | 122 | /** 123 | * 执行监听事件 124 | */ 125 | private void execListener(MessageData data) { 126 | this.messageListener.getMessage(data); 127 | } 128 | 129 | /** 130 | * 设置自动签名 131 | */ 132 | public void setAutoAcknowledge(boolean autoAcknowledge){ 133 | this.isAutoAcknowledge = autoAcknowledge; 134 | } 135 | 136 | /** 137 | * 获取消息监听器 138 | */ 139 | public MessageListener getMessageListener(){ 140 | return this.messageListener; 141 | } 142 | 143 | @Override 144 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 145 | ctx.channel().close(); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/core/LevelDbStorageHelper.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.core; 2 | 3 | import com.xymq_common.message.Message; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.concurrent.ConcurrentHashMap; 8 | import java.util.concurrent.DelayQueue; 9 | import java.util.concurrent.LinkedBlockingDeque; 10 | 11 | /** 12 | * @author 黎勇炫 13 | * @date 2022年07月10日 14:48 14 | */ 15 | @Component 16 | public class LevelDbStorageHelper implements StorageHelper{ 17 | 18 | @Autowired 19 | private LevelDb levelDb; 20 | 21 | /** 22 | * 存储消息到队列消息容器 23 | * @param queueContainer 队列消息容器 24 | * @param message 消息对象 25 | * @return void 26 | * @author 黎勇炫 27 | * @create 2022/7/10 28 | * @email 1677685900@qq.com 29 | */ 30 | @Override 31 | public void storeQueueMessage(ConcurrentHashMap> queueContainer, Message message) { 32 | if (queueContainer.containsKey(message.getDestination())) { 33 | //判断是否已经存在该队列 34 | if (!message.getIsTopPriority()) { 35 | //插队消息,放入队列头 36 | queueContainer.get(message.getDestination()).offer(message); 37 | } else { 38 | queueContainer.get(message.getDestination()).offerFirst(message); 39 | } 40 | } else { 41 | //创建容器 42 | queueContainer.put(message.getDestination(), new LinkedBlockingDeque()); 43 | if (!message.getIsTopPriority()) { 44 | queueContainer.get(message.getDestination()).offer(message); 45 | } else { 46 | queueContainer.get(message.getDestination()).offerFirst(message); 47 | } 48 | } 49 | // 保存到数据库 50 | levelDb.putMessage(message.getMessageId(), message); 51 | } 52 | 53 | /** 54 | * 存储消息到主题消息容器 55 | * 56 | * @param topicContainer 主题消息容器 57 | * @param message 消息对象 58 | * @return void 59 | * @author 黎勇炫 60 | * @create 2022/7/10 61 | * @email 1677685900@qq.com 62 | */ 63 | @Override 64 | public void storeTopicMessage(ConcurrentHashMap> topicContainer, Message message) { 65 | // 消息目的地 66 | String destination = message.getDestination(); 67 | // 如果已经存在这个目的地了就直接存入 68 | if (topicContainer.containsKey(destination)) { 69 | if (message.getIsTopPriority()) { 70 | topicContainer.get(destination).offerFirst(message); 71 | } else { 72 | topicContainer.get(destination).offer(message); 73 | } 74 | } else { 75 | // 不存在就新建后存入 76 | topicContainer.put(destination, new LinkedBlockingDeque<>()); 77 | if (!message.getIsTopPriority()) { 78 | topicContainer.get(destination).offer(message); 79 | } else { 80 | topicContainer.get(destination).offerFirst(message); 81 | } 82 | } 83 | levelDb.putMessage(message.getMessageId(), message); 84 | } 85 | 86 | /** 87 | * 存储消息到延时队列容器(不存入leveldb) 88 | * todo 延迟消息支持持久化 89 | * @param delayQueueMap 延时队列容器 90 | * @param message 消息对象 91 | * @return void 92 | * @author 黎勇炫 93 | * @create 2022/7/10 94 | * @email 1677685900@qq.com 95 | */ 96 | @Override 97 | public void storeDelayMessage(ConcurrentHashMap> delayQueueMap, Message message) { 98 | // 存储消息之前先确认是否已经创建容器 99 | String destination = message.getDestination(); 100 | if(delayQueueMap.containsKey(destination)){ 101 | delayQueueMap.get(destination).offer(message); 102 | }else { 103 | DelayQueue delayQueue = new DelayQueue<>(); 104 | // 插入消息 105 | delayQueue.offer(message); 106 | delayQueueMap.put(destination, delayQueue); 107 | } 108 | levelDb.putMessage(message.getMessageId(), message); 109 | } 110 | 111 | /** 112 | * 存储消息到延时主题消息容器(不存入leveldb) 113 | * todo 延迟消息支持持久化 114 | * @param delayTopicContainer 主题消息容器 115 | * @param message 消息对象 116 | * @return void 117 | * @author 黎勇炫 118 | * @create 2022/7/10 119 | * @email 1677685900@qq.com 120 | */ 121 | @Override 122 | public void storeDelayTopicMessage(ConcurrentHashMap> delayTopicContainer, Message message) { 123 | if (delayTopicContainer.containsKey(message.getDestination())) { 124 | delayTopicContainer.get(message.getDestination()).offer(message); 125 | } else { 126 | delayTopicContainer.put(message.getDestination(), new DelayQueue()); 127 | delayTopicContainer.get(message.getDestination()).offer(message); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/web/service/impl/DataChartServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.web.service.impl; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.xymq_cli.core.XymqServer; 5 | import com.xymq_cli.execution.AckExec; 6 | import com.xymq_cli.execution.ProducerExec; 7 | import com.xymq_cli.web.domain.QueueVO; 8 | import com.xymq_cli.web.service.DataChartService; 9 | import io.netty.channel.Channel; 10 | import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.scheduling.annotation.Async; 13 | import org.springframework.scheduling.annotation.Scheduled; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.time.LocalTime; 17 | import java.time.format.DateTimeFormatter; 18 | import java.util.*; 19 | import java.util.concurrent.atomic.LongAdder; 20 | 21 | /** 22 | * @author 黎勇炫 23 | * @date 2022年08月03日 21:42 24 | */ 25 | @Service 26 | public class DataChartServiceImpl implements DataChartService { 27 | 28 | private ProducerExec producerExec; 29 | private AckExec ackExec; 30 | private XymqServer xymqServer; 31 | 32 | /** 33 | * 消费成功数量 34 | */ 35 | private List successQueueCount = new ArrayList<>(); 36 | /** 37 | * 队列消息堆积情况 38 | */ 39 | private List accQueueCount = new ArrayList<>(); 40 | /** 41 | * 队列消息总数量 42 | */ 43 | private List queueTotal = new ArrayList<>(); 44 | //--------------------------------主题消息----------------------------------------// 45 | /** 46 | * 消费成功数量 47 | */ 48 | private List successTopicCount = new ArrayList<>(); 49 | /** 50 | * 离线消息数量 51 | */ 52 | private List offLineCount = new ArrayList<>(); 53 | /** 54 | * 主题消息堆积情况 55 | */ 56 | private List accTopicCount = new ArrayList<>(); 57 | /** 58 | * 主题消息总数量 59 | */ 60 | private List topicTotal = new ArrayList<>(); 61 | 62 | /** 63 | * 返回结果的map 64 | */ 65 | private Map data = new HashMap<>(); 66 | /** 67 | * 时间数组 68 | */ 69 | private List timeList = new ArrayList<>(); 70 | /** 71 | * 计数器 72 | */ 73 | private LongAdder counter = new LongAdder(); 74 | /** 75 | * websocket客户端容器 76 | */ 77 | private static List channels = new ArrayList<>(); 78 | 79 | /** 80 | * 添加客户端 81 | */ 82 | public static void putChannel(Channel channel){ 83 | channels.add(channel); 84 | } 85 | 86 | @Autowired 87 | public DataChartServiceImpl(ProducerExec producerExec, AckExec ackExec, XymqServer xymqServer) { 88 | this.producerExec = producerExec; 89 | this.ackExec = ackExec; 90 | this.xymqServer = xymqServer; 91 | initData(); 92 | } 93 | 94 | /** 95 | * 初始化数据 96 | * @return void 97 | * @author 黎勇炫 98 | * @create 2022/8/4 99 | * @email 1677685900@qq.com 100 | */ 101 | private void initData() { 102 | // 初始化数据组 103 | add(); 104 | counter.increment(); 105 | } 106 | 107 | /** 108 | * 删除索引位置0的数据,将最新的数据填充到末尾 109 | * @return void 110 | * @author 黎勇炫 111 | * @create 2022/8/4 112 | * @email 1677685900@qq.com 113 | */ 114 | public void resetData(){ 115 | // 清除数据 116 | removeIndex0(); 117 | add(); 118 | } 119 | 120 | /** 121 | * 往数据列表中更新数据 122 | * @return void 123 | * @author 黎勇炫 124 | * @create 2022/8/4 125 | * @email 1677685900@qq.com 126 | */ 127 | private void add() { 128 | timeList.add(LocalTime.now().format(DateTimeFormatter.ofPattern("hh:mm:ss"))); 129 | // 队列消息 130 | successQueueCount.add(ackExec.getQueueMessageCount()); 131 | queueTotal.add(producerExec.getQueuetTotalCount()); 132 | accQueueCount.add(xymqServer.getUnReadQueueMessageCount()); 133 | // 主题消息 134 | successTopicCount.add(xymqServer.topicSuccessCount()); 135 | offLineCount.add(xymqServer.getOffLineTopicMessageCount()); 136 | accTopicCount.add(xymqServer.getUnReadTopicMessageCount()); 137 | topicTotal.add(producerExec.getTopicTotalCount()); 138 | 139 | 140 | } 141 | 142 | /** 143 | * 删除索引位置0的数据 144 | * @return void 145 | * @author 黎勇炫 146 | * @create 2022/8/4 147 | * @email 1677685900@qq.com 148 | */ 149 | private void removeIndex0() { 150 | timeList.remove(0); 151 | successQueueCount.remove(0); 152 | queueTotal.remove(0); 153 | accQueueCount.remove(0); 154 | } 155 | 156 | /** 157 | * 获取队列详细数据 158 | * @return java.util.Map 159 | * @author 黎勇炫 160 | * @create 2022/8/3 161 | * @email 1677685900@qq.com 162 | */ 163 | @Override 164 | public Map queueData() { 165 | data.put("time",timeList); 166 | data.put("successQueueCount",successQueueCount); 167 | data.put("accQueueCount",accQueueCount); 168 | data.put("queueTotal",queueTotal); 169 | data.put("queueAccDetail",xymqServer.queueAccDetail()); 170 | data.put("successTopicCount",successTopicCount); 171 | data.put("accTopicCount",accTopicCount); 172 | data.put("topicTotal",topicTotal); 173 | data.put("offLineCount",offLineCount); 174 | data.put("topicAccDetail",xymqServer.topicAccDetail()); 175 | return data; 176 | } 177 | 178 | /** 179 | * 每隔5s获取一次 180 | * @return void 181 | * @author 黎勇炫 182 | * @create 2022/8/4 183 | * @email 1677685900@qq.com 184 | */ 185 | @Override 186 | @Async 187 | @Scheduled(cron = "0/5 * * * * ?") 188 | public void updateData(){ 189 | if(counter.intValue() == 12){ 190 | resetData(); 191 | }else { 192 | add(); 193 | counter.increment(); 194 | } 195 | Iterator iterator = channels.iterator(); 196 | while (iterator.hasNext()){ 197 | Channel channel = (Channel) iterator.next(); 198 | if(!channel.isActive()){ 199 | iterator.remove(); 200 | return; 201 | } 202 | channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(queueData()))); 203 | } 204 | } 205 | 206 | @Override 207 | public List queueDetail() { 208 | return xymqServer.queueDetails(); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /xymq-cli/src/main/java/com/xymq_cli/client/Producer.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.client; 2 | 3 | import com.xymq_cli.client.initializer.ProducerlInitializer; 4 | import com.xymq_cli.constant.Destination; 5 | import com.xymq_cli.constant.MessageType; 6 | import com.xymq_cli.handler.ProducerHandler; 7 | import com.xymq_cli.util.ResourceUtils; 8 | import com.xymq_common.message.Message; 9 | import com.xymq_common.protocol.MessageUtils; 10 | import com.xymq_common.protocol.Protocol; 11 | import io.netty.bootstrap.Bootstrap; 12 | import io.netty.channel.Channel; 13 | import io.netty.channel.ChannelFuture; 14 | import io.netty.channel.ChannelFutureListener; 15 | import io.netty.channel.nio.NioEventLoopGroup; 16 | import io.netty.channel.socket.nio.NioSocketChannel; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import java.util.concurrent.TimeUnit; 21 | 22 | /** 23 | * @author 黎勇炫 24 | * @date 2022年07月11日 15:09 25 | */ 26 | public class Producer { 27 | /** 28 | * 服务器端地址 29 | */ 30 | private static String host; 31 | /** 32 | * 服务器端口 33 | */ 34 | private static int port; 35 | /** 36 | * 生产者处理器 37 | */ 38 | private ProducerHandler producerHandler; 39 | /** 40 | * 客户端通道 41 | */ 42 | private Channel channel; 43 | /** 44 | * 日志 45 | */ 46 | private NioEventLoopGroup clientGroup = new NioEventLoopGroup(); 47 | private Logger logger = LoggerFactory.getLogger(Producer.class); 48 | 49 | static { 50 | host = ResourceUtils.getKey("server.host"); 51 | port = Integer.parseInt(ResourceUtils.getKey("server.port")); 52 | } 53 | 54 | /** 55 | * 初始化生产者 56 | * 57 | * @return 58 | * @author 黎勇炫 59 | * @create 2022/7/13 60 | * @email 1677685900@qq.com 61 | */ 62 | public Producer() { 63 | producerHandler = new ProducerHandler(); 64 | try { 65 | Bootstrap bootstrap = new Bootstrap(); 66 | bootstrap.group(clientGroup) 67 | .channel(NioSocketChannel.class) 68 | .handler(new ProducerlInitializer(producerHandler)); 69 | // 连接服务器 70 | ChannelFuture sync = bootstrap.connect(host, port).sync(); 71 | this.channel = sync.channel(); 72 | } catch (InterruptedException e) { 73 | clientGroup.shutdownGracefully(); 74 | e.printStackTrace(); 75 | } 76 | } 77 | 78 | /** 79 | * 发送队列消息,需要传入消息内容以及队列名称 80 | */ 81 | public void sendMsg(String content, String destinationName) throws InterruptedException { 82 | Message message = new Message(null, MessageType.PRIVODER.getType(), content, destinationName, Destination.QUEUE.getDestination(), false, 0, TimeUnit.SECONDS); 83 | Protocol protocol = MessageUtils.message2Protocol(message); 84 | channel.writeAndFlush(protocol); 85 | } 86 | 87 | /** 88 | * 发送延时消息,需要传入消息内容、队列名称、延迟数以及延迟单位 89 | * 90 | * @param content 消息内容 91 | * @param destinationName 目的地 92 | * @param delay 延迟数 93 | * @param timeUnit 延迟单位 94 | * @return void 95 | * @author 黎勇炫 96 | * @create 2022/7/16 97 | * @email 1677685900@qq.com 98 | */ 99 | public void sendDelayMessage(String content, String destinationName, long delay, TimeUnit timeUnit) { 100 | Message message = new Message(null, MessageType.PRIVODER.getType(), content,destinationName,Destination.QUEUE.getDestination(), false,delay, timeUnit); 101 | this.channel.writeAndFlush(MessageUtils.message2Protocol(message)); 102 | } 103 | 104 | /** 105 | * 发布主题消息,需要传入消息内容和主题名称 106 | * @param content 消息内容 107 | * @param destinationName i 108 | * @return void 109 | * @author 黎勇炫 110 | * @create 2022/7/13 111 | * @email 1677685900@qq.com 112 | */ 113 | public void publish(String content, String destinationName) { 114 | Message message = new Message(null, MessageType.PRIVODER.getType(), content,destinationName,Destination.TOPIC.getDestination(),false,0,null); 115 | this.channel.writeAndFlush(MessageUtils.message2Protocol(message)); 116 | } 117 | 118 | /** 119 | * 发布延迟主题消息,需要传入消息内容、主题名称、延迟数和延迟单位 120 | * 121 | * @param content 消息内容 122 | * @param destinationName 消息目的地 123 | * @param delay 延迟数 124 | * @param timeUnit 延迟单位 125 | * @return void 126 | * @author 黎勇炫 127 | * @create 2022/7/13 128 | * @email 1677685900@qq.com 129 | */ 130 | public void publishDelayTopicMessage(String content, String destinationName, long delay, TimeUnit timeUnit) { 131 | Message message = new Message(null, MessageType.PRIVODER.getType(), content,destinationName,Destination.TOPIC.getDestination(),false,delay,timeUnit); 132 | channel.writeAndFlush(MessageUtils.message2Protocol(message)); 133 | } 134 | 135 | /** 136 | * 发送插队消息,该方法可以将一条消息放入一个队列的队列头,将会被优先消费 137 | * 138 | * @param content 消息内容 139 | * @param destinationName 消息目的地 140 | * @return void 141 | * @author 黎勇炫 142 | * @create 2022/7/13 143 | * @email 1677685900@qq.com 144 | */ 145 | public void sendPriorityMessage(String content, String destinationName) { 146 | Message message = new Message(null, MessageType.PRIVODER.getType(), content,destinationName, Destination.QUEUE.getDestination(),true,0,TimeUnit.MILLISECONDS); 147 | channel.writeAndFlush(MessageUtils.message2Protocol(message)); 148 | } 149 | 150 | /** 151 | * 等待socket缓冲区的数据都被消费才关闭 152 | * 153 | * @return void 154 | * @author 黎勇炫 155 | * @create 2022/7/13 156 | * @email 1677685900@qq.com 157 | */ 158 | public void close() { 159 | channel.close().channel(); 160 | this.clientGroup.shutdownGracefully(); 161 | } 162 | 163 | public Channel getChannel(){ 164 | return this.channel; 165 | } 166 | 167 | 168 | /** 169 | * 生产者实例 170 | */ 171 | public static void main(String[] args) throws InterruptedException { 172 | // 创建生产者 173 | Producer producer = new Producer(); 174 | // producer.publishDelayTopicMessage("你好,延时30s","topic",30,TimeUnit.SECONDS); 175 | // 推送普通的队列消息 176 | for (int i = 0; i < 150000; i++) { 177 | producer.sendMsg("你好,我是队列消息"+i,"queue3"); 178 | } 179 | // Thread.sleep(300); 180 | // 推送主题消息 181 | // producer.publish("你好,我是主题消息","topic"); 182 | // // 推送延迟消息,设置延迟数和单位,消息会在5分钟后推送给消费者 183 | // producer.sendDelayMessage("你好,我是延时队列消息","queueDelayM",5,TimeUnit.SECONDS); 184 | // // 推送延迟主题消息 185 | // producer.sendDelayMessage("你好,我是延时主题消息","queueDelayT",5,TimeUnit.SECONDS); 186 | // // 设置优先级,消息会插入到队列头 187 | // producer.sendPriorityMessage("你好,我是队列消息","queue"); 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /xymq-server/src/main/resources/templates/components/home.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 245 | 246 | -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/core/LevelDb.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.core; 2 | 3 | 4 | import com.alibaba.fastjson.JSON; 5 | import com.alibaba.fastjson.JSONArray; 6 | import com.alibaba.fastjson.JSONObject; 7 | import com.alibaba.fastjson.serializer.SerializerFeature; 8 | import com.xymq_cli.constant.ServerConstant; 9 | import com.xymq_cli.exception.ExceptionEnum; 10 | import com.xymq_cli.exception.XyException; 11 | import com.xymq_common.message.Message; 12 | import io.netty.channel.Channel; 13 | import io.netty.channel.socket.nio.NioSocketChannel; 14 | import org.iq80.leveldb.DB; 15 | import org.iq80.leveldb.DBFactory; 16 | import org.iq80.leveldb.DBIterator; 17 | import org.iq80.leveldb.Options; 18 | import org.iq80.leveldb.impl.Iq80DBFactory; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | import org.springframework.boot.context.properties.ConfigurationProperties; 22 | import org.springframework.stereotype.Component; 23 | 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.io.UnsupportedEncodingException; 27 | import java.nio.channels.SocketChannel; 28 | import java.util.*; 29 | import java.util.concurrent.ConcurrentHashMap; 30 | 31 | /** 32 | * leveldb操作类 33 | * @return 34 | * @author 黎勇炫 35 | * @create 2022/7/10 36 | * @email 1677685900@qq.com 37 | */ 38 | @Component 39 | @ConfigurationProperties(prefix = "leveldb") 40 | public class LevelDb { 41 | 42 | private DB db = null; 43 | 44 | private Logger logger = LoggerFactory.getLogger(LevelDb.class); 45 | 46 | private String folder; 47 | 48 | private String charset; 49 | 50 | /** 51 | * 初始化levelDb 52 | * @return void 53 | * @author 黎勇炫 54 | * @create 2022/7/10 55 | * @email 1677685900@qq.com 56 | */ 57 | public void initLevelDb() { 58 | DBFactory dbFactory = new Iq80DBFactory(); 59 | Options options = new Options(); 60 | //如果不存在则创建 61 | options.createIfMissing(true); 62 | try { 63 | db = dbFactory.open(new File(folder), options); 64 | System.out.println("初始化db...."); 65 | } catch (IOException e) { 66 | logger.error("levelDb初始化失败"); 67 | throw new XyException(ExceptionEnum.LEVELDB_INIT_ERROR); 68 | } 69 | } 70 | 71 | 72 | /** 73 | * 消息对象存入levelDB数据库 74 | * @param key 75 | * @param message 76 | * @return void 77 | * @author 黎勇炫 78 | * @create 2022/7/10 79 | * @email 1677685900@qq.com 80 | */ 81 | public void putMessage(Long key, Message message) { 82 | byte[] messageByte = JSON.toJSONBytes(message, new SerializerFeature[]{SerializerFeature.DisableCircularReferenceDetect}); 83 | try { 84 | db.put(String.valueOf(key).getBytes(charset), messageByte); 85 | } catch (UnsupportedEncodingException e) { 86 | logger.error("消息编号{}持久化失败",message.getMessageId()); 87 | throw new XyException(ExceptionEnum.FAILED_TO_STORAGE); 88 | } 89 | } 90 | 91 | /** 92 | * 将未消费的消息id和客户端id存入数据库 93 | * @param key 94 | * @param map 95 | * @return void 96 | * @author 黎勇炫 97 | * @create 2022/7/10 98 | * @email 1677685900@qq.com 99 | */ 100 | public void putUnConsumers(String key, HashMap> map) { 101 | try { 102 | byte[] keys = key.getBytes(charset); 103 | byte[] value = JSON.toJSONBytes(map, new SerializerFeature[]{SerializerFeature.DisableCircularReferenceDetect}); 104 | db.put(keys, value); 105 | } catch (UnsupportedEncodingException e) { 106 | logger.error("未消费消息id或客户端id持久化失败"); 107 | throw new XyException(ExceptionEnum.FAILED_TO_STORAGE); 108 | } 109 | } 110 | 111 | /** 112 | * 获取消息对象 113 | * @param key 114 | * @return com.xymq.message.Message 115 | * @author 黎勇炫 116 | * @create 2022/7/10 117 | * @email 1677685900@qq.com 118 | */ 119 | public Message getMessageBean(String key) { 120 | try { 121 | byte[] keyByte = key.getBytes(charset); 122 | byte[] bytes = db.get(keyByte); 123 | if (null != bytes) { 124 | Message messageBean = JSON.parseObject(new String(bytes), Message.class); 125 | return messageBean; 126 | } 127 | return null; 128 | } catch (UnsupportedEncodingException e) { 129 | logger.error("消息时恢复发生异常"); 130 | throw new XyException(ExceptionEnum.FAILED_TO_RECOVERY_DATA); 131 | } 132 | 133 | } 134 | 135 | /** 136 | * 从数据库中删除消息 137 | * @param key 138 | * @return void 139 | * @author 黎勇炫 140 | * @create 2022/7/10 141 | * @email 1677685900@qq.com 142 | */ 143 | public void deleteMessageBean(Long key) { 144 | try { 145 | byte[] keyByte = String.valueOf(key).getBytes(charset); 146 | db.delete(keyByte); 147 | } catch (UnsupportedEncodingException e) { 148 | logger.error("消息清理失败"); 149 | throw new XyException(ExceptionEnum.FAILED_TO_CLEAN_DATA); 150 | } 151 | } 152 | 153 | /** 154 | * 获取所有key 155 | * @return java.util.List 156 | * @author 黎勇炫 157 | * @create 2022/7/10 158 | * @email 1677685900@qq.com 159 | */ 160 | public List getKeys() { 161 | DBIterator iterator = null; 162 | List list = new ArrayList<>(); 163 | try { 164 | iterator = db.iterator(); 165 | while (iterator.hasNext()) { 166 | //如果有数据就初始化数组 167 | Map.Entry entry = iterator.next(); 168 | if (new String(entry.getValue()).equals("")) { 169 | db.delete(entry.getKey()); 170 | } 171 | String key = new String(entry.getKey()); 172 | list.add(key); 173 | } 174 | } catch (Exception e) { 175 | logger.error("获取keys失败"); 176 | throw new XyException(ExceptionEnum.FAILED_TO_GET_KEYS); 177 | } finally { 178 | if (iterator != null) { 179 | try { 180 | iterator.close(); 181 | } catch (Exception e) { 182 | logger.error("levelDb关闭失败"); 183 | throw new XyException(ExceptionEnum.FAILED_TO_CLOSE_DB); 184 | } 185 | } 186 | } 187 | 188 | list.sort(new Comparator() { 189 | @Override 190 | public int compare(String o1, String o2) { 191 | try { 192 | long id1 = Long.parseLong(o1); 193 | long id2 = Long.parseLong(o2); 194 | if (id1 == id2) { 195 | return 0; 196 | } 197 | if (id1 > id2) { 198 | return 1; 199 | } else { 200 | return -1; 201 | } 202 | } catch (NumberFormatException e) { 203 | return -1; 204 | } 205 | } 206 | }); 207 | return list; 208 | } 209 | 210 | /** 211 | * 往数据库中存储离线的订阅者数据 212 | * @param offLineSubscriber 离线订阅者 213 | * @return void 214 | * @author 黎勇炫 215 | * @create 2022/7/10 216 | * @email 1677685900@qq.com 217 | */ 218 | public void storeOffLineSubscriber(ConcurrentHashMap> offLineSubscriber) { 219 | if(null != db){ 220 | try { 221 | byte[] key = ServerConstant.OFFLINE_SUBSCRIBER.getBytes(charset); 222 | byte[] value = JSON.toJSONBytes(offLineSubscriber, new SerializerFeature[]{SerializerFeature.DisableCircularReferenceDetect}); 223 | db.put(key, value); 224 | } catch (UnsupportedEncodingException e) { 225 | logger.error("离线订阅者持久化异常"); 226 | throw new XyException(ExceptionEnum.FAILED_TO_STORAGE); 227 | } 228 | 229 | 230 | } 231 | } 232 | 233 | /** 234 | * 往数据库中存储离线的消息数据 235 | * @param storeOffLineTopicMessage 离线消息 236 | * @return void 237 | * @author 黎勇炫 238 | * @create 2022/7/10 239 | * @email 1677685900@qq.com 240 | */ 241 | public void storeOffLineMessage(ConcurrentHashMap> storeOffLineTopicMessage) { 242 | if (null != db) { 243 | try { 244 | byte[] key = ServerConstant.OFFLINE_MESSAGE_TOPIC.getBytes(charset); 245 | byte[] value = JSON.toJSONBytes(storeOffLineTopicMessage, new SerializerFeature[]{SerializerFeature.DisableCircularReferenceDetect}); 246 | db.put(key, value); 247 | } catch (UnsupportedEncodingException e) { 248 | logger.error("离线消息持久化异常"); 249 | throw new XyException(ExceptionEnum.FAILED_TO_STORAGE); 250 | } 251 | } 252 | } 253 | 254 | /** 255 | * 从数据库中读取离线的订阅者信息 256 | * @return SocketChannel 离线的订阅者信息 257 | * @author 黎勇炫 258 | * @create 2022/7/10 259 | * @email 1677685900@qq.com 260 | */ 261 | public ConcurrentHashMap> getOffLineSubscriber() { 262 | ConcurrentHashMap hashMap = null; 263 | ConcurrentHashMap> realHashMap = null; 264 | try { 265 | byte[] key = ServerConstant.OFFLINE_SUBSCRIBER.getBytes(charset); 266 | byte[] value = db.get(key); 267 | hashMap = JSON.parseObject(new String(value), ConcurrentHashMap.class); 268 | if (null != hashMap) { 269 | realHashMap = new ConcurrentHashMap>(); 270 | for (Map.Entry entry : hashMap.entrySet()) { 271 | String str = entry.getValue().toString(); 272 | HashMap subscribers = JSON.parseObject(str, HashMap.class); 273 | HashMap newSubscribers = new HashMap(); 274 | for (Map.Entry longJSONObjectEntry : subscribers.entrySet()) { 275 | String longString = String.valueOf(longJSONObjectEntry.getKey()); 276 | String socketString = longJSONObjectEntry.getValue().toString(); 277 | Long clientId = Long.valueOf(longString); 278 | Channel channel = JSON.parseObject(socketString, NioSocketChannel.class); 279 | newSubscribers.put(clientId, channel); 280 | } 281 | realHashMap.put(entry.getKey(), newSubscribers); 282 | } 283 | } 284 | } catch (UnsupportedEncodingException e) { 285 | logger.error("离线订阅者信息读取失败"); 286 | throw new XyException(ExceptionEnum.FAILED_TO_RECOVERY_DATA); 287 | } 288 | return realHashMap; 289 | } 290 | 291 | /** 292 | * 从数据库中读取离线的消息数据 293 | * @return 离线的消息数据 294 | * @author 黎勇炫 295 | * @create 2022/7/10 296 | * @email 1677685900@qq.com 297 | */ 298 | public ConcurrentHashMap> getOffLineMessage() { 299 | ConcurrentHashMap hashMap = null; 300 | ConcurrentHashMap> realData = null; 301 | try { 302 | byte[] key = ServerConstant.OFFLINE_MESSAGE_TOPIC.getBytes(charset); 303 | byte[] value = db.get(key); 304 | hashMap = JSON.parseObject(new String(value), ConcurrentHashMap.class); 305 | if (null != hashMap) { 306 | realData = new ConcurrentHashMap>(); 307 | for (Map.Entry hashMapEntry : hashMap.entrySet()) { 308 | Long clientId = Long.valueOf(hashMapEntry.getKey()); 309 | JSONArray jsonObject = hashMapEntry.getValue(); 310 | ArrayList arrayList = JSON.parseObject(jsonObject.toString(), ArrayList.class); 311 | realData.put(clientId, arrayList); 312 | } 313 | } 314 | } catch (UnsupportedEncodingException e) { 315 | logger.error("离线消息读取失败"); 316 | e.printStackTrace(); 317 | } 318 | return realData; 319 | } 320 | 321 | public String getFolder() { 322 | return folder; 323 | } 324 | 325 | public void setFolder(String folder) { 326 | this.folder = folder; 327 | } 328 | 329 | public String getCharset() { 330 | return charset; 331 | } 332 | 333 | public void setCharset(String charset) { 334 | this.charset = charset; 335 | } 336 | } -------------------------------------------------------------------------------- /xymq-server/src/main/resources/static/axios/axios.min.js: -------------------------------------------------------------------------------- 1 | /* axios v0.21.1 | (c) 2020 by Matt Zabriskie */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.axios=t():e.axios=t()}(this,function(){return function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return e[r].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t,n){e.exports=n(1)},function(e,t,n){"use strict";function r(e){var t=new i(e),n=s(i.prototype.request,t);return o.extend(n,i.prototype,t),o.extend(n,t),n}var o=n(2),s=n(3),i=n(4),a=n(22),u=n(10),c=r(u);c.Axios=i,c.create=function(e){return r(a(c.defaults,e))},c.Cancel=n(23),c.CancelToken=n(24),c.isCancel=n(9),c.all=function(e){return Promise.all(e)},c.spread=n(25),c.isAxiosError=n(26),e.exports=c,e.exports.default=c},function(e,t,n){"use strict";function r(e){return"[object Array]"===R.call(e)}function o(e){return"undefined"==typeof e}function s(e){return null!==e&&!o(e)&&null!==e.constructor&&!o(e.constructor)&&"function"==typeof e.constructor.isBuffer&&e.constructor.isBuffer(e)}function i(e){return"[object ArrayBuffer]"===R.call(e)}function a(e){return"undefined"!=typeof FormData&&e instanceof FormData}function u(e){var t;return t="undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):e&&e.buffer&&e.buffer instanceof ArrayBuffer}function c(e){return"string"==typeof e}function f(e){return"number"==typeof e}function p(e){return null!==e&&"object"==typeof e}function d(e){if("[object Object]"!==R.call(e))return!1;var t=Object.getPrototypeOf(e);return null===t||t===Object.prototype}function l(e){return"[object Date]"===R.call(e)}function h(e){return"[object File]"===R.call(e)}function m(e){return"[object Blob]"===R.call(e)}function y(e){return"[object Function]"===R.call(e)}function g(e){return p(e)&&y(e.pipe)}function v(e){return"undefined"!=typeof URLSearchParams&&e instanceof URLSearchParams}function x(e){return e.replace(/^\s*/,"").replace(/\s*$/,"")}function w(){return("undefined"==typeof navigator||"ReactNative"!==navigator.product&&"NativeScript"!==navigator.product&&"NS"!==navigator.product)&&("undefined"!=typeof window&&"undefined"!=typeof document)}function b(e,t){if(null!==e&&"undefined"!=typeof e)if("object"!=typeof e&&(e=[e]),r(e))for(var n=0,o=e.length;n=200&&e<300}};u.headers={common:{Accept:"application/json, text/plain, */*"}},s.forEach(["delete","get","head"],function(e){u.headers[e]={}}),s.forEach(["post","put","patch"],function(e){u.headers[e]=s.merge(a)}),e.exports=u},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){r.forEach(e,function(n,r){r!==t&&r.toUpperCase()===t.toUpperCase()&&(e[t]=n,delete e[r])})}},function(e,t,n){"use strict";var r=n(2),o=n(13),s=n(16),i=n(5),a=n(17),u=n(20),c=n(21),f=n(14);e.exports=function(e){return new Promise(function(t,n){var p=e.data,d=e.headers;r.isFormData(p)&&delete d["Content-Type"];var l=new XMLHttpRequest;if(e.auth){var h=e.auth.username||"",m=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";d.Authorization="Basic "+btoa(h+":"+m)}var y=a(e.baseURL,e.url);if(l.open(e.method.toUpperCase(),i(y,e.params,e.paramsSerializer),!0),l.timeout=e.timeout,l.onreadystatechange=function(){if(l&&4===l.readyState&&(0!==l.status||l.responseURL&&0===l.responseURL.indexOf("file:"))){var r="getAllResponseHeaders"in l?u(l.getAllResponseHeaders()):null,s=e.responseType&&"text"!==e.responseType?l.response:l.responseText,i={data:s,status:l.status,statusText:l.statusText,headers:r,config:e,request:l};o(t,n,i),l=null}},l.onabort=function(){l&&(n(f("Request aborted",e,"ECONNABORTED",l)),l=null)},l.onerror=function(){n(f("Network Error",e,null,l)),l=null},l.ontimeout=function(){var t="timeout of "+e.timeout+"ms exceeded";e.timeoutErrorMessage&&(t=e.timeoutErrorMessage),n(f(t,e,"ECONNABORTED",l)),l=null},r.isStandardBrowserEnv()){var g=(e.withCredentials||c(y))&&e.xsrfCookieName?s.read(e.xsrfCookieName):void 0;g&&(d[e.xsrfHeaderName]=g)}if("setRequestHeader"in l&&r.forEach(d,function(e,t){"undefined"==typeof p&&"content-type"===t.toLowerCase()?delete d[t]:l.setRequestHeader(t,e)}),r.isUndefined(e.withCredentials)||(l.withCredentials=!!e.withCredentials),e.responseType)try{l.responseType=e.responseType}catch(t){if("json"!==e.responseType)throw t}"function"==typeof e.onDownloadProgress&&l.addEventListener("progress",e.onDownloadProgress),"function"==typeof e.onUploadProgress&&l.upload&&l.upload.addEventListener("progress",e.onUploadProgress),e.cancelToken&&e.cancelToken.promise.then(function(e){l&&(l.abort(),n(e),l=null)}),p||(p=null),l.send(p)})}},function(e,t,n){"use strict";var r=n(14);e.exports=function(e,t,n){var o=n.config.validateStatus;n.status&&o&&!o(n.status)?t(r("Request failed with status code "+n.status,n.config,null,n.request,n)):e(n)}},function(e,t,n){"use strict";var r=n(15);e.exports=function(e,t,n,o,s){var i=new Error(e);return r(i,t,n,o,s)}},function(e,t){"use strict";e.exports=function(e,t,n,r,o){return e.config=t,n&&(e.code=n),e.request=r,e.response=o,e.isAxiosError=!0,e.toJSON=function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:this.config,code:this.code}},e}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){return{write:function(e,t,n,o,s,i){var a=[];a.push(e+"="+encodeURIComponent(t)),r.isNumber(n)&&a.push("expires="+new Date(n).toGMTString()),r.isString(o)&&a.push("path="+o),r.isString(s)&&a.push("domain="+s),i===!0&&a.push("secure"),document.cookie=a.join("; ")},read:function(e){var t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove:function(e){this.write(e,"",Date.now()-864e5)}}}():function(){return{write:function(){},read:function(){return null},remove:function(){}}}()},function(e,t,n){"use strict";var r=n(18),o=n(19);e.exports=function(e,t){return e&&!r(t)?o(e,t):t}},function(e,t){"use strict";e.exports=function(e){return/^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(e)}},function(e,t){"use strict";e.exports=function(e,t){return t?e.replace(/\/+$/,"")+"/"+t.replace(/^\/+/,""):e}},function(e,t,n){"use strict";var r=n(2),o=["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"];e.exports=function(e){var t,n,s,i={};return e?(r.forEach(e.split("\n"),function(e){if(s=e.indexOf(":"),t=r.trim(e.substr(0,s)).toLowerCase(),n=r.trim(e.substr(s+1)),t){if(i[t]&&o.indexOf(t)>=0)return;"set-cookie"===t?i[t]=(i[t]?i[t]:[]).concat([n]):i[t]=i[t]?i[t]+", "+n:n}}),i):i}},function(e,t,n){"use strict";var r=n(2);e.exports=r.isStandardBrowserEnv()?function(){function e(e){var t=e;return n&&(o.setAttribute("href",t),t=o.href),o.setAttribute("href",t),{href:o.href,protocol:o.protocol?o.protocol.replace(/:$/,""):"",host:o.host,search:o.search?o.search.replace(/^\?/,""):"",hash:o.hash?o.hash.replace(/^#/,""):"",hostname:o.hostname,port:o.port,pathname:"/"===o.pathname.charAt(0)?o.pathname:"/"+o.pathname}}var t,n=/(msie|trident)/i.test(navigator.userAgent),o=document.createElement("a");return t=e(window.location.href),function(n){var o=r.isString(n)?e(n):n;return o.protocol===t.protocol&&o.host===t.host}}():function(){return function(){return!0}}()},function(e,t,n){"use strict";var r=n(2);e.exports=function(e,t){function n(e,t){return r.isPlainObject(e)&&r.isPlainObject(t)?r.merge(e,t):r.isPlainObject(t)?r.merge({},t):r.isArray(t)?t.slice():t}function o(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(s[o]=n(void 0,e[o])):s[o]=n(e[o],t[o])}t=t||{};var s={},i=["url","method","data"],a=["headers","auth","proxy","params"],u=["baseURL","transformRequest","transformResponse","paramsSerializer","timeout","timeoutMessage","withCredentials","adapter","responseType","xsrfCookieName","xsrfHeaderName","onUploadProgress","onDownloadProgress","decompress","maxContentLength","maxBodyLength","maxRedirects","transport","httpAgent","httpsAgent","cancelToken","socketPath","responseEncoding"],c=["validateStatus"];r.forEach(i,function(e){r.isUndefined(t[e])||(s[e]=n(void 0,t[e]))}),r.forEach(a,o),r.forEach(u,function(o){r.isUndefined(t[o])?r.isUndefined(e[o])||(s[o]=n(void 0,e[o])):s[o]=n(void 0,t[o])}),r.forEach(c,function(r){r in t?s[r]=n(e[r],t[r]):r in e&&(s[r]=n(void 0,e[r]))});var f=i.concat(a).concat(u).concat(c),p=Object.keys(e).concat(Object.keys(t)).filter(function(e){return f.indexOf(e)===-1});return r.forEach(p,o),s}},function(e,t){"use strict";function n(e){this.message=e}n.prototype.toString=function(){return"Cancel"+(this.message?": "+this.message:"")},n.prototype.__CANCEL__=!0,e.exports=n},function(e,t,n){"use strict";function r(e){if("function"!=typeof e)throw new TypeError("executor must be a function.");var t;this.promise=new Promise(function(e){t=e});var n=this;e(function(e){n.reason||(n.reason=new o(e),t(n.reason))})}var o=n(23);r.prototype.throwIfRequested=function(){if(this.reason)throw this.reason},r.source=function(){var e,t=new r(function(t){e=t});return{token:t,cancel:e}},e.exports=r},function(e,t){"use strict";e.exports=function(e){return function(t){return e.apply(null,t)}}},function(e,t){"use strict";e.exports=function(e){return"object"==typeof e&&e.isAxiosError===!0}}])}); 3 | //# sourceMappingURL=axios.min.map -------------------------------------------------------------------------------- /xymq-server/src/main/java/com/xymq_cli/core/XymqServer.java: -------------------------------------------------------------------------------- 1 | package com.xymq_cli.core; 2 | 3 | import com.xymq_cli.constant.Destination; 4 | import com.xymq_cli.constant.ServerConstant; 5 | import com.xymq_cli.execution.AckExec; 6 | import com.xymq_cli.web.domain.QueueVO; 7 | import com.xymq_common.message.Message; 8 | import com.xymq_common.protocol.MessageUtils; 9 | import io.netty.bootstrap.ServerBootstrap; 10 | import io.netty.channel.Channel; 11 | import io.netty.channel.ChannelOption; 12 | import io.netty.channel.nio.NioEventLoopGroup; 13 | import io.netty.channel.socket.nio.NioServerSocketChannel; 14 | import lombok.Data; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.springframework.beans.factory.annotation.Autowired; 18 | import org.springframework.boot.context.properties.ConfigurationProperties; 19 | import org.springframework.scheduling.annotation.Async; 20 | import org.springframework.scheduling.annotation.Scheduled; 21 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 22 | import org.springframework.stereotype.Component; 23 | import org.springframework.util.CollectionUtils; 24 | 25 | import java.util.*; 26 | import java.util.concurrent.CompletableFuture; 27 | import java.util.concurrent.ConcurrentHashMap; 28 | import java.util.concurrent.DelayQueue; 29 | import java.util.concurrent.LinkedBlockingDeque; 30 | import java.util.concurrent.atomic.LongAdder; 31 | 32 | /** 33 | * @author 黎勇炫 34 | * @date 2022年07月09日 16:26 35 | */ 36 | 37 | @Component 38 | @Data 39 | @ConfigurationProperties(prefix = "xymq") 40 | public class XymqServer { 41 | 42 | @Autowired 43 | private ThreadPoolTaskExecutor taskExecutor; 44 | @Autowired 45 | private LevelDb levelDb; 46 | @Autowired 47 | private ClientManager clientManager; 48 | @Autowired 49 | private AckExec ackExec; 50 | 51 | private Logger logger = LoggerFactory.getLogger(XymqServer.class); 52 | 53 | /** 54 | * 记录主题消息推送成功的消息数量 55 | */ 56 | private LongAdder topicSuccess = new LongAdder(); 57 | 58 | /** 59 | * 服务端口号 60 | */ 61 | private int port; 62 | /** 63 | * 前端websocket端口号 64 | */ 65 | private int wbsockport; 66 | /** 67 | * 服务端可连接队列 68 | */ 69 | private int backLog; 70 | /** 71 | * 处理连接请求的工作组 72 | */ 73 | private NioEventLoopGroup bossGroup = new NioEventLoopGroup(); 74 | /** 75 | * 1对1消息队列实施轮询策略 76 | */ 77 | private int queueIndex = 0; 78 | /** 79 | * 处理读写操作的工作组 80 | */ 81 | private NioEventLoopGroup ioGroup = new NioEventLoopGroup(); 82 | /** 83 | * 存储一对一队列消息 84 | */ 85 | private ConcurrentHashMap> queueContainer = new ConcurrentHashMap>(); 86 | /** 87 | * 存储一对多消息 88 | */ 89 | private ConcurrentHashMap> topicContainer = new ConcurrentHashMap>(); 90 | /** 91 | * 存储延时队列消息 92 | */ 93 | private ConcurrentHashMap> delayQueueContainer = new ConcurrentHashMap<>(); 94 | /** 95 | * 存储延时主题消息 96 | */ 97 | private ConcurrentHashMap> delayTopicContainer = new ConcurrentHashMap<>(); 98 | /** 99 | * 队列消费者 100 | */ 101 | private ConcurrentHashMap> consumerContainer = new ConcurrentHashMap>(); 102 | /** 103 | * 在线的订阅者 104 | */ 105 | private ConcurrentHashMap> subscriberContainer = new ConcurrentHashMap>(); 106 | /** 107 | * 存储离线的订阅者 108 | */ 109 | private ConcurrentHashMap> offLineSubscriber = new ConcurrentHashMap>(); 110 | /** 111 | * 存储离线订阅消息,K为客户端id,list存放消息 112 | */ 113 | private ConcurrentHashMap> offLineTopicMessage = new ConcurrentHashMap>(); 114 | 115 | /** 116 | * 服务端初始化工作 117 | * 118 | * @return void 119 | * @author 黎勇炫 120 | * @create 2022/7/9 121 | * @email 1677685900@qq.com 122 | */ 123 | public void init() { 124 | CompletableFuture.runAsync(() -> { 125 | ServerBootstrap server = new ServerBootstrap(); 126 | try { 127 | server.group(bossGroup, ioGroup) 128 | .channel(NioServerSocketChannel.class) 129 | .option(ChannelOption.SO_BACKLOG, backLog) 130 | .childOption(ChannelOption.SO_KEEPALIVE, true); 131 | 132 | server.childHandler(new ServerInitializer()); 133 | Channel socketChannel = server.bind(port).sync().channel(); 134 | 135 | server.childHandler(new WebsocketInitializer()); 136 | Channel websocetChannel = server.bind(wbsockport).sync().channel(); 137 | 138 | // 数据恢复 139 | recoveryMessage(); 140 | // 开始推送消息 141 | satrt(); 142 | socketChannel.closeFuture().sync(); 143 | websocetChannel.closeFuture().sync(); 144 | } catch (InterruptedException e) { 145 | throw new RuntimeException("netty服务端启动失败"); 146 | } finally { 147 | // 关闭两个工作线程组 148 | bossGroup.shutdownGracefully(); 149 | ioGroup.shutdownGracefully(); 150 | } 151 | }, taskExecutor); 152 | } 153 | 154 | /** 155 | * 开始推送消息 156 | * @return void 157 | * @author 黎勇炫 158 | * @create 2022/7/24 159 | * @email 1677685900@qq.com 160 | */ 161 | private void satrt() { 162 | sendMessageToClients(); 163 | sendDelayMessageToClient(); 164 | sendMessageToSubscriber(); 165 | sendDelayTopicMessageToSubscriber(); 166 | sendMessageTorebindingSubscriber(); 167 | } 168 | 169 | /** 170 | * 发送消息到消费者 171 | * 172 | * @return void 173 | * @author 黎勇炫 174 | * @create 2022/7/10 175 | * @email 1677685900@qq.com 176 | */ 177 | public void sendMessageToClients() { 178 | // 异步执行,遍历队列消息容器 179 | CompletableFuture.runAsync(() -> { 180 | try { 181 | while (!bossGroup.isShutdown()) { 182 | // 遍历整个队列容器 183 | for (Map.Entry> entry : queueContainer.entrySet()) { 184 | // key就是队列名 185 | String key = entry.getKey(); 186 | LinkedBlockingDeque queue = entry.getValue(); 187 | // 当消息队列中有数据并且该队列存在消费者,就调用线程池,负责为该队列推送消息 188 | while (queue.size() > 0 && consumerContainer.containsKey(key)) { 189 | if (consumerContainer.get(key).size() != 0) { 190 | Channel channel = getChannel(consumerContainer.get(key)); 191 | // 只有连接在活跃状态下才开始推送消息 192 | if (channel.isActive()) { 193 | // 发送消息到未断开连接的消费者 194 | Message message = queue.poll(); 195 | MessageUtils.message2Protocol(message); 196 | channel.writeAndFlush(MessageUtils.message2Protocol(message)); 197 | } 198 | } 199 | } 200 | 201 | // while (queue.size() > 0 && consumerContainer.containsKey(key)) { 202 | // if (consumerContainer.get(key).size() != 0) { 203 | // Channel channel = getChannel(consumerContainer.get(key)); 204 | // // 只有连接在活跃状态下才开始推送消息 205 | // if (channel.isActive()) { 206 | // // 发送消息到未断开连接的消费者 207 | // Message message = queue.poll(); 208 | // MessageUtils.message2Protocol(message); 209 | // channel.writeAndFlush(MessageUtils.message2Protocol(message)); 210 | // } 211 | // } 212 | // } 213 | } 214 | } 215 | } catch (Exception e) { 216 | logger.error("消息推送失败"); 217 | } 218 | }, taskExecutor); 219 | } 220 | 221 | /** 222 | * 发送延时消息到消费者 223 | * 224 | * @return void 225 | * @author 黎勇炫 226 | * @create 2022/7/16 227 | * @email 1677685900@qq.com 228 | */ 229 | public void sendDelayMessageToClient() { 230 | CompletableFuture.runAsync(() -> { 231 | try { 232 | while (!bossGroup.isShutdown()) { 233 | for (Map.Entry> entry : delayQueueContainer.entrySet()) { 234 | String key = entry.getKey(); 235 | DelayQueue queue = entry.getValue(); 236 | while (queue.size() > 0 && consumerContainer.containsKey(key)) { 237 | if (consumerContainer.get(key).size() != 0) { 238 | Channel channel = getChannel(consumerContainer.get(key)); 239 | if (channel.isActive()) { 240 | /* 241 | * 发送消息到未断开连接的消费者 242 | * */ 243 | Message message = null; 244 | try { 245 | message = queue.take(); 246 | channel.writeAndFlush(MessageUtils.message2Protocol(message)); 247 | } catch (InterruptedException e) { 248 | logger.error("消息{}推送时发生异常", message.getMessageId()); 249 | e.printStackTrace(); 250 | } 251 | } 252 | } 253 | } 254 | } 255 | } 256 | } catch (Exception e) { 257 | e.printStackTrace(); 258 | } 259 | }, taskExecutor); 260 | } 261 | 262 | /** 263 | * 发布消息到订阅了的客户端 264 | * 265 | * @return void 266 | * @author 黎勇炫 267 | * @create 2022/7/17 268 | * @email 1677685900@qq.com 269 | */ 270 | public void sendMessageToSubscriber() { 271 | //判断是否所有消费者都已经消费,如果都消费了就从数据库中删除 272 | CompletableFuture.runAsync(() -> { 273 | try { 274 | while (!bossGroup.isShutdown()) { 275 | for (Map.Entry> entry : topicContainer.entrySet()) { 276 | String key = entry.getKey(); 277 | LinkedBlockingDeque topicQueue = entry.getValue(); 278 | System.out.println(topicQueue); 279 | // 假如某个主题容器有消息并且有订阅者在线 280 | while (topicQueue.size() > 0 && subscriberContainer.containsKey(key)) { 281 | if (!CollectionUtils.isEmpty(subscriberContainer.get(key))) { 282 | Message message = topicQueue.poll(); 283 | // 根据目的地取出指定map。key为订阅者id,value为订阅者的channel 284 | HashMap subscribers = subscriberContainer.get(message.getDestination()); 285 | // 遍历整个订阅者容器,向每一个订阅者推送消息 286 | pushTopicMessage(key, message, subscribers); 287 | // 推送完从leveldb中删除消息 288 | levelDb.deleteMessageBean(message.getMessageId()); 289 | } 290 | } 291 | } 292 | } 293 | } catch (Exception e) { 294 | logger.error("推送主题消息时发生异常"); 295 | e.printStackTrace(); 296 | } 297 | }, taskExecutor); 298 | } 299 | 300 | /** 301 | * 发送延时消息到订阅者 302 | * @return void 303 | * @author 黎勇炫 304 | * @create 2022/7/24 305 | * @email 1677685900@qq.com 306 | */ 307 | public void sendDelayTopicMessageToSubscriber() { 308 | CompletableFuture.runAsync(()->{ 309 | try { 310 | while (!bossGroup.isShutdown()) { 311 | for (Map.Entry> entry : delayTopicContainer.entrySet()) { 312 | String key = entry.getKey(); 313 | DelayQueue delayTopic = entry.getValue(); 314 | while (delayTopic.size() > 0 && subscriberContainer.containsKey(key)) { 315 | if (!CollectionUtils.isEmpty(subscriberContainer.get(key))) { 316 | Message message = delayTopic.take(); 317 | HashMap subscribers = subscriberContainer.get(message.getDestination()); 318 | pushTopicMessage(key, message, subscribers); 319 | } 320 | 321 | } 322 | } 323 | } 324 | } catch (Exception e) { 325 | logger.error("推送延时主题消息时发生异常"); 326 | e.printStackTrace(); 327 | } 328 | },taskExecutor); 329 | } 330 | 331 | /** 332 | * 订阅者上线时,如果是该编号有离线消息,就先推送离线消息 333 | * @return void 334 | * @author 黎勇炫 335 | * @create 2022/7/24 336 | * @email 1677685900@qq.com 337 | */ 338 | public void sendMessageTorebindingSubscriber() { 339 | CompletableFuture.runAsync(()->{ 340 | while (!bossGroup.isShutdown()) { 341 | // 遍历离线订阅者 342 | for (Map.Entry> entry : offLineSubscriber.entrySet()) { 343 | // 主题名称 344 | String destination = entry.getKey(); 345 | // key是编号,value是对应的客户端 346 | HashMap subscribers = entry.getValue(); 347 | for (Map.Entry channelEntry : subscribers.entrySet()) { 348 | Long clientId = channelEntry.getKey(); 349 | Channel channel = channelEntry.getValue(); 350 | // 判断客户端是否在连接状态 351 | if (null != channel && channel.isActive()) { 352 | Iterator iterator = offLineTopicMessage.get(clientId).listIterator(); 353 | // 遍历消息,一个个推送 354 | while (iterator.hasNext()) { 355 | Message message = iterator.next(); 356 | channel.writeAndFlush(MessageUtils.message2Protocol(message)); 357 | iterator.remove(); 358 | topicSuccess.increment(); 359 | } 360 | // 消息推送完后重写把客户端放回在线的容器 361 | if (subscriberContainer.containsKey(destination)) { 362 | subscriberContainer.get(destination).put(clientId, channel); 363 | } else { 364 | subscriberContainer.put(destination, new HashMap()); 365 | subscriberContainer.get(destination).put(clientId, channel); 366 | } 367 | 368 | } 369 | } 370 | } 371 | } 372 | },taskExecutor); 373 | } 374 | 375 | /** 376 | * 推送消息给订阅者 377 | * @param key 378 | * @param message 379 | * @param subscribers 380 | * @return void 381 | * @author 黎勇炫 382 | * @create 2022/7/24 383 | * @email 1677685900@qq.com 384 | */ 385 | private void pushTopicMessage(String key, Message message, HashMap subscribers) { 386 | for (Map.Entry channelEntry : subscribers.entrySet()) { 387 | Long clientId = channelEntry.getKey(); 388 | Channel subscriber = channelEntry.getValue(); 389 | if (subscriber.isActive()) { 390 | subscriber.writeAndFlush(MessageUtils.message2Protocol(message)); 391 | topicSuccess.increment(); 392 | } else { 393 | storeOfflineData(key, message, clientId, subscriber); 394 | } 395 | } 396 | } 397 | 398 | /** 399 | * 存储离线的订阅者和消息 400 | * @param key 主题名称 401 | * @param message 消息对象 402 | * @param clientId 客户端编号 403 | * @param subscriber 订阅者 404 | * @return void 405 | * @author 黎勇炫 406 | * @create 2022/7/24 407 | * @email 1677685900@qq.com 408 | */ 409 | private void storeOfflineData(String key, Message message, Long clientId, Channel subscriber) { 410 | if (offLineSubscriber.containsKey(key)) { 411 | HashMap offLineSubscribers = offLineSubscriber.get(key); 412 | offLineSubscribers.put(clientId, subscriber); 413 | } else { 414 | offLineSubscriber.put(key, new HashMap()); 415 | offLineSubscriber.get(key).put(clientId, subscriber); 416 | } 417 | if (offLineTopicMessage.containsKey(clientId)) { 418 | offLineTopicMessage.get(clientId).add(message); 419 | } else { 420 | offLineTopicMessage.put(clientId, new ArrayList<>()); 421 | offLineTopicMessage.get(clientId).add(message); 422 | } 423 | } 424 | 425 | /** 426 | * 取模算法获取channel(一对一的队列用轮询策略) 427 | * 428 | * @param channels 通道列表 429 | * @return io.netty.channel.Channel 430 | * @author 黎勇炫 431 | * @create 2022/7/10 432 | * @email 1677685900@qq.com 433 | */ 434 | private Channel getChannel(List channels) { 435 | queueIndex = queueIndex % channels.size(); 436 | Channel channel = channels.get(queueIndex); 437 | queueIndex++; 438 | return channel; 439 | } 440 | 441 | /** 442 | * 系统启动时要从leveldb中读取消息,将所有的消息重新读到缓冲中 443 | * 假如时延时队列的消息就要重新判断消息,如果时已经过期的消息就不读到内存中了,否则就重新计算过期事件放入内存 444 | * 445 | * @return void 446 | * @author 黎勇炫 447 | * @create 2022/7/12 448 | * @email 1677685900@qq.com 449 | */ 450 | public void recoveryMessage() { 451 | List keys = levelDb.getKeys(); 452 | Iterator iterator = keys.iterator(); 453 | while (iterator.hasNext()) { 454 | String key = iterator.next(); 455 | // 区分普通消息和离线订阅消息 456 | if (!key.equals("offLineSubscriber") && !key.equals("offLineTopicMessage")) { 457 | Message message = levelDb.getMessageBean(key); 458 | if (null != message) { 459 | // 创建对应的容器并放入 460 | if (message.getDestinationType() == (Destination.QUEUE.getDestination())) { 461 | if (queueContainer.containsKey(message.getDestination())) { 462 | queueContainer.get(message.getDestination()).offer(message); 463 | } else { 464 | queueContainer.put(message.getDestination(), new LinkedBlockingDeque<>()); 465 | queueContainer.get(message.getDestination()).offer(message); 466 | } 467 | } 468 | } 469 | } else { 470 | switch (key) { 471 | case ServerConstant.OFFLINE_MESSAGE_TOPIC: 472 | offLineTopicMessage = levelDb.getOffLineMessage(); 473 | break; 474 | case ServerConstant.OFFLINE_SUBSCRIBER: 475 | offLineSubscriber = levelDb.getOffLineSubscriber(); 476 | break; 477 | default: 478 | //删除没有意义的数据 479 | levelDb.deleteMessageBean(Long.parseLong(key)); 480 | iterator.remove(); 481 | break; 482 | } 483 | } 484 | } 485 | } 486 | 487 | /** 488 | * 清理离线客户端 489 | * 490 | * @return void 491 | * @author 黎勇炫 492 | * @create 2022/7/19 493 | * @email 1677685900@qq.com 494 | */ 495 | @Async 496 | @Scheduled(cron = "0/1 * * * * ? ") 497 | public void clean() { 498 | clientManager.clean(consumerContainer); 499 | } 500 | 501 | /** 502 | * 存储离线客户端和离线消息 503 | */ 504 | @Async 505 | @Scheduled(cron = "0/1 * * * * ? ") 506 | public void storeOfflineData() { 507 | clientManager.storeOfflineData(offLineTopicMessage, offLineSubscriber); 508 | } 509 | 510 | /** 511 | * 获取未读的队列消息数量 512 | * @return long 队列中未读消息的数量 513 | * @author 黎勇炫 514 | * @create 2022/8/1 515 | * @email 1677685900@qq.com 516 | */ 517 | public long getUnReadQueueMessageCount(){ 518 | return getCountFromContainer(this.queueContainer); 519 | } 520 | 521 | /** 522 | * 获取未读的主题消息数量 523 | * @return java.lang.Long 524 | * @author 黎勇炫 525 | * @create 2022/8/6 526 | * @email 1677685900@qq.com 527 | */ 528 | public Long getUnReadTopicMessageCount(){ 529 | return getCountFromContainer(this.topicContainer); 530 | } 531 | 532 | /** 533 | * 获取主题消息中推送失败的消息数量 534 | * @return java.lang.Long 535 | * @author 黎勇炫 536 | * @create 2022/8/6 537 | * @email 1677685900@qq.com 538 | */ 539 | public Long getOffLineTopicMessageCount(){ 540 | long count = 0; 541 | // 遍历容器,拿到每一个队列中的消息的数量(还没推送出去说明还没被消费) 542 | for (Map.Entry> offLineTopicEntry : offLineTopicMessage.entrySet()) { 543 | count += offLineTopicEntry.getValue().size(); 544 | } 545 | return count; 546 | } 547 | 548 | /** 549 | * 遍历容器拿到各容器消息数量 550 | * @param topicContainer 551 | * @return java.lang.Long 552 | * @author 黎勇炫 553 | * @create 2022/8/6 554 | * @email 1677685900@qq.com 555 | */ 556 | private Long getCountFromContainer(ConcurrentHashMap> topicContainer) { 557 | long count = 0; 558 | // 遍历容器,拿到每一个队列中的消息的数量(还没推送出去说明还没被消费) 559 | for (Map.Entry> dequeEntry : topicContainer.entrySet()) { 560 | count += dequeEntry.getValue().size(); 561 | } 562 | return count; 563 | } 564 | 565 | /** 566 | * 返回队列消息容器的堆积情况 567 | * @return java.util.Map 568 | * @author 黎勇炫 569 | * @create 2022/8/8 570 | * @email 1677685900@qq.com 571 | */ 572 | public Map queueAccDetail(){ 573 | return containerAccDetail(queueContainer); 574 | } 575 | 576 | /** 577 | * 返回主题消息容器的堆积情况 578 | * @return java.util.Map 579 | * @author 黎勇炫 580 | * @create 2022/8/8 581 | * @email 1677685900@qq.com 582 | */ 583 | public Map topicAccDetail(){ 584 | return containerAccDetail(topicContainer); 585 | } 586 | 587 | /** 588 | * 获取指定容器中消息的堆积情况 589 | * @return java.util.Map 590 | * @author 黎勇炫 591 | * @create 2022/8/5 592 | * @email 1677685900@qq.com 593 | */ 594 | public Map containerAccDetail(Map> param){ 595 | // 遍历每个队列容器,拿到每个队列的消息堆积情况 596 | Map map = new HashMap<>(); 597 | for (String queue : param.keySet()) { 598 | map.put(queue, (long) param.get(queue).size()); 599 | } 600 | // 如果队列太多了用扇形图展示不好看,容器>6就将少的部分合并为‘其他’ 601 | if(map.size()>6){ 602 | map.clear(); 603 | List> list = new ArrayList>(map.entrySet()); 604 | Collections.sort(list, new Comparator>() { 605 | @Override 606 | public int compare(Map.Entry o1, Map.Entry o2) { 607 | return o1.getValue().compareTo(o2.getValue()); 608 | } 609 | }); 610 | // 重新设值 611 | int num = 0; 612 | map.put("其他",0L); 613 | for (Map.Entry entry : list) { 614 | if(num < 6){ 615 | map.put(entry.getKey(),entry.getValue()); 616 | }else { 617 | map.put("其他",map.get("其他").longValue()+entry.getValue()); 618 | } 619 | num++; 620 | } 621 | } 622 | return map; 623 | } 624 | 625 | /** 626 | * 推送成功的主题消息数 627 | * @return java.lang.Long 628 | * @author 黎勇炫 629 | * @create 2022/8/6 630 | * @email 1677685900@qq.com 631 | */ 632 | public Long topicSuccessCount(){ 633 | return topicSuccess.longValue(); 634 | } 635 | 636 | /** 637 | * 获取队列详情 638 | * @return java.util.List 639 | * @author 黎勇炫 640 | * @create 2022/8/13 641 | * @email 1677685900@qq.com 642 | */ 643 | public List queueDetails(){ 644 | List l = new ArrayList<>(); 645 | // 遍历队列消息容器 646 | for (String key : queueContainer.keySet()) { 647 | QueueVO vo = new QueueVO(); 648 | // key为队列名 649 | vo.setQueueName(key); 650 | // 未消费数量 651 | if(delayQueueContainer.containsKey(key)){ 652 | Integer delayCount = delayQueueContainer.get(key).size(); 653 | vo.setUnConsume((long) (queueContainer.get(key).size()+delayQueueContainer.get(key).size())); 654 | vo.setDelayCount((long)delayCount); 655 | }else{ 656 | vo.setUnConsume((long) queueContainer.get(key).size()); 657 | } 658 | // 该队列的消费者 659 | if(consumerContainer.containsKey(key)){ 660 | vo.setConsumerCount(consumerContainer.get(key).size()); 661 | }else{ 662 | vo.setConsumerCount(0); 663 | } 664 | l.add(vo); 665 | } 666 | return l; 667 | } 668 | } 669 | --------------------------------------------------------------------------------