springboot+bee+sotoken+redis+webSocket 即时通讯,发送给指定用户

1、在pom.xml中引入相关依赖



    org.springframework.boot
    spring-boot-starter-websocket




    org.springframework.boot
    spring-boot-starter-data-redis

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 
     * @return
     */
    public static  T getBean(Class beanClass) {
        return context.getBean(beanClass);
    }
 
    /**
     * 通过BeanName获取
     * @param beanName
     * @param 
     * @return
     */
    public static  T getBean(String beanName) {
        return (T) context.getBean(beanName);
    }
 
    /**
     * 通过beanName和字节码获取
     * @param name
     * @param beanClass
     * @param 
     * @return
     */
    public static  T getBean(String name, Class 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 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 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 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 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 hash = redisTemplate.opsForHash();
        hash.put(key,hashKey,value);
    }
    /**
     * 哈希获取数据
     * @param key
     * @param hashKey
     * @return
     */
    public Object hmGet(String key, Object hashKey){
        HashOperations  hash = redisTemplate.opsForHash();
        return hash.get(key,hashKey);
    }
    /**
     * 列表添加
     * @param k
     * @param v
     */
    public void lPush(String k,Object v){
        ListOperations list = redisTemplate.opsForList();
        list.rightPush(k,v);
    }
    /**
     * 列表获取
     * @param k
     * @param l
     * @param l1
     * @return
     */
    public List lRange(String k, long l, long l1){
        ListOperations list = redisTemplate.opsForList();
        return list.range(k,l,l1);
    }
    /**
     * 集合添加
     * @param key
     * @param value
     */
    public void add(String key,Object value){
        SetOperations set = redisTemplate.opsForSet();
        set.add(key,value);
    }
    /**
     * 集合获取
     * @param key
     * @return
     */
    public Set setMembers(String key){
        SetOperations set = redisTemplate.opsForSet();
        return set.members(key);
    }
    /**
     * 有序集合添加
     * @param key
     * @param value
     * @param scoure
     */
    public void zAdd(String key,Object value,double scoure){
        ZSetOperations zset = redisTemplate.opsForZSet();
        zset.add(key,value,scoure);
    }
    /**
     * 有序集合获取
     * @param key
     * @param scoure
     * @param scoure1
     * @return
     */
    public Set rangeByScore(String key,double scoure,double scoure1){
        ZSetOperations 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> findAllOnelineUsers() {
	    // 获取 StpUtil 登录体系下的实际逻辑对象(单独把 StpLogic 提取出来是为了方便后期修改 StpUtil 的名字
	    StpLogic stpLogic = SaManager.getStpLogic(StpUtil.getLoginType());
	    // 获取所有 token ,如果数量庞大可以分页
	    List tokens = stpLogic.searchTokenValue("", -1, -1);
	    // 保存所有用户id及对应的不同平台下的临时有效时长
	    Map> 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 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> 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 webSocketSet = new CopyOnWriteArraySet();

    private static ConcurrentHashMap 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 index(HttpServletRequest request,HttpServletResponse response) {
		Map map = new HashMap();
		try {
			Object loginId = StpUtil.getLoginId();
			ConditionImpl conditionImpl = new ConditionImpl();
			conditionImpl.op("recipient_id", Op.eq, loginId);
			conditionImpl.op("state", Op.eq, "01");
			List 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.前端





Insert title here
    


	

个人业务逻辑,用于借鉴

你可能感兴趣的:(websocket,redis,spring,boot,java)