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);
       	synchronized(session){//必须加锁,不如高并发报错
           	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");
    }

}

效果展示

在这里插入图片描述
在这里插入图片描述9091端口的推送接收成功
在这里插入图片描述
在这里插入图片描述

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()">开启socket</a></div>
<p>【操作】:<div><a onclick="sendMessage()">发送消息</a></div>
</body>

</html>


后记:

添加视频聊天功能参考链接
后端
修改xiaoxichuli方法(内部用leixing来判断是发的普通消息还是要进行视频通话)

public static void xiaoxichuli(String mapstr)throws Exception{
        System.out.println("收到订阅");
        Map<String,String> map=gson.fromJson(gson.fromJson(mapstr,String.class),HashMap.class);
        System.out.println(gson.fromJson(mapstr,String.class));
        Session session=sessions.get("sess"+map.get("buid"));
        if (session!=null)
            switch (map.get("leixing")){
                case "liaotian"://注意,再发送普通聊天信息需要加入参数leixing值为liaotian
                    sendMessage(map.get("buid"),map.get("xinxi"));
                    break;
                case "shipin":
                    sendMessage(map.get("buid"),gson.fromJson(mapstr,String.class));
                    break;
                default:
                    System.out.println("未命中");
            }
        
    }

前端使用记得修改uid跟buid值

<!DOCTYPE html>
<html>

<head>
    <title>交换SDP信息与ICE信息</title>
    <meta name="viewport" content="width=device-width,
initial-scale=1,maximum-scale=1" />
</head>

<body>
    <button type="button" onclick="startVideo();">开始捕获视频信息</button>
    <button type="button" onclick="stopVideo();">停止捕获视频信息</button>
    &nbsp;&nbsp;&nbsp;&nbsp;
    <button type="button" onclick="connect();">建立连接</button>
    <button type="button" onclick="hangUp();">挂断</button>
    <br />
    <div>
        <video id="local-video" autoplay style="width: 240px; height: 180px;
    border: 1px solid black;"></video>
        <video id="remote-video" autoplay style="width: 240px; height: 180px;
    border: 1px solid black;"></video>
    </div>
    <p>

    <script>
        // ===================以下是socket=======================
        var socketUrl = "ws://127.0.0.1:9091/liaotian/111";
        var socket = null
        var socketRead = false
        window.onload = function() {

            socket = new WebSocket(socketUrl)
            socket.onopen = function() {
                console.log("成功连接到服务器...")
                socketRead = true
            }
            socket.onclose = function(e) {
                console.log('与服务器连接关闭: ' + e.code)
                socketRead = false
            }

            socket.onmessage = function(res) {
                var evt = JSON.parse(res.data)
                console.log(evt)
                if (evt.type === 'offer') {
                    console.log("接收到offer,设置offer,发送answer....")
                    onOffer(evt); 
                } else if (evt.type === 'answer' && peerStarted) {
                    console.log('接收到answer,设置answer SDP');
                    onAnswer(evt);
                } else if (evt.type === 'candidate' && peerStarted) {
                    console.log('接收到ICE候选者..');
                    onCandidate(evt);
                } else if (evt.type === 'bye' && peerStarted) {
                    console.log("WebRTC通信断开");
                    stop();
                }
            }
        }
        
        // ===================以上是socket=======================

        var localVideo = document.getElementById('local-video');
        var remoteVideo = document.getElementById('remote-video');
        var localStream = null;
        var peerConnection = null;
        var peerStarted = false;
        var mediaConstraints = {
            'mandatory': {
                'OfferToReceiveAudio': false,
                'OfferToReceiveVideo': true
            }
        };

        //----------------------交换信息 -----------------------

        function onOffer(evt) {
            console.log("接收到offer...")
            console.log(evt);
            setOffer(evt);
            sendAnswer(evt);
            peerStarted = true
        }

        function onAnswer(evt) {
            console.log("接收到Answer...")
            console.log(evt);
            setAnswer(evt);
        }

        function onCandidate(evt) {
            var candidate = new RTCIceCandidate({
                sdpMLineIndex: evt.sdpMLineIndex,
                sdpMid: evt.sdpMid, candidate: evt.candidate
            });
            console.log("接收到Candidate...")
            console.log(candidate);
            peerConnection.addIceCandidate(candidate);
        }

        function sendSDP(sdp) {
			sdp.leixing="shipin";
			sdp.uid="111";
			sdp.buid="222";
            var text = JSON.stringify(sdp);
            console.log('发送sdp.....')
            console.log(text); // "type":"offer"....
            // textForSendSDP.value = text;
            // 通过socket发送sdp
            socket.send(text)
        }

        function sendCandidate(candidate) {
			candidate.uid="111";
			candidate.buid="222";
			candidate.leixing="shipin";
            var text = JSON.stringify(candidate);
            console.log(text);// "type":"candidate","sdpMLineIndex":0,"sdpMid":"0","candidate":"....
            socket.send(text)// socket发送
        }

        //---------------------- 视频处理 -----------------------
        function startVideo() {
            navigator.webkitGetUserMedia({ video: true, audio: false },
                function (stream) { //success
                    localStream = stream;
                    localVideo.srcObject = stream;
                    //localVideo.src = window.URL.createObjectURL(stream);
                    localVideo.play();
                    localVideo.volume = 0;
                },
                function (error) { //error
                    console.error('发生了一个错误: [错误代码:' + error.code + ']');
                    return;
                });
        }

        function stopVideo() {
            localVideo.src = "";
            localStream.stop();
        }

        //---------------------- 处理连接 -----------------------
        function prepareNewConnection() {
            var pc_config = { "iceServers": [] };
            var peer = null;
            try {
                peer = new webkitRTCPeerConnection(pc_config);
            }
            catch (e) {
                console.log("建立连接失败,错误:" + e.message);
            }

            // 发送所有ICE候选者给对方
            peer.onicecandidate = function (evt) {
                if (evt.candidate) {
                    console.log(evt.candidate);
                    sendCandidate({
                        type: "candidate",
                        sdpMLineIndex: evt.candidate.sdpMLineIndex,
                        sdpMid: evt.candidate.sdpMid,
                        candidate: evt.candidate.candidate
                    });
                }
            };
            console.log('添加本地视频流...');
            peer.addStream(localStream);

            peer.addEventListener("addstream", onRemoteStreamAdded, false);
            peer.addEventListener("removestream", onRemoteStreamRemoved, false);

            // 当接收到远程视频流时,使用本地video元素进行显示
            function onRemoteStreamAdded(event) {
                console.log("添加远程视频流");
                // remoteVideo.src = window.URL.createObjectURL(event.stream);
                remoteVideo.srcObject = event.stream;
            }

            // 当远程结束通信时,取消本地video元素中的显示
            function onRemoteStreamRemoved(event) {
                console.log("移除远程视频流");
                remoteVideo.src = "";
            }

            return peer;
        }

        function sendOffer() {
            peerConnection = prepareNewConnection();
            peerConnection.createOffer(function (sessionDescription) { //成功时调用
                peerConnection.setLocalDescription(sessionDescription);
                console.log("发送: SDP");
                console.log(sessionDescription);
                sendSDP(sessionDescription);
            }, function (err) {  //失败时调用
                console.log("创建Offer失败");
            }, mediaConstraints);
        }

        function setOffer(evt) {
            if (peerConnection) {
                console.error('peerConnection已存在!');
                return;
            }
            peerConnection = prepareNewConnection();
            peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
        }

        function sendAnswer(evt) {
            console.log('发送Answer,创建远程会话描述...');
            if (!peerConnection) {
                console.error('peerConnection不存在!');
                return;
            }

            peerConnection.createAnswer(function (sessionDescription) {//成功时
                peerConnection.setLocalDescription(sessionDescription);
                console.log("发送: SDP");
                console.log(sessionDescription);
                sendSDP(sessionDescription);
            }, function () {                                             //失败时
                console.log("创建Answer失败");
            }, mediaConstraints);
        }

        function setAnswer(evt) {
            if (!peerConnection) {
                console.error('peerConnection不存在!');
                return;
            }
            peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
        }

        //-------- 处理用户UI事件 -----
        // 开始建立连接
        function connect() {
            if (!peerStarted && localStream && socketRead) {
                sendOffer();
                peerStarted = true;
            } else {
                alert("请首先捕获本地视频数据.");
            }
        }

        // 停止连接
        function hangUp() {
            console.log("挂断.");
            stop();
        }

        function stop() {
            peerConnection.close();
            peerConnection = null;
            peerStarted = false;
        }
    </script>
</body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

翎墨袅

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值