Commit 1941a7b3e62da89abafe5a6e47e3c0b08f64b912

Authored by YunaiV
1 parent 6a61db85

websocket:重新封装 websocket 组件,支持 token 认证,并增加 WebSocketMessageListener 方便处理消息

Showing 26 changed files with 521 additions and 522 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-websocket/pom.xml
... ... @@ -12,18 +12,22 @@
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>
29 33 </dependency>
... ...
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
... ... @@ -15,15 +15,8 @@ import org.springframework.validation.annotation.Validated;
15 15 public class WebSocketProperties {
16 16  
17 17 /**
18   - * 路径
  18 + * WebSocket 的连接路径
19 19 */
20   - private String path = "";
21   - /**
22   - * 默认最多允许同时在线用户数
23   - */
24   - private int maxOnlineCount = 0;
25   - /**
26   - * 是否保存session
27   - */
28   - private boolean sessionMap = true;
  20 + private String path = "/ws";
  21 +
29 22 }
... ...
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.websocket.core.handler.JsonWebSocketMessageHandler;
  4 +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
  5 +import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
  6 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionHandlerDecorator;
  7 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
  8 +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManagerImpl;
3 9 import org.springframework.boot.autoconfigure.AutoConfiguration;
4   -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
5 10 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
6 11 import org.springframework.boot.context.properties.EnableConfigurationProperties;
7 12 import org.springframework.context.annotation.Bean;
8 13 import org.springframework.web.socket.WebSocketHandler;
  14 +import org.springframework.web.socket.config.annotation.EnableWebSocket;
9 15 import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
10 16 import org.springframework.web.socket.server.HandshakeInterceptor;
11 17  
... ... @@ -17,18 +23,41 @@ import java.util.List;
17 23 * @author xingyu4j
18 24 */
19 25 @AutoConfiguration
20   -// 允许使用 yudao.websocket.enable=false 禁用websocket
21   -@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true)
  26 +@EnableWebSocket // 开启 websocket
  27 +@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true) // 允许使用 yudao.websocket.enable=false 禁用 websocket
  28 +
22 29 @EnableConfigurationProperties(WebSocketProperties.class)
23 30 public class YudaoWebSocketAutoConfiguration {
  31 +
24 32 @Bean
25   - @ConditionalOnMissingBean
26   - public WebSocketConfigurer webSocketConfigurer(List<HandshakeInterceptor> handshakeInterceptor,
  33 + public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
27 34 WebSocketHandler webSocketHandler,
28 35 WebSocketProperties webSocketProperties) {
29   -
30 36 return registry -> registry
  37 + // 添加 WebSocketHandler
31 38 .addHandler(webSocketHandler, webSocketProperties.getPath())
32   - .addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0]));
  39 + .addInterceptors(handshakeInterceptors)
  40 + // 允许跨域,否则前端连接会直接断开
  41 + .setAllowedOriginPatterns("*");
33 42 }
34   -}
  43 +
  44 + @Bean
  45 + public HandshakeInterceptor handshakeInterceptor() {
  46 + return new LoginUserHandshakeInterceptor();
  47 + }
  48 +
  49 + @Bean
  50 + public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
  51 + List<? extends WebSocketMessageListener<?>> messageListeners) {
  52 + // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息
  53 + JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
  54 + // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接
  55 + return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
  56 + }
  57 +
  58 + @Bean
  59 + public WebSocketSessionManager webSocketSessionManager() {
  60 + return new WebSocketSessionManagerImpl();
  61 + }
  62 +
  63 +}
35 64 \ 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.websocket.core.listener.WebSocketMessageListener;
  7 +import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage;
  8 +import lombok.extern.slf4j.Slf4j;
  9 +import org.springframework.web.socket.TextMessage;
  10 +import org.springframework.web.socket.WebSocketHandler;
  11 +import org.springframework.web.socket.WebSocketSession;
  12 +import org.springframework.web.socket.handler.TextWebSocketHandler;
  13 +
  14 +import java.lang.reflect.Type;
  15 +import java.util.HashMap;
  16 +import java.util.List;
  17 +import java.util.Map;
  18 +import java.util.Objects;
  19 +import java.util.function.Consumer;
  20 +
  21 +/**
  22 + * JSON 格式 {@link WebSocketHandler} 实现类
  23 + *
  24 + * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
  25 + *
  26 + * @author 芋道源码
  27 + */
  28 +@Slf4j
  29 +public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
  30 +
  31 + /**
  32 + * type 与 WebSocketMessageListener 的映射
  33 + */
  34 + private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>();
  35 +
  36 + @SuppressWarnings({"rawtypes", "unchecked"})
  37 + public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) {
  38 + listenersList.forEach((Consumer<WebSocketMessageListener>)
  39 + listener -> listeners.put(listener.getType(), listener));
  40 + }
  41 +
  42 + @Override
  43 + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
  44 + // 1.1 空消息,跳过
  45 + if (message.getPayloadLength() == 0) {
  46 + return;
  47 + }
  48 + // 1.2 ping 心跳消息,直接返回 pong 消息。
  49 + if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
  50 + session.sendMessage(new TextMessage("pong"));
  51 + return;
  52 + }
  53 +
  54 + // 2.1 解析消息
  55 + try {
  56 + JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
  57 + if (jsonMessage == null) {
  58 + log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
  59 + return;
  60 + }
  61 + if (StrUtil.isEmpty(jsonMessage.getType())) {
  62 + log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
  63 + return;
  64 + }
  65 + // 2.2 获得对应的 WebSocketMessageListener
  66 + WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());
  67 + if (messageListener == null) {
  68 + log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
  69 + return;
  70 + }
  71 + // 2.3 处理消息
  72 + Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
  73 + Object messageObj = JsonUtils.parseObject(jsonMessage.getMessage(), type);
  74 + messageListener.onMessage(session, messageObj);
  75 + } catch (Throwable ex) {
  76 + log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
  77 + }
  78 + }
  79 +
  80 +}
... ...
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 +/**
  7 + * JSON 格式的 WebSocket 消息帧
  8 + *
  9 + * @author 芋道源码
  10 + */
  11 +@Data
  12 +public class JsonWebSocketMessage {
  13 +
  14 + /**
  15 + * 消息类型
  16 + *
  17 + * 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类
  18 + */
  19 + private String type;
  20 + /**
  21 + * 消息内容
  22 + *
  23 + * 要求 JSON 对象
  24 + */
  25 + private String message;
  26 +
  27 +}
... ...
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/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.websocket.core.util.WebSocketFrameworkUtils;
  6 +import org.springframework.web.socket.WebSocketSession;
  7 +
  8 +import java.util.*;
  9 +import java.util.concurrent.ConcurrentHashMap;
  10 +import java.util.concurrent.ConcurrentMap;
  11 +import java.util.concurrent.CopyOnWriteArrayList;
  12 +
  13 +/**
  14 + * 默认的 {@link WebSocketSessionManager} 实现类
  15 + *
  16 + * @author 芋道源码
  17 + */
  18 +public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
  19 +
  20 + /**
  21 + * id 与 WebSocketSession 映射
  22 + *
  23 + * key:Session 编号
  24 + */
  25 + private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>();
  26 +
  27 + /**
  28 + * user 与 WebSocketSession 映射
  29 + *
  30 + * key1:用户类型
  31 + * key2:用户编号
  32 + */
  33 + private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions
  34 + = new ConcurrentHashMap<>();
  35 +
  36 + @Override
  37 + public void addSession(WebSocketSession session) {
  38 + // 添加到 idSessions 中
  39 + idSessions.put(session.getId(), session);
  40 + // 添加到 userSessions 中
  41 + LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
  42 + if (user == null) {
  43 + return;
  44 + }
  45 + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
  46 + if (userSessionsMap == null) {
  47 + userSessionsMap = new ConcurrentHashMap<>();
  48 + if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) {
  49 + userSessionsMap = userSessions.get(user.getUserType());
  50 + }
  51 + }
  52 + CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
  53 + if (sessions == null) {
  54 + sessions = new CopyOnWriteArrayList<>();
  55 + if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) {
  56 + sessions = userSessionsMap.get(user.getId());
  57 + }
  58 + }
  59 + sessions.add(session);
  60 + }
  61 +
  62 + @Override
  63 + public void removeSession(WebSocketSession session) {
  64 + // 移除从 idSessions 中
  65 + idSessions.remove(session.getId(), session);
  66 + // 移除从 idSessions 中
  67 + LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
  68 + if (user == null) {
  69 + return;
  70 + }
  71 + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
  72 + if (userSessionsMap == null) {
  73 + return;
  74 + }
  75 + CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
  76 + sessions.removeIf(session0 -> session0.getId().equals(session.getId()));
  77 + if (CollUtil.isEmpty(sessions)) {
  78 + userSessionsMap.remove(user.getId(), sessions);
  79 + }
  80 + }
  81 +
  82 + @Override
  83 + public WebSocketSession getSession(String id) {
  84 + return idSessions.get(id);
  85 + }
  86 +
  87 + @Override
  88 + public Collection<WebSocketSession> getSessionList(Integer userType) {
  89 + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
  90 + if (CollUtil.isEmpty(userSessionsMap)) {
  91 + return new ArrayList<>();
  92 + }
  93 + LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容
  94 + for (List<WebSocketSession> sessions : userSessionsMap.values()) {
  95 + if (CollUtil.isNotEmpty(sessions)) {
  96 + continue;
  97 + }
  98 + result.addAll(sessions);
  99 + }
  100 + return result;
  101 + }
  102 +
  103 + @Override
  104 + public Collection<WebSocketSession> getSessionList(Integer userType, Long userId) {
  105 + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
  106 + if (CollUtil.isEmpty(userSessionsMap)) {
  107 + return new ArrayList<>();
  108 + }
  109 + CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(userId);
  110 + return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>();
  111 + }
  112 +
  113 +}
... ...
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.lang.Nullable;
  5 +import org.springframework.web.socket.WebSocketSession;
  6 +
  7 +import java.util.Map;
  8 +
  9 +/**
  10 + * 专属于 web 包的工具类
  11 + *
  12 + * @author 芋道源码
  13 + */
  14 +public class WebSocketFrameworkUtils {
  15 +
  16 + public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER";
  17 +
  18 + /**
  19 + * 设置当前用户
  20 + *
  21 + * @param loginUser 登录用户
  22 + * @param attributes Session
  23 + */
  24 + public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) {
  25 + attributes.put(ATTRIBUTE_LOGIN_USER, loginUser);
  26 + }
  27 +
  28 + /**
  29 + * 获取当前用户
  30 + *
  31 + * @return 当前用户
  32 + */
  33 + public static LoginUser getLoginUser(WebSocketSession session) {
  34 + return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER);
  35 + }
  36 +
  37 + /**
  38 + * 获得当前用户的编号
  39 + *
  40 + * @return 用户编号
  41 + */
  42 + @Nullable
  43 + public static Long getLoginUserId(WebSocketSession session) {
  44 + LoginUser loginUser = getLoginUser(session);
  45 + return loginUser != null ? loginUser.getId() : null;
  46 + }
  47 +
  48 + /**
  49 + * 获得当前用户的类型
  50 + *
  51 + * @return 用户编号
  52 + */
  53 + @Nullable
  54 + public static Integer getLoginUserType(WebSocketSession session) {
  55 + LoginUser loginUser = getLoginUser(session);
  56 + return loginUser != null ? loginUser.getUserType() : null;
  57 + }
  58 +
  59 +}
... ...
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-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/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-server/src/main/resources/application.yaml
... ... @@ -146,9 +146,7 @@ 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 # 路径
152 150 swagger:
153 151 title: 芋道快速开发平台
154 152 description: 提供管理后台、用户 App 的所有功能
... ...