Spring WebSocket 服务实现的主流方案与最佳实践

本文主要介绍了在 Spring 框架中实现 WebSocket 服务的几种解决方案,并提供了 Spring WebSocket 最佳实践,以及需要注意的问题。

WebSocket 实现方案概述

在 Spring 项目中实现 WebSocket 服务一般有如下几种解决方案:

  • Spring-WebSocket 模块:Spring 官方提供的原生支持,与 Spring 生态深度整合。
  • Jakarta EE 规范 API:基于 Java EE 标准的 WebSocket 实现,适用于兼容 Jakarta EE 的容器。
  • Netty 实现:基于高性能网络框架 Netty 自定义开发,灵活性高但开发成本较大

本文重点探讨前两种主流方案的实现与实践。

Jakarta EE WebSocket

启动 WebScoket 支持

通过配置 ServerEndpointExporter 扫描并注册所有带有 @ServerEndpoint 注解的端点:

java 复制代码
@Configuration  
public class WebSocketConfig {
    
   /**
     * ServerEndpointExporter类的作用是,会扫描所有的服务器端点,
     * 把带有@ServerEndpoint 注解的所有类都添加进来
     */
    @Bean  
    public ServerEndpointExporter serverEndpointExporter() {  
        return new ServerEndpointExporter();  
    }  
} 

WebSocketServer

WebSocketServer 类相当于 WS 协议的控制器,通过 @ServerEndpoint@Component 注解启用,并实现生命周期方法:@OnOpen@OnClose@OnMessage等方法。

  • @OnOpen:当WebSocket建立连接时,会触发这个注解修饰的方法。
  • @onClose: 当WebSocket关闭连接时,会触发这个注解修饰的方法。
  • @onMessage: 当WebSocket接收到消息时,会触发这个注解修饰的方法。
java 复制代码
/**
 * 消息中心 websocket 连接
 */
@ServerEndpoint("/subscribe/{userName}")
@CrossOrigin
@Component
@Slf4j
public class MessageWsServer {

    /**
     * key: userName
     * value: 连接的客户端
     */
    @Getter
    private static final Map<String, CopyOnWriteArraySet<Session>> clients = new ConcurrentHashMap<>();
    /**
     * 当前在线连接数统计。线程安全
     */
    private static final AtomicInteger onlineCount = new AtomicInteger(0);

    public static <T> void sendToAllClientByUserName(String userName, WsMessage<T> message) {
        CopyOnWriteArraySet<Session> sessions = clients.get(userName);
        if (sessions != null) {
            final Iterator<Session> iterator = sessions.iterator();
            while (iterator.hasNext()) {
                Session session = iterator.next();
                if (!session.isOpen()) {
                    iterator.remove();
                    log.warn("{} 的 session 关闭, 数据无法发送", userName);
                    continue;
                }
                sendMessage(session, JSON.toJSONString(message));
            }
        } else {
            log.warn("{} 没有在线的客户端", userName);
        }
    }

    public static int getOnlineCount() {
        return onlineCount.get();
    }

    private static void sendMessage(Session session, String message) {
        try {
            session.getBasicRemote().sendText(message);
        } catch (Exception e) {
            log.error("WebSocket 数据发送异常:{}", e.getMessage());
        }
    }

    @OnOpen
    public void onOpen(Session session, @PathParam("userName") String param) {
        Collection<Session> list = clients.computeIfAbsent(param, c -> new CopyOnWriteArraySet<>());
        list.add(session);
        incrementCount(param);
    }

    @OnMessage
    public void onMessage(Session session, @PathParam("userName") String param, String message) {
        log.info("WebSocket 收到 {} 客户端发来的消息: {}", param, message);
        try {
            session.getBasicRemote().sendText("ok");
        } catch (Exception e) {
            log.error(e.toString());
        }
    }

    @OnError
    public void onError(Session session, Throwable error) {
        log.error("WebSocket 连接发生未知错误", error);
    }

    @OnClose
    public void onClose(Session session, @PathParam("userName") String param) {
        Collection<Session> list = clients.get(param);
        if (CollUtil.isNotEmpty(list) && (list.remove(session))) {
            decrementCount(param);
        }
    }

    private void incrementCount(String param) {
        onlineCount.incrementAndGet();
        log.info("{} 建立新的连接, 当前在线客户端总数: {}", param, getOnlineCount());
    }

    private void decrementCount(String param) {
        onlineCount.decrementAndGet();
        log.info("{} 连接断开, 当前在线客户端总数: {}", param, getOnlineCount());
    }
}

会话管理 :使用 ConcurrentHashMapCopyOnWriteArraySet 存储用户会话,保证多线程环境下的安全操作。

生命周期方法

  • @OnOpen:连接建立时将会话加入集合,并更新在线计数。
  • @OnClose:连接关闭时移除会话,并更新在线计数。
  • @OnMessage:接收消息后回复确认,并记录日志。

消息发送:支持向指定用户的所有在线客户端发送消息,自动过滤已关闭的会话

跨域问题

如果您想要使用@ServerEndpoint来创建WebSocket服务端,并且允许来自不同源的客户端连接,您可能需要配合@CrossOrigin使用,否则可能会遇到跨域问题。

高并发问题

在高并发下的问题,如果你同时向在线的 3 个 WebSocket 在线客户端发送消息,即广播所有在线用户(目前是 3 个),每个用户每秒10条,那就是说,你每秒要发送 30 条数据,我们调用上述的 sendText() 方法,有时候会出现

ini 复制代码
java.lang.IllegalStateException: 远程 endpoint 处于 [xxxxxx] 状态,如:
The remote endpoint was in state [TEXT_FULL_WRITING] which is an invalid state for calle

这是因为在高并发的情况下,出现了 session 抢占的问题,导致 session 的状态不一致,所以这里需要加锁操作

Spring WebSocket(推荐用法)

在介绍 Spring WebSocket 中我会拿出已经实现的封装,目前来看还是够用的,所以配置代码会相对较多,而不是简单的配置

首先我们需要集成 Spring Websocket 的 starter 包

xml 复制代码
<dependency>  
  <groupId>org.springframework.boot</groupId>  
  <artifactId>spring-boot-starter-websocket</artifactId>  
</dependency> 

配置 WebSocketConfig

新增可配置属性类 WebSocketProperties

java 复制代码
@Data
@ConfigurationProperties("allin.ws")
public class WebSocketProperties {

    /**
     * 发送时间的限制,默认3秒,单位:毫秒
     */
    private Integer sendTimeLimit = 1000 * 3;

    /**
     * 发送消息缓冲上线,5MB
     */
    private Integer bufferSizeLimit = 1024 * 1024 * 5;

    /**
     * 核心线程池数量,默认10个
     */
    private Integer coreThreadSize = 10;

    /**
     * 最大线程池数量,默认50个
     */
    private Integer maxThreadSize = 50;

    /**
     * 消息队列容量,默认100
     */
    private Integer queueCapacity = 100;
}

新增配置类,实现 WebSocketConfigurer ,主要配置 websocket 的注册连接地址。

  • 通过注入 List<CustomParamWebSocketHandler> 自动收集所有注册的 WebSocket 处理器
  • defaultWebSocketConfigurer方法中遍历所有处理器,并根据各自的 urlPath 进行注册
java 复制代码
/**
 * 开启 websocket
 *
 */
@Slf4j
@EnableConfigurationProperties(WebSocketProperties.class)
@EnableWebSocket
@Configuration
public class WebSocketAutoConfiguration implements InitializingBean {

    private final WebSocketProperties webSocketProperties;

    private final TokenApi tokenApi;

    public WebSocketAutoConfiguration(WebSocketProperties webSocketProperties,
                                      TokenApi tokenApi) {
        this.webSocketProperties = webSocketProperties;
        this.tokenApi = tokenApi;
    }

    @Bean
    public WebSocketConfigurer defaultWebSocketConfigurer(List<CustomParamWebSocketHandler> customParamWebSocketHandlers) {
        return registry -> {
            for (CustomParamWebSocketHandler customParamWebSocketHandler : customParamWebSocketHandlers) {
                registry.addHandler(customParamWebSocketHandler, customParamWebSocketHandler.getUrlPath())
                        .setAllowedOrigins("*");
                log.info("注册 WebSocketHandler, 连接路径:{}, 路径模板:{}, 连接参数:{}",
                        customParamWebSocketHandler.getUrlPath(),
                        customParamWebSocketHandler.getUriTemplate(),
                        customParamWebSocketHandler.getParamKey());
            }
        };
    }

    
    @Primary
    @Bean("defaultWebSocketHandler")
    public CustomParamWebSocketHandler customParamWebSocketHandler() {
        return new CustomParamWebSocketHandler(webSocketProperties);
    }

    @Override
    public void afterPropertiesSet() {
        // 初始化ws消息发送的线程池
        WebSocketMessageSender.initializeExecutor(webSocketProperties);
    }
}

封装通用的处理器 WebSocketHandler

WebSocketHandler 就是监听 websocket 连接之后的操作,也是上面继承的TextWebSocketHandle,我们只要在原有的基础上进行业务处理就行了。

它提供了一些方法来处理 WebSocket 会话的各个阶段,使用只要继承 TextWebSocketHandler 类就行。

  • afterConnectionEstablished():当客户端建立连接时调用,用于执行连接建立后的操作。
  • handleTextMessage():当接收到消息时调用,用于处理客户端发送的消息
  • handleTransportError():当连接发生错误时调用,用于处理连接错误
  • afterConnectionClosed():当连接关闭时调用,用于执行连接关闭后的操作。

在这里我们继承了TextWebSocketHandler并实现了部分封装,添加了 urlPath 和 paramKey 属性,分别用于指定 WebSocket 的 URL 路径和参数名,这样就可以根据注释中的部分实现复用

java 复制代码
/**
 * 复用示例
 */
@Configuration
public class MyWebSocketConfig {

    @Bean
    public CustomParamWebSocketHandler userWebSocketHandler(WebSocketProperties properties) {
        // 自定义URL路径、URI模板和参数名
        return new CustomParamWebSocketHandler(
            properties,
            "/user/*",           // URL路径模式
            "/user/{userId}",    // URI模板
            "userId"             // 参数名
        );
    }

    @Bean
    public CustomParamWebSocketHandler roomWebSocketHandler(WebSocketProperties properties) {
        return new CustomParamWebSocketHandler(
            properties,
            "/room/*",
            "/room/{roomId}",
            "roomId"
        );
    }
}

/**
 * 自定义参数发送信息的WebSocket
 */
@Slf4j
public class CustomParamWebSocketHandler extends TextWebSocketHandler implements ApplicationContextAware {

    @Getter
    private final WebSocketSessionManager sessionManager;

    @Getter
    private final WebSocketMessageSender sender;

    private final WebSocketProperties properties;

    @Getter
    private final String uriTemplate;

    @Getter
    private final String urlPath;

    /**
     * 用于标识 WebSocket 连接的主键参数名
     * 默认为"param",可以通过构造函数自定义
     */
    @Getter
    private final String paramKey;

    /**
     * Spring 事件发布器,用于发布 WebSocket 消息事件
     */
    private ApplicationEventPublisher eventPublisher;

    /**
     * 构造函数
     *
     * @param properties WebSocket属性配置
     */
    public CustomParamWebSocketHandler(WebSocketProperties properties) {
        this(properties, "/websocket/*", "/websocket/{param}", "param");
    }

    /**
     * 构造函数
     *
     * @param properties  WebSocket属性配置
     * @param urlPath     URL路径模式,例如"/websocket/*"
     * @param uriTemplate URI模板,例如"/websocket/{param}
     * @param paramKey    用于标识 WebSocket 连接的主键参数名
     */
    public CustomParamWebSocketHandler(WebSocketProperties properties, String urlPath, String uriTemplate, String paramKey) {
        this.sessionManager = new WebSocketSessionManager(uriTemplate);
        this.sender = new WebSocketMessageSender(sessionManager);
        this.properties = properties;
        this.uriTemplate = uriTemplate;
        this.urlPath = urlPath;
        this.paramKey = paramKey;
    }

    /**
     * 获取路径变量映射
     *
     * @param session WebSocket会话
     * @return 路径变量映射
     */
    protected String getPathVariable(WebSocketSession session) {
        final URI uri = session.getUri();
        if (uri == null || uri.getPath() == null) {
            log.error("获取 websocket url 失败");
            return null;
        }

        UriTemplate template = new UriTemplate(uriTemplate);
        Map<String, String> pathVariables = template.match(uri.getPath());

        return pathVariables.getOrDefault(paramKey, "");
    }


    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        String pathVariable = getPathVariable(session);
        if (StrUtil.isNotBlank(pathVariable)) {
            // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149
            session = new ConcurrentWebSocketSessionDecorator(session,
                    properties.getSendTimeLimit(),
                    properties.getBufferSizeLimit());
            sessionManager.add(pathVariable, session);
            log.info("{}, {} 建立连接, 该Key连接总数: {}, 系统连接总数: {}", uriTemplate, pathVariable,
                    sessionManager.getSession(pathVariable).size(),
                    sessionManager.getAllSession().size());
        }
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        String pathVariable = getPathVariable(session);
        if (StrUtil.isBlank(pathVariable)) {
            return;
        }

        String messagePayload = message.getPayload();

        if (!JSON.isValid(messagePayload)) {
            // 非 JSON 格式,直接回复 ok
            sender.sendToParam(pathVariable, "ok");
            return;
        }
        // 解析为 JSON
        JSONObject jsonMessage = JSON.parseObject(messagePayload);

        // 检查是否包含 type 字段
        if (jsonMessage != null && jsonMessage.containsKey("type")) {
            String messageType = jsonMessage.getString("type");

            // 异步发布事件
            if (eventPublisher != null) {
                var eventData =
                        new WebSocketMessageEvent.WebSocketMessageEventData(messageType, jsonMessage, session);

                // 发布事件,避免阻塞 WebSocket 消息处理
                eventPublisher.publishEvent(new WebSocketMessageEvent(eventData));
                log.debug("WebSocket 消息事件已发布: paramKey={}, type={}", pathVariable, messageType);
            }
        } else {
            // JSON 格式但没有 type 字段,回复 ok
            sender.sendToParam(pathVariable, "ok");
        }
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) {
        if (!(exception instanceof EOFException)) {
            log.error("WebSocket 连接发生错误", exception);
        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) {
        String primaryKey = getPathVariable(session);
        if (StrUtil.isNotBlank(primaryKey)) {
            sessionManager.remove(primaryKey, session);
        }
        log.info("{}, {} 关闭连接, 该Key连接总数: {}, 系统连接总数: {}", uriTemplate, primaryKey,
                sessionManager.getSession(primaryKey).size(),
                sessionManager.getAllSession().size());
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        eventPublisher = applicationContext;
    }
}

在这里我们使用ConcurrentWebSocketSessionDecorator来处理线程安全问题,他是 Spring WebSocket 提供的一个装饰器类,用于增强底层的 WebSocketSession 的线程安全性。它通过并发安全的方式包装原始的 WebSocketSession 对象,确保在多线程环境下安全地访问和修改会话属性,以及进行消息发送操作。

构造方法

  • delegate 需要代理的session
  • sendTimeLimit 表示发送单个消息的最大时间
  • bufferSizeLimit 表示发送消息的队列最大字节数,不是消息的数量而是消息的总大小
  • overflowStrategy 表示大小超过限制时的策略,默认是断开连接,还有个选项就是丢弃最老的数据,直到大小满足

会话管理

实现会话管理,用来实现服务端向指定的客户端单发或者群发消息

java 复制代码
/**
 * WebSocket Session管理实现,不同的CustomParamWebSocketHandler之间WebSocketSessionManager不复用
 * <p>
 * 每个key是一个分组,每个key下支持多个客户端
 *
 */
@Slf4j
public class WebSocketSessionManager {

    /**
     * 全局管理器
     */
    public static Map<String, Map<String, CopyOnWriteArraySet<WebSocketSession>>> webSocketSessionManagers = new HashMap<>();

    /**
     * key 与 WebSocketSession 映射
     * value 为集合
     */
    private final ConcurrentMap<String, CopyOnWriteArraySet<WebSocketSession>> sessions
            = new ConcurrentHashMap<>();

    public WebSocketSessionManager(String uriTemplate) {
        webSocketSessionManagers.put(uriTemplate, this.sessions);
    }

    /**
     * 添加 Session
     *
     * @param session Session
     */
    public void add(String key, WebSocketSession session) {
        // 使用compute方法来确保线程安全地添加会话
        sessions.compute(key, (k, v) -> {
            if (v == null) {
                v = new CopyOnWriteArraySet<>();
            }
            v.add(session);
            return v;
        });
    }

    /**
     * 移除 Session
     *
     * @param session Session
     */
    public void remove(String key, WebSocketSession session) {
        CopyOnWriteArraySet<WebSocketSession> webSocketSessions = sessions.get(key);
        if (CollUtil.isNotEmpty(webSocketSessions)) {
            webSocketSessions.removeIf(t -> t.getId().equals(session.getId()));
        }
    }

    /**
     * 移除 key 下的 所有 Session
     */
    public void remove(String key) {
        CopyOnWriteArraySet<WebSocketSession> sessionByKeys = sessions.get(key);
        if (CollUtil.isNotEmpty(sessionByKeys)) {
            synchronized (sessionByKeys) {
                for (WebSocketSession session : sessionByKeys) {
                    try {
                        session.close();
                    } catch (IOException e) {
                        log.error("关闭 {} 的 ws 连接失败", key);
                    }
                }
                sessions.remove(key);
            }
        }
    }

    /**
     * 获得指定 key 的 Session 列表
     *
     * @param key key
     * @return Session
     */
    public Collection<WebSocketSession> getSession(String key) {
        if (StrUtil.isEmpty(key)) {
            return Collections.emptyList();
        }
        return sessions.getOrDefault(key, new CopyOnWriteArraySet<>());
    }

    /**
     * 获取所有session
     */
    public Collection<WebSocketSession> getAllSession() {
        return sessions.values().stream().flatMap(Collection::stream).toList();
    }


    /**
     * 获取所有key
     */
    public Set<String> getAllKeys() {
        return sessions.keySet();
    }

}

客户端消息广播

当接收到客户端消息时,我们可以约定一种规范,来将这个消息做为 Spring 事件广播出去,由事件监听者来处理后续动作,例如以下约束

非 JSON 消息或JSON 消息(不包含 type 字段)

  • 输入:任何非 JSON 格式的文本消息或JSON 消息(不包含 type 字段)
  • 处理:直接回复 "ok"
  • 示例
arduino 复制代码
客户端发送: "hello"
服务端回复: "ok"

JSON 消息(包含 type 字段)

  • 输入:有效 JSON 且包含 type 字段
  • 处理
    1. 构造 WsMessage 格式回复
    2. 异步发布 WebSocketMessageEvent 事件
  • 示例
css 复制代码
客户端发送: {"type": "heartbeat"}
服务端回复: {
  "type": "heartbeat",
  "payload": "客户端需要的数据",
  "sendTime": "2025-01-08 10:30:00"
}

封装的事件类如下

java 复制代码
/**
 * WebSocket 消息事件
 * 当 WebSocket 接收到包含 type 字段的 JSON 消息时触发此事件
 *
 * @see #verify(String)
 */
public class WebSocketMessageEvent extends ApplicationEvent {

    public WebSocketMessageEvent(WebSocketMessageEventData source) {
        super(source);
    }

    /**
     * 获取事件数据
     */
    public WebSocketMessageEventData getData() {
        return (WebSocketMessageEventData) getSource();
    }

    /**
     * 校验是不是需要处理的类型
     */
    public boolean verify(String type) {
        if (type == null) {
            return false;
        }
        return type.equals(getData().getType());
    }

    /**
     * WebSocket 消息事件数据
     */
    @Data
    public static class WebSocketMessageEventData {
        /**
         * 消息类型
         */
        private String type;

        /**
         * 解析后的 JSON 对象
         */
        private JSONObject parsedMessage;

        /**
         * WebSocket 会话
         */
        private WebSocketSession session;

        public WebSocketMessageEventData(String type,
                                         JSONObject parsedMessage,
                                         WebSocketSession session) {
            this.type = type;
            this.parsedMessage = parsedMessage;
            this.session = session;
        }

        /**
         * 获取项目id
         */
        public String getProjectId() {
            return WsContextHolder.getProjectId(session);
        }

        /**
         * 获取用户id
         */
        public String getUserId() {
            return WsContextHolder.getUserId(session);
        }
    }
}

监听器示例

java 复制代码
@Component
public class EventInvasionWebSocketController {

    private final String EventInvasionRedDotWsMessageType = "event_invasion_red_dot";

    private final EventInvasionQueryService queryService;

    public EventInvasionWebSocketController(EventInvasionQueryService queryService) {
        this.queryService = queryService;
    }

    @Async
    @EventListener
    public void redDot(WebSocketMessageEvent messageEvent) {
        // 校验是不是自己关注的消息类型
        if (messageEvent.verify(EventInvasionRedDotWsMessageType)) {
            final WebSocketMessageEvent.WebSocketMessageEventData messageEventData = messageEvent.getData();
            final WebSocketSession session = messageEventData.getSession();
            final String userId = messageEventData.getUserId();
            final String projectId = messageEventData.getProjectId();
            WebSocketMessageSender.sendToSession(session, WsMessage.of(EventInvasionRedDotWsMessageType,
                                                                       queryService.unhandled(userId, projectId)));
        }
    }
}

消息发送

这里需要提醒一点,WebSocketMessageSender 消息发送类所有的消息都由该类实例化的对象发送,但所有的对象共用一个线程池,但是线程池的参数可以通过配置文件配置,所以你可以根据项目实际情况去修改这些参数。

java 复制代码
/**
 * 消息发送类
 *
 */
@Slf4j
public class WebSocketMessageSender {

    /**
     * 共用一个线程池
     */
    private static ThreadPoolTaskExecutor executor;

    private final WebSocketSessionManager sessionManager;

    public WebSocketMessageSender(WebSocketSessionManager sessionManager) {
        this.sessionManager = sessionManager;
    }

    public static synchronized void initializeExecutor(WebSocketProperties properties) {
        if (executor != null) {
            return;
        }
        executor = ThreadPoolUtils.createThreadPoolTaskExecutor(
                "websocket-sender",
                properties.getCoreThreadSize(),
                properties.getMaxThreadSize(),
                properties.getQueueCapacity()
        );
        log.info("初始化 WebSocketMessageSender 线程池成功");
    }

    /**
     * 发送消息到某个连接
     *
     * @param session websocket连接
     * @param message 发送的消息
     */
    public static void sendToSession(WebSocketSession session, WsMessage<?> message) {
        executor.execute(() -> {
            // 1. 各种校验,保证 Session 可以被发送
            if (session == null || !session.isOpen()) {
                return;
            }
            // 2. 执行发送
            try {
                session.sendMessage(new TextMessage(JSON.toJSONString(message)));
            } catch (IOException ex) {
                log.error(StrUtil.format("给[{}]发送消息失败", session.getId()), ex);
            } catch (SessionLimitExceededException ex) {
                // 一旦有一条消息发送超时,或者发送数据大于限制,limitExceeded 标志位就会被设置成true,标志这这个 session 被关闭
                // 后面的发送调用都是直接返回不处理,但只是被标记为关闭连接本身可能实际上并没有关闭,这是一个坑需要注意。
                try {
                    session.close();
                } catch (IOException e) {
                    log.error(StrUtil.format("主动关闭[{}]连接失败", session.getId()), e);
                }
                log.error(StrUtil.format("给[[{}]发送消息失败", session.getId()), ex);
            }
        });
    }

    /**
     * 发送消息到某个参数的客户端
     *
     * @param param   websocket连接时的参数
     * @param message 发送的消息
     */
    public void sendToParam(String param, String message) {
        executor.execute(() -> {
            // 1. 获得 Session 列表
            Collection<WebSocketSession> sessions = sessionManager.getSession(param);
            if (CollUtil.isEmpty(sessions)) {
                return;
            }
            // 2. 执行发送
            sessions.forEach(session -> {
                // 1. 各种校验,保证 Session 可以被发送
                if (session == null || !session.isOpen()) {
                    sessionManager.remove(param, session);
                    return;
                }
                // 2. 执行发送
                try {
                    session.sendMessage(new TextMessage(message));
                } catch (IOException ex) {
                    log.error(StrUtil.format("给[{}]分组的[{}]发送消息失败", param, session.getId()), ex);
                } catch (SessionLimitExceededException ex) {
                    // 一旦有一条消息发送超时,或者发送数据大于限制,limitExceeded 标志位就会被设置成true,标志这这个 session 被关闭
                    // 后面的发送调用都是直接返回不处理,但只是被标记为关闭连接本身可能实际上并没有关闭,这是一个坑需要注意。
                    try {
                        session.close();
                        sessionManager.remove(param, session);
                    } catch (IOException e) {
                        log.error(StrUtil.format("主动关闭[{}]分组的[{}]连接失败", param, session.getId()), e);
                    }
                    log.error(StrUtil.format("给[{}]分组的[{}]发送消息失败", param, session.getId()), ex);
                }
            });
        });
    }

    /**
     * 发送消息到客户端
     *
     * @param param   分组
     * @param message 发送的消息
     */
    public void sendToParam(String param, WsMessage<?> message) {
        sendToParam(param, JSON.toJSONString(message));
    }

    /**
     * 广播消息到全部客户端
     *
     * @param message 发送的消息
     */
    public void sendToAll(String message) {
        for (String key : sessionManager.getAllKeys()) {
            sendToParam(key, message);
        }
    }

    /**
     * 广播消息到全部客户端
     *
     * @param message 发送的消息
     */
    public void sendToAll(WsMessage<?> message) {
        for (String key : sessionManager.getAllKeys()) {
            sendToParam(key, message);
        }
    }

}

注意 @EnableScheduling 的自动配置线程池失效场景

在 SpringBoot 2.x 中同时使用 @EnableWebSocket@EnableScheduling 时,org.springframework.web.socket.config.annotation.WebSocketConfigurationSupport#defaultSockJsTaskScheduler会导致@EnableScheduling 中自动配置线程池失效,所以需要手动创建线程池。

参考:github.com/spring-proj...

java 复制代码
@Bean(name = "taskScheduler")
public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
    ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
    scheduler.setThreadNamePrefix("CommonScheduling-");
    // 线程数
    scheduler.setPoolSize(corePoolSize);
    scheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    return scheduler;
}

在 SpringBoot 3.x 中该问题已经被修复,参考: github.com/spring-proj...

相关推荐
一线大码30 分钟前
Gradle 高级篇之构建多模块项目的方法
spring boot·gradle·intellij idea
javadaydayup2 小时前
别再逐个注入了!@Autowired 批量获取接口实现类的核心逻辑拆解
spring
MarkGosling2 小时前
【开源项目】网络诊断告别命令行!NetSonar:开源多协议网络诊断利器
运维·后端·自动化运维
congvee2 小时前
springboot 学习第1期 - 创建工程
spring boot
Codebee2 小时前
OneCode3.0 VFS分布式文件管理API速查手册
后端·架构·开源
_新一2 小时前
Go 调度器(二):一个线程的执行流程
后端
estarlee2 小时前
腾讯云轻量服务器创建镜像免费API接口教程
后端
风流 少年3 小时前
Cursor创建Spring Boot项目
java·spring boot·后端
毕设源码_钟学姐3 小时前
计算机毕业设计springboot宿舍管理信息系统 基于Spring Boot的高校宿舍管理平台设计与实现 Spring Boot框架下的宿舍管理系统开发
spring boot·后端·课程设计
军军君013 小时前
基于Springboot+UniApp+Ai实现模拟面试小工具二:后端项目搭建
前端·javascript·spring boot·spring·微信小程序·前端框架·集成学习