使用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>
相关推荐
ps酷教程24 分钟前
WebSocketFrameEncoder&WebSocketFrameDecoder源码浅析
websocket·netty
newbe365243 小时前
我们如何使用 impeccable 优化前端界面设计与实现稳定性
前端·人工智能·分布式·github·aigc·wpf
风向决定发型丶7 小时前
redis集群搭建
数据库·redis·缓存
梦想的颜色9 小时前
硬核实践:使用 Docker 部署生产级 Redis(持久化 + 安全配置 + 高可用)
redis·docker·redis持久化·docker compose·redis哨兵·rdb aof
宠友信息11 小时前
多端数据互通场景下Spring Boot仿小红书源码结构设计
数据库·spring boot·redis·缓存·架构
清心歌11 小时前
Seata AT 模式简单学习及总结
分布式·seata
长不胖的路人甲11 小时前
Redis 缓存的数据持久化方案讲解
数据库·redis·缓存
长不胖的路人甲12 小时前
Redis 单线程为什么速度很快
数据库·redis·缓存
彦为君12 小时前
算法思维与经典智力题
java·前端·redis·算法
彦为君13 小时前
Redis最新版本特性
java·数据库·redis·算法·bootstrap