zoukankan      html  css  js  c++  java
  • SpringBoot+WebSocket基于Redis订阅发布实现集群化

    SpringBoot+WebSocket基于Redis订阅发布实现集群化

    前面讲了单机版的websocket如何使用发送群聊(2020-03-24-springboot快速集成websocket实现群聊),那么要是部署多个服务实现集群话怎么实现呢?

    由于websocket是长连接,session保持在一个server中,所以在不同server在使用websocket推送消息时就需要获取对应的session进行推送,在分布式系统中就无法获取到所有session,这里就需要使用一个中间件将消息推送到各个系统中,在这里使用的redis,使用redis的sub/pub功能。

    实现步骤

    • Redis要配置消息监听容器
    package cn.pconline.pcloud.admin.config;
    
    import cn.pconline.pcloud.admin.mq.ChatMessageListener;
    import cn.pconline.pcloud.base.util.RedisKey;
    import org.springframework.boot.autoconfigure.AutoConfigureAfter;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.listener.PatternTopic;
    import org.springframework.data.redis.listener.RedisMessageListenerContainer;
    import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
    
    /**
     * @Description 消息订阅配置类
     * @Author jie.zhao
     * @Date 2020/3/31 13:54
     */
    @Configuration 
    public class RedisSubscriberConfig {
        /**
         * 消息监听适配器,注入接受消息方法
         *
         * @param receiver
         * @return
         */
        @Bean
        public MessageListenerAdapter messageListenerAdapter(ChatMessageListener receiver) {
            return new MessageListenerAdapter(receiver);
        }
    
        /**
         * 创建消息监听容器
         *
         * @param redisConnectionFactory 
         * @param messageListenerAdapter2
         * @return
         */
        @Bean
        public RedisMessageListenerContainer getRedisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory, MessageListenerAdapter messageListenerAdapter) {
            RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
            redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);
            redisMessageListenerContainer.addMessageListener(messageListenerAdapter, new PatternTopic(RedisKey.REDIS_MQ_CHAT));
            return redisMessageListenerContainer;
        }
    }
    
    • Redis消息处理类

    收到消息后,给当前Server的session发送消息

    package cn.pconline.pcloud.admin.mq;
    
    import cn.pconline.framework.util.StringUtils;
    import cn.pconline.pcloud.admin.ws.ChatWebsocketEndpoint;
    import cn.pconline.pcloud.base.dto.MessageDto;
    import cn.pconline.pcloud.base.util.JsonUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.connection.Message;
    import org.springframework.data.redis.connection.MessageListener;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.data.redis.serializer.RedisSerializer;
    import org.springframework.stereotype.Component;
    
    /**
     * @Description 集群聊天消息监听器
     * @Author jie.zhao
     * @Date 2020/3/29 15:07
     */
    @Component
    public class ChatMessageListener implements MessageListener {
    
        private Logger logger = LoggerFactory.getLogger(getClass());
    
        @Autowired
        private StringRedisTemplate redisTemplate;
    
        @Autowired
        private ChatWebsocketEndpoint websocketEndpoint;
    
        @Override
        public void onMessage(Message message, byte[] pattern) {
            RedisSerializer<String> valueSerializer = redisTemplate.getStringSerializer();
            String value = valueSerializer.deserialize(message.getBody());
    
            if (StringUtils.isNotBlank(value)) {
                MessageDto dto = JsonUtils.jsonToPojo(value, MessageDto.class);
                logger.info("监听集群websocket消息--- {}", value);
                //集群模式 推送消息
                websocketEndpoint.sendClusterWebsocketMessage(dto);
            }
        }
    
    }
    
    
    • websoketEndpoint修改

    原先直接发消息的地方,改为发送Rdis订阅消息。

    package cn.pconline.pcloud.admin.ws;
    
    import cn.pconline.pcloud.base.dto.MessageDto;
    import cn.pconline.pcloud.base.util.*;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.data.redis.core.StringRedisTemplate;
    import org.springframework.stereotype.Component;
    
    import javax.websocket.*;
    import javax.websocket.server.PathParam;
    import javax.websocket.server.ServerEndpoint;
    import java.io.IOException;
    import java.util.ArrayList;
    import java.util.Date;
    import java.util.List;
    import java.util.concurrent.ConcurrentHashMap;
    
    /**
     * @Description 群聊websocket改造为集群模式
     * @Author jie.zhao
     * @Date 2020/3/29 15:07
     */
    @Component
    @ServerEndpoint("/groupChat/{groupNo}/{uuid}/{name}")
    public class ChatWebsocketEndpoint {
    
        private Logger logger = LoggerFactory.getLogger(getClass());
    
        private StringRedisTemplate stringRedisTemplate = SpringBeanUtils.getBean(StringRedisTemplate.class);
    
        private RedisTemplateUtil redisTemplateUtil = SpringBeanUtils.getBean(RedisTemplateUtil.class);
    
        /**
         * 保存 组id->组成员 的映射关系
         * 之所以使用ConcurrentHashMap因为这个是线程安全的
         */
        private static ConcurrentHashMap<String, List<Session>> groupMemberInfoMap = new ConcurrentHashMap<>();
    
        /**
         * 收到消息调用的方法,群成员发送消息
         *
         * @param groupNo
         * @param uuid
         * @param message
         */
        @OnMessage
        public void onMessage(Session session, @PathParam("groupNo") String groupNo, @PathParam("uuid") String uuid,
                              @PathParam("name") String name, String message) {
            //前端心跳检测
            if("PING".equals(message)){
                try {
                    session.getBasicRemote().sendText("PING");
                } catch (Exception e) {
                    logger.info("会话异常!" + e.getMessage());
                    e.printStackTrace();
                }
            }else{
                //message过滤html、script、css标签
                message = HtmlUtil.htmlEncode(message);
                MessageDto messageDto = new MessageDto(groupNo, MessageDto.MessageTypeEnum.CHAT, uuid, name, message, DateUtil.format(new Date()));
                this.sendClusterMessage(messageDto);
                this.writeRedisChatList(messageDto);
            }
        }
    
        /**
         * 建立连接调用的方法,群成员加入
         *
         * @param session 会话
         * @param groupNo 群id
         */
        @OnOpen
        public void onOpen(Session session, @PathParam("groupNo") String groupNo, @PathParam("uuid") String uuid, @PathParam("name") String name) {
            List<Session> sessionList = groupMemberInfoMap.get(groupNo);
            if (sessionList == null) {
                sessionList = new ArrayList<>();
                groupMemberInfoMap.put(groupNo, sessionList);
            }
            sessionList.add(session);
            this.addClusterSessionCount(groupNo, uuid);
            long onlineNum = this.getClusterSessionCount(groupNo);
            MessageDto messageDto = new MessageDto(groupNo, MessageDto.MessageTypeEnum.SYSTEM, uuid, name, "连接建立", DateUtil.format(new Date()), onlineNum);
            this.sendClusterMessage(messageDto);
            logger.info("连接建立");
            logger.info("直播房间号: {},当前集群在线人数:{} ", groupNo, onlineNum);
        }
    
        /**
         * 关闭连接调用的方法,群成员退出
         *
         * @param session
         * @param groupNo
         */
        @OnClose
        public void onClose(Session session, @PathParam("groupNo") String groupNo, @PathParam("uuid") String uuid, @PathParam("name") String name) {
            List<Session> sessionList = groupMemberInfoMap.get(groupNo);
            sessionList.remove(session);
            this.removeClusterSessionCount(groupNo, uuid);
            long onlineNum = this.getClusterSessionCount(groupNo);
            MessageDto messageDto = new MessageDto(groupNo, MessageDto.MessageTypeEnum.SYSTEM, uuid, name, "连接关闭", DateUtil.format(new Date()), onlineNum);
            this.sendClusterMessage(messageDto);
            logger.info("连接关闭");
            logger.info("直播房间号: {},当前集群在线人数:{} ", groupNo, onlineNum);
        }
    
        /**
         * 传输消息错误调用的方法
         *
         * @param error
         */
        @OnError
        public void OnError(Throwable error) {
            logger.info("连接出错:{}", error.getMessage());
        }
    
        /**
         * 发布集群的消息
         *
         * @param dto
         */
        private void sendClusterMessage(MessageDto dto) {
            stringRedisTemplate.convertAndSend(RedisKey.REDIS_MQ_CHAT, JsonUtils.objectToJson(dto));
        }
    
        /**
         * 添加集群的在线人数
         *
         * @param groupNo
         * @param uuid
         * @return
         */
        private void addClusterSessionCount(String groupNo, String uuid) {
            redisTemplateUtil.sSet(RedisKey.REDIS_MQ_CHAT_SESSION_ID + groupNo, uuid);
        }
    
        /**
         * 移除集群的在线人数
         *
         * @param groupNo
         * @param uuid
         * @return
         */
        private void removeClusterSessionCount(String groupNo, String uuid) {
            redisTemplateUtil.setRemove(RedisKey.REDIS_MQ_CHAT_SESSION_ID + groupNo, uuid);
        }
    
        /**
         * 获取集群的在线人数
         *
         * @param groupNo
         * @return
         */
        private long getClusterSessionCount(String groupNo) {
            return redisTemplateUtil.sGetSetSize(RedisKey.REDIS_MQ_CHAT_SESSION_ID + groupNo);
        }
    
        /**
         * 将聊天内容写入redis,定时同步mysql
         *
         * @param dto
         * @return
         */
        private void writeRedisChatList(MessageDto dto) {
            if (dto != null) {
                redisTemplateUtil.lSet(RedisKey.REDIS_CHAT_LIST + dto.getGroupNo(), JsonUtils.objectToJson(dto));
                //双写模式 同时写入历史消息
                redisTemplateUtil.lSet(RedisKey.REDIS_CHAT_LIST_HISTORY + dto.getGroupNo(), JsonUtils.objectToJson(dto));
            }
        }
    
        /**
         * 发布websocket消息
         *
         * @param dto
         * @return
         */
        public void sendClusterWebsocketMessage(MessageDto dto) {
            if (dto != null) {
                //得到当前群的所有会话,也就是所有用户
                List<Session> sessionList = groupMemberInfoMap.get(dto.getGroupNo());
    
                if (sessionList != null && sessionList.size() > 0) {
                    // 遍历Session集合给每个会话发送文本消息
                    sessionList.forEach(item -> {
                        try {
                            item.getBasicRemote().sendText(JsonUtils.objectToJson(dto));
                        } catch (Exception e) {
                            logger.info("会话异常!" + e.getMessage());
                            e.printStackTrace();
                        }
                    });
                }
            }
        }
    }
    

    至此,websocket基于Redis订阅发布实现集群化改造完成。

  • 相关阅读:
    设计模式实战应用之五:工厂方法模式
    Codeforces445A_DZY Loves Chessboard(预处理)
    void f(int(&amp;p)[3]){} 和void f(int(*p)[3]){}的差别
    《linux 内核全然剖析》 mktime.c
    Java中对象、对象引用、堆、栈、值传递以及引用传递的详解
    android 仿ios开关控件
    ViewDragHelper实战 自己打造Drawerlayout
    [javase学习笔记]-8.5 statickeyword的使用场景
    玩转图片Base64编码
    Android stuido viewpagerindicator的使用
  • 原文地址:https://www.cnblogs.com/cnsyear/p/12638121.html
Copyright © 2011-2022 走看看