Commit ae763f727fe1a4dc3c251d07002f2cc5b7f6f548

Authored by 芋道源码
Committed by Gitee
2 parents d4bc6ce5 a597eb17

!749 增加 WebSocket 的全新支持

Merge pull request !749 from 芋道源码/feature/websocket
Showing 58 changed files with 1687 additions and 555 deletions
yudao-dependencies/pom.xml
... ... @@ -176,6 +176,12 @@
176 176 </dependency>
177 177  
178 178 <dependency>
  179 + <groupId>cn.iocoder.boot</groupId>
  180 + <artifactId>yudao-spring-boot-starter-websocket</artifactId>
  181 + <version>${revision}</version>
  182 + </dependency>
  183 +
  184 + <dependency>
179 185 <groupId>com.github.xiaoymin</groupId>
180 186 <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
181 187 <version>${knife4j.version}</version>
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQAutoConfiguration.java renamed to yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
... ... @@ -5,7 +5,6 @@ import cn.hutool.core.util.StrUtil;
5 5 import cn.hutool.system.SystemUtil;
6 6 import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
7 7 import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
8   -import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
9 8 import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob;
10 9 import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
11 10 import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener;
... ... @@ -23,7 +22,6 @@ import org.springframework.data.redis.connection.stream.ReadOffset;
23 22 import org.springframework.data.redis.connection.stream.StreamOffset;
24 23 import org.springframework.data.redis.core.RedisCallback;
25 24 import org.springframework.data.redis.core.RedisTemplate;
26   -import org.springframework.data.redis.core.StringRedisTemplate;
27 25 import org.springframework.data.redis.listener.ChannelTopic;
28 26 import org.springframework.data.redis.listener.RedisMessageListenerContainer;
29 27 import org.springframework.data.redis.stream.StreamMessageListenerContainer;
... ... @@ -33,30 +31,19 @@ import java.util.List;
33 31 import java.util.Properties;
34 32  
35 33 /**
36   - * 消息队列配置类
  34 + * Redis 消息队列 Consumer 配置类
37 35 *
38 36 * @author 芋道源码
39 37 */
40 38 @Slf4j
41 39 @EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息
42 40 @AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
43   -public class YudaoRedisMQAutoConfiguration {
44   -
45   - @Bean
46   - public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
47   - List<RedisMessageInterceptor> interceptors) {
48   - RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
49   - // 添加拦截器
50   - interceptors.forEach(redisMQTemplate::addInterceptor);
51   - return redisMQTemplate;
52   - }
53   -
54   - // ========== 消费者相关 ==========
  41 +public class YudaoRedisMQConsumerAutoConfiguration {
55 42  
56 43 /**
57 44 * 创建 Redis Pub/Sub 广播消费的容器
58 45 */
59   - @Bean(initMethod = "start", destroyMethod = "stop")
  46 + @Bean
60 47 @ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听
61 48 public RedisMessageListenerContainer redisMessageListenerContainer(
62 49 RedisMQTemplate redisMQTemplate, List<AbstractRedisChannelMessageListener<?>> listeners) {
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mq.redis.config;
  2 +
  3 +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
  4 +import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor;
  5 +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.boot.autoconfigure.AutoConfiguration;
  8 +import org.springframework.context.annotation.Bean;
  9 +import org.springframework.data.redis.core.StringRedisTemplate;
  10 +
  11 +import java.util.List;
  12 +
  13 +/**
  14 + * Redis 消息队列 Producer 配置类
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +@Slf4j
  19 +@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
  20 +public class YudaoRedisMQProducerAutoConfiguration {
  21 +
  22 + @Bean
  23 + public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
  24 + List<RedisMessageInterceptor> interceptors) {
  25 + RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
  26 + // 添加拦截器
  27 + interceptors.forEach(redisMQTemplate::addInterceptor);
  28 + return redisMQTemplate;
  29 + }
  30 +
  31 +}
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
1   -cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQAutoConfiguration
  1 +cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQProducerAutoConfiguration
  2 +cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration
2 3 cn.iocoder.yudao.framework.mq.rabbitmq.config.YudaoRabbitMQAutoConfiguration
3 4 \ No newline at end of file
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java
... ... @@ -19,6 +19,13 @@ public class SecurityProperties {
19 19 */
20 20 @NotEmpty(message = "Token Header 不能为空")
21 21 private String tokenHeader = "Authorization";
  22 + /**
  23 + * HTTP 请求时,访问令牌的请求参数
  24 + *
  25 + * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接
  26 + */
  27 + @NotEmpty(message = "Token Parameter 不能为空")
  28 + private String tokenParameter = "token";
22 29  
23 30 /**
24 31 * mock 模式的开关
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
... ... @@ -129,8 +129,6 @@ public class YudaoWebSecurityConfigurerAdapter {
129 129 .antMatchers(buildAppApi("/**")).permitAll()
130 130 // 1.5 验证码captcha 允许匿名访问
131 131 .antMatchers("/captcha/get", "/captcha/check").permitAll()
132   - // 1.6 webSocket 允许匿名访问
133   - .antMatchers("/websocket/message").permitAll()
134 132 // ②:每个项目的自定义规则
135 133 .and().authorizeRequests(registry -> // 下面,循环设置自定义规则
136 134 authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry)))
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
... ... @@ -41,7 +41,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
41 41 @SuppressWarnings("NullableProblems")
42 42 protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
43 43 throws ServletException, IOException {
44   - String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
  44 + String token = SecurityFrameworkUtils.obtainAuthorization(request,
  45 + securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
45 46 if (StrUtil.isNotEmpty(token)) {
46 47 Integer userType = WebFrameworkUtils.getLoginUserType(request);
47 48 try {
... ... @@ -74,7 +75,10 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
74 75 return null;
75 76 }
76 77 // 用户类型不匹配,无权限
77   - if (ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
  78 + // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型
  79 + // TODO 芋艿:ws 要不要区分开?
  80 + if (userType != null
  81 + && ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
78 82 throw new AccessDeniedException("错误的用户类型");
79 83 }
80 84 // 构建登录用户
... ...
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
1 1 package cn.iocoder.yudao.framework.security.core.util;
2 2  
  3 +import cn.hutool.core.util.StrUtil;
3 4 import cn.iocoder.yudao.framework.security.core.LoginUser;
4 5 import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
5 6 import org.springframework.lang.Nullable;
... ... @@ -20,6 +21,9 @@ import java.util.Collections;
20 21 */
21 22 public class SecurityFrameworkUtils {
22 23  
  24 + /**
  25 + * HEADER 认证头 value 的前缀
  26 + */
23 27 public static final String AUTHORIZATION_BEARER = "Bearer";
24 28  
25 29 private SecurityFrameworkUtils() {}
... ... @@ -28,19 +32,23 @@ public class SecurityFrameworkUtils {
28 32 * 从请求中,获得认证 Token
29 33 *
30 34 * @param request 请求
31   - * @param header 认证 Token 对应的 Header 名字
  35 + * @param headerName 认证 Token 对应的 Header 名字
  36 + * @param parameterName 认证 Token 对应的 Parameter 名字
32 37 * @return 认证 Token
33 38 */
34   - public static String obtainAuthorization(HttpServletRequest request, String header) {
35   - String authorization = request.getHeader(header);
36   - if (!StringUtils.hasText(authorization)) {
37   - return null;
  39 + public static String obtainAuthorization(HttpServletRequest request,
  40 + String headerName, String parameterName) {
  41 + // 1. 获得 Token。优先级:Header > Parameter
  42 + String token = request.getHeader(headerName);
  43 + if (StrUtil.isEmpty(token)) {
  44 + token = request.getParameter(parameterName);
38 45 }
39   - int index = authorization.indexOf(AUTHORIZATION_BEARER + " ");
40   - if (index == -1) { // 未找到
  46 + if (!StringUtils.hasText(token)) {
41 47 return null;
42 48 }
43   - return authorization.substring(index + 7).trim();
  49 + // 2. 去除 Token 中带的 Bearer
  50 + int index = token.indexOf(AUTHORIZATION_BEARER + " ");
  51 + return index >= 0 ? token.substring(index + 7).trim() : token;
44 52 }
45 53  
46 54 /**
... ...
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
... ... @@ -12,26 +12,73 @@
12 12 <packaging>jar</packaging>
13 13  
14 14 <name>${project.artifactId}</name>
15   - <description>WebSocket</description>
  15 + <description>WebSocket 框架,支持多节点的广播</description>
16 16 <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
17 17  
18 18  
19 19 <dependencies>
20   -
21 20 <dependency>
22 21 <groupId>cn.iocoder.boot</groupId>
23 22 <artifactId>yudao-common</artifactId>
24 23 </dependency>
25 24  
  25 + <!-- Web 相关 -->
26 26 <dependency>
  27 + <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
  28 + 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
  29 + 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
  30 + -->
27 31 <groupId>cn.iocoder.boot</groupId>
28 32 <artifactId>yudao-spring-boot-starter-security</artifactId>
  33 + <scope>provided</scope>
29 34 </dependency>
30 35  
31 36 <dependency>
32 37 <groupId>org.springframework.boot</groupId>
33 38 <artifactId>spring-boot-starter-websocket</artifactId>
34 39 </dependency>
  40 +
  41 + <!-- Web 相关 -->
  42 + <dependency>
  43 + <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
  44 + 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
  45 + 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
  46 + -->
  47 + <groupId>cn.iocoder.boot</groupId>
  48 + <artifactId>yudao-spring-boot-starter-security</artifactId>
  49 + <scope>provided</scope>
  50 + </dependency>
  51 +
  52 + <!-- 消息队列相关 -->
  53 + <dependency>
  54 + <groupId>cn.iocoder.boot</groupId>
  55 + <artifactId>yudao-spring-boot-starter-mq</artifactId>
  56 + </dependency>
  57 + <dependency>
  58 + <groupId>org.springframework.kafka</groupId>
  59 + <artifactId>spring-kafka</artifactId>
  60 + <optional>true</optional>
  61 + </dependency>
  62 + <dependency>
  63 + <groupId>org.springframework.amqp</groupId>
  64 + <artifactId>spring-rabbit</artifactId>
  65 + <optional>true</optional>
  66 + </dependency>
  67 + <dependency>
  68 + <groupId>org.apache.rocketmq</groupId>
  69 + <artifactId>rocketmq-spring-boot-starter</artifactId>
  70 + <optional>true</optional>
  71 + </dependency>
  72 +
  73 + <!-- 业务组件 -->
  74 + <dependency>
  75 + <!-- 为什么要依赖 tenant 组件?
  76 + 因为广播某个类型的用户时候,需要根据租户过滤下,避免广播到别的租户!
  77 + -->
  78 + <groupId>cn.iocoder.boot</groupId>
  79 + <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId>
  80 + <scope>provided</scope>
  81 + </dependency>
35 82 </dependencies>
36 83  
37 84 </project>
38 85 \ No newline at end of file
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java deleted
1   -package cn.iocoder.yudao.framework.websocket.config;
2   -
3   -import cn.iocoder.yudao.framework.websocket.core.UserHandshakeInterceptor;
4   -import org.springframework.boot.context.properties.EnableConfigurationProperties;
5   -import org.springframework.context.annotation.Bean;
6   -import org.springframework.web.socket.server.HandshakeInterceptor;
7   -
8   -@EnableConfigurationProperties(WebSocketProperties.class)
9   -public class WebSocketHandlerConfig {
10   - @Bean
11   - public HandshakeInterceptor handshakeInterceptor() {
12   - return new UserHandshakeInterceptor();
13   - }
14   -}
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
... ... @@ -4,6 +4,9 @@ import lombok.Data;
4 4 import org.springframework.boot.context.properties.ConfigurationProperties;
5 5 import org.springframework.validation.annotation.Validated;
6 6  
  7 +import javax.validation.constraints.NotEmpty;
  8 +import javax.validation.constraints.NotNull;
  9 +
7 10 /**
8 11 * WebSocket 配置项
9 12 *
... ... @@ -15,15 +18,17 @@ import org.springframework.validation.annotation.Validated;
15 18 public class WebSocketProperties {
16 19  
17 20 /**
18   - * 路径
19   - */
20   - private String path = "";
21   - /**
22   - * 默认最多允许同时在线用户数
  21 + * WebSocket 的连接路径
23 22 */
24   - private int maxOnlineCount = 0;
  23 + @NotEmpty(message = "WebSocket 的连接路径不能为空")
  24 + private String path = "/ws";
  25 +
25 26 /**
26   - * 是否保存session
  27 + * 消息发送器的类型
  28 + *
  29 + * 可选值:local、redis、rocketmq、kafka、rabbitmq
27 30 */
28   - private boolean sessionMap = true;
  31 + @NotNull(message = "WebSocket 的消息发送者不能为空")
  32 + private String senderType = "local";
  33 +
29 34 }
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
1 1 package cn.iocoder.yudao.framework.websocket.config;
2 2  
  3 +import cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration;
  4 +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
  5 +import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler;
  6 +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
  7 +import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
  8 +import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
  9 +import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
  10 +import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
  11 +import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer;
  12 +import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender;
  13 +import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer;
  14 +import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageSender;
  15 +import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer;
  16 +import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender;
  17 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionHandlerDecorator;
  18 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
  19 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManagerImpl;
  20 +import org.apache.rocketmq.spring.core.RocketMQTemplate;
  21 +import org.springframework.amqp.core.TopicExchange;
  22 +import org.springframework.amqp.rabbit.core.RabbitTemplate;
  23 +import org.springframework.beans.factory.annotation.Value;
3 24 import org.springframework.boot.autoconfigure.AutoConfiguration;
4   -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
5 25 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
6 26 import org.springframework.boot.context.properties.EnableConfigurationProperties;
7 27 import org.springframework.context.annotation.Bean;
  28 +import org.springframework.context.annotation.Configuration;
  29 +import org.springframework.kafka.core.KafkaTemplate;
8 30 import org.springframework.web.socket.WebSocketHandler;
  31 +import org.springframework.web.socket.config.annotation.EnableWebSocket;
9 32 import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
10 33 import org.springframework.web.socket.server.HandshakeInterceptor;
11 34  
... ... @@ -16,19 +39,139 @@ import java.util.List;
16 39 *
17 40 * @author xingyu4j
18 41 */
19   -@AutoConfiguration
20   -// 允许使用 yudao.websocket.enable=false 禁用websocket
21   -@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true)
  42 +@AutoConfiguration(before = YudaoRedisMQConsumerAutoConfiguration.class) // before YudaoRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer
  43 +@EnableWebSocket // 开启 websocket
  44 +@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true) // 允许使用 yudao.websocket.enable=false 禁用 websocket
22 45 @EnableConfigurationProperties(WebSocketProperties.class)
23 46 public class YudaoWebSocketAutoConfiguration {
  47 +
24 48 @Bean
25   - @ConditionalOnMissingBean
26   - public WebSocketConfigurer webSocketConfigurer(List<HandshakeInterceptor> handshakeInterceptor,
  49 + public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
27 50 WebSocketHandler webSocketHandler,
28 51 WebSocketProperties webSocketProperties) {
29   -
30 52 return registry -> registry
  53 + // 添加 WebSocketHandler
31 54 .addHandler(webSocketHandler, webSocketProperties.getPath())
32   - .addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0]));
  55 + .addInterceptors(handshakeInterceptors)
  56 + // 允许跨域,否则前端连接会直接断开
  57 + .setAllowedOriginPatterns("*");
  58 + }
  59 +
  60 + @Bean
  61 + public HandshakeInterceptor handshakeInterceptor() {
  62 + return new LoginUserHandshakeInterceptor();
  63 + }
  64 +
  65 + @Bean
  66 + public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
  67 + List<? extends WebSocketMessageListener<?>> messageListeners) {
  68 + // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息
  69 + JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
  70 + // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接
  71 + return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
  72 + }
  73 +
  74 + @Bean
  75 + public WebSocketSessionManager webSocketSessionManager() {
  76 + return new WebSocketSessionManagerImpl();
  77 + }
  78 +
  79 + // ==================== Sender 相关 ====================
  80 +
  81 + @Configuration
  82 + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true)
  83 + public class LocalWebSocketMessageSenderConfiguration {
  84 +
  85 + @Bean
  86 + public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) {
  87 + return new LocalWebSocketMessageSender(sessionManager);
  88 + }
  89 +
  90 + }
  91 +
  92 + @Configuration
  93 + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true)
  94 + public class RedisWebSocketMessageSenderConfiguration {
  95 +
  96 + @Bean
  97 + public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager,
  98 + RedisMQTemplate redisMQTemplate) {
  99 + return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate);
  100 + }
  101 +
  102 + @Bean
  103 + public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer(
  104 + RedisWebSocketMessageSender redisWebSocketMessageSender) {
  105 + return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender);
  106 + }
  107 +
  108 + }
  109 +
  110 + @Configuration
  111 + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true)
  112 + public class RocketMQWebSocketMessageSenderConfiguration {
  113 +
  114 + @Bean
  115 + public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender(
  116 + WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate,
  117 + @Value("${yudao.websocket.sender-rocketmq.topic}") String topic) {
  118 + return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic);
  119 + }
  120 +
  121 + @Bean
  122 + public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer(
  123 + RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) {
  124 + return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender);
  125 + }
  126 +
33 127 }
34   -}
  128 +
  129 + @Configuration
  130 + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true)
  131 + public class RabbitMQWebSocketMessageSenderConfiguration {
  132 +
  133 + @Bean
  134 + public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender(
  135 + WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate,
  136 + TopicExchange websocketTopicExchange) {
  137 + return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange);
  138 + }
  139 +
  140 + @Bean
  141 + public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer(
  142 + RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) {
  143 + return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender);
  144 + }
  145 +
  146 + /**
  147 + * 创建 Topic Exchange
  148 + */
  149 + @Bean
  150 + public TopicExchange websocketTopicExchange(@Value("${yudao.websocket.sender-rabbitmq.exchange}") String exchange) {
  151 + return new TopicExchange(exchange,
  152 + true, // durable: 是否持久化
  153 + false); // exclusive: 是否排它
  154 + }
  155 +
  156 + }
  157 +
  158 + @Configuration
  159 + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true)
  160 + public class KafkaWebSocketMessageSenderConfiguration {
  161 +
  162 + @Bean
  163 + public KafkaWebSocketMessageSender kafkaWebSocketMessageSender(
  164 + WebSocketSessionManager sessionManager, KafkaTemplate<Object, Object> kafkaTemplate,
  165 + @Value("${yudao.websocket.sender-kafka.topic}") String topic) {
  166 + return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic);
  167 + }
  168 +
  169 + @Bean
  170 + public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer(
  171 + KafkaWebSocketMessageSender kafkaWebSocketMessageSender) {
  172 + return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender);
  173 + }
  174 +
  175 + }
  176 +
  177 +}
35 178 \ No newline at end of file
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java deleted
1   -package cn.iocoder.yudao.framework.websocket.core;
2   -
3   -
4   -import lombok.Data;
5   -
6   -@Data
7   -public class WebSocketKeyDefine {
8   - public static final String LOGIN_USER ="LOGIN_USER";
9   -}
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java deleted
1   -package cn.iocoder.yudao.framework.websocket.core;
2   -
3   -import lombok.Data;
4   -import lombok.experimental.Accessors;
5   -
6   -import java.util.List;
7   -
8   -@Data
9   -@Accessors(chain = true)
10   -public class WebSocketMessageDO {
11   - /**
12   - * 接收消息的seesion
13   - */
14   - private List<Object> seesionKeyList;
15   - /**
16   - * 发送消息
17   - */
18   - private String msgText;
19   -
20   - public static WebSocketMessageDO build(List<Object> seesionKeyList, String msgText) {
21   - return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList);
22   - }
23   -
24   -}
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java deleted
1   -package cn.iocoder.yudao.framework.websocket.core;
2   -
3   -import org.springframework.web.socket.WebSocketSession;
4   -
5   -import java.util.Collection;
6   -import java.util.Map;
7   -import java.util.Set;
8   -import java.util.concurrent.ConcurrentHashMap;
9   -
10   -public final class WebSocketSessionHandler {
11   - private WebSocketSessionHandler() {
12   - }
13   -
14   - private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
15   -
16   - public static void addSession(Object sessionKey, WebSocketSession session) {
17   - SESSION_MAP.put(sessionKey.toString(), session);
18   - }
19   -
20   - public static void removeSession(Object sessionKey) {
21   - SESSION_MAP.remove(sessionKey.toString());
22   - }
23   -
24   - public static WebSocketSession getSession(Object sessionKey) {
25   - return SESSION_MAP.get(sessionKey.toString());
26   - }
27   -
28   - public static Collection<WebSocketSession> getSessions() {
29   - return SESSION_MAP.values();
30   - }
31   -
32   - public static Set<String> getSessionKeys() {
33   - return SESSION_MAP.keySet();
34   - }
35   -
36   -}
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java deleted
1   -package cn.iocoder.yudao.framework.websocket.core;
2   -
3   -import lombok.extern.slf4j.Slf4j;
4   -import org.springframework.web.socket.TextMessage;
5   -import org.springframework.web.socket.WebSocketSession;
6   -
7   -import java.io.IOException;
8   -
9   -@Slf4j
10   -public class WebSocketUtils {
11   - public static boolean sendMessage(WebSocketSession seesion, String message) {
12   - if (seesion == null) {
13   - log.error("seesion 不存在");
14   - return false;
15   - }
16   - if (seesion.isOpen()) {
17   - try {
18   - seesion.sendMessage(new TextMessage(message));
19   - } catch (IOException e) {
20   - log.error("WebSocket 消息发送异常 Session={} | msg= {} | exception={}", seesion, message, e);
21   - return false;
22   - }
23   - }
24   - return true;
25   - }
26   -
27   - public static boolean sendMessage(Object sessionKey, String message) {
28   - WebSocketSession session = WebSocketSessionHandler.getSession(sessionKey);
29   - return sendMessage(session, message);
30   - }
31   -}
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java deleted
1   -package cn.iocoder.yudao.framework.websocket.core;
2   -
3   -import cn.iocoder.yudao.framework.security.core.LoginUser;
4   -import org.springframework.web.socket.CloseStatus;
5   -import org.springframework.web.socket.WebSocketHandler;
6   -import org.springframework.web.socket.WebSocketSession;
7   -import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
8   -
9   -public class YudaoWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
10   - public YudaoWebSocketHandlerDecorator(WebSocketHandler delegate) {
11   - super(delegate);
12   - }
13   -
14   - /**
15   - * websocket 连接时执行的动作
16   - * @param session websocket session 对象
17   - * @throws Exception 异常对象
18   - */
19   - @Override
20   - public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
21   - Object sessionKey = sessionKeyGen(session);
22   - WebSocketSessionHandler.addSession(sessionKey, session);
23   - }
24   -
25   - /**
26   - * websocket 关闭连接时执行的动作
27   - * @param session websocket session 对象
28   - * @param closeStatus 关闭状态对象
29   - * @throws Exception 异常对象
30   - */
31   - @Override
32   - public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {
33   - Object sessionKey = sessionKeyGen(session);
34   - WebSocketSessionHandler.removeSession(sessionKey);
35   - }
36   -
37   - public Object sessionKeyGen(WebSocketSession webSocketSession) {
38   -
39   - Object obj = webSocketSession.getAttributes().get(WebSocketKeyDefine.LOGIN_USER);
40   -
41   - if (obj instanceof LoginUser) {
42   - LoginUser loginUser = (LoginUser) obj;
43   - // userId 作为唯一区分
44   - return String.valueOf(loginUser.getId());
45   - }
46   -
47   - return null;
48   - }
49   -}
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.handler;
  2 +
  3 +import cn.hutool.core.util.StrUtil;
  4 +import cn.hutool.core.util.TypeUtil;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
  7 +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
  8 +import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
  9 +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
  10 +import lombok.extern.slf4j.Slf4j;
  11 +import org.springframework.web.socket.TextMessage;
  12 +import org.springframework.web.socket.WebSocketHandler;
  13 +import org.springframework.web.socket.WebSocketSession;
  14 +import org.springframework.web.socket.handler.TextWebSocketHandler;
  15 +
  16 +import java.lang.reflect.Type;
  17 +import java.util.HashMap;
  18 +import java.util.List;
  19 +import java.util.Map;
  20 +import java.util.Objects;
  21 +import java.util.function.Consumer;
  22 +
  23 +/**
  24 + * JSON 格式 {@link WebSocketHandler} 实现类
  25 + *
  26 + * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
  27 + *
  28 + * @author 芋道源码
  29 + */
  30 +@Slf4j
  31 +public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
  32 +
  33 + /**
  34 + * type 与 WebSocketMessageListener 的映射
  35 + */
  36 + private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>();
  37 +
  38 + @SuppressWarnings({"rawtypes", "unchecked"})
  39 + public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) {
  40 + listenersList.forEach((Consumer<WebSocketMessageListener>)
  41 + listener -> listeners.put(listener.getType(), listener));
  42 + }
  43 +
  44 + @Override
  45 + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
  46 + // 1.1 空消息,跳过
  47 + if (message.getPayloadLength() == 0) {
  48 + return;
  49 + }
  50 + // 1.2 ping 心跳消息,直接返回 pong 消息。
  51 + if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
  52 + session.sendMessage(new TextMessage("pong"));
  53 + return;
  54 + }
  55 +
  56 + // 2.1 解析消息
  57 + try {
  58 + JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
  59 + if (jsonMessage == null) {
  60 + log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
  61 + return;
  62 + }
  63 + if (StrUtil.isEmpty(jsonMessage.getType())) {
  64 + log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
  65 + return;
  66 + }
  67 + // 2.2 获得对应的 WebSocketMessageListener
  68 + WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());
  69 + if (messageListener == null) {
  70 + log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
  71 + return;
  72 + }
  73 + // 2.3 处理消息
  74 + Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
  75 + Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
  76 + Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
  77 + TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
  78 + } catch (Throwable ex) {
  79 + log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
  80 + }
  81 + }
  82 +
  83 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/listener/WebSocketMessageListener.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.listener;
  2 +
  3 +import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
  4 +import org.springframework.web.socket.WebSocketSession;
  5 +
  6 +/**
  7 + * WebSocket 消息监听器接口
  8 + *
  9 + * 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息
  10 + *
  11 + * @param <T> 泛型,消息类型
  12 + */
  13 +public interface WebSocketMessageListener<T> {
  14 +
  15 + /**
  16 + * 处理消息
  17 + *
  18 + * @param session Session
  19 + * @param message 消息
  20 + */
  21 + void onMessage(WebSocketSession session, T message);
  22 +
  23 + /**
  24 + * 获得消息类型
  25 + *
  26 + * @see JsonWebSocketMessage#getType()
  27 + * @return 消息类型
  28 + */
  29 + String getType();
  30 +
  31 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.message;
  2 +
  3 +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
  4 +import lombok.Data;
  5 +
  6 +import java.io.Serializable;
  7 +
  8 +/**
  9 + * JSON 格式的 WebSocket 消息帧
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Data
  14 +public class JsonWebSocketMessage implements Serializable {
  15 +
  16 + /**
  17 + * 消息类型
  18 + *
  19 + * 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类
  20 + */
  21 + private String type;
  22 + /**
  23 + * 消息内容
  24 + *
  25 + * 要求 JSON 对象
  26 + */
  27 + private String content;
  28 +
  29 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java renamed to yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/LoginUserHandshakeInterceptor.java
1   -package cn.iocoder.yudao.framework.websocket.core;
  1 +package cn.iocoder.yudao.framework.websocket.core.security;
2 2  
3 3 import cn.iocoder.yudao.framework.security.core.LoginUser;
  4 +import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
4 5 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  6 +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
5 7 import org.springframework.http.server.ServerHttpRequest;
6 8 import org.springframework.http.server.ServerHttpResponse;
7 9 import org.springframework.web.socket.WebSocketHandler;
  10 +import org.springframework.web.socket.WebSocketSession;
8 11 import org.springframework.web.socket.server.HandshakeInterceptor;
9 12  
10 13 import java.util.Map;
11 14  
12   -public class UserHandshakeInterceptor implements HandshakeInterceptor {
  15 +/**
  16 + * 登录用户的 {@link HandshakeInterceptor} 实现类
  17 + *
  18 + * 流程如下:
  19 + * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过
  20 + * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中
  21 + *
  22 + * @author 芋道源码
  23 + */
  24 +public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
  25 +
13 26 @Override
14   - public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
  27 + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
  28 + WebSocketHandler wsHandler, Map<String, Object> attributes) {
15 29 LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
16   - attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser);
  30 + if (loginUser != null) {
  31 + WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
  32 + }
17 33 return true;
18 34 }
19 35  
20 36 @Override
21   - public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
22   -
  37 + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
  38 + WebSocketHandler wsHandler, Exception exception) {
  39 + // do nothing
23 40 }
  41 +
24 42 }
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.security;
  2 +
  3 +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer;
  4 +import cn.iocoder.yudao.framework.websocket.config.WebSocketProperties;
  5 +import lombok.RequiredArgsConstructor;
  6 +import org.springframework.security.config.annotation.web.builders.HttpSecurity;
  7 +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
  8 +
  9 +/**
  10 + * WebSocket 的权限自定义
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@RequiredArgsConstructor
  15 +public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {
  16 +
  17 + private final WebSocketProperties webSocketProperties;
  18 +
  19 + @Override
  20 + public void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) {
  21 + registry.antMatchers(webSocketProperties.getPath()).permitAll();
  22 + }
  23 +
  24 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
  7 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
  8 +import lombok.RequiredArgsConstructor;
  9 +import lombok.extern.slf4j.Slf4j;
  10 +import org.springframework.web.socket.TextMessage;
  11 +import org.springframework.web.socket.WebSocketSession;
  12 +
  13 +import java.io.IOException;
  14 +import java.util.Collection;
  15 +import java.util.Collections;
  16 +import java.util.List;
  17 +
  18 +/**
  19 + * WebSocketMessageSender 实现类
  20 + *
  21 + * @author 芋道源码
  22 + */
  23 +@Slf4j
  24 +@RequiredArgsConstructor
  25 +public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender {
  26 +
  27 + private final WebSocketSessionManager sessionManager;
  28 +
  29 + @Override
  30 + public void send(Integer userType, Long userId, String messageType, String messageContent) {
  31 + send(null, userType, userId, messageType, messageContent);
  32 + }
  33 +
  34 + @Override
  35 + public void send(Integer userType, String messageType, String messageContent) {
  36 + send(null, userType, null, messageType, messageContent);
  37 + }
  38 +
  39 + @Override
  40 + public void send(String sessionId, String messageType, String messageContent) {
  41 + send(sessionId, null, null, messageType, messageContent);
  42 + }
  43 +
  44 + /**
  45 + * 发送消息
  46 + *
  47 + * @param sessionId Session 编号
  48 + * @param userType 用户类型
  49 + * @param userId 用户编号
  50 + * @param messageType 消息类型
  51 + * @param messageContent 消息内容
  52 + */
  53 + public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) {
  54 + // 1. 获得 Session 列表
  55 + List<WebSocketSession> sessions = Collections.emptyList();
  56 + if (StrUtil.isNotEmpty(sessionId)) {
  57 + WebSocketSession session = sessionManager.getSession(sessionId);
  58 + if (session != null) {
  59 + sessions = Collections.singletonList(session);
  60 + }
  61 + } else if (userType != null && userId != null) {
  62 + sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType, userId);
  63 + } else if (userType != null) {
  64 + sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType);
  65 + }
  66 + if (CollUtil.isEmpty(sessions)) {
  67 + log.info("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
  68 + sessionId, userType, userId, messageType, messageContent);
  69 + }
  70 + // 2. 执行发送
  71 + doSend(sessions, messageType, messageContent);
  72 + }
  73 +
  74 + /**
  75 + * 发送消息的具体实现
  76 + *
  77 + * @param sessions Session 列表
  78 + * @param messageType 消息类型
  79 + * @param messageContent 消息内容
  80 + */
  81 + public void doSend(Collection<WebSocketSession> sessions, String messageType, String messageContent) {
  82 + JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent);
  83 + String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化
  84 + sessions.forEach(session -> {
  85 + // 1. 各种校验,保证 Session 可以被发送
  86 + if (session == null) {
  87 + log.error("[doSend][session 为空, message({})]", message);
  88 + return;
  89 + }
  90 + if (!session.isOpen()) {
  91 + log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message);
  92 + return;
  93 + }
  94 + // 2. 执行发送
  95 + try {
  96 + session.sendMessage(new TextMessage(payload));
  97 + log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message);
  98 + } catch (IOException ex) {
  99 + log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex);
  100 + }
  101 + });
  102 + }
  103 +
  104 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/WebSocketMessageSender.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  4 +
  5 +/**
  6 + * WebSocket 消息的发送器接口
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +public interface WebSocketMessageSender {
  11 +
  12 + /**
  13 + * 发送消息给指定用户
  14 + *
  15 + * @param userType 用户类型
  16 + * @param userId 用户编号
  17 + * @param messageType 消息类型
  18 + * @param messageContent 消息内容,JSON 格式
  19 + */
  20 + void send(Integer userType, Long userId, String messageType, String messageContent);
  21 +
  22 + /**
  23 + * 发送消息给指定用户类型
  24 + *
  25 + * @param userType 用户类型
  26 + * @param messageType 消息类型
  27 + * @param messageContent 消息内容,JSON 格式
  28 + */
  29 + void send(Integer userType, String messageType, String messageContent);
  30 +
  31 + /**
  32 + * 发送消息给指定 Session
  33 + *
  34 + * @param sessionId Session 编号
  35 + * @param messageType 消息类型
  36 + * @param messageContent 消息内容,JSON 格式
  37 + */
  38 + void send(String sessionId, String messageType, String messageContent);
  39 +
  40 + default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
  41 + send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
  42 + }
  43 +
  44 + default void sendObject(Integer userType, String messageType, Object messageContent) {
  45 + send(userType, messageType, JsonUtils.toJsonString(messageContent));
  46 + }
  47 +
  48 + default void sendObject(String sessionId, String messageType, Object messageContent) {
  49 + send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
  50 + }
  51 +
  52 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * Kafka 广播 WebSocket 的消息
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +@Data
  11 +public class KafkaWebSocketMessage {
  12 +
  13 + /**
  14 + * Session 编号
  15 + */
  16 + private String sessionId;
  17 + /**
  18 + * 用户类型
  19 + */
  20 + private Integer userType;
  21 + /**
  22 + * 用户编号
  23 + */
  24 + private Long userId;
  25 +
  26 + /**
  27 + * 消息类型
  28 + */
  29 + private String messageType;
  30 + /**
  31 + * 消息内容
  32 + */
  33 + private String messageContent;
  34 +
  35 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import org.springframework.amqp.rabbit.annotation.RabbitHandler;
  5 +import org.springframework.kafka.annotation.KafkaListener;
  6 +
  7 +/**
  8 + * {@link KafkaWebSocketMessage} 广播消息的消费者,真正把消息发送出去
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@RequiredArgsConstructor
  13 +public class KafkaWebSocketMessageConsumer {
  14 +
  15 + private final KafkaWebSocketMessageSender rabbitMQWebSocketMessageSender;
  16 +
  17 + @RabbitHandler
  18 + @KafkaListener(
  19 + topics = "${yudao.websocket.sender-kafka.topic}",
  20 + // 在 Group 上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Group 不同,以达到广播消费的目的
  21 + groupId = "${yudao.websocket.sender-kafka.consumer-group}" + "-" + "#{T(java.util.UUID).randomUUID()}")
  22 + public void onMessage(KafkaWebSocketMessage message) {
  23 + rabbitMQWebSocketMessageSender.send(message.getSessionId(),
  24 + message.getUserType(), message.getUserId(),
  25 + message.getMessageType(), message.getMessageContent());
  26 + }
  27 +
  28 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.kafka;
  2 +
  3 +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
  4 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
  5 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.kafka.core.KafkaTemplate;
  8 +
  9 +import java.util.concurrent.ExecutionException;
  10 +
  11 +/**
  12 + * 基于 Kafka 的 {@link WebSocketMessageSender} 实现类
  13 + *
  14 + * @author 芋道源码
  15 + */
  16 +@Slf4j
  17 +public class KafkaWebSocketMessageSender extends AbstractWebSocketMessageSender {
  18 +
  19 + private final KafkaTemplate<Object, Object> kafkaTemplate;
  20 +
  21 + private final String topic;
  22 +
  23 + public KafkaWebSocketMessageSender(WebSocketSessionManager sessionManager,
  24 + KafkaTemplate<Object, Object> kafkaTemplate,
  25 + String topic) {
  26 + super(sessionManager);
  27 + this.kafkaTemplate = kafkaTemplate;
  28 + this.topic = topic;
  29 + }
  30 +
  31 + @Override
  32 + public void send(Integer userType, Long userId, String messageType, String messageContent) {
  33 + sendKafkaMessage(null, userId, userType, messageType, messageContent);
  34 + }
  35 +
  36 + @Override
  37 + public void send(Integer userType, String messageType, String messageContent) {
  38 + sendKafkaMessage(null, null, userType, messageType, messageContent);
  39 + }
  40 +
  41 + @Override
  42 + public void send(String sessionId, String messageType, String messageContent) {
  43 + sendKafkaMessage(sessionId, null, null, messageType, messageContent);
  44 + }
  45 +
  46 + /**
  47 + * 通过 Kafka 广播消息
  48 + *
  49 + * @param sessionId Session 编号
  50 + * @param userId 用户编号
  51 + * @param userType 用户类型
  52 + * @param messageType 消息类型
  53 + * @param messageContent 消息内容
  54 + */
  55 + private void sendKafkaMessage(String sessionId, Long userId, Integer userType,
  56 + String messageType, String messageContent) {
  57 + KafkaWebSocketMessage mqMessage = new KafkaWebSocketMessage()
  58 + .setSessionId(sessionId).setUserId(userId).setUserType(userType)
  59 + .setMessageType(messageType).setMessageContent(messageContent);
  60 + try {
  61 + kafkaTemplate.send(topic, mqMessage).get();
  62 + } catch (InterruptedException | ExecutionException e) {
  63 + log.error("[sendKafkaMessage][发送消息({}) 到 Kafka 失败]", mqMessage, e);
  64 + }
  65 + }
  66 +
  67 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.local;
  2 +
  3 +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
  4 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
  5 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
  6 +
  7 +/**
  8 + * 本地的 {@link WebSocketMessageSender} 实现类
  9 + *
  10 + * 注意:仅仅适合单机场景!!!
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender {
  15 +
  16 + public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) {
  17 + super(sessionManager);
  18 + }
  19 +
  20 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
  2 +
  3 +import lombok.Data;
  4 +
  5 +import java.io.Serializable;
  6 +
  7 +/**
  8 + * RabbitMQ 广播 WebSocket 的消息
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@Data
  13 +public class RabbitMQWebSocketMessage implements Serializable {
  14 +
  15 + /**
  16 + * Session 编号
  17 + */
  18 + private String sessionId;
  19 + /**
  20 + * 用户类型
  21 + */
  22 + private Integer userType;
  23 + /**
  24 + * 用户编号
  25 + */
  26 + private Long userId;
  27 +
  28 + /**
  29 + * 消息类型
  30 + */
  31 + private String messageType;
  32 + /**
  33 + * 消息内容
  34 + */
  35 + private String messageContent;
  36 +
  37 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import org.springframework.amqp.core.ExchangeTypes;
  5 +import org.springframework.amqp.rabbit.annotation.*;
  6 +
  7 +/**
  8 + * {@link RabbitMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +@RabbitListener(
  13 + bindings = @QueueBinding(
  14 + value = @Queue(
  15 + // 在 Queue 的名字上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Queue 不同,以达到广播消费的目的
  16 + name = "${yudao.websocket.sender-rabbitmq.queue}" + "-" + "#{T(java.util.UUID).randomUUID()}",
  17 + // Consumer 关闭时,该队列就可以被自动删除了
  18 + autoDelete = "true"
  19 + ),
  20 + exchange = @Exchange(
  21 + name = "${yudao.websocket.sender-rabbitmq.exchange}",
  22 + type = ExchangeTypes.TOPIC,
  23 + declare = "false"
  24 + )
  25 + )
  26 +)
  27 +@RequiredArgsConstructor
  28 +public class RabbitMQWebSocketMessageConsumer {
  29 +
  30 + private final RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender;
  31 +
  32 + @RabbitHandler
  33 + public void onMessage(RabbitMQWebSocketMessage message) {
  34 + rabbitMQWebSocketMessageSender.send(message.getSessionId(),
  35 + message.getUserType(), message.getUserId(),
  36 + message.getMessageType(), message.getMessageContent());
  37 + }
  38 +
  39 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq;
  2 +
  3 +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
  4 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
  5 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.springframework.amqp.core.TopicExchange;
  8 +import org.springframework.amqp.rabbit.core.RabbitTemplate;
  9 +
  10 +/**
  11 + * 基于 RabbitMQ 的 {@link WebSocketMessageSender} 实现类
  12 + *
  13 + * @author 芋道源码
  14 + */
  15 +@Slf4j
  16 +public class RabbitMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
  17 +
  18 + private final RabbitTemplate rabbitTemplate;
  19 +
  20 + private final TopicExchange topicExchange;
  21 +
  22 + public RabbitMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
  23 + RabbitTemplate rabbitTemplate,
  24 + TopicExchange topicExchange) {
  25 + super(sessionManager);
  26 + this.rabbitTemplate = rabbitTemplate;
  27 + this.topicExchange = topicExchange;
  28 + }
  29 +
  30 + @Override
  31 + public void send(Integer userType, Long userId, String messageType, String messageContent) {
  32 + sendRabbitMQMessage(null, userId, userType, messageType, messageContent);
  33 + }
  34 +
  35 + @Override
  36 + public void send(Integer userType, String messageType, String messageContent) {
  37 + sendRabbitMQMessage(null, null, userType, messageType, messageContent);
  38 + }
  39 +
  40 + @Override
  41 + public void send(String sessionId, String messageType, String messageContent) {
  42 + sendRabbitMQMessage(sessionId, null, null, messageType, messageContent);
  43 + }
  44 +
  45 + /**
  46 + * 通过 RabbitMQ 广播消息
  47 + *
  48 + * @param sessionId Session 编号
  49 + * @param userId 用户编号
  50 + * @param userType 用户类型
  51 + * @param messageType 消息类型
  52 + * @param messageContent 消息内容
  53 + */
  54 + private void sendRabbitMQMessage(String sessionId, Long userId, Integer userType,
  55 + String messageType, String messageContent) {
  56 + RabbitMQWebSocketMessage mqMessage = new RabbitMQWebSocketMessage()
  57 + .setSessionId(sessionId).setUserId(userId).setUserType(userType)
  58 + .setMessageType(messageType).setMessageContent(messageContent);
  59 + rabbitTemplate.convertAndSend(topicExchange.getName(), null, mqMessage);
  60 + }
  61 +
  62 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.redis;
  2 +
  3 +import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage;
  4 +import lombok.Data;
  5 +
  6 +/**
  7 + * Redis 广播 WebSocket 的消息
  8 + */
  9 +@Data
  10 +public class RedisWebSocketMessage extends AbstractRedisChannelMessage {
  11 +
  12 + /**
  13 + * Session 编号
  14 + */
  15 + private String sessionId;
  16 + /**
  17 + * 用户类型
  18 + */
  19 + private Integer userType;
  20 + /**
  21 + * 用户编号
  22 + */
  23 + private Long userId;
  24 +
  25 + /**
  26 + * 消息类型
  27 + */
  28 + private String messageType;
  29 + /**
  30 + * 消息内容
  31 + */
  32 + private String messageContent;
  33 +
  34 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.redis;
  2 +
  3 +import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener;
  4 +import lombok.RequiredArgsConstructor;
  5 +
  6 +/**
  7 + * {@link RedisWebSocketMessage} 广播消息的消费者,真正把消息发送出去
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@RequiredArgsConstructor
  12 +public class RedisWebSocketMessageConsumer extends AbstractRedisChannelMessageListener<RedisWebSocketMessage> {
  13 +
  14 + private final RedisWebSocketMessageSender redisWebSocketMessageSender;
  15 +
  16 + @Override
  17 + public void onMessage(RedisWebSocketMessage message) {
  18 + redisWebSocketMessageSender.send(message.getSessionId(),
  19 + message.getUserType(), message.getUserId(),
  20 + message.getMessageType(), message.getMessageContent());
  21 + }
  22 +
  23 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.redis;
  2 +
  3 +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
  4 +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
  5 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
  6 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
  7 +import lombok.extern.slf4j.Slf4j;
  8 +
  9 +/**
  10 + * 基于 Redis 的 {@link WebSocketMessageSender} 实现类
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender {
  16 +
  17 + private final RedisMQTemplate redisMQTemplate;
  18 +
  19 + public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager,
  20 + RedisMQTemplate redisMQTemplate) {
  21 + super(sessionManager);
  22 + this.redisMQTemplate = redisMQTemplate;
  23 + }
  24 +
  25 + @Override
  26 + public void send(Integer userType, Long userId, String messageType, String messageContent) {
  27 + sendRedisMessage(null, userId, userType, messageType, messageContent);
  28 + }
  29 +
  30 + @Override
  31 + public void send(Integer userType, String messageType, String messageContent) {
  32 + sendRedisMessage(null, null, userType, messageType, messageContent);
  33 + }
  34 +
  35 + @Override
  36 + public void send(String sessionId, String messageType, String messageContent) {
  37 + sendRedisMessage(sessionId, null, null, messageType, messageContent);
  38 + }
  39 +
  40 + /**
  41 + * 通过 Redis 广播消息
  42 + *
  43 + * @param sessionId Session 编号
  44 + * @param userId 用户编号
  45 + * @param userType 用户类型
  46 + * @param messageType 消息类型
  47 + * @param messageContent 消息内容
  48 + */
  49 + private void sendRedisMessage(String sessionId, Long userId, Integer userType,
  50 + String messageType, String messageContent) {
  51 + RedisWebSocketMessage mqMessage = new RedisWebSocketMessage()
  52 + .setSessionId(sessionId).setUserId(userId).setUserType(userType)
  53 + .setMessageType(messageType).setMessageContent(messageContent);
  54 + redisMQTemplate.send(mqMessage);
  55 + }
  56 +
  57 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * RocketMQ 广播 WebSocket 的消息
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +@Data
  11 +public class RocketMQWebSocketMessage {
  12 +
  13 + /**
  14 + * Session 编号
  15 + */
  16 + private String sessionId;
  17 + /**
  18 + * 用户类型
  19 + */
  20 + private Integer userType;
  21 + /**
  22 + * 用户编号
  23 + */
  24 + private Long userId;
  25 +
  26 + /**
  27 + * 消息类型
  28 + */
  29 + private String messageType;
  30 + /**
  31 + * 消息内容
  32 + */
  33 + private String messageContent;
  34 +
  35 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
  2 +
  3 +import lombok.RequiredArgsConstructor;
  4 +import org.apache.rocketmq.spring.annotation.MessageModel;
  5 +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
  6 +import org.apache.rocketmq.spring.core.RocketMQListener;
  7 +
  8 +/**
  9 + * {@link RocketMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@RocketMQMessageListener( // 重点:添加 @RocketMQMessageListener 注解,声明消费的 topic
  14 + topic = "${yudao.websocket.sender-rocketmq.topic}",
  15 + consumerGroup = "${yudao.websocket.sender-rocketmq.consumer-group}",
  16 + messageModel = MessageModel.BROADCASTING // 设置为广播模式,保证每个实例都能收到消息
  17 +)
  18 +@RequiredArgsConstructor
  19 +public class RocketMQWebSocketMessageConsumer implements RocketMQListener<RocketMQWebSocketMessage> {
  20 +
  21 + private final RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender;
  22 +
  23 + @Override
  24 + public void onMessage(RocketMQWebSocketMessage message) {
  25 + rocketMQWebSocketMessageSender.send(message.getSessionId(),
  26 + message.getUserType(), message.getUserId(),
  27 + message.getMessageType(), message.getMessageContent());
  28 + }
  29 +
  30 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq;
  2 +
  3 +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender;
  4 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
  5 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
  6 +import lombok.extern.slf4j.Slf4j;
  7 +import org.apache.rocketmq.spring.core.RocketMQTemplate;
  8 +
  9 +/**
  10 + * 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +@Slf4j
  15 +public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender {
  16 +
  17 + private final RocketMQTemplate rocketMQTemplate;
  18 +
  19 + private final String topic;
  20 +
  21 + public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager,
  22 + RocketMQTemplate rocketMQTemplate,
  23 + String topic) {
  24 + super(sessionManager);
  25 + this.rocketMQTemplate = rocketMQTemplate;
  26 + this.topic = topic;
  27 + }
  28 +
  29 + @Override
  30 + public void send(Integer userType, Long userId, String messageType, String messageContent) {
  31 + sendRocketMQMessage(null, userId, userType, messageType, messageContent);
  32 + }
  33 +
  34 + @Override
  35 + public void send(Integer userType, String messageType, String messageContent) {
  36 + sendRocketMQMessage(null, null, userType, messageType, messageContent);
  37 + }
  38 +
  39 + @Override
  40 + public void send(String sessionId, String messageType, String messageContent) {
  41 + sendRocketMQMessage(sessionId, null, null, messageType, messageContent);
  42 + }
  43 +
  44 + /**
  45 + * 通过 RocketMQ 广播消息
  46 + *
  47 + * @param sessionId Session 编号
  48 + * @param userId 用户编号
  49 + * @param userType 用户类型
  50 + * @param messageType 消息类型
  51 + * @param messageContent 消息内容
  52 + */
  53 + private void sendRocketMQMessage(String sessionId, Long userId, Integer userType,
  54 + String messageType, String messageContent) {
  55 + RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage()
  56 + .setSessionId(sessionId).setUserId(userId).setUserType(userType)
  57 + .setMessageType(messageType).setMessageContent(messageContent);
  58 + rocketMQTemplate.syncSend(topic, mqMessage);
  59 + }
  60 +
  61 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.session;
  2 +
  3 +import org.springframework.web.socket.CloseStatus;
  4 +import org.springframework.web.socket.WebSocketHandler;
  5 +import org.springframework.web.socket.WebSocketSession;
  6 +import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
  7 +import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
  8 +
  9 +/**
  10 + * {@link WebSocketHandler} 的装饰类,实现了以下功能:
  11 + *
  12 + * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理
  13 + * 2. 封装 {@link WebSocketSession} 支持并发操作
  14 + *
  15 + * @author 芋道源码
  16 + */
  17 +public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator {
  18 +
  19 + /**
  20 + * 发送时间的限制,单位:毫秒
  21 + */
  22 + private static final Integer SEND_TIME_LIMIT = 1000 * 5;
  23 + /**
  24 + * 发送消息缓冲上线,单位:bytes
  25 + */
  26 + private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100;
  27 +
  28 + private final WebSocketSessionManager sessionManager;
  29 +
  30 + public WebSocketSessionHandlerDecorator(WebSocketHandler delegate,
  31 + WebSocketSessionManager sessionManager) {
  32 + super(delegate);
  33 + this.sessionManager = sessionManager;
  34 + }
  35 +
  36 + @Override
  37 + public void afterConnectionEstablished(WebSocketSession session) {
  38 + // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149
  39 + session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT);
  40 + // 添加到 WebSocketSessionManager 中
  41 + sessionManager.addSession(session);
  42 + }
  43 +
  44 + @Override
  45 + public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
  46 + sessionManager.removeSession(session);
  47 + }
  48 +
  49 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManager.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.session;
  2 +
  3 +import org.springframework.web.socket.WebSocketSession;
  4 +
  5 +import java.util.Collection;
  6 +
  7 +/**
  8 + * {@link WebSocketSession} 管理器的接口
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +public interface WebSocketSessionManager {
  13 +
  14 + /**
  15 + * 添加 Session
  16 + *
  17 + * @param session Session
  18 + */
  19 + void addSession(WebSocketSession session);
  20 +
  21 + /**
  22 + * 移除 Session
  23 + *
  24 + * @param session Session
  25 + */
  26 + void removeSession(WebSocketSession session);
  27 +
  28 + /**
  29 + * 获得指定编号的 Session
  30 + *
  31 + * @param id Session 编号
  32 + * @return Session
  33 + */
  34 + WebSocketSession getSession(String id);
  35 +
  36 + /**
  37 + * 获得指定用户类型的 Session 列表
  38 + *
  39 + * @param userType 用户类型
  40 + * @return Session 列表
  41 + */
  42 + Collection<WebSocketSession> getSessionList(Integer userType);
  43 +
  44 + /**
  45 + * 获得指定用户编号的 Session 列表
  46 + *
  47 + * @param userType 用户类型
  48 + * @param userId 用户编号
  49 + * @return Session 列表
  50 + */
  51 + Collection<WebSocketSession> getSessionList(Integer userType, Long userId);
  52 +
  53 +}
0 54 \ No newline at end of file
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.session;
  2 +
  3 +import cn.hutool.core.collection.CollUtil;
  4 +import cn.iocoder.yudao.framework.security.core.LoginUser;
  5 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
  6 +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
  7 +import org.springframework.web.socket.WebSocketSession;
  8 +
  9 +import java.util.ArrayList;
  10 +import java.util.Collection;
  11 +import java.util.LinkedList;
  12 +import java.util.List;
  13 +import java.util.concurrent.ConcurrentHashMap;
  14 +import java.util.concurrent.ConcurrentMap;
  15 +import java.util.concurrent.CopyOnWriteArrayList;
  16 +
  17 +/**
  18 + * 默认的 {@link WebSocketSessionManager} 实现类
  19 + *
  20 + * @author 芋道源码
  21 + */
  22 +public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
  23 +
  24 + /**
  25 + * id 与 WebSocketSession 映射
  26 + *
  27 + * key:Session 编号
  28 + */
  29 + private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>();
  30 +
  31 + /**
  32 + * user 与 WebSocketSession 映射
  33 + *
  34 + * key1:用户类型
  35 + * key2:用户编号
  36 + */
  37 + private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions
  38 + = new ConcurrentHashMap<>();
  39 +
  40 + @Override
  41 + public void addSession(WebSocketSession session) {
  42 + // 添加到 idSessions 中
  43 + idSessions.put(session.getId(), session);
  44 + // 添加到 userSessions 中
  45 + LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
  46 + if (user == null) {
  47 + return;
  48 + }
  49 + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
  50 + if (userSessionsMap == null) {
  51 + userSessionsMap = new ConcurrentHashMap<>();
  52 + if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) {
  53 + userSessionsMap = userSessions.get(user.getUserType());
  54 + }
  55 + }
  56 + CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
  57 + if (sessions == null) {
  58 + sessions = new CopyOnWriteArrayList<>();
  59 + if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) {
  60 + sessions = userSessionsMap.get(user.getId());
  61 + }
  62 + }
  63 + sessions.add(session);
  64 + }
  65 +
  66 + @Override
  67 + public void removeSession(WebSocketSession session) {
  68 + // 移除从 idSessions 中
  69 + idSessions.remove(session.getId(), session);
  70 + // 移除从 idSessions 中
  71 + LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
  72 + if (user == null) {
  73 + return;
  74 + }
  75 + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
  76 + if (userSessionsMap == null) {
  77 + return;
  78 + }
  79 + CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
  80 + sessions.removeIf(session0 -> session0.getId().equals(session.getId()));
  81 + if (CollUtil.isEmpty(sessions)) {
  82 + userSessionsMap.remove(user.getId(), sessions);
  83 + }
  84 + }
  85 +
  86 + @Override
  87 + public WebSocketSession getSession(String id) {
  88 + return idSessions.get(id);
  89 + }
  90 +
  91 + @Override
  92 + public Collection<WebSocketSession> getSessionList(Integer userType) {
  93 + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
  94 + if (CollUtil.isEmpty(userSessionsMap)) {
  95 + return new ArrayList<>();
  96 + }
  97 + LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容
  98 + Long contextTenantId = TenantContextHolder.getTenantId();
  99 + for (List<WebSocketSession> sessions : userSessionsMap.values()) {
  100 + if (CollUtil.isEmpty(sessions)) {
  101 + continue;
  102 + }
  103 + // 特殊:如果租户不匹配,则直接排除
  104 + if (contextTenantId != null) {
  105 + Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0));
  106 + if (!contextTenantId.equals(userTenantId)) {
  107 + continue;
  108 + }
  109 + }
  110 + result.addAll(sessions);
  111 + }
  112 + return result;
  113 + }
  114 +
  115 + @Override
  116 + public Collection<WebSocketSession> getSessionList(Integer userType, Long userId) {
  117 + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
  118 + if (CollUtil.isEmpty(userSessionsMap)) {
  119 + return new ArrayList<>();
  120 + }
  121 + CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(userId);
  122 + return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>();
  123 + }
  124 +
  125 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.websocket.core.util;
  2 +
  3 +import cn.iocoder.yudao.framework.security.core.LoginUser;
  4 +import org.springframework.web.socket.WebSocketSession;
  5 +
  6 +import java.util.Map;
  7 +
  8 +/**
  9 + * 专属于 web 包的工具类
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +public class WebSocketFrameworkUtils {
  14 +
  15 + public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER";
  16 +
  17 + /**
  18 + * 设置当前用户
  19 + *
  20 + * @param loginUser 登录用户
  21 + * @param attributes Session
  22 + */
  23 + public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) {
  24 + attributes.put(ATTRIBUTE_LOGIN_USER, loginUser);
  25 + }
  26 +
  27 + /**
  28 + * 获取当前用户
  29 + *
  30 + * @return 当前用户
  31 + */
  32 + public static LoginUser getLoginUser(WebSocketSession session) {
  33 + return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER);
  34 + }
  35 +
  36 + /**
  37 + * 获得当前用户的编号
  38 + *
  39 + * @return 用户编号
  40 + */
  41 + public static Long getLoginUserId(WebSocketSession session) {
  42 + LoginUser loginUser = getLoginUser(session);
  43 + return loginUser != null ? loginUser.getId() : null;
  44 + }
  45 +
  46 + /**
  47 + * 获得当前用户的类型
  48 + *
  49 + * @return 用户编号
  50 + */
  51 + public static Integer getLoginUserType(WebSocketSession session) {
  52 + LoginUser loginUser = getLoginUser(session);
  53 + return loginUser != null ? loginUser.getUserType() : null;
  54 + }
  55 +
  56 + /**
  57 + * 获得当前用户的租户编号
  58 + *
  59 + * @param session Session
  60 + * @return 租户编号
  61 + */
  62 + public static Long getTenantId(WebSocketSession session) {
  63 + LoginUser loginUser = getLoginUser(session);
  64 + return loginUser != null ? loginUser.getTenantId() : null;
  65 + }
  66 +
  67 +}
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java
  1 +/**
  2 + * WebSocket 框架,支持多节点的广播
  3 + */
1 4 package cn.iocoder.yudao.framework.websocket;
... ...
yudao-framework/yudao-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md 0 → 100644
  1 +<http://www.iocoder.cn/Spring-Boot/WebSocket/?yudao>
... ...
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApi.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.api.websocket;
  2 +
  3 +import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  4 +
  5 +/**
  6 + * WebSocket 发送器的 API 接口
  7 + *
  8 + * 对 WebSocketMessageSender 进行封装,提供给其它模块使用
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +public interface WebSocketSenderApi {
  13 +
  14 + /**
  15 + * 发送消息给指定用户
  16 + *
  17 + * @param userType 用户类型
  18 + * @param userId 用户编号
  19 + * @param messageType 消息类型
  20 + * @param messageContent 消息内容,JSON 格式
  21 + */
  22 + void send(Integer userType, Long userId, String messageType, String messageContent);
  23 +
  24 + /**
  25 + * 发送消息给指定用户类型
  26 + *
  27 + * @param userType 用户类型
  28 + * @param messageType 消息类型
  29 + * @param messageContent 消息内容,JSON 格式
  30 + */
  31 + void send(Integer userType, String messageType, String messageContent);
  32 +
  33 + /**
  34 + * 发送消息给指定 Session
  35 + *
  36 + * @param sessionId Session 编号
  37 + * @param messageType 消息类型
  38 + * @param messageContent 消息内容,JSON 格式
  39 + */
  40 + void send(String sessionId, String messageType, String messageContent);
  41 +
  42 + default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
  43 + send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
  44 + }
  45 +
  46 + default void sendObject(Integer userType, String messageType, Object messageContent) {
  47 + send(userType, messageType, JsonUtils.toJsonString(messageContent));
  48 + }
  49 +
  50 + default void sendObject(String sessionId, String messageType, Object messageContent) {
  51 + send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
  52 + }
  53 +
  54 +}
... ...
yudao-module-infra/yudao-module-infra-biz/pom.xml
... ... @@ -46,6 +46,11 @@
46 46 <artifactId>yudao-spring-boot-starter-security</artifactId>
47 47 </dependency>
48 48  
  49 + <dependency>
  50 + <groupId>cn.iocoder.boot</groupId>
  51 + <artifactId>yudao-spring-boot-starter-websocket</artifactId>
  52 + </dependency>
  53 +
49 54 <!-- DB 相关 -->
50 55 <dependency>
51 56 <groupId>cn.iocoder.boot</groupId>
... ... @@ -116,11 +121,6 @@
116 121 <artifactId>yudao-spring-boot-starter-file</artifactId>
117 122 </dependency>
118 123  
119   - <!-- WebSocket -->
120   - <dependency>
121   - <groupId>org.springframework.boot</groupId>
122   - <artifactId>spring-boot-starter-websocket</artifactId>
123   - </dependency>
124 124 </dependencies>
125 125  
126 126 </project>
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.api.websocket;
  2 +
  3 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
  4 +import org.springframework.stereotype.Component;
  5 +
  6 +import javax.annotation.Resource;
  7 +
  8 +/**
  9 + * WebSocket 发送器的 API 实现类
  10 + *
  11 + * @author 芋道源码
  12 + */
  13 +@Component
  14 +public class WebSocketSenderApiImpl implements WebSocketSenderApi {
  15 +
  16 + @Resource
  17 + private WebSocketMessageSender webSocketMessageSender;
  18 +
  19 + @Override
  20 + public void send(Integer userType, Long userId, String messageType, String messageContent) {
  21 + webSocketMessageSender.send(userType, userId, messageType, messageContent);
  22 + }
  23 +
  24 + @Override
  25 + public void send(Integer userType, String messageType, String messageContent) {
  26 + webSocketMessageSender.send(userType, messageType, messageContent);
  27 + }
  28 +
  29 + @Override
  30 + public void send(String sessionId, String messageType, String messageContent) {
  31 + webSocketMessageSender.send(sessionId, messageType, messageContent);
  32 + }
  33 +
  34 +}
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.websocket;
  2 +
  3 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
  4 +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
  5 +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
  6 +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
  7 +import cn.iocoder.yudao.module.infra.websocket.message.DemoReceiveMessage;
  8 +import cn.iocoder.yudao.module.infra.websocket.message.DemoSendMessage;
  9 +import org.springframework.stereotype.Component;
  10 +import org.springframework.web.socket.WebSocketSession;
  11 +
  12 +import javax.annotation.Resource;
  13 +
  14 +/**
  15 + * WebSocket 示例:单发消息
  16 + *
  17 + * @author 芋道源码
  18 + */
  19 +@Component
  20 +public class DemoWebSocketMessageListener implements WebSocketMessageListener<DemoSendMessage> {
  21 +
  22 + @Resource
  23 + private WebSocketMessageSender webSocketMessageSender;
  24 +
  25 + @Override
  26 + public void onMessage(WebSocketSession session, DemoSendMessage message) {
  27 + Long fromUserId = WebSocketFrameworkUtils.getLoginUserId(session);
  28 + // 情况一:单发
  29 + if (message.getToUserId() != null) {
  30 + DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId)
  31 + .setText(message.getText()).setSingle(true);
  32 + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getToUserId(), // 给指定用户
  33 + "demo-message-receive", toMessage);
  34 + return;
  35 + }
  36 + // 情况二:群发
  37 + DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId)
  38 + .setText(message.getText()).setSingle(false);
  39 + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), // 给所有用户
  40 + "demo-message-receive", toMessage);
  41 + }
  42 +
  43 + @Override
  44 + public String getType() {
  45 + return "demo-message-send";
  46 + }
  47 +
  48 +}
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/SemaphoreUtils.java deleted
1   -package cn.iocoder.yudao.module.infra.websocket;
2   -
3   -import lombok.extern.slf4j.Slf4j;
4   -
5   -import java.util.concurrent.Semaphore;
6   -
7   -/**
8   - * 信号量相关处理
9   - *
10   - */
11   -@Slf4j
12   -public class SemaphoreUtils {
13   -
14   - /**
15   - * 获取信号量
16   - *
17   - * @param semaphore
18   - * @return
19   - */
20   - public static boolean tryAcquire(Semaphore semaphore) {
21   - boolean flag = false;
22   -
23   - try {
24   - flag = semaphore.tryAcquire();
25   - } catch (Exception e) {
26   - log.error("获取信号量异常", e);
27   - }
28   -
29   - return flag;
30   - }
31   -
32   - /**
33   - * 释放信号量
34   - *
35   - * @param semaphore
36   - */
37   - public static void release(Semaphore semaphore) {
38   -
39   - try {
40   - semaphore.release();
41   - } catch (Exception e) {
42   - log.error("释放信号量异常", e);
43   - }
44   - }
45   -}
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketConfig.java deleted
1   -package cn.iocoder.yudao.module.infra.websocket;
2   -
3   -import org.springframework.context.annotation.Bean;
4   -import org.springframework.context.annotation.Configuration;
5   -import org.springframework.web.socket.server.standard.ServerEndpointExporter;
6   -
7   -/**
8   - * websocket 配置
9   - */
10   -@Configuration
11   -public class WebSocketConfig {
12   - @Bean
13   - public ServerEndpointExporter serverEndpointExporter() {
14   - return new ServerEndpointExporter();
15   - }
16   -}
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketServer.java deleted
1   -package cn.iocoder.yudao.module.infra.websocket;
2   -
3   -import lombok.extern.slf4j.Slf4j;
4   -import org.springframework.stereotype.Component;
5   -
6   -import javax.websocket.*;
7   -import javax.websocket.server.ServerEndpoint;
8   -import java.util.concurrent.Semaphore;
9   -
10   -/**
11   - * websocket 消息处理
12   - */
13   -@Component
14   -@ServerEndpoint("/websocket/message")
15   -@Slf4j
16   -public class WebSocketServer {
17   -
18   - /**
19   - * 默认最多允许同时在线用户数100
20   - */
21   - public static int socketMaxOnlineCount = 100;
22   -
23   - private static final Semaphore SOCKET_SEMAPHORE = new Semaphore(socketMaxOnlineCount);
24   -
25   - /**
26   - * 连接建立成功调用的方法
27   - */
28   - @OnOpen
29   - public void onOpen(Session session) throws Exception {
30   - // 尝试获取信号量
31   - boolean semaphoreFlag = SemaphoreUtils.tryAcquire(SOCKET_SEMAPHORE);
32   - if (!semaphoreFlag) {
33   - // 未获取到信号量
34   - log.error("当前在线人数超过限制数:{}", socketMaxOnlineCount);
35   - WebSocketUsers.sendMessage(session, "当前在线人数超过限制数:" + socketMaxOnlineCount);
36   - session.close();
37   - } else {
38   - String userId = WebSocketUsers.getParam("userId", session);
39   - if (userId != null) {
40   - // 添加用户
41   - WebSocketUsers.addSession(userId, session);
42   - log.info("用户【userId={}】建立连接,当前连接用户总数:{}", userId, WebSocketUsers.getUsers().size());
43   - WebSocketUsers.sendMessage(session, "接收内容:连接成功");
44   - } else {
45   - WebSocketUsers.sendMessage(session, "接收内容:连接失败");
46   - }
47   - }
48   - }
49   -
50   - /**
51   - * 连接关闭时处理
52   - */
53   - @OnClose
54   - public void onClose(Session session) {
55   - log.info("用户【sessionId={}】关闭连接!", session.getId());
56   - // 移除用户
57   - WebSocketUsers.removeSession(session);
58   - // 获取到信号量则需释放
59   - SemaphoreUtils.release(SOCKET_SEMAPHORE);
60   - }
61   -
62   - /**
63   - * 抛出异常时处理
64   - */
65   - @OnError
66   - public void onError(Session session, Throwable exception) throws Exception {
67   - if (session.isOpen()) {
68   - // 关闭连接
69   - session.close();
70   - }
71   - String sessionId = session.getId();
72   - log.info("用户【sessionId={}】连接异常!异常信息:{}", sessionId, exception);
73   - // 移出用户
74   - WebSocketUsers.removeSession(session);
75   - // 获取到信号量则需释放
76   - SemaphoreUtils.release(SOCKET_SEMAPHORE);
77   - }
78   -
79   - /**
80   - * 收到客户端消息时调用的方法
81   - */
82   - @OnMessage
83   - public void onMessage(Session session, String message) {
84   - WebSocketUsers.sendMessage(session, "接收内容:" + message);
85   - }
86   -}
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketUsers.java deleted
1   -package cn.iocoder.yudao.module.infra.websocket;
2   -
3   -import cn.hutool.core.map.MapUtil;
4   -import cn.hutool.core.util.StrUtil;
5   -import lombok.extern.slf4j.Slf4j;
6   -import org.bouncycastle.util.Strings;
7   -
8   -import javax.validation.constraints.NotNull;
9   -import javax.websocket.Session;
10   -import java.io.IOException;
11   -import java.util.List;
12   -import java.util.Map;
13   -import java.util.Set;
14   -import java.util.concurrent.ConcurrentHashMap;
15   -
16   -/**
17   - * websocket 客户端用户
18   - */
19   -@Slf4j
20   -public class WebSocketUsers {
21   -
22   - /**
23   - * 用户集
24   - * TODO 需要登录用户的session?
25   - */
26   - private static final Map<String, Session> SESSION_MAP = new ConcurrentHashMap<>();
27   -
28   - /**
29   - * 存储用户
30   - *
31   - * @param userId 唯一键
32   - * @param session 用户信息
33   - */
34   - public static void addSession(String userId, Session session) {
35   - SESSION_MAP.put(userId, session);
36   - }
37   -
38   - /**
39   - * 移除用户
40   - *
41   - * @param session 用户信息
42   - * @return 移除结果
43   - */
44   - public static boolean removeSession(Session session) {
45   - String key = null;
46   - boolean flag = SESSION_MAP.containsValue(session);
47   - if (flag) {
48   - Set<Map.Entry<String, Session>> entries = SESSION_MAP.entrySet();
49   - for (Map.Entry<String, Session> entry : entries) {
50   - Session value = entry.getValue();
51   - if (value.equals(session)) {
52   - key = entry.getKey();
53   - break;
54   - }
55   - }
56   - } else {
57   - return true;
58   - }
59   - return removeSession(key);
60   - }
61   -
62   - /**
63   - * 移出用户
64   - *
65   - * @param userId 用户id
66   - */
67   - public static boolean removeSession(String userId) {
68   - log.info("用户【userId={}】退出", userId);
69   - Session remove = SESSION_MAP.remove(userId);
70   - if (remove != null) {
71   - boolean containsValue = SESSION_MAP.containsValue(remove);
72   - log.info("用户【userId={}】退出{},当前连接用户总数:{}", userId, containsValue ? "失败" : "成功", SESSION_MAP.size());
73   - return containsValue;
74   - } else {
75   - return true;
76   - }
77   - }
78   -
79   - /**
80   - * 获取在线用户列表
81   - *
82   - * @return 返回用户集合
83   - */
84   - public static Map<String, Session> getUsers() {
85   - return SESSION_MAP;
86   - }
87   -
88   - /**
89   - * 向所有在线人发送消息
90   - *
91   - * @param message 消息内容
92   - */
93   - public static void sendMessageToAll(String message) {
94   - SESSION_MAP.forEach((userId, session) -> {
95   - if (session.isOpen()) {
96   - sendMessage(session, message);
97   - }
98   - });
99   - }
100   -
101   - /**
102   - * 异步发送文本消息
103   - *
104   - * @param session 用户session
105   - * @param message 消息内容
106   - */
107   - public static void sendMessageAsync(Session session, String message) {
108   - if (session.isOpen()) {
109   - // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程?
110   - session.getAsyncRemote().sendText(message);
111   - } else {
112   - log.warn("用户【session={}】不在线", session.getId());
113   - }
114   - }
115   -
116   - /**
117   - * 同步发送文本消息
118   - *
119   - * @param session 用户session
120   - * @param message 消息内容
121   - */
122   - public static void sendMessage(Session session, String message) {
123   - try {
124   - if (session.isOpen()) {
125   - // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程?
126   - session.getBasicRemote().sendText(message);
127   - } else {
128   - log.warn("用户【session={}】不在线", session.getId());
129   - }
130   - } catch (IOException e) {
131   - log.error("发送消息异常", e);
132   - }
133   -
134   - }
135   -
136   - /**
137   - * 根据用户id发送消息
138   - *
139   - * @param userId 用户id
140   - * @param message 消息内容
141   - */
142   - public static void sendMessage(String userId, String message) {
143   - Session session = SESSION_MAP.get(userId);
144   - //判断是否存在该用户的session,并且是否在线
145   - if (session == null || !session.isOpen()) {
146   - return;
147   - }
148   - sendMessage(session, message);
149   - }
150   -
151   -
152   - /**
153   - * 获取session中的指定参数值
154   - *
155   - * @param key 参数key
156   - * @param session 用户session
157   - */
158   - public static String getParam(@NotNull String key, Session session) {
159   - //TODO 目前只针对获取一个key的值,后期根据情况拓展多个 或者直接在onClose onOpen上获取参数?
160   - String value = null;
161   - Map<String, List<String>> parameters = session.getRequestParameterMap();
162   - if (MapUtil.isNotEmpty(parameters)) {
163   - value = parameters.get(key).get(0);
164   - } else {
165   - String queryString = session.getQueryString();
166   - if (!StrUtil.isEmpty(queryString)) {
167   - String[] params = Strings.split(queryString, '&');
168   - for (String paramPair : params) {
169   - String[] nameValues = Strings.split(paramPair, '=');
170   - if (key.equals(nameValues[0])) {
171   - value = nameValues[1];
172   - }
173   - }
174   - }
175   - }
176   - return value;
177   - }
178   -}
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoReceiveMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.websocket.message;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 示例:server -> client 同步消息
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +@Data
  11 +public class DemoReceiveMessage {
  12 +
  13 + /**
  14 + * 接收人的编号
  15 + */
  16 + private Long fromUserId;
  17 + /**
  18 + * 内容
  19 + */
  20 + private String text;
  21 +
  22 + /**
  23 + * 是否单聊
  24 + */
  25 + private Boolean single;
  26 +
  27 +}
... ...
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoSendMessage.java 0 → 100644
  1 +package cn.iocoder.yudao.module.infra.websocket.message;
  2 +
  3 +import lombok.Data;
  4 +
  5 +/**
  6 + * 示例:client -> server 发送消息
  7 + *
  8 + * @author 芋道源码
  9 + */
  10 +@Data
  11 +public class DemoSendMessage {
  12 +
  13 + /**
  14 + * 发送给谁
  15 + *
  16 + * 如果为空,说明发送给所有人
  17 + */
  18 + private Long toUserId;
  19 + /**
  20 + * 内容
  21 + */
  22 + private String text;
  23 +
  24 +}
... ...
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java
... ... @@ -53,7 +53,8 @@ public class AppAuthController {
53 53 @PermitAll
54 54 @Operation(summary = "登出系统")
55 55 public CommonResult<Boolean> logout(HttpServletRequest request) {
56   - String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader());
  56 + String token = SecurityFrameworkUtils.obtainAuthorization(request,
  57 + securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
57 58 if (StrUtil.isNotBlank(token)) {
58 59 authService.logout(token);
59 60 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
... ... @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
7 7 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
8 8 import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
9 9 import cn.iocoder.yudao.framework.security.config.SecurityProperties;
  10 +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
10 11 import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*;
11 12 import cn.iocoder.yudao.module.system.convert.auth.AuthConvert;
12 13 import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO;
... ... @@ -38,7 +39,6 @@ import java.util.Set;
38 39 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
39 40 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
40 41 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
41   -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.obtainAuthorization;
42 42  
43 43 @Tag(name = "管理后台 - 认证")
44 44 @RestController
... ... @@ -76,7 +76,8 @@ public class AuthController {
76 76 @Operation(summary = "登出系统")
77 77 @OperateLog(enable = false) // 避免 Post 请求被记录操作日志
78 78 public CommonResult<Boolean> logout(HttpServletRequest request) {
79   - String token = obtainAuthorization(request, securityProperties.getTokenHeader());
  79 + String token = SecurityFrameworkUtils.obtainAuthorization(request,
  80 + securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
80 81 if (StrUtil.isNotBlank(token)) {
81 82 authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType());
82 83 }
... ...
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/NoticeController.java
1 1 package cn.iocoder.yudao.module.system.controller.admin.notice;
2 2  
  3 +import cn.hutool.core.lang.Assert;
  4 +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
3 5 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
4 6 import cn.iocoder.yudao.framework.common.pojo.PageResult;
  7 +import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi;
5 8 import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeCreateReqVO;
6 9 import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticePageReqVO;
7 10 import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeRespVO;
8 11 import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeUpdateReqVO;
9 12 import cn.iocoder.yudao.module.system.convert.notice.NoticeConvert;
  13 +import cn.iocoder.yudao.module.system.dal.dataobject.notice.NoticeDO;
10 14 import cn.iocoder.yudao.module.system.service.notice.NoticeService;
11 15 import io.swagger.v3.oas.annotations.tags.Tag;
12 16 import io.swagger.v3.oas.annotations.Parameter;
... ... @@ -29,6 +33,9 @@ public class NoticeController {
29 33 @Resource
30 34 private NoticeService noticeService;
31 35  
  36 + @Resource
  37 + private WebSocketSenderApi webSocketSenderApi;
  38 +
32 39 @PostMapping("/create")
33 40 @Operation(summary = "创建通知公告")
34 41 @PreAuthorize("@ss.hasPermission('system:notice:create')")
... ... @@ -69,4 +76,16 @@ public class NoticeController {
69 76 return success(NoticeConvert.INSTANCE.convert(noticeService.getNotice(id)));
70 77 }
71 78  
  79 + @PostMapping("/push")
  80 + @Operation(summary = "推送通知公告", description = "只发送给 websocket 连接在线的用户")
  81 + @Parameter(name = "id", description = "编号", required = true, example = "1024")
  82 + @PreAuthorize("@ss.hasPermission('system:notice:update')")
  83 + public CommonResult<Boolean> push(@RequestParam("id") Long id) {
  84 + NoticeDO notice = noticeService.getNotice(id);
  85 + Assert.notNull(notice, "公告不能为空");
  86 + // 通过 websocket 推送给在线的用户
  87 + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), "notice-push", notice);
  88 + return success(true);
  89 + }
  90 +
72 91 }
... ...
yudao-server/src/main/resources/application-local.yaml
... ... @@ -123,8 +123,8 @@ spring:
123 123 rabbitmq:
124 124 host: 127.0.0.1 # RabbitMQ 服务的地址
125 125 port: 5672 # RabbitMQ 服务的端口
126   - username: guest # RabbitMQ 服务的账号
127   - password: guest # RabbitMQ 服务的密码
  126 + username: rabbit # RabbitMQ 服务的账号
  127 + password: rabbit # RabbitMQ 服务的密码
128 128 # Kafka 配置项,对应 KafkaProperties 配置类
129 129 kafka:
130 130 bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔
... ...
yudao-server/src/main/resources/application.yaml
... ... @@ -146,9 +146,17 @@ yudao:
146 146 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
147 147 websocket:
148 148 enable: true # websocket的开关
149   - path: /websocket/message # 路径
150   - maxOnlineCount: 0 # 最大连接人数
151   - sessionMap: true # 保存sessionMap
  149 + path: /infra/ws # 路径
  150 + sender-type: local # 消息发送的类型,可选值为 local、redis、rocketmq、kafka、rabbitmq
  151 + sender-rocketmq:
  152 + topic: ${spring.application.name}-websocket # 消息发送的 RocketMQ Topic
  153 + consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 RocketMQ Consumer Group
  154 + sender-rabbitmq:
  155 + exchange: ${spring.application.name}-websocket-exchange # 消息发送的 RabbitMQ Exchange
  156 + queue: ${spring.application.name}-websocket-queue # 消息发送的 RabbitMQ Queue
  157 + sender-kafka:
  158 + topic: ${spring.application.name}-websocket # 消息发送的 Kafka Topic
  159 + consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 Kafka Consumer Group
152 160 swagger:
153 161 title: 芋道快速开发平台
154 162 description: 提供管理后台、用户 App 的所有功能
... ...