使用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>
相关推荐
float_六七2 小时前
Redis:极速缓存与数据结构存储揭秘
数据结构·redis·缓存
blammmp2 小时前
Redis : set集合
数据库·redis·缓存
冷崖4 小时前
Redis事务与驱动的学习(一)
数据库·redis·学习
苏格拉没有底_coder5 小时前
引入 Kafka 消息队列解耦热点操作
分布式·kafka
顧棟7 小时前
Zookeeper 3.8.4 安装部署帮助手册
分布式·zookeeper
你好龙卷风!!!8 小时前
mac redis以守护进程重新启动
redis
猕员桃10 小时前
《Elasticsearch 分布式搜索在聊天记录检索中的深度优化》
分布式·elasticsearch·wpf
沛沛老爹10 小时前
深入剖析 Celery:分布式异步任务处理的利器
分布式·python·微服务·celery·架构设计·worker节点
~央千澈~12 小时前
WebSocket与XMPP:即时通讯技术的本质区别与选择逻辑优雅草卓伊凡|片翼|许贝贝
网络·websocket·网络协议
fajianchen12 小时前
如何调优Kafka
分布式·kafka