背景:最近遇到微服务框架,聊天功能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>
<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>