WebApplicationType.REACTIVE 的webSocket 多实例问题处理

  1. 配置类
java 复制代码
@Configuration
public class WebFluxWebSocketConfig {

  /** 让 Spring 注入已经带依赖的 Handler */
  @Bean
  public HandlerMapping webSocketMapping(WebSocketReceivedHandler handler) {
    return new SimpleUrlHandlerMapping(
            Map.of("/api/xxx/ws", handler),   // 用注入的 handler
            -1
    );
  }

  @Bean
  public WebSocketHandlerAdapter handlerAdapter() {
    return new WebSocketHandlerAdapter();
  }
}
java 复制代码
@Configuration
public class RedisPubSubConfig {

  @Bean
  public RedisMessageListenerContainer redisMessageListenerContainer(
          RedisConnectionFactory connectionFactory,
          RedisBroadcastListener listener
  ) {
    RedisMessageListenerContainer container = new RedisMessageListenerContainer();
    container.setConnectionFactory(connectionFactory);
    container.addMessageListener(listener, new ChannelTopic("ws-broadcast"));
    return container;
  }
}
  1. handler
java 复制代码
@Component
@RequiredArgsConstructor
@Slf4j
public class WebSocketReceivedHandler implements WebSocketHandler {

  @Autowired
  private AiBroadcastEventHandlerDispatcher<?, ?> dispatcher;

  @Autowired
  private WsSessionPool wsSessionPool;

  @Override
  public @NotNull Mono<Void> handle(@NotNull WebSocketSession session) {

    log.info("websocket 连接成功,sessionId:{}", session.getId());
    wsSessionPool.add(session);
    String sid = session.getId();

    // 处理客户端请求消息,生成响应消息流
    Flux<WebSocketMessage> inputFlux = session.receive()
            .map(WebSocketMessage::getPayloadAsText)
            .flatMap(payload -> dispatcher.doDispatch(session, payload)
                    .map(session::textMessage)
            );

    // 服务端广播消息流
    Flux<WebSocketMessage> broadcastFlux = wsSessionPool.getPersonalFlux(sid)
            .map(session::textMessage);

    // 合并两个流,确保 session.send 只调用一次
    Flux<WebSocketMessage> mergedFlux = Flux.merge(inputFlux, broadcastFlux);
    log.info("websocket 开始发送消息,sessionId:{}", session.getId());
    return session.send(mergedFlux)
            .doFinally(sig -> {
              wsSessionPool.remove(session);
              log.info("websocket 关闭,sessionId:{},signal:{}", session.getId(), sig);
            });
  }
}

3.连接池

java 复制代码
@Component
@Slf4j
public class WsSessionPool {

  /** session 实体 */
  private final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>();

  /** 消息推送通道:Replay 可以避免未订阅者时失败,limit(1) 限制内存 */
  private final Map<String, Sinks.Many<String>> SINKS = new ConcurrentHashMap<>();

  /** 添加新的连接 */
  public void add(WebSocketSession session) {
    String sid = session.getId();
    SESSIONS.put(sid, session);

    // 推荐使用 replay 降低 emit 失败风险
    Sinks.Many<String> sink = Sinks.many().replay().limit(1);
    SINKS.put(sid, sink);

    log.info("WS 连接上线: {}, 当前连接数={}", sid, SESSIONS.size());
  }

  /** 移除连接(主动或异常关闭) */
  public void remove(WebSocketSession session) {
    removeById(session.getId());
  }

  public void removeById(String sessionId) {
    SESSIONS.remove(sessionId);
    Sinks.Many<String> sink = SINKS.remove(sessionId);
    if (sink != null) {
      sink.tryEmitComplete(); // 通知关闭
    }
    log.info("WS 连接下线: {}, 当前连接数={}", sessionId, SESSIONS.size());
  }

  /** 获取指定 session 的推送 Flux */
  public Flux<String> getPersonalFlux(String sessionId) {
    Sinks.Many<String> sink = SINKS.get(sessionId);
    if (sink == null) {
      log.warn("sessionId={} 不存在,返回空流", sessionId);
      return Flux.empty();
    }

    return sink.asFlux()
            .doOnCancel(() -> {
              log.info("sessionId={} Flux 被取消订阅", sessionId);
              removeById(sessionId);
            })
            .doOnTerminate(() -> {
              log.info("sessionId={} Flux 被终止", sessionId);
              removeById(sessionId);
            });
  }

  /** 广播推送消息到所有连接 */
  public void broadcast(String json) {
    for (Map.Entry<String, Sinks.Many<String>> entry : SINKS.entrySet()) {
      String sid = entry.getKey();
      Sinks.Many<String> sink = entry.getValue();
      Sinks.EmitResult result = sink.tryEmitNext(json);
      if (result.isFailure()) {
        log.warn("广播失败 sid={}, result={}, 自动移除连接", sid, result);
        removeById(sid);
      }
    }
    log.info("广播成功,消息: {}, 当前在线连接: {}", json, SINKS.size());
  }
}
  1. 心跳机制
java 复制代码
@Component
@Log4j2
public class WsHeartbeatTask {

  private final WsSessionPool wsSessionPool;

  public WsHeartbeatTask(WsSessionPool wsSessionPool) {
    this.wsSessionPool = wsSessionPool;
  }

  @PostConstruct
  public void init() {
    log.info("WebSocket心跳任务已启动");
  }

  // 每30秒广播一个心跳消息
  @Scheduled(fixedRate = 30_000)
  public void sendHeartbeat() {
    String timeStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    String json = String.format("{\"type\":\"ping\",\"timestamp\":\"%s\"}", timeStr);
    wsSessionPool.broadcast(json);
  }
}
  1. listener
java 复制代码
@Component
@Log4j2
public class RedisBroadcastListener implements MessageListener {

  private final WsSessionPool wsSessionPool;

  public RedisBroadcastListener(WsSessionPool wsSessionPool) {
    this.wsSessionPool = wsSessionPool;
  }

  @Override
  public void onMessage(Message message, byte[] pattern) {
    String body = new String(message.getBody(), StandardCharsets.UTF_8);
    log.info("Redis广播监听器收到消息:{}", body);
    wsSessionPool.broadcast(body);
  }
}
  1. 监听kafka 转成redis发送websocket消息
java 复制代码
@Component
@Log4j2
public class MapAlarmMsgHandler implements AiDetectionScreenMessageHandler<List<FaultAlarmVO>> {

  // 用于发布消息
  @Autowired
  private StringRedisTemplate redisTemplate;

  @Override
  public Integer messageType() {
    return AiBroadcastEventEnum.MAP_ALARM_CHARGING.getCode();
  }

  @Override
  public void handler(AiDetectionMessageRedisEvent event) {
    log.info("FaultAlarmMsgHandler:{}", event);
    try {
      List<FaultAlarmVO> vo = getSource(event);
      Mono<WebSocketResponse<List<FaultAlarmVO>>> response = WebSocketResponse.ok(AiBroadcastEventEnum.MAP_ALARM_CHARGING.getCode(), vo);
      WebSocketResponse<List<FaultAlarmVO>> block = response.block();
      String json = JSONUtil.toJsonStr(block);
      redisTemplate.convertAndSend("ws-broadcast", json);
      log.info("FaultAlarmMsgHandler-广播消息转发到 Redis:{}", json);
    } catch (Exception e) {
      log.error("消息处理error:{}", event, e);
    }
  }

  @Override
  public List<FaultAlarmVO> getSource(AiDetectionMessageRedisEvent event) {
    return (List<FaultAlarmVO>) event.getContent();
  }
}
相关推荐
RuoyiOffice3 小时前
Springboot+WebSocket×场景×渠道:企业统一消息中心编排实战
spring boot·websocket·短信·消息中心·spring boot 3·站内信·场景编排
烟雨江南7853 小时前
国产化语音识别如何落地?——灵声智库国产 CPU/OS、GPU/NPU、流式转写与离线私有化部署实践
人工智能·websocket·语音识别·ai客服·企业agent
Lucis__1 天前
如何实现长连接通信机制?Websocket的最佳实践
网络·websocket·网络协议
AI领路人.2 天前
[特殊字符] 用 OpenClaw 实现一个 C++ 复刻版 QuantClaw
websocket·单元测试·discord·rest api·内存占用·c++17
counting money2 天前
Tomcat 专题:从基础架构到性能调优与 WebSocket 实战
java·websocket·tomcat
k4m7v2pz3 天前
Rust 高并发 WebSocket 连接管理:从线程地狱到 tokio 异步架构
websocket·架构·rust·并发编程·tokio
Sylvia33.4 天前
篮球数据API的技术架构与工程实践:基于火星数据WebSocket实时推送体系
java·python·websocket·网络协议·架构
Sylvia33.5 天前
从轮询到推送:足球数据API架构演进与火星数据技术拆解
java·服务器·网络·python·websocket·架构
kels88995 天前
实战排坑:黄金实时API开发,XAUUSD Tick报文异常处理实践
开发语言·python·websocket·网络协议·信息可视化
agent8977 天前
实战升级|SpringBoot WebSocket实现多轮对话AI流式问答(上下文记忆+自动重连+会话隔离)
人工智能·spring boot·websocket