spring cloud 同一服务多实例 websocket跨实例无法共享Session 的解决

思路>>使用redis发布消息,通知其他实例,查询符合条件的Session用于发送消息

复制代码
package com.ruoyi.common.redis.message;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

/**
 * ApplicationContext 本身就实现了 ApplicationEventPublisher 接口
 */
@Component
public class ApplicationContextProvider implements ApplicationContextAware {
    private static ApplicationContext context;

    public static ApplicationContext getApplicationContext() {
        return context;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        context = applicationContext;
    }

    public static ApplicationEventPublisher getEventPublisher() {
        return context;
    }
}

package com.ruoyi.common.redis.message;

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;

@Configuration
public class RedisMessageConfig {
    /**
     * 创建消息监听器容器
     * @param connectionFactory Redis连接工厂
     * @param listenerAdapter 消息监听器适配器
     * @return RedisMessageListenerContainer
     */
    @Bean
    public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        // 将监听器适配器添加到容器,并订阅指定频道
        container.addMessageListener(listenerAdapter, new PatternTopic("*"));
        // 你也可以订阅多个频道,或者使用PatternTopic进行模式匹配订阅
        // container.addMessageListener(listenerAdapter, new PatternTopic("news.*"));
        return container;
    }

    /**
     * 将我们实现了MessageListener接口的订阅者包装成MessageListenerAdapter
     * Spring会默认寻找名为"onMessage"的方法
     * @param subscriber 我们的订阅者实例
     * @return MessageListenerAdapter
     */
    @Bean
    public MessageListenerAdapter listenerAdapter() {
        return new MessageListenerAdapter(new  RedisMessageSubscriber(),"onMessage");
    }
}

package com.ruoyi.common.redis.message;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

@Component
public class RedisMessagePublisher {
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 发布消息
     * @param channel
     * @param message
     */
    public void publish(String channel, Object message) {
        redisTemplate.convertAndSend(channel, message);
    }
}

package com.ruoyi.common.redis.message;

import com.ruoyi.common.core.utils.SpringUtils;
import com.ruoyi.common.core.utils.StringUtils;
import com.ruoyi.common.redis.message.event.Commemorate3dCemeteryEvent;
import com.ruoyi.common.redis.message.event.Commemorate3dHallEvent;
import com.ruoyi.common.redis.message.event.KinMemberChangeEvent;
import com.ruoyi.common.redis.message.event.MemorialHallEvent;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;

import java.nio.charset.StandardCharsets;
@Slf4j
@Component
public class RedisMessageSubscriber implements  MessageListener {

    @Override
    public void onMessage(Message message, byte[] pattern) {
        ApplicationEventPublisher eventPublisher = ApplicationContextProvider.getEventPublisher();


        String actualChannel = new String(message.getChannel(), StandardCharsets.UTF_8);

        System.out.println("redisMessage频道: " + actualChannel);
        System.out.println("redisMessage消息: " + message);
        //System.out.println("消息类型: " + message.getClass().getSimpleName());

        //举例 module:business:key websocket:kinMemberChange:kinMemberChange-userid
        String[] split = StringUtils.split(actualChannel, ':');
        if (split.length != 3) {
            log.error("redis onMessage channel error");
        }
        if(StringUtils.equals(split[0], "websocket")){
            //同一模块部署多实例的时候,任何一个模块发送websocket消息,都要通知其他实例,在内存中找session对象,进行发送消息
            if(StringUtils.equals(split[1], "kinMemberChange")){
                //其他实例,kinMemberChange 业务,发布了新消息
                String[] split1 = StringUtils.split(split[2], "_");
                eventPublisher.publishEvent(new KinMemberChangeEvent(this,split1[0],split1[1],""));
            }else if(StringUtils.equals(split[1], "commemorate3dCemetery")){
                String[] split1 = StringUtils.split(split[2], "_");

                byte[] bodyBytes = message.getBody();
                String messageBody = new String(bodyBytes);
                messageBody = removeExtraQuotes(messageBody);
                messageBody = StringUtils.replace(messageBody,"\\r\\n","");
                messageBody = StringUtils.replace(messageBody,"\\\"","\"");
                eventPublisher.publishEvent(new Commemorate3dCemeteryEvent(this,split1[0],split1[1],messageBody));
            }else if(StringUtils.equals(split[1], "commemorate3dHall")){
                String[] split1 = StringUtils.split(split[2], "_");

                byte[] bodyBytes = message.getBody();
                String messageBody = new String(bodyBytes);
                messageBody = removeExtraQuotes(messageBody);
                messageBody = StringUtils.replace(messageBody,"\\r\\n","");
                messageBody = StringUtils.replace(messageBody,"\\\"","\"");
                eventPublisher.publishEvent(new Commemorate3dHallEvent(this,split1[0],split1[1],messageBody));
            }else if(StringUtils.equals(split[1], "memorialHall")){
                String[] split1 = StringUtils.split(split[2], "_");

                byte[] bodyBytes = message.getBody();
                String messageBody = new String(bodyBytes);
                messageBody = removeExtraQuotes(messageBody);
                messageBody = StringUtils.replace(messageBody,"\\r\\n","");
                messageBody = StringUtils.replace(messageBody,"\\\"","\"");
                eventPublisher.publishEvent(new MemorialHallEvent(this,split1[0],split1[1],messageBody));
            }
        }
    }


    /**
     * 去除字符串首尾的多余引号
     */
    private String removeExtraQuotes(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }

        // 去除首尾的双引号
        if (str.startsWith("\"") && str.endsWith("\"")) {
            return str.substring(1, str.length() - 1);
        }

        // 去除首尾的单引号
        if (str.startsWith("'") && str.endsWith("'")) {
            return str.substring(1, str.length() - 1);
        }

        return str;
    }
}

在websocket收到消息时,发布redis消息,通知所有实例

复制代码
package com.ruoyi.foundation.apicontroller;

import com.google.gson.Gson;
import com.ruoyi.common.redis.message.RedisMessagePublisher;
import com.ruoyi.foundation.apicontroller.req.MemorialHallWebsocketReq;
import com.ruoyi.foundation.event.dto.AnimationBroadcastDto;
import com.ruoyi.foundation.webSocket.WebsocketUtil;
import io.seata.common.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@Service
@ServerEndpoint(value = "/api/mingmenCommemorate3dHallWebsocket/{commemorate3d}/{userId}")
public class MmCommemorate3dHallWebsocketController {
    /**
     * 保存每日一念纪念馆中当前在线的用户ID
     */
    public static final Map<String, List<String>> memorialHallUsers = new ConcurrentHashMap<>();

    private Gson gson=new Gson();
    private static ApplicationEventPublisher eventPublisher;

    private static RedisMessagePublisher redisMessagePublisher;

    @Autowired
    public void setEventPublisher(ApplicationEventPublisher publisher) {
        MmCommemorate3dHallWebsocketController.eventPublisher = publisher;
    }

    @Autowired
    public void setRedisMessagePublisher(RedisMessagePublisher publisher) {
        MmCommemorate3dHallWebsocketController.redisMessagePublisher = publisher;
    }

    /*public MmMemorialHallWebsocketController() {
        // 从 Spring 容器中获取 ApplicationEventPublisher
        this.eventPublisher = SpringContextUtil.getBean(ApplicationEventPublisher.class);
    }*/

    @OnOpen
    public void onOpen(@PathParam(value = "commemorate3d") String commemorate3d,@PathParam(value = "userId") String userId, Session session) {
        WebsocketUtil.addSession("commemorate3d:"+userId, session);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if (strings == null){
            List<String> list=new ArrayList<>();
            list.add("commemorate3d:"+userId);
            memorialHallUsers.put(commemorate3d,list);
        }else{
            strings.add("commemorate3d:"+userId);
        }
    }

    @OnClose
    public void onClose(@PathParam(value = "commemorate3d") String commemorate3d,@PathParam(value = "userId") String userId, Session session) {
        WebsocketUtil.removeSession("commemorate3d:"+userId);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if(strings != null){
            strings.remove("commemorate3d:"+userId);
        }
    }

    @OnMessage
    public void onMessage(@PathParam(value = "commemorate3d") String commemorate3d,@PathParam(value = "userId") String userId, Session session, String message) {
        /*System.out.println(dailyMissId);
        System.out.println(userId);
        System.out.println(session);
        System.out.println(message);*/

        //发送redis消息,使消息扩散到每个spring boot 实例
        redisMessagePublisher.publish("websocket:commemorate3dHall:"+commemorate3d + "_" + userId,message);

        /*MemorialHallWebsocketReq memorialHallWebsocketReq = gson.fromJson(message, MemorialHallWebsocketReq.class);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if(strings == null || strings.isEmpty()){
            return;
        }

        Set<String> collect = strings.stream().filter(userId1 -> !StringUtils.equals(userId1, "commemorate3d:"+userId)).collect(Collectors.toSet());

        //对在线的用户广播前,记录动效已经播放
        if(StringUtils.isNotBlank(memorialHallWebsocketReq.getMingmenSacrificeRecordId()) && collect.size() > 0){
            List<Long> collect1 = collect.stream().map(u -> Long.valueOf(u.split(":")[1])).collect(Collectors.toList());
            eventPublisher.publishEvent(new AnimationBroadcastDto(this,Long.valueOf(memorialHallWebsocketReq.getMingmenSacrificeRecordId()),collect1));
        }


        //对同纪念馆的在线用户进行广播
        WebsocketUtil.sendMessageForUsers(collect,message);*/
    }

    @OnError
    public void onError(Session session, Throwable throwable) {
        try {
            session.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        throwable.printStackTrace();
    }

    /**
     * 消息来自redis,通过redis可使同一模块所有实例都收到消息
     * @param commemorate3d
     * @param userId
     * @param message
     */
    public void onMessageFromRedis(String commemorate3d,String userId,String message){
        MemorialHallWebsocketReq memorialHallWebsocketReq = gson.fromJson(message, MemorialHallWebsocketReq.class);

        List<String> strings = memorialHallUsers.get(commemorate3d);
        if(strings == null || strings.isEmpty()){
            return;
        }

        Set<String> collect = strings.stream().filter(userId1 -> !StringUtils.equals(userId1, "commemorate3d:"+userId)).collect(Collectors.toSet());

        //对在线的用户广播前,记录动效已经播放
        if(StringUtils.isNotBlank(memorialHallWebsocketReq.getMingmenSacrificeRecordId()) && collect.size() > 0){
            List<Long> collect1 = collect.stream().map(u -> Long.valueOf(u.split(":")[1])).collect(Collectors.toList());
            eventPublisher.publishEvent(new AnimationBroadcastDto(this,Long.valueOf(memorialHallWebsocketReq.getMingmenSacrificeRecordId()),collect1));
        }


        //对同纪念馆的在线用户进行广播
        WebsocketUtil.sendMessageForUsers(collect,message);
    }
}

实例收到redis消息后,转化为ApplicationEventPublisher 消息,从基础层调用业务层

复制代码
package com.ruoyi.foundation.redisevents;

import com.ruoyi.common.redis.message.event.Commemorate3dHallEvent;
import com.ruoyi.foundation.apicontroller.MmCommemorate3dHallWebsocketController;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class Commemorate3dHallListen {
    @Async
    @EventListener
    public void commemorate3dCemetery(Commemorate3dHallEvent event) {
        MmCommemorate3dHallWebsocketController mmCommemorate3dHallWebsocketController = new MmCommemorate3dHallWebsocketController();
        mmCommemorate3dHallWebsocketController.onMessageFromRedis(event.getCommemorate3d(),event.getUserId(),event.getMessage());
    }
}
相关推荐
SimonKing3 小时前
消息积压、排查困难?Provectus Kafka UI 让你的数据流一目了然
java·后端·程序员
考虑考虑3 小时前
点阵图更改背景文字
java·后端·java ee
CDwenhuohuo3 小时前
WebSocket 前端node启用ws调试
前端·websocket·网络协议
ZHE|张恒3 小时前
Spring Boot 3 + Flyway 全流程教程
java·spring boot·后端
许心月3 小时前
坑#Spring Cloud Gateway#DataBufferLimitException
spring cloud
TDengine (老段)3 小时前
TDengine 数学函数 CRC32 用户手册
java·大数据·数据库·sql·时序数据库·tdengine·1024程序员节
心随雨下4 小时前
Tomcat日志配置与优化指南
java·服务器·tomcat
Kapaseker4 小时前
Java 25 中值得关注的新特性
java
wljt4 小时前
Linux 常用命令速查手册(Java开发版)
java·linux·python
撩得Android一次心动4 小时前
Android 四大组件——BroadcastReceiver(广播)
android·java·android 四大组件