1、在pom.xml中引入相关依赖
<!-- 集成websocket依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- 集成redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.创建获取上下文(getBean)的工具类
package com.bjprd.config;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
/**
* @author Jiang
* @create 2022-06-15 14:17
*/
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static void set(ApplicationContext applicationContext) {
context = applicationContext;
}
/**
* 通过字节码获取
* @param beanClass
* @param <T>
* @return
*/
public static <T> T getBean(Class<T> beanClass) {
return context.getBean(beanClass);
}
/**
* 通过BeanName获取
* @param beanName
* @param <T>
* @return
*/
public static <T> T getBean(String beanName) {
return (T) context.getBean(beanName);
}
/**
* 通过beanName和字节码获取
* @param name
* @param beanClass
* @param <T>
* @return
*/
public static <T> T getBean(String name, Class<T> beanClass) {
return context.getBean(name, beanClass);
}
}
3.创建Redis工具类,主要调用publish()方法。用于redis发布消息
package com.bjprd.config.redis;
import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SetOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* 写入缓存
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存设置时效时间
* @param key
* @param value
* @return
*/
public boolean set(final String key, Object value, Long expireTime ,TimeUnit timeUnit) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, timeUnit);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 批量删除对应的value
* @param keys
*/
public void remove(final String... keys) {
for (String key : keys) {
remove(key);
}
}
/**
* 批量删除key
* @param pattern
*/
public void removePattern(final String pattern) {
Set<Serializable> keys = redisTemplate.keys(pattern);
if (keys.size() > 0){
redisTemplate.delete(keys);
}
}
/**
* 删除对应的value
* @param key
*/
public void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 判断缓存中是否有对应的value
* @param key
* @return
*/
public boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 读取缓存
* @param key
* @return
*/
public Object get(final String key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}
/**
* 哈希 添加
* @param key
* @param hashKey
* @param value
*/
public void hmSet(String key, Object hashKey, Object value){
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
hash.put(key,hashKey,value);
}
/**
* 哈希获取数据
* @param key
* @param hashKey
* @return
*/
public Object hmGet(String key, Object hashKey){
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
return hash.get(key,hashKey);
}
/**
* 列表添加
* @param k
* @param v
*/
public void lPush(String k,Object v){
ListOperations<String, Object> list = redisTemplate.opsForList();
list.rightPush(k,v);
}
/**
* 列表获取
* @param k
* @param l
* @param l1
* @return
*/
public List<Object> lRange(String k, long l, long l1){
ListOperations<String, Object> list = redisTemplate.opsForList();
return list.range(k,l,l1);
}
/**
* 集合添加
* @param key
* @param value
*/
public void add(String key,Object value){
SetOperations<String, Object> set = redisTemplate.opsForSet();
set.add(key,value);
}
/**
* 集合获取
* @param key
* @return
*/
public Set<Object> setMembers(String key){
SetOperations<String, Object> set = redisTemplate.opsForSet();
return set.members(key);
}
/**
* 有序集合添加
* @param key
* @param value
* @param scoure
*/
public void zAdd(String key,Object value,double scoure){
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
zset.add(key,value,scoure);
}
/**
* 有序集合获取
* @param key
* @param scoure
* @param scoure1
* @return
*/
public Set<Object> rangeByScore(String key,double scoure,double scoure1){
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
return zset.rangeByScore(key, scoure, scoure1);
}
/**
* 发布
*
* @param key
*/
public void publish(String channel, Object message) {
stringRedisTemplate.convertAndSend(channel, message);
}
}
4.创建redis配置文件
package com.bjprd.config.redis;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import cn.dev33.satoken.stp.StpUtil;
@Configuration //相当于xml中的beans
public class RedisConfig {
@Bean("container")
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(redisConnectionFactory);
// 添加订阅者监听类,数量不限.PatternTopic定义监听主题,这里监听test-topic主题
container.addMessageListener(listenerAdapter, new PatternTopic("topic_all"));//建立topic_all通道
container.addMessageListener(listenerAdapter, new PatternTopic("topic_byid"));
return container;
}
@Bean
MessageListenerAdapter listenerAdapter(RedisMessageListener receiver) {
//设置监听
//利用反射机制,调用RedisMessageListener类中onMessage()方法
return new MessageListenerAdapter(receiver,"onMessage");
}
@Bean
StringRedisTemplate template(RedisConnectionFactory connectionFactory) {
//StringRedisTemplate继承了RedisTemplate,是专门用于字符串操作
return new StringRedisTemplate(connectionFactory);
}
}
5.创建WebSocketConfig
package com.bjprd.config.webSocket;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* 这个配置类的作用是要注入ServerEndpointExporter,这个bean会自动注册使用了@ServerEndpoint注解声明的Websocket endpoint。
* 如果是使用独立的servlet容器,而不是直接使用springboot的内置容器,就不要注入ServerEndpointExporter,因为它将由容器自己提供和管理。
*/
@Component
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
6.创建redis发送消息类
package com.bjprd.config.util;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.stereotype.Component;
import cn.dev33.satoken.SaManager;
import cn.dev33.satoken.stp.StpLogic;
import cn.dev33.satoken.stp.StpUtil;
/**
* 获取在线用户 id、token
* @author jiang
*
* 2022年6月15日上午8:35:55
*/
@Component
public class FindAllOnelineUsers {
public Map<String, List<String>> findAllOnelineUsers() {
// 获取 StpUtil 登录体系下的实际逻辑对象(单独把 StpLogic 提取出来是为了方便后期修改 StpUtil 的名字
StpLogic stpLogic = SaManager.getStpLogic(StpUtil.getLoginType());
// 获取所有 token ,如果数量庞大可以分页
List<String> tokens = stpLogic.searchTokenValue("", -1, -1);
// 保存所有用户id及对应的不同平台下的临时有效时长
Map<String, List<String>> idTokensMap = new HashMap<>();
for (String sourceToken: tokens) {
// 截取真实 token 值
String token = sourceToken.substring(sourceToken.lastIndexOf(":")+1);
// 获取临时有效期
long activityTimeout = stpLogic.getTokenActivityTimeoutByToken(token);
if (activityTimeout == -2) {
// 当前 token 所代表的会话已经临时过期了, 直接跳过
continue;
}
// 尝试根据 token 获取 loginId
Object loginIdByToken = stpLogic.getLoginIdByToken(token);
if (loginIdByToken != null) {
// 转换为原始类型, 这里根据登录时的参数类型动态修改
String loginId = (String)loginIdByToken;
// 每个用户id可以多次登录, 也可以在不同平台登录
// String loginTokenAndActivityTimeout = token + "|"+activityTimeout;
String loginTokenAndActivityTimeout = token;
// 同一个用户如果多端登录需要同时记录不同平台的时限
if (idTokensMap.containsKey(loginId)) {
// 如果有这个 loginId 直接获取并添加
idTokensMap.get(loginId).add(loginTokenAndActivityTimeout);
} else {
// 如果没有则新建并绑定 loginId
List<String> infoArr = new ArrayList<>();
infoArr.add(loginTokenAndActivityTimeout);
idTokensMap.put(loginId, infoArr);
}
}
}
return idTokensMap;
}
public Integer getUserId(){
Object loginId = StpUtil.getLoginId();
Integer creatorId = (Integer)loginId;
return creatorId;
}
}
package com.bjprd.config.redis;
import java.io.IOException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.websocket.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.stereotype.Component;
import com.bjprd.config.SpringUtils;
import com.bjprd.config.db.SuidRichUtils;
import com.bjprd.config.util.FindAllOnelineUsers;
import com.bjprd.config.webSocket.WebsocketEndpoint;
import com.bjprd.entity.MessageTable;
import cn.dev33.satoken.stp.StpUtil;
@Component
public class RedisMessageListener implements MessageListener {
@Autowired
FindAllOnelineUsers findAUsers;
private Logger logger = LoggerFactory.getLogger(this.getClass());
WebsocketEndpoint websocketEndpoint = (WebsocketEndpoint)SpringUtils.getBean("websocketEndpoint");
//用户的session
private Session session;
//用户的ID
private String userId;
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public Session getSession() {
return session;
}
public void setSession(Session session) {
this.session = session;
}
@Override
public void onMessage(Message message, byte[] bytes) {
// 获主题名称
String channel = new String(bytes);
String msg = new String(message.getBody()); //消息体
if (msg!= null) {
synchronized (this) {
System.out.println("redis中订阅消息");
if(channel.equals("topic_byid")){
//根据自身的业务需要编写
//下面是通过用户id,调用socket给对应的用户发送信息
String[] strarray=message.toString().split (",");
for(String id : strarray){
Map<String, List<String>> findAllUsers = findAUsers.findAllOnelineUsers();
writeMessage(id);
if(findAllUsers.containsKey(id)){
websocketEndpoint.sendMessageToUser("您收到一条审核信息!",id);
}
}
}else if(channel.equals("topic_all")){
// writeMessage(message.toString());
//根据自身的业务需要编写
//下面是通过用户id,调用socket给对应的用户发送信息
websocketEndpoint.sendMessageToUser(message.toString(),"userId");
}
}
}
}
/**
* 自定义类,用于把消息保存进数据库
* 逻辑可自定义
* @param id
* @return
*/
private void writeMessage(String id) {
if (id != null) {
Integer creatorId = findAUsers.getUserId();
MessageTable messageTable = new MessageTable();
messageTable.setMessage("您收到一条审核信息!");
messageTable.setRecipientId(Integer.valueOf(id));//接受人id
messageTable.setCreatorId(creatorId);//创建人id
messageTable.setCreationTime(new Date());//创建时间
SuidRichUtils.suidRich.insert(messageTable);
}
}
}
7、创建WebsocketEndpoint用于处理websocket请求
package com.bjprd.config.webSocket;
import com.alibaba.fastjson.JSONObject;
import com.bjprd.config.SpringUtils;
import com.bjprd.config.redis.RedisMessageListener;
import com.bjprd.config.redis.RedisUtil;
import cn.dev33.satoken.stp.StpUtil;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.bind.annotation.RestController;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* userId 用户id
* topic redis中的主题
*/
@ServerEndpoint("/socket/controller/{userId}")
@RestController
@Slf4j
public class WebsocketEndpoint {
private RedisUtil redisUtil = SpringUtils.getBean("redisUtil");
/***
* 用来记录当前连接数的变量
*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/***
* concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象
*/
// private static CopyOnWriteArraySet<WebsocketEndpoint> webSocketSet = new CopyOnWriteArraySet<WebsocketEndpoint>();
private static ConcurrentHashMap<String, WebsocketEndpoint> webSocketSet = new ConcurrentHashMap<>();
//用户id
private String tokenid;
/**
* 得到线程池,执行并发操作
*/
private ThreadPoolTaskExecutor threadPoolTaskExecutor = SpringUtils.getBean(ThreadPoolTaskExecutor.class);
/**
* 与某个客户端的连接会话,需要通过它来与客户端进行数据收发
*/
private Session session;
private static final Logger LOGGER = LoggerFactory.getLogger(WebsocketEndpoint.class);
//用来引入刚才在RedisConfig注入的类
private RedisMessageListenerContainer container = SpringUtils.getBean("container");
// 自定义redis监听器
private RedisMessageListener listener2;
/***
* socket打开的处理逻辑
* @param session
* @param tokenid
* @throws Exception
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) throws Exception {
LOGGER.info(String.format("用户:%s 打开了Socket链接", tokenid));
this.session = session;
this.session.isOpen();
this.tokenid = tokenid;
//webSocketSet中存当前用户对象
webSocketSet.put(userId,this);
//在线人数加一
addOnlineCount();
listener2 = new RedisMessageListener();
// 放入session
listener2.setSession(session);
// 放入用户ID
// listener2.setUserId(userId);
//初始化监听器
container.addMessageListener(listener2, new PatternTopic(userId));
}
/**
* socket关闭的处理逻辑
*/
@OnClose
public void onClose() {
// 删除当前对象(this)
webSocketSet.remove(this);
subOnlineCount();
getOnlineCount();
container.removeMessageListener(listener2);
LOGGER.info(String.format("%s关闭了Socket链接Close a html ", tokenid));
}
/**
* socket收到消息的处理逻辑
*/
@OnMessage
public void onMessage(String message, Session session) {
getOnlineCount();
LOGGER.info("收到一条数据消息----------" + message + "----------------------------------------");
//可以自己根据业务处理
try {
// socket心跳返回
Map map = new HashMap();
map.put("type", "0");
map.put("data", "soeket连接已建立");
map.put("message", message);
JSONObject jsonObject = new JSONObject(map);
// this.sendMessage(jsonObject.toJSONString());
redisUtil.publish("topic_byid", message);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 加一方法
*/
public volatile int p = 0;
public synchronized void addOne() {
p++;
System.out.println(Thread.currentThread().getName() + "------->" + "自增==>" + p);
}
/**
* socket链接错误
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
LOGGER.error("socket链接错误", error);
}
/**
* 发送消息
*
* @param message
*/
public void sendMessage(String message) {
if (session.isOpen()) {
// getOnlineCount();
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* 发送给指定用户
*
* @param message
* @param userid
*/
public void sendMessageToUser(String message, String userid) {
if (userid != null) {
webSocketSet.get(userid).sendMessage(message);
LOGGER.info("消息發送成功");
//System.out.println("消息發送成功");
}
}
//AtomicInteger是线程安全的 不需要synchronized修饰
public static AtomicInteger getOnlineCount() {
System.out.println(new Date() + "在线人数为" + onlineCount);
return onlineCount;
}
//AtomicInteger是线程安全的 内置自增与自减的方法getAndIncrement()
public static void addOnlineCount() {
WebsocketEndpoint.onlineCount.getAndIncrement();
}
//AtomicInteger是线程安全的 内置自增与自减的方法getAndDecrement()
public static void subOnlineCount() {
WebsocketEndpoint.onlineCount.getAndDecrement();
}
}
8.创建controller层方法,用于返回页面想要的数据
@ResponseBody
@GetMapping("/index")
public Map<String, Object> index(HttpServletRequest request,HttpServletResponse response) {
Map<String, Object> map = new HashMap<String, Object>();
try {
Object loginId = StpUtil.getLoginId();
ConditionImpl conditionImpl = new ConditionImpl();
conditionImpl.op("recipient_id", Op.eq, loginId);
conditionImpl.op("state", Op.eq, "01");
List<MessageTable> select = SuidRichUtils.suidRich.select(new MessageTable(), conditionImpl);
map.put("data", select);
map.put("id", loginId);
} catch (Exception e) {
// TODO: handle exception
e.getMessage();
}
return map;
}
9.前端
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="../js/jquery-1.8.3.min.js" charset="utf-8"></script>
</head>
<body>
<div>
<div>
<input type="text" id="inputid"><button onclick="fs()">发送</button>
</div>
<div>
<input type="text" id="price">
<input type="text" id="hiddenid">
</div>
</div>
</body>
<script>
var searchURL = window.location.search;
searchURL = searchURL.substring(1, searchURL.length);
var id = searchURL.split("&")[0].split("=")[1];//0-新增;1-修改
var websocket;
$(function () {
// var id = searchURL.split("&")[1].split("=")[1];
$.ajax({
url: '/test/index',
async: false,
// data: {id: 20},
success: function (data) {
console.log(data);
var datas = data.data;
var id = data.id;
$("#hiddenid").val(id);
console.log(websocket);
$.each(datas, function (index, item) {
alert(item.message);
});
}
});
if (window.WebSocket){
var id = document.getElementById('hiddenid').value;
websocket = new WebSocket("ws://127.0.0.1:8080/socket/controller/"+id+"");
console.log(id);
// websocket = new WebSocket("ws://127.0.0.1:8080/socket/controller/"+id+"");
//连接成功建立的回调方法
websocket.onopen = function () {
// websocket.send("客户端链接成功");
}
//接收到消息的回调方法
websocket.onmessage = function (event) {
setMessageInnerHTML(event.data);
}
//连接发生错误的回调方法
websocket.onerror = function () {
alert("WebSocket连接发生错误");
};
//连接关闭的回调方法
websocket.onclose = function () {
alert("WebSocket连接关闭");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function () {
closeWebSocket();
}
}
else {
alert('当前浏览器 Not support websocket')
}
});
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
console.log(innerHTML);
/* var bitcoin = eval("("+innerHTML+")");
console.log(bitcoin); */
$("#price").val(innerHTML);
// document.getElementById('price').innerHTML = bitcoin.price;
// document.getElementById('total').innerHTML = bitcoin.total;
}
//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}
function fs(){
var message = document.getElementById('inputid').value;
websocket.send(message);
}
</script>
</html>
个人业务逻辑,用于借鉴