Commit 1941a7b3e62da89abafe5a6e47e3c0b08f64b912
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
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
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 的所有功能 | ... | ... |