背景:最近遇到微服务框架,聊天功能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");
}
}
效果展示
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>