Spring Boot+NettySocketIo+RabbitMQ实现websocket集群
效果
连接到不同的服务器的两个客户端可以互发消息
websocket的连接Client是不能序列化的,所以不能使用session共享那样的方法来做websocket的集群
nettySocket已经支持了Redission的集群方案,这里在原来有的基础上使用RabbitMQ实现的集群方案
-
目录结构
-
引入依赖
<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>
-
配置文件
spring: rabbitmq: port: 5672 host: 127.0.0.1 username: guest password: guest connection-timeout: 15000 server: port: 8080 address: 127.0.0.1
-
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启动成功!"); } }
-
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() { } }
- 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; } }
- 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())); } } } }
-
事件控制器 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); } }
- 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); } }
- 前端页面
<!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>
- 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