java聊天功能(集群)redis订阅发布实现

背景:最近遇到微服务框架,聊天功能session不互通的问题,最初希望用redis保存session对象,实行时发现session对象存不进去。
实现:
使用redis的订阅发布功能
redis依赖(我用的2.7.4)

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

之前写了聊天的单机版博客链接

(必须)在它的基础上修改了java部分的代码(直接复制粘贴覆盖)

package qiesiyv.ceshi.tool;

import com.google.gson.Gson;
import org.junit.platform.commons.util.StringUtils;
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.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint("/liaotian/{userId}")
@Component
public class WebSocketServer {

    /**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/
    private static int onlineCount = 0;
    /**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
    private static ConcurrentHashMap<String,Session> sessions = new ConcurrentHashMap<>();
    /**接收userId*/
    private String userId="";
    private String type ="";
    private String touserid ="";
    private String content ="";

    private static Gson gson = new Gson();
    /**
     * 连接建立成功调用的方法*/
    @OnOpen
    public void onOpen( Session session, @PathParam("userId") String userId) {
        sessions.put("sess"+userId,session);
        this.userId= userId;//获取发送人id
        System.out.println("用户"+userId+"加入WebSocketServer");
        if(sessions.containsKey("sess"+userId)){
            sessions.remove("sess"+userId);
            sessions.put("sess"+userId,session);
        }else{
            sessions.put("sess"+userId,session);
            //人数+1
            addOnlineCount();
        }

        try {
            Map<String,Object> map=new HashMap<>();
            map.put("mag","连接成功");
            sendMessage(userId,gson.toJson(map));
        } catch (Exception e) {
            System.out.println("对方网络异常!!!!!!");
        }
    }


    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if(sessions.containsKey("sess"+userId)){
            sessions.remove("sess"+userId);
            //人数-1
            subOnlineCount();
        }
        System.out.println("用户"+userId+"退出,当前在线人数为:"+getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("用户消息:"+userId+",报文:"+message);
        if(StringUtils.isNotBlank(message)){
            try {
                Gongjv.getRedisTemp().convertAndSend("uidSession",message);//向uidSession订阅者发布新信息,发布频道名要与订阅频道名对应起来
            }catch (Exception e){
                e.printStackTrace();
            }
        }

    }


    /**
     *
     * @param error
     */
    @OnError
    public void onError(Throwable error) {
        System.out.println("用户错误:"+this.userId+",原因:"+error.getMessage());
        error.printStackTrace();
    }
    /**
     * 实现服务器主动推送
     */
    public static void sendMessage(String buid,String message) throws IOException {
        System.out.println("执行推送,被推送人id"+buid);
        Session session=sessions.get("sess"+buid);
        System.out.println(session);
        if (session!=null)
            session.getBasicRemote().sendText(message);
    }

    //当前使用人数
    public static synchronized int getOnlineCount() {
        return onlineCount;
    }
    //当前使用人数+1
    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }
    //当前使用人数-1
    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }

    /**
     * @Description: 数据推送,参数1目标uid,参数2推动的信息
     * @Param:
     * @return:
     * @Author: 翎墨袅
     * @Date: 2022/10/11
     */
    public static boolean tuixinxi(String buid,Map xinxi){
        try {
            Map<String,String> map=new HashMap<>();
            map.put("buid",buid);
            map.put("xinxi",gson.toJson(xinxi));
            Gongjv.getRedisTemp().convertAndSend("uidSession",gson.toJson(map));//向订阅者发布新信息
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * @Description: 接收到订阅后消息处理
     * @Param:
     * @return:
     * @Author: 翎墨袅
     * @Date: 2022/10/11
     */
    public static void xiaoxichuli(String mapstr)throws Exception{
        System.out.println("收到订阅");
        Map<String,String> map=gson.fromJson(gson.fromJson(mapstr,String.class),HashMap.class);
        Session session=sessions.get("sess"+map.get("buid"));
        if (session!=null)
            sendMessage(map.get("buid"),map.get("xinxi"));
    }

}


(无所谓)添加了一个工具类,用于返回一个静态RedisTemplate对象,这个类无所谓,可以直接在WebSocketServer注入RedisTemplate对象也行,寻思节约点空间的。

package qiesiyv.ceshi.tool;


import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;

/**
 * @Author: 翎墨袅
 * @Description: 工具
 * @Date Created in 2022-10-02 14:37
 * @Modified By:
 */
@Component
public class Gongjv {

    @Resource
    private RedisTemplate redisTemplate;
    private static RedisTemplate staticRedisTemp;

    @PostConstruct
    public void init(){
        staticRedisTemp = redisTemplate;
    }
    /**
    * @Description: 返回一个静态RedisTemplate
    * @Param:
    * @return:
    * @Author: 翎墨袅
    * @Date: 2022/10/23
    */
    public static RedisTemplate getRedisTemp() {
        return staticRedisTemp;
    }
}

(必须)在redis配置类加了两个@Bean(没写配置类的我下面贴了完整的)

 /**
     * redis消息监听器容器
     * 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
     * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
     * @param connectionFactory
     * @param listenerAdapter
     * @return
     */
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        //可以添加多个 messageListener
        container.addMessageListener(listenerAdapter, new PatternTopic("uidSession"));//订阅uidSession自己随便起的名字,要和发布对应

        return container;
    }


    /**
     * 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
     * @param webSocketServer
     * @return
     *///此方法参数就是你在收到订阅的消息后要调用的类,里边那个字符串就是类里要调用的方法名
    @Bean
    MessageListenerAdapter listenerAdapter(WebSocketServer webSocketServer) {
        System.out.println("消息适配器进来了");
        return new MessageListenerAdapter(webSocketServer, "xiaoxichuli");
    }

完整redis配置类

package qiesiyv.ceshi.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import qiesiyv.ceshi.tool.WebSocketServer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        //设置key的序列化方式
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        //设置值的序列化方式
        template.setValueSerializer(jackson2JsonRedisSerializer());
        template.setHashValueSerializer(jackson2JsonRedisSerializer());
        //更新一下RedisTemplate对象的默认配置
        template.afterPropertiesSet();
        return template;
    }
    //自定义序列化方式
    public Jackson2JsonRedisSerializer jackson2JsonRedisSerializer(){
        Jackson2JsonRedisSerializer jackson2Json = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper=new ObjectMapper();
        //2.2设置按哪些方法规则进行序列化
        objectMapper.setVisibility(PropertyAccessor.GETTER,//get方法
                JsonAutoDetect.Visibility.ANY);//Any 表示任意方法访问修饰符
        //对象属性值为null时,不进行序列化存储
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        //2.2激活序列化类型存储,对象序列化时还会将对象的类型存储到redis数据库
        //假如没有这个配置,redis存储数据时不存储类型,反序列化时会默认将其数据存储到map
        objectMapper.activateDefaultTyping(
                objectMapper.getPolymorphicTypeValidator(),//多态校验分析
                ObjectMapper.DefaultTyping.NON_FINAL,//激活序列化类型存储,类不能使用final修饰
                JsonTypeInfo.As.PROPERTY);//PROPERTY 表示类型会以json对象属性形式存储
        jackson2Json.setObjectMapper(objectMapper);
        return jackson2Json;
    }

    /**
     * redis消息监听器容器
     * 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
     * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
     * @param connectionFactory
     * @param listenerAdapter
     * @return
     */
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,MessageListenerAdapter listenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        //可以添加多个 messageListener
        container.addMessageListener(listenerAdapter, new PatternTopic("uidSession"));//订阅uidSession

        return container;
    }


    /**
     * 消息监听器适配器,绑定消息处理器,利用反射技术调用消息处理器的业务方法
     * @param webSocketServer
     * @return
     */
    @Bean
    MessageListenerAdapter listenerAdapter(WebSocketServer webSocketServer) {
        System.out.println("消息适配器进来了");
        return new MessageListenerAdapter(webSocketServer, "xiaoxichuli");
    }

}

效果展示

java聊天功能(集群)redis订阅发布实现_第1张图片
java聊天功能(集群)redis订阅发布实现_第2张图片9091端口的推送接收成功
java聊天功能(集群)redis订阅发布实现_第3张图片
java聊天功能(集群)redis订阅发布实现_第4张图片

9092端口的推送接收成功
完成

用于测试的html

DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>websocket通讯title>
head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js">script>
<script>
    var socket;
    function openSocket() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else{
            console.log("您的浏览器支持WebSocket");
            //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
            var socketUrl="ws://localhost:9091/liaotian/"+$("#userId").val();//如果是https则wss加域名
            console.log(socketUrl);
            if(socket!=null){
                socket.close();
                socket=null;
            }
            socket = new WebSocket(socketUrl);
            //打开事件
            socket.onopen = function(msg) {
                console.log("websocket已打开",JSON.stringify(msg));
                //socket.send("这是来自客户端的消息" + location.href + new Date());
            };
            //获得消息事件
            socket.onmessage = function(msg) {
                console.log(msg.data);
                //发现消息进入    开始处理前端触发逻辑
            };
            //关闭事件
            socket.onclose = function(close) {
                console.log("websocket已关闭",JSON.stringify(close));
            };
            //发生了错误事件
            socket.onerror = function(error) {
                console.log("websocket发生了错误",JSON.stringify(error));
            }
        }
    }
    function sendMessage() {
        if(typeof(WebSocket) == "undefined") {
            console.log("您的浏览器不支持WebSocket");
        }else {
            console.log("您的浏览器支持WebSocket");
            // socket.send('{"buid":"'+$("#buid").val()+'","xinxi":"'+$("#xinxi").val()+'","type":"'+'"}');
			socket.send(`{
				"buid":"${$("#buid").val()}",
				"xinxi":"${$("#xinxi").val()}"
			}`)
        }
    }
script>
<body>
<p>【userId】:<div><input id="userId" name="userId" type="text" value="1">div>
<p>【buid】:<div><input id="buid" name="buid" type="text" value="4">div>
<p>【xinxi】:<div><input id="xinxi" name="xinxi" type="text" value="hello websocket">div>
<p>【操作】:<div><a onclick="openSocket()">开启socketa>div>
<p>【操作】:<div><a onclick="sendMessage()">发送消息a>div>
body>

html>


你可能感兴趣的:(1024程序员节)