Spring Boot+NettySocketIo+RabbitMQ实现websocket集群

Spring Boot+NettySocketIo+RabbitMQ实现websocket集群

效果
在这里插入图片描述
在这里插入图片描述
连接到不同的服务器的两个客户端可以互发消息

websocket的连接Client是不能序列化的,所以不能使用session共享那样的方法来做websocket的集群
nettySocket已经支持了Redission的集群方案,这里在原来有的基础上使用RabbitMQ实现的集群方案

  1. 目录结构
    在这里插入图片描述

  2. 引入依赖

    	 <dependency>
             <groupId>com.corundumstudio.socketio</groupId>
             <artifactId>netty-socketio</artifactId>
             <version>1.7.18</version>
         </dependency>
    
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-amqp</artifactId>
         </dependency>
    
    
  3. 配置文件

    spring:
      rabbitmq:
        port: 5672
        host: 127.0.0.1
        username: guest
        password: guest
        connection-timeout: 15000
    server:
      port: 8080
      address: 127.0.0.1
    
  4. NettySocketIoConfig

    @Slf4j
    @org.springframework.context.annotation.Configuration
    public class NettySocketIoConfig implements CommandLineRunner {
    
        @Value("${server.port}")
        private String port;
    
        @Value("${server.address}")
        private String address;
    
        @Autowired
        private RabbitTemplate rabbitPub;
        @Autowired
        private RabbitMQService rabbitMQService;
    
        /**
         * netty-socketIo服务器
         */
        @Bean("socketIoServer")
        public SocketIOServer socketIoServer() {
            Configuration config = new Configuration();
            //单次发送数据最大值1M
            config.setMaxFramePayloadLength(1024 * 1024);
            //socket连接数大小(如只监听一个端口boss线程组为1即可)
            config.setBossThreads(1);
            config.setWorkerThreads(100);
            //身份验证
            config.setAuthorizationListener(handshakeData -> true);
            config.setHostname(address);
            config.setPort(Integer.parseInt(port) + 1);
            //这里是重点
            StoreFactory storeFactory = new RabbitMQStoreFactory(rabbitPub, rabbitMQService);
            config.setStoreFactory(storeFactory);
            return new SocketIOServer(config);
        }
        
        /**
         * 用于扫描netty-socketIo的注解,比如 @OnConnect、@OnEvent
         */
        @Bean
        public SpringAnnotationScanner springAnnotationScanner() {
            return new SpringAnnotationScanner(socketIoServer());
        }
    
    
        @Override
        public void run(String... strings) {
            SocketIOServer server = socketIoServer();
            server.start();
            log.info("socket.io启动成功!");
        }
    }
    
  5. RabbitMQStoreFactory

    	public class RabbitMQStoreFactory extends BaseStoreFactory {
    
        private final PubSubStore pubSubStore;
    
        public RabbitMQStoreFactory(RabbitTemplate rabbitPub, RabbitMQService rabbitMQService) {
            this.pubSubStore = new RabbitMQPubSubStore(rabbitPub, rabbitMQService, getNodeId());
        }
    
    
        @Override
        public void init(NamespacesHub namespacesHub, AuthorizeHandler authorizeHandler, JsonSupport jsonSupport) {
            //只对 DISPATCH 消息进行集群共享
            pubSubStore.subscribe(PubSubType.DISPATCH,
                    data -> {
                        Packet packet = data.getPacket();
                        SpringContextUtil.getBeanByClass(CustomBroadcastOperations.class).sendNotDispatch(packet);
                    },
                    DispatchMessage.class);
        }
    
        @Override
        public PubSubStore pubSubStore() {
            return pubSubStore;
        }
    
        @Override
        public <K, V> Map<K, V> createMap(String name) {
            return PlatformDependent.newConcurrentHashMap();
        }
    
        @Override
        public Store createStore(UUID sessionId) {
            return new MemoryStore();
        }
    
        @Override
        public void shutdown() {
    
        }
    }
    
    1. RabbitMQPubSubStore
    @Slf4j
    public class RabbitMQPubSubStore implements PubSubStore {
    
        private MessageConverter messageConverter = new SimpleMessageConverter();
        private final RabbitTemplate rabbitPub;
        private final RabbitMQService rabbitMQService;
        private final Long nodeId;
    
        private final ConcurrentMap<String, Queue<String>> map = PlatformDependent.newConcurrentHashMap();
    
        public RabbitMQPubSubStore(RabbitTemplate rabbitPub, RabbitMQService rabbitMQService, Long nodeId) {
            this.rabbitPub = rabbitPub;
            this.rabbitMQService = rabbitMQService;
            this.nodeId = nodeId;
        }
    
        @Override
        public void publish(PubSubType type, PubSubMessage msg) {
            msg.setNodeId(nodeId);
            if (type == PubSubType.DISPATCH) {
                rabbitPub.convertSendAndReceive(type.toString(), "", msg);
            }
        }
    
        @Override
        @SuppressWarnings("unchecked")
        public <T extends PubSubMessage> void subscribe(PubSubType type, PubSubListener<T> listener, Class<T> clazz) {
            String name = type.toString();
    
            Exchange exchange = rabbitMQService.createExchange(ExchangeTypes.FANOUT, false, name);
            org.springframework.amqp.core.Queue queue = rabbitMQService.createQueue();
            rabbitMQService.queueBindExchange(queue, exchange);
    
            rabbitMQService.queueAddListener(queue, message -> {
                T t = (T) messageConverter.fromMessage(message);
                if (!nodeId.equals(t.getNodeId())) {
                    listener.onMessage(t);
                }
            });
    
            Queue<String> list = map.get(name);
            if (list == null) {
                list = new ConcurrentLinkedQueue<>();
                Queue<String> oldList = map.putIfAbsent(name, list);
                if (oldList != null) {
                    list = oldList;
                }
            }
            list.add(queue.getName());
        }
    
        @Override
        public void unsubscribe(PubSubType type) {
            String name = type.toString();
            Queue<String> queueNames = map.remove(name);
            for (String queueName : queueNames) {
                rabbitMQService.deleteQueueAndRemoveListener(queueName);
            }
        }
    
        @Override
        public void shutdown() {
    
        }
    
        public MessageConverter getMessageConverter() {
            return messageConverter;
        }
    
        public void setMessageConverter(MessageConverter messageConverter) {
            this.messageConverter = messageConverter;
        }
    
    }
    
    1. CustomBroadcastOperations 默认的发送消息会发送给本地的所有连接,重写一下
    @Service
    public class CustomBroadcastOperations extends BroadcastOperations {
    
    
        private final Iterable<SocketIOClient> clients;
        private final StoreFactory storeFactory;
    
        public CustomBroadcastOperations(SocketIOServer socketIoServer) {
            super(socketIoServer.getAllClients(), socketIoServer.getConfiguration().getStoreFactory());
            clients = socketIoServer.getAllClients();
            storeFactory = socketIoServer.getConfiguration().getStoreFactory();
        }
    
    
        @Override
        public void send(Packet packet) {
            sendNotDispatch(packet);
            dispatchV2(packet);
        }
    
        public void sendNotDispatch(Packet packet) {
            Object ob = packet.getData();
            if (ob instanceof List) {
                List<?> list = (List<?>) ob;
                for (Object o : list) {
                    if (o instanceof Message) {
                        Message message = (Message) o;
                        List<SocketIOClient> clients = NettySocketEvent.CLIENT_MAP.get(message.getTo());
                        if (!CollectionUtils.isEmpty(clients)) {
                            for (SocketIOClient client : clients) {
                                client.sendEvent(packet.getName(), message.toString());
                            }
                        }
                    }
                }
            }
        }
    
    
        private void dispatchV2(Packet packet) {
            Map<String, Set<String>> namespaceRooms = new HashMap<>();
            for (SocketIOClient client : clients) {
                Namespace namespace = (Namespace) client.getNamespace();
                Set<String> rooms = namespace.getRooms(client);
    
                Set<String> roomsList = namespaceRooms.computeIfAbsent(namespace.getName(), k -> new HashSet<>());
                roomsList.addAll(rooms);
            }
            for (Map.Entry<String, Set<String>> entry : namespaceRooms.entrySet()) {
                for (String room : entry.getValue()) {
                    storeFactory.pubSubStore().publish(PubSubType.DISPATCH, new DispatchMessage(room, packet, entry.getKey()));
                }
            }
        }
    }
    
    
  6. 事件控制器 NettySocketEvent

    @Slf4j
    @Component
    public class NettySocketEvent {
    
        @Autowired
        private CustomBroadcastOperations customBroadcastOperations;
    
    
        public static final Map<String, List<SocketIOClient>> CLIENT_MAP = new ConcurrentHashMap<>();
    
        @OnConnect
        public void onConnect(SocketIOClient client) {
            String mac = client.getHandshakeData().getSingleUrlParam("mac");
    
            List<SocketIOClient> clients = CLIENT_MAP.computeIfAbsent(mac, k -> new ArrayList<>());
            clients.add(client);
    
            client.sendEvent("messageevent", "嗨," + mac + " 你好! 咱们已建立连接");
            log.info("客户端:" + client.getSessionId() + "已连接,mac=" + mac);
        }
    
        @OnDisconnect
        public void onDisconnect(SocketIOClient client) {
            String mac = client.getHandshakeData().getSingleUrlParam("mac");
            List<SocketIOClient> clients = CLIENT_MAP.get(mac);
            clients.remove(client);
            log.info("客户端:" + client.getSessionId() + "断开连接");
        }
    
        @OnEvent(value = "sendEvent")
        public void receiveMessage(Message message) {
            customBroadcastOperations.sendEvent("messageevent", message);
        }
    
    }
    
    1. mq交换器队列绑定关系的动态生成 RabbitMQServiceImpl
    @Slf4j
    @Service
    public class RabbitMQServiceImpl implements RabbitMQService {
    
        @Autowired
        private AmqpAdmin amqpAdmin;
        @Autowired
        private RabbitTemplate rabbitTemplate;
    
        private static final Map<String, DirectMessageListenerContainer> CONTAINER_MAP = new ConcurrentHashMap<>(8);
    
    
        @Override
        public Exchange createExchange(String type, Boolean durable, String name) {
            Exchange exchange = null;
            switch (type) {
                case ExchangeTypes.FANOUT:
                    exchange = ExchangeBuilder.fanoutExchange(name).durable(durable).build();
                    break;
                case ExchangeTypes.TOPIC:
                    exchange = ExchangeBuilder.topicExchange(name).durable(durable).build();
                    break;
                case ExchangeTypes.HEADERS:
                    exchange = ExchangeBuilder.headersExchange(name).durable(durable).build();
                    break;
                case ExchangeTypes.DIRECT:
                    exchange = ExchangeBuilder.directExchange(name).durable(durable).build();
                    break;
                default:
                    break;
            }
            amqpAdmin.declareExchange(exchange);
            log.info("声明交换机【{}】", name);
            return exchange;
        }
    
        @Override
        public Queue createQueue() {
            Queue queue = amqpAdmin.declareQueue();
            assert queue != null;
            log.info("声明队列【{}】", queue.getName());
            return queue;
        }
    
        @Override
        public Binding queueBindExchange(Queue queue, Exchange exchange) {
            Binding binding = BindingBuilder.bind(queue).to(exchange).with("").noargs();
            amqpAdmin.declareBinding(binding);
            log.info("绑定队列【{}】到交换机【{}】", queue.getName(), exchange.getName());
            return binding;
        }
    
        @Override
        public void queueAddListener(Queue queue, MessageListener listener) {
            DirectMessageListenerContainer container = new DirectMessageListenerContainer(rabbitTemplate.getConnectionFactory());
            container.setQueueNames(queue.getName());
            container.setExposeListenerChannel(true);
            container.setPrefetchCount(1);
            container.setConsumersPerQueue(1);
            container.setAcknowledgeMode(AcknowledgeMode.AUTO);
            container.setMessageListener(listener);
            container.start();
            CONTAINER_MAP.put(queue.getName(), container);
        }
    
        @Override
        public void deleteQueueAndRemoveListener(String queueName) {
            DirectMessageListenerContainer container = CONTAINER_MAP.get(queueName);
            if (container != null) {
                container.stop();
                container.destroy();
                CONTAINER_MAP.remove(queueName);
            }
            log.info("停止监听队列{}", queueName);
            amqpAdmin.deleteQueue(queueName);
            log.info("成功删除队列{}", queueName);
        }
    
    }
    
    1. 前端页面
    <!doctype html>
    <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width,initial-scale=1, maximum-scale=1, user-scalable=no">
        <title>websocket-java-socketIo</title>
        <script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
    </head>
    
    <body>
    <h1>Socket.io Test</h1>
    
    <p id="address" style="display: none" th:text="${address}"></p>
    
    <div id="status">
        <p>状态</p>
    </div>
    
    <div id="message">
        <p>消息</p>
    </div>
    
    <div>
        <p>内容</p>
        <label>
            <input id="input" type="text"/>
        </label>
    </div>
    <div>
        <p>接收人</p>
        <label>
            <input id="to" type="text"/>
        </label>
    </div>
    
    
    <br><br>
    
    <button id="connect" onClick='connect()'>连接</button>
    <button id="disconnect" onClick='disconnect()'>断开</button>
    <button id="send" onClick='send()'>发送消息</button>
    </body>
    
    <script type="text/javascript">
    
      let socket = io.connect(document.getElementById('address').innerHTML);
      let firstconnect = true;
    
      //监听服务器连接事件
      socket.on('connect', function () {
        status_update("连接成功");
      });
    
      socket.on('reconnect', function () {
        status_update("重新连接成功");
      });
    
      //监听服务器关闭服务事件
      socket.on('disconnect', function () {
        firstconnect = false;
        status_update("断开连接");
      });
    
      socket.on('connect_error', function (error) {
        console.log(error);
      });
    
      //监听服务器端发送消息事件
      socket.on('messageevent', function (data) {
        message(data)
        //console.log("服务器发送的消息是:"+data);
      });
    
    
      function message(data) {
        document.getElementById('message').innerHTML = document.getElementById('message').innerHTML
          + "<p>" + "收到服务端消息: " + data + "</p>";
      }
    
      function getDate() {
        let myDate = new Date();
        let y = myDate.getFullYear();    //获取完整的年份(4位,1970-????)
        let m = myDate.getMonth() + 1;       //获取当前月份(0-11,0代表1月)
        let d = myDate.getDate();        //获取当前日(1-31)
        let t = myDate.getTime();        //获取当前时间(从1970.1.1开始的毫秒数)
        let h = myDate.getHours();       //获取当前小时数(0-23)
        let min = myDate.getMinutes();     //获取当前分钟数(0-59)
        let s = myDate.getSeconds();     //获取当前秒数(0-59)
        let ms = myDate.getMilliseconds();    //获取当前毫秒数(0-999)
        if (m >= 1 && m <= 9) {
          m = "0" + m;
        }
        if (d >= 0 && d <= 9) {
          d = "0" + d;
        }
        if (h >= 0 && h <= 9) {
          h = "0" + h;
        }
        if (min >= 0 && min <= 9) {
          min = "0" + min;
        }
        if (s >= 0 && s <= 9) {
          s = "0" + s;
        }
        let fh = '-';
        let fh2 = ':'
        return y + fh + m + fh + d + ' ' + h + fh2 + min + fh2 + s + fh2 + ms;//当前时间
      }
    
    
      function status_update(txt) {
        document.getElementById('status').innerHTML = document.getElementById('status').innerHTML
          + "<p>" + txt + "  --  " + new Date() + "</p>";
      }
    
      function esc(msg) {
        return msg.replace(/</g, '<').replace(/>/g, '>');
      }
    
      //点击发送消息触发
      function send() {
        socket.emit('sendEvent', {
          content: document.getElementById('input').value,
          from: document.getElementById('address').innerHTML.split("?mac=")[1],
          to: document.getElementById('to').value
        });
        document.getElementById('input').value = ''
      }
    
      //断开连接
      function disconnect() {
        console.log("发起断开连接请求")
        socket.disconnect();
      }
    
      function connect() {
        socket.connect();
      }
    
    </script>
    </html>
    
    
    1. TestController
    @Controller
    public class TestController {
    
        @Value("${server.port}")
        private Integer port;
        @Value("${server.address}")
        private String address;
    
        @GetMapping("/index")
        public String index1(HttpServletRequest request) {
            request.setAttribute("address", "http://" + address + ":" + (port + 1) + "?mac=" + (port + 1));
            return "index";
        }
    }
    

    完整代码路径: https://github.com/silence934/nettysocker-rabbitMQ

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值