初始化
This commit is contained in:
196
rdms-framework/rdms-spring-boot-starter-websocket/README.md
Normal file
196
rdms-framework/rdms-spring-boot-starter-websocket/README.md
Normal file
@@ -0,0 +1,196 @@
|
||||
# rdms-spring-boot-starter-websocket
|
||||
|
||||
## 1. 模块定位
|
||||
|
||||
`rdms-spring-boot-starter-websocket` 是 WebSocket 基础设施模块,用于统一处理连接、会话管理、消息分发与消息发送,并支持多节点场景下的广播投递。
|
||||
|
||||
模块聚合的核心能力:
|
||||
|
||||
- WebSocket 自动装配与路径注册
|
||||
- 登录用户绑定与会话管理
|
||||
- JSON 消息协议与监听器分发
|
||||
- 消息发送与多节点广播
|
||||
|
||||
## 2. 设计思路
|
||||
|
||||
- 统一 JSON 消息协议(`type` + `content`),通过 `type` 分发到对应监听器,降低业务耦合。
|
||||
- 通过 `WebSocketSessionManager` 统一管理会话,支持按用户类型/用户编号/会话 ID 进行推送。
|
||||
- 通过可插拔 `sender-type` 支持单机与多节点广播(local/redis/rocketmq/rabbitmq/kafka)。
|
||||
- 与安全体系结合,在握手阶段写入 `LoginUser`,便于后续鉴权与定向发送。
|
||||
|
||||
## 3. 功能模块
|
||||
|
||||
### 3.1 自动装配
|
||||
|
||||
`RdmsWebSocketAutoConfiguration` 会完成:
|
||||
|
||||
- 注册 WebSocket 路径与处理器
|
||||
- 注册握手拦截器(默认 `LoginUserHandshakeInterceptor`)
|
||||
- 注册 `WebSocketSessionManager`
|
||||
- 放行 WebSocket 路径的安全校验
|
||||
- 注册消息发送器(按 `sender-type`)
|
||||
|
||||
### 3.2 JSON 消息协议
|
||||
|
||||
内置统一消息结构:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "notice",
|
||||
"content": "{...}"
|
||||
}
|
||||
```
|
||||
|
||||
处理流程:
|
||||
|
||||
- `JsonWebSocketMessageHandler` 解析 JSON
|
||||
- 根据 `type` 选择对应 `WebSocketMessageListener`
|
||||
- 将 `content` 反序列化为监听器泛型类型并交给业务处理
|
||||
|
||||
内置 `ping/pong`:收到 `ping` 会直接返回 `pong`。
|
||||
|
||||
### 3.3 会话管理
|
||||
|
||||
`WebSocketSessionManager` 支持:
|
||||
|
||||
- 按 `sessionId` 获取会话
|
||||
- 按 `userType` 获取会话列表
|
||||
- 按 `userType + userId` 获取会话列表
|
||||
|
||||
用于定向推送和广播推送。
|
||||
|
||||
### 3.4 消息发送
|
||||
|
||||
统一使用 `WebSocketMessageSender` 发送消息:
|
||||
|
||||
- 按用户推送
|
||||
- 按用户类型广播
|
||||
- 按会话 ID 推送
|
||||
|
||||
消息会被封装为统一的 `JsonWebSocketMessage` 后发送。
|
||||
|
||||
### 3.5 多节点广播
|
||||
|
||||
`sender-type` 支持以下类型:
|
||||
|
||||
- `local`:单机直连(默认)
|
||||
- `redis` / `rocketmq` / `rabbitmq` / `kafka`:通过 MQ 广播到各实例,再由各实例推送到本机会话
|
||||
|
||||
### 3.6 登录用户绑定
|
||||
|
||||
握手阶段会读取当前登录用户并写入 WebSocket Session:
|
||||
|
||||
- 需要前端通过 `?token={token}` 形式携带令牌
|
||||
- `LoginUserHandshakeInterceptor` 会将 `LoginUser` 写入 Session
|
||||
- `WebSocketFrameworkUtils` 可获取 `userId/userType/tenantId`
|
||||
|
||||
## 4. 开发人员上手
|
||||
|
||||
### 4.1 引入依赖
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 4.2 基础配置
|
||||
|
||||
```yaml
|
||||
rdms:
|
||||
websocket:
|
||||
enable: true
|
||||
path: /ws
|
||||
sender-type: local
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- `path` 默认 `/ws`
|
||||
- `sender-type` 默认 `local`
|
||||
- `enable` 默认 `true`
|
||||
|
||||
### 4.3 连接方式
|
||||
|
||||
前端连接示例:
|
||||
|
||||
```
|
||||
ws://{host}:{port}/ws?token={token}
|
||||
```
|
||||
|
||||
### 4.4 编写消息监听器
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class NoticeMessageListener implements WebSocketMessageListener<NoticeDTO> {
|
||||
|
||||
@Override
|
||||
public void onMessage(WebSocketSession session, NoticeDTO message) {
|
||||
// 处理消息
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return "notice";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.5 服务端推送
|
||||
|
||||
```java
|
||||
@Resource
|
||||
private WebSocketMessageSender webSocketMessageSender;
|
||||
|
||||
public void push(Long userId, NoticeDTO notice) {
|
||||
webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), userId, "notice", notice);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.6 多节点广播配置示例
|
||||
|
||||
Redis:
|
||||
|
||||
```yaml
|
||||
rdms:
|
||||
websocket:
|
||||
sender-type: redis
|
||||
```
|
||||
|
||||
RocketMQ:
|
||||
|
||||
```yaml
|
||||
rdms:
|
||||
websocket:
|
||||
sender-type: rocketmq
|
||||
sender-rocketmq:
|
||||
topic: ws-topic
|
||||
consumer-group: ws-group
|
||||
```
|
||||
|
||||
Kafka:
|
||||
|
||||
```yaml
|
||||
rdms:
|
||||
websocket:
|
||||
sender-type: kafka
|
||||
sender-kafka:
|
||||
topic: ws-topic
|
||||
consumer-group: ws-group
|
||||
```
|
||||
|
||||
RabbitMQ:
|
||||
|
||||
```yaml
|
||||
rdms:
|
||||
websocket:
|
||||
sender-type: rabbitmq
|
||||
sender-rabbitmq:
|
||||
exchange: ws-exchange
|
||||
queue: ws-queue
|
||||
```
|
||||
|
||||
注意:
|
||||
|
||||
- 使用 MQ 模式时,请确保对应 MQ 客户端依赖与连接配置已就绪。
|
||||
66
rdms-framework/rdms-spring-boot-starter-websocket/pom.xml
Normal file
66
rdms-framework/rdms-spring-boot-starter-websocket/pom.xml
Normal file
@@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-framework</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>rdms-spring-boot-starter-websocket</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
<description>WebSocket 框架,支持多节点的广播</description>
|
||||
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Web 相关 -->
|
||||
<dependency>
|
||||
<!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢?
|
||||
因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。
|
||||
如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。
|
||||
-->
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-security</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 消息队列相关 -->
|
||||
<dependency>
|
||||
<groupId>com.njcn</groupId>
|
||||
<artifactId>rdms-spring-boot-starter-mq</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.amqp</groupId>
|
||||
<artifactId>spring-rabbit</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.njcn.rdms.framework.websocket.config;
|
||||
|
||||
import com.njcn.rdms.framework.websocket.core.handler.JsonWebSocketMessageHandler;
|
||||
import com.njcn.rdms.framework.websocket.core.listener.WebSocketMessageListener;
|
||||
import com.njcn.rdms.framework.websocket.core.security.LoginUserHandshakeInterceptor;
|
||||
import com.njcn.rdms.framework.websocket.core.security.WebSocketAuthorizeRequestsCustomizer;
|
||||
import com.njcn.rdms.framework.websocket.core.sender.local.LocalWebSocketMessageSender;
|
||||
import com.njcn.rdms.framework.websocket.core.session.WebSocketSessionHandlerDecorator;
|
||||
import com.njcn.rdms.framework.websocket.core.session.WebSocketSessionManager;
|
||||
import com.njcn.rdms.framework.websocket.core.session.WebSocketSessionManagerImpl;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* WebSocket 自动配置
|
||||
*
|
||||
* @author xingyu4j
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@EnableWebSocket // 开启 websocket
|
||||
@ConditionalOnProperty(prefix = "rdms.websocket", value = "enable", matchIfMissing = true) // 允许使用 rdms.websocket.enable=false 禁用 websocket
|
||||
@EnableConfigurationProperties(WebSocketProperties.class)
|
||||
public class RdmsWebSocketAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
|
||||
WebSocketHandler webSocketHandler,
|
||||
WebSocketProperties webSocketProperties) {
|
||||
return registry -> registry
|
||||
// 添加 WebSocketHandler
|
||||
.addHandler(webSocketHandler, webSocketProperties.getPath())
|
||||
.addInterceptors(handshakeInterceptors)
|
||||
// 允许跨域,否则前端连接会直接断开
|
||||
.setAllowedOriginPatterns("*");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public HandshakeInterceptor handshakeInterceptor() {
|
||||
return new LoginUserHandshakeInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
|
||||
List<? extends WebSocketMessageListener<?>> messageListeners) {
|
||||
// 1. 创建 JsonWebSocketMessageHandler 对象,处理消息
|
||||
JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
|
||||
// 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接
|
||||
return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketSessionManager webSocketSessionManager() {
|
||||
return new WebSocketSessionManagerImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocalWebSocketMessageSender webSocketMessageSender(WebSocketSessionManager sessionManager) {
|
||||
return new LocalWebSocketMessageSender(sessionManager);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) {
|
||||
return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.njcn.rdms.framework.websocket.config;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
/**
|
||||
* WebSocket 配置项
|
||||
*
|
||||
* @author xingyu4j
|
||||
*/
|
||||
@ConfigurationProperties("rdms.websocket")
|
||||
@Data
|
||||
@Validated
|
||||
public class WebSocketProperties {
|
||||
|
||||
/**
|
||||
* WebSocket 的连接路径
|
||||
*/
|
||||
@NotEmpty(message = "WebSocket 的连接路径不能为空")
|
||||
private String path = "/ws";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.njcn.rdms.framework.websocket.core.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.core.util.TypeUtil;
|
||||
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||
import com.njcn.rdms.framework.websocket.core.listener.WebSocketMessageListener;
|
||||
import com.njcn.rdms.framework.websocket.core.message.JsonWebSocketMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* JSON 格式 {@link WebSocketHandler} 实现类
|
||||
*
|
||||
* 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@Slf4j
|
||||
public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
|
||||
|
||||
/**
|
||||
* type 与 WebSocketMessageListener 的映射
|
||||
*/
|
||||
private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>();
|
||||
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) {
|
||||
listenersList.forEach((Consumer<WebSocketMessageListener>)
|
||||
listener -> listeners.put(listener.getType(), listener));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
// 1.1 空消息,跳过
|
||||
if (message.getPayloadLength() == 0) {
|
||||
return;
|
||||
}
|
||||
// 1.2 ping 心跳消息,直接返回 pong 消息。
|
||||
if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
|
||||
session.sendMessage(new TextMessage("pong"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.1 解析消息
|
||||
try {
|
||||
JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class);
|
||||
if (jsonMessage == null) {
|
||||
log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload());
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isEmpty(jsonMessage.getType())) {
|
||||
log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload());
|
||||
return;
|
||||
}
|
||||
// 2.2 获得对应的 WebSocketMessageListener
|
||||
WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType());
|
||||
if (messageListener == null) {
|
||||
log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload());
|
||||
return;
|
||||
}
|
||||
// 2.3 处理消息
|
||||
Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0);
|
||||
Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type);
|
||||
messageListener.onMessage(session, messageObj);
|
||||
} catch (Throwable ex) {
|
||||
log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.njcn.rdms.framework.websocket.core.listener;
|
||||
|
||||
import com.njcn.rdms.framework.websocket.core.message.JsonWebSocketMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
/**
|
||||
* WebSocket 消息监听器接口
|
||||
*
|
||||
* 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息
|
||||
*
|
||||
* @param <T> 泛型,消息类型
|
||||
*/
|
||||
public interface WebSocketMessageListener<T> {
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
*
|
||||
* @param session Session
|
||||
* @param message 消息
|
||||
*/
|
||||
void onMessage(WebSocketSession session, T message);
|
||||
|
||||
/**
|
||||
* 获得消息类型
|
||||
*
|
||||
* @see JsonWebSocketMessage#getType()
|
||||
* @return 消息类型
|
||||
*/
|
||||
String getType();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.njcn.rdms.framework.websocket.core.message;
|
||||
|
||||
import com.njcn.rdms.framework.websocket.core.listener.WebSocketMessageListener;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* JSON 格式的 WebSocket 消息帧
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@Data
|
||||
public class JsonWebSocketMessage implements Serializable {
|
||||
|
||||
/**
|
||||
* 消息类型
|
||||
*
|
||||
* 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类
|
||||
*/
|
||||
private String type;
|
||||
/**
|
||||
* 消息内容
|
||||
*
|
||||
* 要求 JSON 对象
|
||||
*/
|
||||
private String content;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.njcn.rdms.framework.websocket.core.security;
|
||||
|
||||
import com.njcn.rdms.framework.security.core.LoginUser;
|
||||
import com.njcn.rdms.framework.security.core.filter.TokenAuthenticationFilter;
|
||||
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.njcn.rdms.framework.websocket.core.util.WebSocketFrameworkUtils;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 登录用户的 {@link HandshakeInterceptor} 实现类
|
||||
*
|
||||
* 流程如下:
|
||||
* 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过
|
||||
* 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
||||
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
|
||||
WebSocketHandler wsHandler, Exception exception) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.njcn.rdms.framework.websocket.core.security;
|
||||
|
||||
import com.njcn.rdms.framework.security.config.AuthorizeRequestsCustomizer;
|
||||
import com.njcn.rdms.framework.websocket.config.WebSocketProperties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||
|
||||
/**
|
||||
* WebSocket 的权限自定义
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {
|
||||
|
||||
private final WebSocketProperties webSocketProperties;
|
||||
|
||||
@Override
|
||||
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
|
||||
registry.requestMatchers(webSocketProperties.getPath()).permitAll();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.njcn.rdms.framework.websocket.core.sender;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||
import com.njcn.rdms.framework.websocket.core.message.JsonWebSocketMessage;
|
||||
import com.njcn.rdms.framework.websocket.core.session.WebSocketSessionManager;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* WebSocketMessageSender 实现类
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender {
|
||||
|
||||
private final WebSocketSessionManager sessionManager;
|
||||
|
||||
@Override
|
||||
public void send(Integer userType, Long userId, String messageType, String messageContent) {
|
||||
send(null, userType, userId, messageType, messageContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(Integer userType, String messageType, String messageContent) {
|
||||
send(null, userType, null, messageType, messageContent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void send(String sessionId, String messageType, String messageContent) {
|
||||
send(sessionId, null, null, messageType, messageContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
*
|
||||
* @param sessionId Session 编号
|
||||
* @param userType 用户类型
|
||||
* @param userId 用户编号
|
||||
* @param messageType 消息类型
|
||||
* @param messageContent 消息内容
|
||||
*/
|
||||
public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) {
|
||||
// 1. 获得 Session 列表
|
||||
List<WebSocketSession> sessions = Collections.emptyList();
|
||||
if (StrUtil.isNotEmpty(sessionId)) {
|
||||
WebSocketSession session = sessionManager.getSession(sessionId);
|
||||
if (session != null) {
|
||||
sessions = Collections.singletonList(session);
|
||||
}
|
||||
} else if (userType != null && userId != null) {
|
||||
sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType, userId);
|
||||
} else if (userType != null) {
|
||||
sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType);
|
||||
}
|
||||
if (CollUtil.isEmpty(sessions)) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]",
|
||||
sessionId, userType, userId, messageType, messageContent);
|
||||
}
|
||||
}
|
||||
// 2. 执行发送
|
||||
doSend(sessions, messageType, messageContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息的具体实现
|
||||
*
|
||||
* @param sessions Session 列表
|
||||
* @param messageType 消息类型
|
||||
* @param messageContent 消息内容
|
||||
*/
|
||||
public void doSend(Collection<WebSocketSession> sessions, String messageType, String messageContent) {
|
||||
JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent);
|
||||
String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化
|
||||
sessions.forEach(session -> {
|
||||
// 1. 各种校验,保证 Session 可以被发送
|
||||
if (session == null) {
|
||||
log.error("[doSend][session 为空, message({})]", message);
|
||||
return;
|
||||
}
|
||||
if (!session.isOpen()) {
|
||||
log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message);
|
||||
return;
|
||||
}
|
||||
// 2. 执行发送
|
||||
try {
|
||||
session.sendMessage(new TextMessage(payload));
|
||||
log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message);
|
||||
} catch (IOException ex) {
|
||||
log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.njcn.rdms.framework.websocket.core.sender;
|
||||
|
||||
import com.njcn.rdms.framework.common.util.json.JsonUtils;
|
||||
|
||||
/**
|
||||
* WebSocket 消息的发送器接口
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
public interface WebSocketMessageSender {
|
||||
|
||||
/**
|
||||
* 发送消息给指定用户
|
||||
*
|
||||
* @param userType 用户类型
|
||||
* @param userId 用户编号
|
||||
* @param messageType 消息类型
|
||||
* @param messageContent 消息内容,JSON 格式
|
||||
*/
|
||||
void send(Integer userType, Long userId, String messageType, String messageContent);
|
||||
|
||||
/**
|
||||
* 发送消息给指定用户类型
|
||||
*
|
||||
* @param userType 用户类型
|
||||
* @param messageType 消息类型
|
||||
* @param messageContent 消息内容,JSON 格式
|
||||
*/
|
||||
void send(Integer userType, String messageType, String messageContent);
|
||||
|
||||
/**
|
||||
* 发送消息给指定 Session
|
||||
*
|
||||
* @param sessionId Session 编号
|
||||
* @param messageType 消息类型
|
||||
* @param messageContent 消息内容,JSON 格式
|
||||
*/
|
||||
void send(String sessionId, String messageType, String messageContent);
|
||||
|
||||
default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
|
||||
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
|
||||
}
|
||||
|
||||
default void sendObject(Integer userType, String messageType, Object messageContent) {
|
||||
send(userType, messageType, JsonUtils.toJsonString(messageContent));
|
||||
}
|
||||
|
||||
default void sendObject(String sessionId, String messageType, Object messageContent) {
|
||||
send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.njcn.rdms.framework.websocket.core.sender.local;
|
||||
|
||||
import com.njcn.rdms.framework.websocket.core.sender.AbstractWebSocketMessageSender;
|
||||
import com.njcn.rdms.framework.websocket.core.sender.WebSocketMessageSender;
|
||||
import com.njcn.rdms.framework.websocket.core.session.WebSocketSessionManager;
|
||||
|
||||
/**
|
||||
* 本地的 {@link WebSocketMessageSender} 实现类
|
||||
*
|
||||
* 注意:仅仅适合单机场景!!!
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender {
|
||||
|
||||
public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) {
|
||||
super(sessionManager);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.njcn.rdms.framework.websocket.core.session;
|
||||
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
|
||||
import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
|
||||
|
||||
/**
|
||||
* {@link WebSocketHandler} 的装饰类,实现了以下功能:
|
||||
*
|
||||
* 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理
|
||||
* 2. 封装 {@link WebSocketSession} 支持并发操作
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator {
|
||||
|
||||
/**
|
||||
* 发送时间的限制,单位:毫秒
|
||||
*/
|
||||
private static final Integer SEND_TIME_LIMIT = 1000 * 5;
|
||||
/**
|
||||
* 发送消息缓冲上线,单位:bytes
|
||||
*/
|
||||
private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100;
|
||||
|
||||
private final WebSocketSessionManager sessionManager;
|
||||
|
||||
public WebSocketSessionHandlerDecorator(WebSocketHandler delegate,
|
||||
WebSocketSessionManager sessionManager) {
|
||||
super(delegate);
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
// 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149
|
||||
session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT);
|
||||
// 添加到 WebSocketSessionManager 中
|
||||
sessionManager.addSession(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
|
||||
sessionManager.removeSession(session);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.njcn.rdms.framework.websocket.core.session;
|
||||
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* {@link WebSocketSession} 管理器的接口
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
public interface WebSocketSessionManager {
|
||||
|
||||
/**
|
||||
* 添加 Session
|
||||
*
|
||||
* @param session Session
|
||||
*/
|
||||
void addSession(WebSocketSession session);
|
||||
|
||||
/**
|
||||
* 移除 Session
|
||||
*
|
||||
* @param session Session
|
||||
*/
|
||||
void removeSession(WebSocketSession session);
|
||||
|
||||
/**
|
||||
* 获得指定编号的 Session
|
||||
*
|
||||
* @param id Session 编号
|
||||
* @return Session
|
||||
*/
|
||||
WebSocketSession getSession(String id);
|
||||
|
||||
/**
|
||||
* 获得指定用户类型的 Session 列表
|
||||
*
|
||||
* @param userType 用户类型
|
||||
* @return Session 列表
|
||||
*/
|
||||
Collection<WebSocketSession> getSessionList(Integer userType);
|
||||
|
||||
/**
|
||||
* 获得指定用户编号的 Session 列表
|
||||
*
|
||||
* @param userType 用户类型
|
||||
* @param userId 用户编号
|
||||
* @return Session 列表
|
||||
*/
|
||||
Collection<WebSocketSession> getSessionList(Integer userType, Long userId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.njcn.rdms.framework.websocket.core.session;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.njcn.rdms.framework.security.core.LoginUser;
|
||||
import com.njcn.rdms.framework.websocket.core.util.WebSocketFrameworkUtils;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
|
||||
/**
|
||||
* 默认的 {@link WebSocketSessionManager} 实现类
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
public class WebSocketSessionManagerImpl implements WebSocketSessionManager {
|
||||
|
||||
/**
|
||||
* id 与 WebSocketSession 映射
|
||||
*
|
||||
* key:Session 编号
|
||||
*/
|
||||
private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* user 与 WebSocketSession 映射
|
||||
*
|
||||
* key1:用户类型
|
||||
* key2:用户编号
|
||||
*/
|
||||
private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions
|
||||
= new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void addSession(WebSocketSession session) {
|
||||
// 添加到 idSessions 中
|
||||
idSessions.put(session.getId(), session);
|
||||
// 添加到 userSessions 中
|
||||
LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
|
||||
if (userSessionsMap == null) {
|
||||
userSessionsMap = new ConcurrentHashMap<>();
|
||||
if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) {
|
||||
userSessionsMap = userSessions.get(user.getUserType());
|
||||
}
|
||||
}
|
||||
CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
|
||||
if (sessions == null) {
|
||||
sessions = new CopyOnWriteArrayList<>();
|
||||
if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) {
|
||||
sessions = userSessionsMap.get(user.getId());
|
||||
}
|
||||
}
|
||||
sessions.add(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSession(WebSocketSession session) {
|
||||
// 移除从 idSessions 中
|
||||
idSessions.remove(session.getId());
|
||||
// 移除从 idSessions 中
|
||||
LoginUser user = WebSocketFrameworkUtils.getLoginUser(session);
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType());
|
||||
if (userSessionsMap == null) {
|
||||
return;
|
||||
}
|
||||
CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId());
|
||||
sessions.removeIf(session0 -> session0.getId().equals(session.getId()));
|
||||
if (CollUtil.isEmpty(sessions)) {
|
||||
userSessionsMap.remove(user.getId(), sessions);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebSocketSession getSession(String id) {
|
||||
return idSessions.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<WebSocketSession> getSessionList(Integer userType) {
|
||||
ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
|
||||
if (CollUtil.isEmpty(userSessionsMap)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容
|
||||
for (List<WebSocketSession> sessions : userSessionsMap.values()) {
|
||||
if (CollUtil.isEmpty(sessions)) {
|
||||
continue;
|
||||
}
|
||||
result.addAll(sessions);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<WebSocketSession> getSessionList(Integer userType, Long userId) {
|
||||
ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType);
|
||||
if (CollUtil.isEmpty(userSessionsMap)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(userId);
|
||||
return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.njcn.rdms.framework.websocket.core.util;
|
||||
|
||||
import com.njcn.rdms.framework.security.core.LoginUser;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 专属于 web 包的工具类
|
||||
*
|
||||
* @author hongawen
|
||||
*/
|
||||
public class WebSocketFrameworkUtils {
|
||||
|
||||
public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER";
|
||||
|
||||
/**
|
||||
* 设置当前用户
|
||||
*
|
||||
* @param loginUser 登录用户
|
||||
* @param attributes Session
|
||||
*/
|
||||
public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) {
|
||||
attributes.put(ATTRIBUTE_LOGIN_USER, loginUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户
|
||||
*
|
||||
* @return 当前用户
|
||||
*/
|
||||
public static LoginUser getLoginUser(WebSocketSession session) {
|
||||
return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的编号
|
||||
*
|
||||
* @return 用户编号
|
||||
*/
|
||||
public static Long getLoginUserId(WebSocketSession session) {
|
||||
LoginUser loginUser = getLoginUser(session);
|
||||
return loginUser != null ? loginUser.getId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的类型
|
||||
*
|
||||
* @return 用户编号
|
||||
*/
|
||||
public static Integer getLoginUserType(WebSocketSession session) {
|
||||
LoginUser loginUser = getLoginUser(session);
|
||||
return loginUser != null ? loginUser.getUserType() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得当前用户的租户编号
|
||||
*
|
||||
* @param session Session
|
||||
* @return 租户编号
|
||||
*/
|
||||
public static Long getTenantId(WebSocketSession session) {
|
||||
LoginUser loginUser = getLoginUser(session);
|
||||
return loginUser != null ? loginUser.getTenantId() : null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.njcn.rdms.framework.websocket.config.RdmsWebSocketAutoConfiguration
|
||||
Reference in New Issue
Block a user