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,6 +176,12 @@
176 </dependency> 176 </dependency>
177 177
178 <dependency> 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 <groupId>com.github.xiaoymin</groupId> 185 <groupId>com.github.xiaoymin</groupId>
180 <artifactId>knife4j-openapi3-spring-boot-starter</artifactId> 186 <artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
181 <version>${knife4j.version}</version> 187 <version>${knife4j.version}</version>
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
@@ -12,18 +12,22 @@ @@ -12,18 +12,22 @@
12 <packaging>jar</packaging> 12 <packaging>jar</packaging>
13 13
14 <name>${project.artifactId}</name> 14 <name>${project.artifactId}</name>
15 - <description>WebSocket</description> 15 + <description>WebSocket 框架,支持多节点的广播</description>
16 <url>https://github.com/YunaiV/ruoyi-vue-pro</url> 16 <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
17 17
18 18
19 <dependencies> 19 <dependencies>
20 -  
21 <dependency> 20 <dependency>
22 <groupId>cn.iocoder.boot</groupId> 21 <groupId>cn.iocoder.boot</groupId>
23 <artifactId>yudao-common</artifactId> 22 <artifactId>yudao-common</artifactId>
24 </dependency> 23 </dependency>
25 24
  25 + <!-- Web 相关 -->
26 <dependency> 26 <dependency>
  27 + <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
  28 + 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
  29 + 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
  30 + -->
27 <groupId>cn.iocoder.boot</groupId> 31 <groupId>cn.iocoder.boot</groupId>
28 <artifactId>yudao-spring-boot-starter-security</artifactId> 32 <artifactId>yudao-spring-boot-starter-security</artifactId>
29 </dependency> 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 +15,8 @@ import org.springframework.validation.annotation.Validated;
15 public class WebSocketProperties { 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 package cn.iocoder.yudao.framework.websocket.config; 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 import org.springframework.boot.autoconfigure.AutoConfiguration; 9 import org.springframework.boot.autoconfigure.AutoConfiguration;
4 -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;  
5 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 10 import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
6 import org.springframework.boot.context.properties.EnableConfigurationProperties; 11 import org.springframework.boot.context.properties.EnableConfigurationProperties;
7 import org.springframework.context.annotation.Bean; 12 import org.springframework.context.annotation.Bean;
8 import org.springframework.web.socket.WebSocketHandler; 13 import org.springframework.web.socket.WebSocketHandler;
  14 +import org.springframework.web.socket.config.annotation.EnableWebSocket;
9 import org.springframework.web.socket.config.annotation.WebSocketConfigurer; 15 import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
10 import org.springframework.web.socket.server.HandshakeInterceptor; 16 import org.springframework.web.socket.server.HandshakeInterceptor;
11 17
@@ -17,18 +23,41 @@ import java.util.List; @@ -17,18 +23,41 @@ import java.util.List;
17 * @author xingyu4j 23 * @author xingyu4j
18 */ 24 */
19 @AutoConfiguration 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 @EnableConfigurationProperties(WebSocketProperties.class) 29 @EnableConfigurationProperties(WebSocketProperties.class)
23 public class YudaoWebSocketAutoConfiguration { 30 public class YudaoWebSocketAutoConfiguration {
  31 +
24 @Bean 32 @Bean
25 - @ConditionalOnMissingBean  
26 - public WebSocketConfigurer webSocketConfigurer(List<HandshakeInterceptor> handshakeInterceptor, 33 + public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
27 WebSocketHandler webSocketHandler, 34 WebSocketHandler webSocketHandler,
28 WebSocketProperties webSocketProperties) { 35 WebSocketProperties webSocketProperties) {
29 -  
30 return registry -> registry 36 return registry -> registry
  37 + // 添加 WebSocketHandler
31 .addHandler(webSocketHandler, webSocketProperties.getPath()) 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 \ No newline at end of file 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 import cn.iocoder.yudao.framework.security.core.LoginUser; 3 import cn.iocoder.yudao.framework.security.core.LoginUser;
  4 +import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
4 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; 5 import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
  6 +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils;
5 import org.springframework.http.server.ServerHttpRequest; 7 import org.springframework.http.server.ServerHttpRequest;
6 import org.springframework.http.server.ServerHttpResponse; 8 import org.springframework.http.server.ServerHttpResponse;
7 import org.springframework.web.socket.WebSocketHandler; 9 import org.springframework.web.socket.WebSocketHandler;
  10 +import org.springframework.web.socket.WebSocketSession;
8 import org.springframework.web.socket.server.HandshakeInterceptor; 11 import org.springframework.web.socket.server.HandshakeInterceptor;
9 12
10 import java.util.Map; 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 @Override 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 LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); 29 LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
16 - attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser); 30 + if (loginUser != null) {
  31 + WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
  32 + }
17 return true; 33 return true;
18 } 34 }
19 35
20 @Override 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 \ No newline at end of file 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 package cn.iocoder.yudao.framework.websocket; 4 package cn.iocoder.yudao.framework.websocket;
yudao-module-infra/yudao-module-infra-biz/pom.xml
@@ -46,6 +46,11 @@ @@ -46,6 +46,11 @@
46 <artifactId>yudao-spring-boot-starter-security</artifactId> 46 <artifactId>yudao-spring-boot-starter-security</artifactId>
47 </dependency> 47 </dependency>
48 48
  49 + <dependency>
  50 + <groupId>cn.iocoder.boot</groupId>
  51 + <artifactId>yudao-spring-boot-starter-websocket</artifactId>
  52 + </dependency>
  53 +
49 <!-- DB 相关 --> 54 <!-- DB 相关 -->
50 <dependency> 55 <dependency>
51 <groupId>cn.iocoder.boot</groupId> 56 <groupId>cn.iocoder.boot</groupId>
@@ -116,11 +121,6 @@ @@ -116,11 +121,6 @@
116 <artifactId>yudao-spring-boot-starter-file</artifactId> 121 <artifactId>yudao-spring-boot-starter-file</artifactId>
117 </dependency> 122 </dependency>
118 123
119 - <!-- WebSocket -->  
120 - <dependency>  
121 - <groupId>org.springframework.boot</groupId>  
122 - <artifactId>spring-boot-starter-websocket</artifactId>  
123 - </dependency>  
124 </dependencies> 124 </dependencies>
125 125
126 </project> 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,9 +146,7 @@ yudao:
146 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 146 - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录
147 websocket: 147 websocket:
148 enable: true # websocket的开关 148 enable: true # websocket的开关
149 - path: /websocket/message # 路径  
150 - maxOnlineCount: 0 # 最大连接人数  
151 - sessionMap: true # 保存sessionMap 149 + path: /infra/ws # 路径
152 swagger: 150 swagger:
153 title: 芋道快速开发平台 151 title: 芋道快速开发平台
154 description: 提供管理后台、用户 App 的所有功能 152 description: 提供管理后台、用户 App 的所有功能