使用redis发布订阅实现websocket分布式消息推送服务

WebSocketConfig

java 复制代码
@Configuration
public class WebSocketConfig {

    private static  final Logger logger = LoggerFactory.getLogger(WebSocketConfig.class);

    @Bean
    public ServerEndpointExporter serverEndpointExporter (){
        return new ServerEndpointExporter();
    }
}

RedisConfig

java 复制代码
@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName("192.168.124.91"); // Redis服务器地址
        config.setPort(6379); // Redis服务器端口
        return new JedisConnectionFactory(config);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());

        // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        // 如果需要操作hash结构的数据,还可以设置hash的key和value的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        template.afterPropertiesSet();
        return template;
    }

    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 可以添加多个 messageListener,配置不同的交换机
        container.addMessageListener(listenerAdapter, new PatternTopic("showNewestMsg"));// 订阅最新消息频道
        return container;
    }

    @Bean
    public MessageListenerAdapter listenerAdapter(RedisReceiver receiver) {
        // 消息监听适配器
        return new MessageListenerAdapter(receiver, "onMessage");
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
        return new StringRedisTemplate(connectionFactory);
    }
}

RedisReceiver

java 复制代码
@Component
public class RedisReceiver implements MessageListener {
    private static Logger logger = LoggerFactory.getLogger(RedisReceiver.class);

    @Autowired
    WebSocketServer webSocketServer;

    /**
     * 处理接收到的订阅消息
     */
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel());// 订阅的频道名称
        String msg = "";
        try {
            msg = new String(message.getBody(), StandardCharsets.UTF_8);//注意与发布消息编码一致,否则会乱码
            if (StringUtils.isNotBlank(msg)) {
                if ("showNewestMsg".endsWith(channel))// 最新消息
                {
                    JSONObject json = JSONObject.parseObject(msg);
                    webSocketServer.sendMessage(json.getString("receive"), json.getString("msg"));
                } else {
                    //TODO 其他订阅的消息处理
                }
            } else {
                logger.info("消息内容为空,不处理。");
            }
        } catch (Exception e) {
            logger.error("处理消息异常:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

WebSocketHandle

java 复制代码
ServerEndpoint("/websocket/redis/{userId}")
@Component
public class WebSocketHandle {

    private static Logger logger = LoggerFactory.getLogger(WebSocketHandle.class);

    // 当前在线连接数
    private static int onlineCount = 0;

    // 存放每个用户对应的WebSocket连接对象,key为userId,确保一个登录用户只建立一个连接
    private static Map<String, Session> webSocketSessionMap = new ConcurrentHashMap<String, Session>();

    // 与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    // 接收用户id
    private String userId = "";

    private static WebSocketHandle webSocketServer;

    // 通过@PostConstruct实现初始化bean之前进行的操作
    @PostConstruct
    public void init() {
        // 初使化时将已静态化的webSocketServer实例化
        webSocketServer = this;
    }

    /**
     * 连接建立成功调用的方法
     *
     * @param session 连接会话,由框架创建
     * @param userId  用户id, 为处理用户多点登录都能收到消息,需传该格式userId
     * @author csf
     * @date 2020年8月10日
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("userId") String userId) {
        if (!webSocketSessionMap.containsKey(userId)) {
            this.session = session;
            webSocketSessionMap.put(userId, session);
            addOnlineCount(); // 在线数加1
            logger.info("有新连接[{}]接入,当前websocket连接数为:{}", userId, getOnlineCount());
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(@PathParam("userId") String sessionKey) {
        if (webSocketSessionMap.containsKey(sessionKey)) {
            try {
                webSocketSessionMap.get(sessionKey).close();
                webSocketSessionMap.remove(sessionKey);
            } catch (IOException e) {
                logger.error("连接[{}]关闭失败。", sessionKey);
                e.printStackTrace();
            }
            subOnlineCount();
            logger.info("连接[{}]关闭,当前websocket连接数:{}", sessionKey, onlineCount);
        }
    }

    /**
     * 接收客户端发送的消息
     *
     * @param message 客户端发送过来的消息
     * @param session websocket会话
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        logger.info("收到来自客户端" + userId + "的信息:" + message);
    }

    /**
     * 连接错误时触发
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        try {
            session.close();
        } catch (IOException e) {
            logger.error("发生错误,连接[{}]关闭失败。");
            e.printStackTrace();
        }
        // log.error("websocket发生错误");
        // error.printStackTrace();
    }

    /**
     * 给指定的客户端推送消息,可单发和群发
     *
     * @param sessionKeys 发送消息给目标客户端sessionKey,多个逗号","隔开1234,2345...
     * @param message
     * @throws IOException
     * @author csf
     * @date 2020年8月11日
     */
    public void sendMessage(String sessionKeys, String message) {
        if (StringUtils.isNotBlank(sessionKeys)) {
            String[] sessionKeyArr = sessionKeys.split(",");
            for (String key : sessionKeyArr) {
                try {
                    // 可能存在一个账号多点登录
                    List<Session> sessionList = getLikeByMap(webSocketSessionMap, key);
                    for (Session session : sessionList) {
                        session.getBasicRemote().sendText(message);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    continue;// 某个客户端发送异常,不影响其他客户端发送
                }
            }
        } else {
            logger.info("sessionKeys为空,没有目标客户端");
        }
    }

    /**
     * 给当前客户端推送消息,首次建立连接时调用
     */
    public void sendMessage(String message)
            throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 检查webSocket连接是否在线
     *
     * @param sesstionKey webSocketMap中维护的key
     * @return 是否在线
     */
    public static boolean checkOnline(String sesstionKey) {
        if (webSocketSessionMap.containsKey(sesstionKey)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 获取包含key的所有map值
     *
     * @param map
     * @param keyLike
     * @return
     * @author csf
     * @date 2020年8月13日
     */
    private List<Session> getLikeByMap(Map<String, Session> map, String keyLike) {
        List<Session> list = new ArrayList<>();
        for (String key : map.keySet()) {
            if (key.contains(keyLike)) {
                list.add(map.get(key));
            }
        }
        return list;
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketHandle.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketHandle.onlineCount--;
    }
}

RedisTestController

java 复制代码
@RequestMapping("redis")
@RestController
public class RedisTestController {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 发布消息测试
     *
     * @param userId
     * @param msg
     * @return
     */
    @PostMapping("sendMessage")
    public String sendMessage(@RequestParam String userId, @RequestParam String msg) {
        String newMessge = new String(msg.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
        Map<String, String> map = new HashMap<>();
        map.put("receive", userId);
        map.put("msg", newMessge);
        String json = JSONObject.toJSONString(map);//map转String
        JSONObject jsonObject = JSONObject.parseObject(json);
        stringRedisTemplate.convertAndSend("showNewestMsg", jsonObject.toJSONString());
        return "消息发布成功!";
    }
}

补充

1.java.lang.NoClassDefFoundError: redis/clients/jedis/util/Pool

xml 复制代码
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.3.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.1</version>
</dependency>

版本不一致引起的,

jedis版本改为3.6.0

xml 复制代码
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-redis</artifactId>
    <version>2.3.9.RELEASE</version>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.6.1</version>
</dependency>
相关推荐
WX187021128737 小时前
在分布式光伏电站如何进行电能质量的治理?
分布式
水月梦镜花8 小时前
redis:list列表命令和内部编码
数据库·redis·list
掘金-我是哪吒9 小时前
微服务mysql,redis,elasticsearch, kibana,cassandra,mongodb, kafka
redis·mysql·mongodb·elasticsearch·微服务
不能再留遗憾了10 小时前
RabbitMQ 高级特性——消息分发
分布式·rabbitmq·ruby
茶馆大橘10 小时前
微服务系列六:分布式事务与seata
分布式·docker·微服务·nacos·seata·springcloud
ketil2711 小时前
Ubuntu 安装 redis
redis
王佑辉12 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
Karoku06613 小时前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
半桶水专家13 小时前
用go实现创建WebSocket服务器
服务器·websocket·golang
材料苦逼不会梦到计算机白富美13 小时前
golang分布式缓存项目 Day 1
分布式·缓存·golang