Commit 0233b092b81220b38007ff6be592f204fac91b10

Authored by YunaiV
1 parent 1941a7b3

websocket:重新封装 websocket 组件,支持 sender 广播

Showing 31 changed files with 1107 additions and 12 deletions
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
... ... @@ -30,12 +30,55 @@
30 30 -->
31 31 <groupId>cn.iocoder.boot</groupId>
32 32 <artifactId>yudao-spring-boot-starter-security</artifactId>
  33 + <scope>provided</scope>
33 34 </dependency>
34 35  
35 36 <dependency>
36 37 <groupId>org.springframework.boot</groupId>
37 38 <artifactId>spring-boot-starter-websocket</artifactId>
38 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>
39 82 </dependencies>
40 83  
41 84 </project>
42 85 \ No newline at end of file
... ...
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 *
... ... @@ -17,6 +20,15 @@ public class WebSocketProperties {
17 20 /**
18 21 * WebSocket 的连接路径
19 22 */
  23 + @NotEmpty(message = "WebSocket 的连接路径不能为空")
20 24 private String path = "/ws";
21 25  
  26 + /**
  27 + * 消息发送器的类型
  28 + *
  29 + * 可选值:local、redis、rocketmq、kafka、rabbitmq
  30 + */
  31 + @NotNull(message = "WebSocket 的消息发送者不能为空")
  32 + private String senderType = "local";
  33 +
22 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.core.RedisMQTemplate;
3 4 import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler;
4 5 import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
5 6 import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
  7 +import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer;
  8 +import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender;
  9 +import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
  10 +import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer;
  11 +import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender;
  12 +import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer;
  13 +import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageSender;
  14 +import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer;
  15 +import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender;
6 16 import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionHandlerDecorator;
7 17 import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
8 18 import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManagerImpl;
  19 +import org.apache.rocketmq.spring.core.RocketMQTemplate;
  20 +import org.springframework.amqp.core.TopicExchange;
  21 +import org.springframework.amqp.rabbit.core.RabbitTemplate;
  22 +import org.springframework.beans.factory.annotation.Value;
9 23 import org.springframework.boot.autoconfigure.AutoConfiguration;
10 24 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
11 25 import org.springframework.boot.context.properties.EnableConfigurationProperties;
12 26 import org.springframework.context.annotation.Bean;
  27 +import org.springframework.context.annotation.Configuration;
  28 +import org.springframework.kafka.core.KafkaTemplate;
13 29 import org.springframework.web.socket.WebSocketHandler;
14 30 import org.springframework.web.socket.config.annotation.EnableWebSocket;
15 31 import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
... ... @@ -25,7 +41,6 @@ import java.util.List;
25 41 @AutoConfiguration
26 42 @EnableWebSocket // 开启 websocket
27 43 @ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true) // 允许使用 yudao.websocket.enable=false 禁用 websocket
28   -
29 44 @EnableConfigurationProperties(WebSocketProperties.class)
30 45 public class YudaoWebSocketAutoConfiguration {
31 46  
... ... @@ -60,4 +75,103 @@ public class YudaoWebSocketAutoConfiguration {
60 75 return new WebSocketSessionManagerImpl();
61 76 }
62 77  
  78 + // ==================== Sender 相关 ====================
  79 +
  80 + @Configuration
  81 + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true)
  82 + public class LocalWebSocketMessageSenderConfiguration {
  83 +
  84 + @Bean
  85 + public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) {
  86 + return new LocalWebSocketMessageSender(sessionManager);
  87 + }
  88 +
  89 + }
  90 +
  91 + @Configuration
  92 + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true)
  93 + public class RedisWebSocketMessageSenderConfiguration {
  94 +
  95 + @Bean
  96 + public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager,
  97 + RedisMQTemplate redisMQTemplate) {
  98 + return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate);
  99 + }
  100 +
  101 + // TODO 芋艿:需要额外删除 YudaoRedisMQAutoConfiguration 的 RedisMessageListenerContainer Bean 上的 @ConditionalOnBean 注解。可能是 spring boot 的 bug!
  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 +
  127 + }
  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 +
63 177 }
64 178 \ No newline at end of file
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
... ... @@ -3,8 +3,10 @@ package cn.iocoder.yudao.framework.websocket.core.handler;
3 3 import cn.hutool.core.util.StrUtil;
4 4 import cn.hutool.core.util.TypeUtil;
5 5 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  6 +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
6 7 import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
7 8 import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
  9 +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
8 10 import lombok.extern.slf4j.Slf4j;
9 11 import org.springframework.web.socket.TextMessage;
10 12 import org.springframework.web.socket.WebSocketHandler;
... ... @@ -70,8 +72,9 @@ public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
70 72 }
71 73 // 2.3 处理消息
72 74 Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
73   - Object messageObj = JsonUtils.parseObject(jsonMessage.getMessage(), type);
74   - messageListener.onMessage(session, messageObj);
  75 + Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
  76 + Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
  77 + TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
75 78 } catch (Throwable ex) {
76 79 log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
77 80 }
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java
... ... @@ -3,13 +3,15 @@ package cn.iocoder.yudao.framework.websocket.core.message;
3 3 import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
4 4 import lombok.Data;
5 5  
  6 +import java.io.Serializable;
  7 +
6 8 /**
7 9 * JSON 格式的 WebSocket 消息帧
8 10 *
9 11 * @author 芋道源码
10 12 */
11 13 @Data
12   -public class JsonWebSocketMessage {
  14 +public class JsonWebSocketMessage implements Serializable {
13 15  
14 16 /**
15 17 * 消息类型
... ... @@ -22,6 +24,6 @@ public class JsonWebSocketMessage {
22 24 *
23 25 * 要求 JSON 对象
24 26 */
25   - private String message;
  27 + private String content;
26 28  
27 29 }
... ...
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/WebSocketSessionManagerImpl.java
... ... @@ -2,10 +2,14 @@ package cn.iocoder.yudao.framework.websocket.core.session;
2 2  
3 3 import cn.hutool.core.collection.CollUtil;
4 4 import cn.iocoder.yudao.framework.security.core.LoginUser;
  5 +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
5 6 import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
6 7 import org.springframework.web.socket.WebSocketSession;
7 8  
8   -import java.util.*;
  9 +import java.util.ArrayList;
  10 +import java.util.Collection;
  11 +import java.util.LinkedList;
  12 +import java.util.List;
9 13 import java.util.concurrent.ConcurrentHashMap;
10 14 import java.util.concurrent.ConcurrentMap;
11 15 import java.util.concurrent.CopyOnWriteArrayList;
... ... @@ -91,10 +95,18 @@ public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
91 95 return new ArrayList<>();
92 96 }
93 97 LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容
  98 + Long contextTenantId = TenantContextHolder.getTenantId();
94 99 for (List<WebSocketSession> sessions : userSessionsMap.values()) {
95   - if (CollUtil.isNotEmpty(sessions)) {
  100 + if (CollUtil.isEmpty(sessions)) {
96 101 continue;
97 102 }
  103 + // 特殊:如果租户不匹配,则直接排除
  104 + if (contextTenantId != null) {
  105 + Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0));
  106 + if (!contextTenantId.equals(userTenantId)) {
  107 + continue;
  108 + }
  109 + }
98 110 result.addAll(sessions);
99 111 }
100 112 return result;
... ...
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java
1 1 package cn.iocoder.yudao.framework.websocket.core.util;
2 2  
3 3 import cn.iocoder.yudao.framework.security.core.LoginUser;
4   -import org.springframework.lang.Nullable;
5 4 import org.springframework.web.socket.WebSocketSession;
6 5  
7 6 import java.util.Map;
... ... @@ -39,7 +38,6 @@ public class WebSocketFrameworkUtils {
39 38 *
40 39 * @return 用户编号
41 40 */
42   - @Nullable
43 41 public static Long getLoginUserId(WebSocketSession session) {
44 42 LoginUser loginUser = getLoginUser(session);
45 43 return loginUser != null ? loginUser.getId() : null;
... ... @@ -50,10 +48,20 @@ public class WebSocketFrameworkUtils {
50 48 *
51 49 * @return 用户编号
52 50 */
53   - @Nullable
54 51 public static Integer getLoginUserType(WebSocketSession session) {
55 52 LoginUser loginUser = getLoginUser(session);
56 53 return loginUser != null ? loginUser.getUserType() : null;
57 54 }
58 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 +
59 67 }
... ...
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/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/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-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
... ... @@ -147,6 +147,16 @@ yudao:
147 147 websocket:
148 148 enable: true # websocket的开关
149 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
150 160 swagger:
151 161 title: 芋道快速开发平台
152 162 description: 提供管理后台、用户 App 的所有功能
... ...