java每日精进 5.11【WebSocket】

1.纯Websocket实现消息发送

1.1一对一发送

前端

  1. 用户在输入框输入消息内容(sendText)

  2. 选择特定接收用户(sendUserId)

  3. 点击发送按钮触发handlerSend方法

  4. 构造消息内容JSON:

    复制代码
    {
      text: "Hello", // 消息内容
      toUserId: 123   // 目标用户ID
    }
  5. 包装为WebSocket标准格式:

    复制代码
    {
      type: "demo-message-send", // 消息类型
      content: '{"text":"Hello","toUserId":123}' // 字符串化的内容
    }
  6. 通过send()方法发送

  • 前端在setup函数中,使用useWebSocket方法,根据server变量(WebSocket 服务地址)建立连接。server地址由VITE_BASE_URL(环境变量)、/infra/ws路径和token(通过getRefreshToken获取)组成。
  • 设置autoReconnecttrue,表示自动重连;heartbeattrue,表示开启心跳机制。
  • 当用户在前端输入消息并点击发送按钮时,handlerSend函数被调用。
  • 首先将发送内容sendText和接收用户sendUserId进行 JSON 化处理,构建消息内容messageContent
  • 然后将消息类型typedemo-message-send)和消息内容messageContent再次 JSON 化,形成最终的消息jsonMessage
  • 最后使用send函数将jsonMessage发送到后端。
javascript 复制代码
const server = ref(
  (import.meta.env.VITE_BASE_URL + '/infra/ws').replace('http', 'ws') +
    '?token=' +
    getRefreshToken() // 使用 getRefreshToken() 方法,而不使用 getAccessToken() 方法的原因:WebSocket 无法方便的刷新访问令牌
) // WebSocket 服务地址
const getIsOpen = computed(() => status.value === 'OPEN') // WebSocket 连接是否打开
const getTagColor = computed(() => (getIsOpen.value ? 'success' : 'red')) // WebSocket 连接的展示颜色

/** 发起 WebSocket 连接 */
const { status, data, send, close, open } = useWebSocket(server.value, {
  autoReconnect: true,
  heartbeat: true
})
javascript 复制代码
/** 发送消息 */
const sendText = ref('') // 发送内容
const sendUserId = ref('') // 发送人
const handlerSend = () => {
  // 1.1 先 JSON 化 message 消息内容
  const messageContent = JSON.stringify({
    text: sendText.value,
    toUserId: sendUserId.value
  })
  // 1.2 再 JSON 化整个消息
  const jsonMessage = JSON.stringify({
    type: 'demo-message-send',
    content: messageContent
  })
  // 2. 最后发送消息
  send(jsonMessage)
  sendText.value = ''
}

后端

  • 注册监听器DemoWebSocketMessageListener 类通过实现 WebSocketMessageListener<DemoSendMessage> 接口,并使用 @Component 注解将自己注册为 Spring Bean。框架启动时会扫描所有实现了该接口的 Bean,并将它们注册到消息处理器中。
java 复制代码
/**
 * WebSocket 示例:单发消息
 */
@Component
public class DemoWebSocketMessageListener implements WebSocketMessageListener<DemoSendMessage> {

    @Resource
    private WebSocketMessageSender webSocketMessageSender;

    @Override
    public void onMessage(WebSocketSession session, DemoSendMessage message) {
        Long fromUserId = WebSocketFrameworkUtils.getLoginUserId(session);
        // 情况一:单发
        if (message.getToUserId() != null) {
            DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId)
                    .setText(message.getText()).setSingle(true);
            webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getToUserId(), // 给指定用户
                    "demo-message-receive", toMessage);
            return;
        }
        // 情况二:群发
        DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId)
                .setText(message.getText()).setSingle(false);
        webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), // 给所有用户
                "demo-message-receive", toMessage);
    }

    @Override
    public String getType() {
        return "demo-message-send";
    }

}
  • 消息类型绑定getType() 方法返回 "demo-message-send",这表明该监听器专门处理类型为 "demo-message-send" 的消息。当后端接收到消息时,会根据消息类型路由到对应的监听器。

当 WebSocket 服务器接收到消息后:

  1. 消息解析 :框架首先解析消息的 JSON 格式,提取 type 字段(如 "demo-message-send")。
  2. 类型匹配 :后端框架会自动将 type"demo-message-send" 的消息路由到 DemoWebSocketMessageListeneronMessage 方法。
  3. 调用回调 :将消息反序列化为 DemoSendMessage 对象,并调用监听器的 onMessage 方法。
java 复制代码
/**
 * JSON 格式 {@link WebSocketHandler} 实现类
 * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。
 */
@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 -> {
                    // 将监听器的类型(通过 getType() 方法获取)作为键,监听器实例作为值,存入 listeners 映射中
                    listeners.put(listener.getType(), listener);
                });
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        // 1.1 空消息,跳过
        // 如果消息的负载长度为 0,说明是一个空消息,直接返回,不进行后续处理
        if (message.getPayloadLength() == 0) {
            return;
        }
        // 1.2 ping 心跳消息,直接返回 pong 消息。
        // 如果消息的负载长度为 4 且负载内容为 "ping",则向客户端发送 "pong" 消息,表示响应心跳
        if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) {
            session.sendMessage(new TextMessage("pong"));
            return;
        }

        // 2.1 解析消息
        try {
            // 将文本消息的负载解析为 JsonWebSocketMessage 对象
            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
            // 根据消息类型从 listeners 映射中获取对应的监听器
            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);
            // 获取当前会话的租户 ID
            Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
            // 执行租户相关的操作,调用监听器的 onMessage 方法处理消息
            TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
        } catch (Throwable ex) {
            // 如果在处理消息过程中发生异常,记录错误日志
            log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
        }
    }

}

WebSocketMessageListener 之所以能监听消息,是因为:

  1. 接口契约:实现 WebSocketMessageListener 接口并指定消息类型(getType())。
  2. 框架支持:Spring 框架自动扫描并注册监听器,实现消息的解析和分发。
  3. 类型匹配:前端发送的消息 type 与后端监听器的 getType() 一致,触发回调。

这个过程类似于 HTTP 请求的路由机制,只不过 WebSocket 是长连接,需要持续监听消息。

通常,WebSocket 框架(如 Spring WebSocket)会提供以下核心组件:

  • 消息解码器 :将二进制数据转换为 Java 对象(如 DemoSendMessage)。
  • 消息路由器:根据消息类型将消息路由到对应的监听器。
  • 会话管理器 :维护所有 WebSocket 会话(WebSocketSession),并提供获取用户信息的工具(如 WebSocketFrameworkUtils.getLoginUserId)。
  • 后端的DemoWebSocketMessageListener类实现了WebSocketMessageListener接口的onMessage方法。
  • 当有消息到达时,onMessage方法被调用,从WebSocketSession中获取登录用户 ID(fromUserId)。
  • 根据消息中的toUserId判断是单发还是群发:
    • 如果toUserId不为空,则创建DemoReceiveMessage对象,设置fromUserIdtextsingletrue,通过webSocketMessageSendersendObject方法将消息发送给指定用户。
    • 如果toUserId为空,则创建DemoReceiveMessage对象,设置fromUserIdtextsinglefalse,通过webSocketMessageSendersendObject方法将消息发送给所有用户。
  1. JsonWebSocketMessageHandler接收并解析消息

  2. 根据type="demo-message-send"找到DemoWebSocketMessageListener

  3. 调用onMessage方法:

    • 从Session中获取发送者ID(fromUserId)

    • 检查message.getToUserId()不为null,进入单发逻辑

  4. 构造响应消息:

    复制代码
    new DemoReceiveMessage()
      .setFromUserId(fromUserId)
      .setText(message.getText())
      .setSingle(true)
  5. 通过webSocketMessageSender发送给指定用户:

    复制代码
    webSocketMessageSender.sendObject(
      UserTypeEnum.ADMIN.getValue(), // 用户类型
      message.getToUserId(),         // 目标用户ID
      "demo-message-receive",       // 消息类型
      toMessage                    // 消息内容
    )

实际示例:

  • 用户A(ID:100)发送"下午开会"给用户B(ID:101)

  • 前端发送:

    java 复制代码
    {"type":"demo-message-send","content":"{\"text\":\"下午开会\",\"toUserId\":101}"}
  • 后端处理后发送给用户B:

    复制代码
    {"type":"demo-message-receive","content":"{\"fromUserId\":100,\"text\":\"下午开会\",\"single\":true}"}

1.2一对多发送

前端

  1. 用户在输入框输入消息内容(sendText)

  2. 不选择特定用户(或选择"所有人")

  3. 点击发送按钮触发handlerSend方法

  4. 构造消息内容JSON:

    复制代码
    {
      text: "系统维护通知", // 消息内容
      toUserId: ""      // 空表示群发
    }
  5. 包装为WebSocket标准格式并发送

后端

  1. 同上接收解析流程

  2. onMessage方法中检查message.getToUserId()为null,进入群发逻辑

  3. 构造响应消息:

    复制代码
    new DemoReceiveMessage()
      .setFromUserId(fromUserId)
      .setText(message.getText())
      .setSingle(false)
  4. 通过webSocketMessageSender发送给所有用户:

    复制代码
    webSocketMessageSender.sendObject(
      UserTypeEnum.ADMIN.getValue(), // 用户类型
      "demo-message-receive",       // 消息类型
      toMessage                    // 消息内容
    )

实际示例:

  • 管理员发送"系统即将升级"给所有用户

  • 前端发送:

    java 复制代码
    {"type":"demo-message-send","content":"{\"text\":\"系统即将升级\",\"toUserId\":\"\"}"}

2.总结及类补充

后端代码

配置类:

  • YudaoWebSocketAutoConfiguration: 配置 WebSocket 端点、拦截器、会话管理和消息发送器,支持多种发送类型(local, redis, rocketmq, rabbitmq, kafka)。
  • 条件注解 @ConditionalOnProperty 允许通过配置启用/禁用 WebSocket 或切换发送类型。
  • 注册 WebSocketConfigurer、HandshakeInterceptor、WebSocketHandler 和 WebSocketSessionManager。
复制代码
 ```java
 /**
  * WebSocket 自动配置类
  * 负责 WebSocket 服务的初始化和相关组件的注册
  */
 @AutoConfiguration(before = YudaoRedisMQConsumerAutoConfiguration.class) 
 // 在 YudaoRedisMQConsumerAutoConfiguration 之前加载,确保 RedisWebSocketMessageConsumer 先创建
 @EnableWebSocket // 启用 Spring WebSocket 支持
 @ConditionalOnProperty(prefix = "moyun.websocket", value = "enable", matchIfMissing = true) 
 // 通过配置项 moyun.websocket.enable 控制是否启用 WebSocket,默认启用
 @EnableConfigurationProperties(WebSocketProperties.class) // 启用 WebSocket 配置属性类
 public class YudaoWebSocketAutoConfiguration {

     /**
      * 配置 WebSocket 处理器和握手拦截器
      */
     @Bean
     public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors,
                                                    WebSocketHandler webSocketHandler,
                                                    WebSocketProperties webSocketProperties) {
         return registry -> registry
                 // 注册 WebSocket 处理器并指定连接路径
                 .addHandler(webSocketHandler, webSocketProperties.getPath())
                 // 添加握手拦截器,用于验证和预处理
                 .addInterceptors(handshakeInterceptors)
                 // 允许所有域名跨域访问,否则前端连接会被阻止
                 .setAllowedOriginPatterns("*");
     }

     /**
      * 创建握手拦截器,用于在 WebSocket 握手阶段进行用户认证和权限检查
      */
     @Bean
     public HandshakeInterceptor handshakeInterceptor() {
         return new LoginUserHandshakeInterceptor();
     }

     /**
      * 创建 WebSocket 消息处理器
      * 包装 JsonWebSocketMessageHandler 并添加会话管理功能
      */
     @Bean
     public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager,
                                              List<? extends WebSocketMessageListener<?>> messageListeners) {
         // 1. 创建消息处理器,负责消息类型路由和分发
         JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners);
         // 2. 包装消息处理器,添加会话管理功能(如连接建立和关闭时的回调)
         return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager);
     }

     /**
      * 创建 WebSocket 会话管理器,用于管理所有活动的 WebSocket 会话
      */
     @Bean
     public WebSocketSessionManager webSocketSessionManager() {
         return new WebSocketSessionManagerImpl();
     }

     /**
      * 创建 WebSocket 请求授权自定义器,用于配置安全规则
      */
     @Bean
     public WebSocketAuthorizeRequestsCustomizer webSocketAuthorizeRequestsCustomizer(WebSocketProperties webSocketProperties) {
         return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
     }

     // ==================== 消息发送器配置 ====================

     /**
      * 本地模式消息发送器配置(单节点部署)
      */
     @Configuration
     @ConditionalOnProperty(prefix = "moyun.websocket", name = "sender-type", havingValue = "local")
     public class LocalWebSocketMessageSenderConfiguration {

         @Bean
         public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) {
             return new LocalWebSocketMessageSender(sessionManager);
         }

     }

     /**
      * Redis 模式消息发送器配置(分布式部署)
      */
     @Configuration
     @ConditionalOnProperty(prefix = "moyun.websocket", name = "sender-type", havingValue = "redis")
     public class RedisWebSocketMessageSenderConfiguration {

         @Bean
         public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager,
                                                                        RedisMQTemplate redisMQTemplate) {
             return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate);
         }

         @Bean
         public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer(
                 RedisWebSocketMessageSender redisWebSocketMessageSender) {
             return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender);
         }

     }

     /**
      * RocketMQ 模式消息发送器配置
      */
     @Configuration
     @ConditionalOnProperty(prefix = "moyun.websocket", name = "sender-type", havingValue = "rocketmq")
     public class RocketMQWebSocketMessageSenderConfiguration {

         @Bean
         public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender(
                 WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate,
                 @Value("${moyun.websocket.sender-rocketmq.topic}") String topic) {
             return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic);
         }

         @Bean
         public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer(
                 RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) {
             return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender);
         }

     }

     /**
      * RabbitMQ 模式消息发送器配置
      */
     @Configuration
     @ConditionalOnProperty(prefix = "moyun.websocket", name = "sender-type", havingValue = "rabbitmq")
     public class RabbitMQWebSocketMessageSenderConfiguration {

         @Bean
         public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender(
                 WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate,
                 TopicExchange websocketTopicExchange) {
             return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange);
         }

         @Bean
         public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer(
                 RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) {
             return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender);
         }

         /**
          * 创建 RabbitMQ 主题交换机,用于消息广播
          */
         @Bean
         public TopicExchange websocketTopicExchange(@Value("${moyun.websocket.sender-rabbitmq.exchange}") String exchange) {
             return new TopicExchange(exchange,
                     true,  // durable: 持久化交换机,重启后不丢失
                     false); // exclusive: 非排他性,允许多个连接使用
         }

     }

     /**
      * Kafka 模式消息发送器配置
      */
     @Configuration
     @ConditionalOnProperty(prefix = "moyun.websocket", name = "sender-type", havingValue = "kafka")
     public class KafkaWebSocketMessageSenderConfiguration {

         @Bean
         public KafkaWebSocketMessageSender kafkaWebSocketMessageSender(
                 WebSocketSessionManager sessionManager, KafkaTemplate<Object, Object> kafkaTemplate,
                 @Value("${moyun.websocket.sender-kafka.topic}") String topic) {
             return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic);
         }

         @Bean
         public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer(
                 KafkaWebSocketMessageSender kafkaWebSocketMessageSender) {
             return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender);
         }

     }

 }
 ```

认证与拦截:

  • TokenAuthenticationFilter: 解析 WebSocket URL 中的 token 参数,验证用户身份,构建 LoginUser 并存储到 Spring Security 上下文中。
复制代码
 ```java
 /**
  * Token 过滤器,验证 token 的有效性
  * 验证通过后,获得 {@link LoginUser} 信息,并加入到 Spring Security 上下文
  */
 @RequiredArgsConstructor
 public class TokenAuthenticationFilter extends OncePerRequestFilter {

     private final SecurityProperties securityProperties;

     private final GlobalExceptionHandler globalExceptionHandler;

     private final OAuth2TokenApi oauth2TokenApi;

     @Override
     @SuppressWarnings("NullableProblems")
     protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
             throws ServletException, IOException {
         String token = SecurityFrameworkUtils.obtainAuthorization(request,
                 securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
         if (StrUtil.isNotEmpty(token)) {
             Integer userType = WebFrameworkUtils.getLoginUserType(request);
             try {
                 // 1.1 基于 token 构建登录用户
                 LoginUser loginUser = buildLoginUserByToken(token, userType);
                 // 1.2 模拟 Login 功能,方便日常开发调试
                 if (loginUser == null) {
                     loginUser = mockLoginUser(request, token, userType);
                 }

                 // 2. 设置当前用户
                 if (loginUser != null) {
                     SecurityFrameworkUtils.setLoginUser(loginUser, request);
                 }
             } catch (Throwable ex) {
                 CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);
                 ServletUtils.writeJSON(response, result);
                 return;
             }
         }

         // 继续过滤链
         chain.doFilter(request, response);
     }

     private LoginUser buildLoginUserByToken(String token, Integer userType) {
         try {
             OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token);
             if (accessToken == null) {
                 return null;
             }
             // 用户类型不匹配,无权限
             // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型
             // 类似 WebSocket 的 /ws/* 连接地址,是不需要比对用户类型的
             if (userType != null
                     && ObjectUtil.notEqual(accessToken.getUserType(), userType)) {
                 throw new AccessDeniedException("错误的用户类型");
             }
             // 构建登录用户
             return new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
                     .setInfo(accessToken.getUserInfo()) // 额外的用户信息
                     .setTenantId(accessToken.getTenantId()).setScopes(accessToken.getScopes())
                     .setExpiresTime(accessToken.getExpiresTime());
         } catch (ServiceException serviceException) {
             // 校验 Token 不通过时,考虑到一些接口是无需登录的,所以直接返回 null 即可
             return null;
         }
     }

     /**
      * 模拟登录用户,方便日常开发调试
      *
      * 注意,在线上环境下,一定要关闭该功能!!!
      *
      * @param request 请求
      * @param token 模拟的 token,格式为 {@link SecurityProperties#getMockSecret()} + 用户编号
      * @param userType 用户类型
      * @return 模拟的 LoginUser
      */
     private LoginUser mockLoginUser(HttpServletRequest request, String token, Integer userType) {
         if (!securityProperties.getMockEnable()) {
             return null;
         }
         // 必须以 mockSecret 开头
         if (!token.startsWith(securityProperties.getMockSecret())) {
             return null;
         }
         // 构建模拟用户
         Long userId = Long.valueOf(token.substring(securityProperties.getMockSecret().length()));
         return new LoginUser().setId(userId).setUserType(userType)
                 .setTenantId(WebFrameworkUtils.getTenantId(request));
     }

 }
 ```
  • LoginUserHandshakeInterceptor: 在 WebSocket 握手阶段将 LoginUser 存入 WebSocketSession 的 attributes。
复制代码
 ```java
 /**
  * 登录用户的 {@link HandshakeInterceptor} 实现类
  *
  * 流程如下:
  * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过
  * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中
  */
 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
     }

 }
 ```

会话管理:

  • WebSocketSessionHandlerDecorator: 装饰 WebSocketHandler,在连接建立/关闭时管理 WebSocketSession。
复制代码
 ```java
 /**
  * {@link WebSocketHandler} 的装饰类,实现了以下功能:
  *
  * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理
  * 2. 封装 {@link WebSocketSession} 支持并发操作
  */
 public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator {

     /**
      * 发送时间的限制,单位:毫秒
      * 这里定义了发送消息的时间限制为 5 秒(1000 毫秒 * 5),用于控制消息发送的时间范围,
      * 可能是为了防止长时间的消息发送操作,避免资源占用或其他潜在问题。
      */
     private static final Integer SEND_TIME_LIMIT = 1000 * 5;
     /**
      * 发送消息缓冲上限,单位:bytes
      * 定义了发送消息的缓冲大小上限为 1024 * 100 字节,用于限制消息缓冲的大小,
      * 防止缓冲过大导致内存占用过多等问题。
      */
     private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100;

     // WebSocket 会话管理器,用于管理 WebSocket 会话
     private final WebSocketSessionManager sessionManager;

     /**
      * 构造函数,接收被装饰的 WebSocketHandler 和 WebSocketSessionManager
      *
      * @param delegate      被装饰的 WebSocketHandler
      * @param sessionManager WebSocket 会话管理器
      */
     public WebSocketSessionHandlerDecorator(WebSocketHandler delegate,
                                             WebSocketSessionManager sessionManager) {
         // 调用父类构造函数,传入被装饰的 WebSocketHandler
         super(delegate);
         // 初始化会话管理器
         this.sessionManager = sessionManager;
     }

     /**
      * 当 WebSocket 连接建立时调用此方法
      *
      * @param session WebSocket 会话
      */
     @Override
     public void afterConnectionEstablished(WebSocketSession session) {
         // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149
         // 使用定义的时间限制和缓冲大小限制,创建一个支持并发的 WebSocketSession 装饰类实例
         session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT);
         // 将新的会话添加到 WebSocketSessionManager 中进行管理
         sessionManager.addSession(session);
     }

     /**
      * 当 WebSocket 连接关闭时调用此方法
      *
      * @param session    WebSocket 会话
      * @param closeStatus 关闭状态
      */
     @Override
     public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
         // 从 WebSocketSessionManager 中移除对应的会话,完成连接关闭时的会话管理操作
         sessionManager.removeSession(session);
     }
 }
 ```
  • WebSocketSessionManager 和 WebSocketSessionManagerImpl:管理会话,支持按用户类型和 ID 查询。
复制代码
 ```java
 /**
  * {@link WebSocketSession} 管理器的接口
  */
 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);

 }
 ```

 ```java
 /**
  * 默认的 {@link WebSocketSessionManager} 实现类
  */
 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<>(); // 避免扩容
         Long contextTenantId = TenantContextHolder.getTenantId();
         for (List<WebSocketSession> sessions : userSessionsMap.values()) {
             if (CollUtil.isEmpty(sessions)) {
                 continue;
             }
             // 特殊:如果租户不匹配,则直接排除
             if (contextTenantId != null) {
                 Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0));
                 if (!contextTenantId.equals(userTenantId)) {
                     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<>();
     }

 }
 ```

消息处理:

  • JsonWebSocketMessageHandler:解析 JsonWebSocketMessage,根据 type 分发给 WebSocketMessageListener。
  • DemoWebSocketMessageListener: 处理 demo-message-send 类型的消息,支持单发和群发。
  • JsonWebSocketMessage:包含 type 和 content 字段。

消息发送:

  • WebSocketMessageSender: 定义消息发送接口。
  • AbstractWebSocketMessageSender: 实现消息发送逻辑,查询会话并发送 JsonWebSocketMessage。
  • LocalWebSocketMessageSender: 本地发送实现,适合单机场景。
  1. 配置websocket的Ss权限
java 复制代码
/**
 * WebSocket 的权限自定义
 * 负责为 WebSocket 端点的 HTTP 握手请求配置 Spring Security 权限,通过 permitAll() 确保握手请求不被阻止
 * 同时保留自定义的 token 认证逻辑(由 TokenAuthenticationFilter 和 LoginUserHandshakeInterceptor 处理)
 * 它解决了 Spring Security 对 WebSocket 握手请求的限制问题,是集成 Spring Security 的 WebSocket 功能的关键组件
 */
@RequiredArgsConstructor
public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer {

    private final WebSocketProperties webSocketProperties;

    @Override
    public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
        registry.requestMatchers(webSocketProperties.getPath()).permitAll();
    }

}

前端代码

  • InfraWebSocket.vue: 使用 @vueuse/core 的 useWebSocket 建立连接,发送/接收 JsonWebSocketMessage,支持单发和群发消息。
  • 依赖用户列表 API(UserApi.getSimpleUserList)和 token 获取逻辑(getRefreshToken)。
相关推荐
bing_15825 分钟前
什么是IoT长连接服务?
网络·物联网·长连接服务
伊成25 分钟前
一文详解Spring Boot如何配置日志
java·spring boot·单元测试
学渣y26 分钟前
React状态管理-对state进行保留和重置
javascript·react.js·ecmascript
lybugproducer33 分钟前
浅谈 Redis 数据类型
java·数据库·redis·后端·链表·缓存
christine-rr33 分钟前
【25软考网工】第六章(4)VPN虚拟专用网 L2TP、PPTP、PPP认证方式;IPSec、GRE
运维·网络·网络协议·网络工程师·ip·软考·考试
小白自救计划37 分钟前
网络协议分析 实验四 ICMPv4与ICMPv6
网络·网络协议
_龙衣1 小时前
将 swagger 接口导入 apifox 查看及调试
前端·javascript·css·vue.js·css3
purrrew1 小时前
【Java ee初阶】网络编程 UDP socket
java·网络·网络协议·udp·java-ee
上海合宙LuatOS1 小时前
全栈工程师实战手册:LuatOS日志系统开发指南!
java·开发语言·单片机·嵌入式硬件·物联网·php·硬件工程
多敲代码防脱发1 小时前
导出导入Excel文件(详解-基于EasyExcel)
java·开发语言·jvm·数据库·mysql·excel