commit
ae763f727f
@ -0,0 +1,31 @@
|
|||||||
|
package cn.iocoder.yudao.framework.mq.redis.config;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
|
||||||
|
import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
|
||||||
|
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 消息队列 Producer 配置类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
|
||||||
|
public class YudaoRedisMQProducerAutoConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
|
||||||
|
List<RedisMessageInterceptor> interceptors) {
|
||||||
|
RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
|
||||||
|
// 添加拦截器
|
||||||
|
interceptors.forEach(redisMQTemplate::addInterceptor);
|
||||||
|
return redisMQTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,2 +1,3 @@
|
|||||||
cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQAutoConfiguration
|
cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQProducerAutoConfiguration
|
||||||
|
cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration
|
||||||
cn.iocoder.yudao.framework.mq.rabbitmq.config.YudaoRabbitMQAutoConfiguration
|
cn.iocoder.yudao.framework.mq.rabbitmq.config.YudaoRabbitMQAutoConfiguration
|
||||||
@ -1,14 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.websocket.config;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.websocket.core.UserHandshakeInterceptor;
|
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
|
||||||
|
|
||||||
@EnableConfigurationProperties(WebSocketProperties.class)
|
|
||||||
public class WebSocketHandlerConfig {
|
|
||||||
@Bean
|
|
||||||
public HandshakeInterceptor handshakeInterceptor() {
|
|
||||||
return new UserHandshakeInterceptor();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.websocket.core;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
|
||||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
|
||||||
import org.springframework.http.server.ServerHttpRequest;
|
|
||||||
import org.springframework.http.server.ServerHttpResponse;
|
|
||||||
import org.springframework.web.socket.WebSocketHandler;
|
|
||||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class UserHandshakeInterceptor implements HandshakeInterceptor {
|
|
||||||
@Override
|
|
||||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
|
|
||||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
|
||||||
attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,9 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.websocket.core;
|
|
||||||
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public class WebSocketKeyDefine {
|
|
||||||
public static final String LOGIN_USER ="LOGIN_USER";
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.websocket.core;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.experimental.Accessors;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Data
|
|
||||||
@Accessors(chain = true)
|
|
||||||
public class WebSocketMessageDO {
|
|
||||||
/**
|
|
||||||
* 接收消息的seesion
|
|
||||||
*/
|
|
||||||
private List<Object> seesionKeyList;
|
|
||||||
/**
|
|
||||||
* 发送消息
|
|
||||||
*/
|
|
||||||
private String msgText;
|
|
||||||
|
|
||||||
public static WebSocketMessageDO build(List<Object> seesionKeyList, String msgText) {
|
|
||||||
return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.websocket.core;
|
|
||||||
|
|
||||||
import org.springframework.web.socket.WebSocketSession;
|
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
public final class WebSocketSessionHandler {
|
|
||||||
private WebSocketSessionHandler() {
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public static void addSession(Object sessionKey, WebSocketSession session) {
|
|
||||||
SESSION_MAP.put(sessionKey.toString(), session);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void removeSession(Object sessionKey) {
|
|
||||||
SESSION_MAP.remove(sessionKey.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static WebSocketSession getSession(Object sessionKey) {
|
|
||||||
return SESSION_MAP.get(sessionKey.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Collection<WebSocketSession> getSessions() {
|
|
||||||
return SESSION_MAP.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Set<String> getSessionKeys() {
|
|
||||||
return SESSION_MAP.keySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.websocket.core;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.web.socket.TextMessage;
|
|
||||||
import org.springframework.web.socket.WebSocketSession;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@Slf4j
|
|
||||||
public class WebSocketUtils {
|
|
||||||
public static boolean sendMessage(WebSocketSession seesion, String message) {
|
|
||||||
if (seesion == null) {
|
|
||||||
log.error("seesion 不存在");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (seesion.isOpen()) {
|
|
||||||
try {
|
|
||||||
seesion.sendMessage(new TextMessage(message));
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("WebSocket 消息发送异常 Session={} | msg= {} | exception={}", seesion, message, e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean sendMessage(Object sessionKey, String message) {
|
|
||||||
WebSocketSession session = WebSocketSessionHandler.getSession(sessionKey);
|
|
||||||
return sendMessage(session, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.websocket.core;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
|
||||||
import org.springframework.web.socket.CloseStatus;
|
|
||||||
import org.springframework.web.socket.WebSocketHandler;
|
|
||||||
import org.springframework.web.socket.WebSocketSession;
|
|
||||||
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
|
|
||||||
|
|
||||||
public class YudaoWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
|
|
||||||
public YudaoWebSocketHandlerDecorator(WebSocketHandler delegate) {
|
|
||||||
super(delegate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* websocket 连接时执行的动作
|
|
||||||
* @param session websocket session 对象
|
|
||||||
* @throws Exception 异常对象
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
|
|
||||||
Object sessionKey = sessionKeyGen(session);
|
|
||||||
WebSocketSessionHandler.addSession(sessionKey, session);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* websocket 关闭连接时执行的动作
|
|
||||||
* @param session websocket session 对象
|
|
||||||
* @param closeStatus 关闭状态对象
|
|
||||||
* @throws Exception 异常对象
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
|
||||||
Object sessionKey = sessionKeyGen(session);
|
|
||||||
WebSocketSessionHandler.removeSession(sessionKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Object sessionKeyGen(WebSocketSession webSocketSession) {
|
|
||||||
|
|
||||||
Object obj = webSocketSession.getAttributes().get(WebSocketKeyDefine.LOGIN_USER);
|
|
||||||
|
|
||||||
if (obj instanceof LoginUser) {
|
|
||||||
LoginUser loginUser = (LoginUser) obj;
|
|
||||||
// userId 作为唯一区分
|
|
||||||
return String.valueOf(loginUser.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.security;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
|
||||||
|
import cn.iocoder.yudao.framework.websocket.config.WebSocketProperties;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 的权限自定义
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {
|
||||||
|
|
||||||
|
private final WebSocketProperties webSocketProperties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
|
||||||
|
registry.antMatchers(webSocketProperties.getPath()).permitAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kafka 广播 WebSocket 的消息
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class KafkaWebSocketMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session 编号
|
||||||
|
*/
|
||||||
|
private String sessionId;
|
||||||
|
/**
|
||||||
|
* 用户类型
|
||||||
|
*/
|
||||||
|
private Integer userType;
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型
|
||||||
|
*/
|
||||||
|
private String messageType;
|
||||||
|
/**
|
||||||
|
* 消息内容
|
||||||
|
*/
|
||||||
|
private String messageContent;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.kafka.core.KafkaTemplate;
|
||||||
|
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 Kafka 的 {@link WebSocketMessageSender} 实现类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class KafkaWebSocketMessageSender extends AbstractWebSocketMessageSender {
|
||||||
|
|
||||||
|
private final KafkaTemplate<Object, Object> kafkaTemplate;
|
||||||
|
|
||||||
|
private final String topic;
|
||||||
|
|
||||||
|
public KafkaWebSocketMessageSender(WebSocketSessionManager sessionManager,
|
||||||
|
KafkaTemplate<Object, Object> kafkaTemplate,
|
||||||
|
String topic) {
|
||||||
|
super(sessionManager);
|
||||||
|
this.kafkaTemplate = kafkaTemplate;
|
||||||
|
this.topic = topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Integer userType, Long userId, String messageType, String messageContent) {
|
||||||
|
sendKafkaMessage(null, userId, userType, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Integer userType, String messageType, String messageContent) {
|
||||||
|
sendKafkaMessage(null, null, userType, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String sessionId, String messageType, String messageContent) {
|
||||||
|
sendKafkaMessage(sessionId, null, null, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 Kafka 广播消息
|
||||||
|
*
|
||||||
|
* @param sessionId Session 编号
|
||||||
|
* @param userId 用户编号
|
||||||
|
* @param userType 用户类型
|
||||||
|
* @param messageType 消息类型
|
||||||
|
* @param messageContent 消息内容
|
||||||
|
*/
|
||||||
|
private void sendKafkaMessage(String sessionId, Long userId, Integer userType,
|
||||||
|
String messageType, String messageContent) {
|
||||||
|
KafkaWebSocketMessage mqMessage = new KafkaWebSocketMessage()
|
||||||
|
.setSessionId(sessionId).setUserId(userId).setUserType(userType)
|
||||||
|
.setMessageType(messageType).setMessageContent(messageContent);
|
||||||
|
try {
|
||||||
|
kafkaTemplate.send(topic, mqMessage).get();
|
||||||
|
} catch (InterruptedException | ExecutionException e) {
|
||||||
|
log.error("[sendKafkaMessage][发送消息({}) 到 Kafka 失败]", mqMessage, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RabbitMQ 广播 WebSocket 的消息
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RabbitMQWebSocketMessage implements Serializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session 编号
|
||||||
|
*/
|
||||||
|
private String sessionId;
|
||||||
|
/**
|
||||||
|
* 用户类型
|
||||||
|
*/
|
||||||
|
private Integer userType;
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型
|
||||||
|
*/
|
||||||
|
private String messageType;
|
||||||
|
/**
|
||||||
|
* 消息内容
|
||||||
|
*/
|
||||||
|
private String messageContent;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,62 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.amqp.core.TopicExchange;
|
||||||
|
import org.springframework.amqp.rabbit.core.RabbitTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 RabbitMQ 的 {@link WebSocketMessageSender} 实现类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class RabbitMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
|
||||||
|
|
||||||
|
private final RabbitTemplate rabbitTemplate;
|
||||||
|
|
||||||
|
private final TopicExchange topicExchange;
|
||||||
|
|
||||||
|
public RabbitMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
|
||||||
|
RabbitTemplate rabbitTemplate,
|
||||||
|
TopicExchange topicExchange) {
|
||||||
|
super(sessionManager);
|
||||||
|
this.rabbitTemplate = rabbitTemplate;
|
||||||
|
this.topicExchange = topicExchange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Integer userType, Long userId, String messageType, String messageContent) {
|
||||||
|
sendRabbitMQMessage(null, userId, userType, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Integer userType, String messageType, String messageContent) {
|
||||||
|
sendRabbitMQMessage(null, null, userType, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String sessionId, String messageType, String messageContent) {
|
||||||
|
sendRabbitMQMessage(sessionId, null, null, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 RabbitMQ 广播消息
|
||||||
|
*
|
||||||
|
* @param sessionId Session 编号
|
||||||
|
* @param userId 用户编号
|
||||||
|
* @param userType 用户类型
|
||||||
|
* @param messageType 消息类型
|
||||||
|
* @param messageContent 消息内容
|
||||||
|
*/
|
||||||
|
private void sendRabbitMQMessage(String sessionId, Long userId, Integer userType,
|
||||||
|
String messageType, String messageContent) {
|
||||||
|
RabbitMQWebSocketMessage mqMessage = new RabbitMQWebSocketMessage()
|
||||||
|
.setSessionId(sessionId).setUserId(userId).setUserType(userType)
|
||||||
|
.setMessageType(messageType).setMessageContent(messageContent);
|
||||||
|
rabbitTemplate.convertAndSend(topicExchange.getName(), null, mqMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.sender.redis;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis 广播 WebSocket 的消息
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RedisWebSocketMessage extends AbstractRedisChannelMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session 编号
|
||||||
|
*/
|
||||||
|
private String sessionId;
|
||||||
|
/**
|
||||||
|
* 用户类型
|
||||||
|
*/
|
||||||
|
private Integer userType;
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型
|
||||||
|
*/
|
||||||
|
private String messageType;
|
||||||
|
/**
|
||||||
|
* 消息内容
|
||||||
|
*/
|
||||||
|
private String messageContent;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.sender.redis;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 Redis 的 {@link WebSocketMessageSender} 实现类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender {
|
||||||
|
|
||||||
|
private final RedisMQTemplate redisMQTemplate;
|
||||||
|
|
||||||
|
public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager,
|
||||||
|
RedisMQTemplate redisMQTemplate) {
|
||||||
|
super(sessionManager);
|
||||||
|
this.redisMQTemplate = redisMQTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Integer userType, Long userId, String messageType, String messageContent) {
|
||||||
|
sendRedisMessage(null, userId, userType, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Integer userType, String messageType, String messageContent) {
|
||||||
|
sendRedisMessage(null, null, userType, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String sessionId, String messageType, String messageContent) {
|
||||||
|
sendRedisMessage(sessionId, null, null, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 Redis 广播消息
|
||||||
|
*
|
||||||
|
* @param sessionId Session 编号
|
||||||
|
* @param userId 用户编号
|
||||||
|
* @param userType 用户类型
|
||||||
|
* @param messageType 消息类型
|
||||||
|
* @param messageContent 消息内容
|
||||||
|
*/
|
||||||
|
private void sendRedisMessage(String sessionId, Long userId, Integer userType,
|
||||||
|
String messageType, String messageContent) {
|
||||||
|
RedisWebSocketMessage mqMessage = new RedisWebSocketMessage()
|
||||||
|
.setSessionId(sessionId).setUserId(userId).setUserType(userType)
|
||||||
|
.setMessageType(messageType).setMessageContent(messageContent);
|
||||||
|
redisMQTemplate.send(mqMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RocketMQ 广播 WebSocket 的消息
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class RocketMQWebSocketMessage {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session 编号
|
||||||
|
*/
|
||||||
|
private String sessionId;
|
||||||
|
/**
|
||||||
|
* 用户类型
|
||||||
|
*/
|
||||||
|
private Integer userType;
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 消息类型
|
||||||
|
*/
|
||||||
|
private String messageType;
|
||||||
|
/**
|
||||||
|
* 消息内容
|
||||||
|
*/
|
||||||
|
private String messageContent;
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
|
||||||
|
|
||||||
|
private final RocketMQTemplate rocketMQTemplate;
|
||||||
|
|
||||||
|
private final String topic;
|
||||||
|
|
||||||
|
public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
|
||||||
|
RocketMQTemplate rocketMQTemplate,
|
||||||
|
String topic) {
|
||||||
|
super(sessionManager);
|
||||||
|
this.rocketMQTemplate = rocketMQTemplate;
|
||||||
|
this.topic = topic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Integer userType, Long userId, String messageType, String messageContent) {
|
||||||
|
sendRocketMQMessage(null, userId, userType, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Integer userType, String messageType, String messageContent) {
|
||||||
|
sendRocketMQMessage(null, null, userType, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String sessionId, String messageType, String messageContent) {
|
||||||
|
sendRocketMQMessage(sessionId, null, null, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 RocketMQ 广播消息
|
||||||
|
*
|
||||||
|
* @param sessionId Session 编号
|
||||||
|
* @param userId 用户编号
|
||||||
|
* @param userType 用户类型
|
||||||
|
* @param messageType 消息类型
|
||||||
|
* @param messageContent 消息内容
|
||||||
|
*/
|
||||||
|
private void sendRocketMQMessage(String sessionId, Long userId, Integer userType,
|
||||||
|
String messageType, String messageContent) {
|
||||||
|
RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage()
|
||||||
|
.setSessionId(sessionId).setUserId(userId).setUserType(userType)
|
||||||
|
.setMessageType(messageType).setMessageContent(messageContent);
|
||||||
|
rocketMQTemplate.syncSend(topic, mqMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.session;
|
||||||
|
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link WebSocketSession} 管理器的接口
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public interface WebSocketSessionManager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加 Session
|
||||||
|
*
|
||||||
|
* @param session Session
|
||||||
|
*/
|
||||||
|
void addSession(WebSocketSession session);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除 Session
|
||||||
|
*
|
||||||
|
* @param session Session
|
||||||
|
*/
|
||||||
|
void removeSession(WebSocketSession session);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得指定编号的 Session
|
||||||
|
*
|
||||||
|
* @param id Session 编号
|
||||||
|
* @return Session
|
||||||
|
*/
|
||||||
|
WebSocketSession getSession(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得指定用户类型的 Session 列表
|
||||||
|
*
|
||||||
|
* @param userType 用户类型
|
||||||
|
* @return Session 列表
|
||||||
|
*/
|
||||||
|
Collection<WebSocketSession> getSessionList(Integer userType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得指定用户编号的 Session 列表
|
||||||
|
*
|
||||||
|
* @param userType 用户类型
|
||||||
|
* @param userId 用户编号
|
||||||
|
* @return Session 列表
|
||||||
|
*/
|
||||||
|
Collection<WebSocketSession> getSessionList(Integer userType, Long userId);
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
package cn.iocoder.yudao.framework.websocket.core.util;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 专属于 web 包的工具类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public class WebSocketFrameworkUtils {
|
||||||
|
|
||||||
|
public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置当前用户
|
||||||
|
*
|
||||||
|
* @param loginUser 登录用户
|
||||||
|
* @param attributes Session
|
||||||
|
*/
|
||||||
|
public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) {
|
||||||
|
attributes.put(ATTRIBUTE_LOGIN_USER, loginUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前用户
|
||||||
|
*
|
||||||
|
* @return 当前用户
|
||||||
|
*/
|
||||||
|
public static LoginUser getLoginUser(WebSocketSession session) {
|
||||||
|
return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前用户的编号
|
||||||
|
*
|
||||||
|
* @return 用户编号
|
||||||
|
*/
|
||||||
|
public static Long getLoginUserId(WebSocketSession session) {
|
||||||
|
LoginUser loginUser = getLoginUser(session);
|
||||||
|
return loginUser != null ? loginUser.getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前用户的类型
|
||||||
|
*
|
||||||
|
* @return 用户编号
|
||||||
|
*/
|
||||||
|
public static Integer getLoginUserType(WebSocketSession session) {
|
||||||
|
LoginUser loginUser = getLoginUser(session);
|
||||||
|
return loginUser != null ? loginUser.getUserType() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得当前用户的租户编号
|
||||||
|
*
|
||||||
|
* @param session Session
|
||||||
|
* @return 租户编号
|
||||||
|
*/
|
||||||
|
public static Long getTenantId(WebSocketSession session) {
|
||||||
|
LoginUser loginUser = getLoginUser(session);
|
||||||
|
return loginUser != null ? loginUser.getTenantId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
<http://www.iocoder.cn/Spring-Boot/WebSocket/?yudao>
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.api.websocket;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import javax.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 发送器的 API 实现类
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class WebSocketSenderApiImpl implements WebSocketSenderApi {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private WebSocketMessageSender webSocketMessageSender;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Integer userType, Long userId, String messageType, String messageContent) {
|
||||||
|
webSocketMessageSender.send(userType, userId, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(Integer userType, String messageType, String messageContent) {
|
||||||
|
webSocketMessageSender.send(userType, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void send(String sessionId, String messageType, String messageContent) {
|
||||||
|
webSocketMessageSender.send(sessionId, messageType, messageContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -1,45 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.infra.websocket;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
|
|
||||||
import java.util.concurrent.Semaphore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 信号量相关处理
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
public class SemaphoreUtils {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取信号量
|
|
||||||
*
|
|
||||||
* @param semaphore
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static boolean tryAcquire(Semaphore semaphore) {
|
|
||||||
boolean flag = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
flag = semaphore.tryAcquire();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("获取信号量异常", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return flag;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 释放信号量
|
|
||||||
*
|
|
||||||
* @param semaphore
|
|
||||||
*/
|
|
||||||
public static void release(Semaphore semaphore) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
semaphore.release();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("释放信号量异常", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.infra.websocket;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* websocket 配置
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
public class WebSocketConfig {
|
|
||||||
@Bean
|
|
||||||
public ServerEndpointExporter serverEndpointExporter() {
|
|
||||||
return new ServerEndpointExporter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
package cn.iocoder.yudao.module.infra.websocket;
|
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
|
|
||||||
import javax.websocket.*;
|
|
||||||
import javax.websocket.server.ServerEndpoint;
|
|
||||||
import java.util.concurrent.Semaphore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* websocket 消息处理
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
@ServerEndpoint("/websocket/message")
|
|
||||||
@Slf4j
|
|
||||||
public class WebSocketServer {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 默认最多允许同时在线用户数100
|
|
||||||
*/
|
|
||||||
public static int socketMaxOnlineCount = 100;
|
|
||||||
|
|
||||||
private static final Semaphore SOCKET_SEMAPHORE = new Semaphore(socketMaxOnlineCount);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接建立成功调用的方法
|
|
||||||
*/
|
|
||||||
@OnOpen
|
|
||||||
public void onOpen(Session session) throws Exception {
|
|
||||||
// 尝试获取信号量
|
|
||||||
boolean semaphoreFlag = SemaphoreUtils.tryAcquire(SOCKET_SEMAPHORE);
|
|
||||||
if (!semaphoreFlag) {
|
|
||||||
// 未获取到信号量
|
|
||||||
log.error("当前在线人数超过限制数:{}", socketMaxOnlineCount);
|
|
||||||
WebSocketUsers.sendMessage(session, "当前在线人数超过限制数:" + socketMaxOnlineCount);
|
|
||||||
session.close();
|
|
||||||
} else {
|
|
||||||
String userId = WebSocketUsers.getParam("userId", session);
|
|
||||||
if (userId != null) {
|
|
||||||
// 添加用户
|
|
||||||
WebSocketUsers.addSession(userId, session);
|
|
||||||
log.info("用户【userId={}】建立连接,当前连接用户总数:{}", userId, WebSocketUsers.getUsers().size());
|
|
||||||
WebSocketUsers.sendMessage(session, "接收内容:连接成功");
|
|
||||||
} else {
|
|
||||||
WebSocketUsers.sendMessage(session, "接收内容:连接失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接关闭时处理
|
|
||||||
*/
|
|
||||||
@OnClose
|
|
||||||
public void onClose(Session session) {
|
|
||||||
log.info("用户【sessionId={}】关闭连接!", session.getId());
|
|
||||||
// 移除用户
|
|
||||||
WebSocketUsers.removeSession(session);
|
|
||||||
// 获取到信号量则需释放
|
|
||||||
SemaphoreUtils.release(SOCKET_SEMAPHORE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 抛出异常时处理
|
|
||||||
*/
|
|
||||||
@OnError
|
|
||||||
public void onError(Session session, Throwable exception) throws Exception {
|
|
||||||
if (session.isOpen()) {
|
|
||||||
// 关闭连接
|
|
||||||
session.close();
|
|
||||||
}
|
|
||||||
String sessionId = session.getId();
|
|
||||||
log.info("用户【sessionId={}】连接异常!异常信息:{}", sessionId, exception);
|
|
||||||
// 移出用户
|
|
||||||
WebSocketUsers.removeSession(session);
|
|
||||||
// 获取到信号量则需释放
|
|
||||||
SemaphoreUtils.release(SOCKET_SEMAPHORE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 收到客户端消息时调用的方法
|
|
||||||
*/
|
|
||||||
@OnMessage
|
|
||||||
public void onMessage(Session session, String message) {
|
|
||||||
WebSocketUsers.sendMessage(session, "接收内容:" + message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue