package com.xcong.excoin.modules.gateApi; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.xcong.excoin.modules.gateApi.wsHandler.GateChannelHandler; import com.xcong.excoin.modules.okxNewPrice.utils.SSLConfig; import lombok.extern.slf4j.Slf4j; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** * Gate WebSocket 连接管理器。 * *

职责

* 负责 TCP 连接的建立、维持和恢复。频道逻辑(订阅/解析)全部委托给 {@link GateChannelHandler} 实现类。 * *

生命周期

*
 *   init()        → connect() → startHeartbeat()
 *   destroy()     → destroyed=true → unsubscribe 所有 handler → closeBlocking() → shutdown 线程池
 *   onClose()     → scheduleReconnect() → reconnectWithBackoff()(无限重试,指数退避,上限 60s)
 * 
* *

消息路由

*
 *   onMessage → handleMessage:
 *     1. futures.pong         → cancelPongTimeout
 *     2. subscribe/unsubscribe → 日志
 *     3. error                → 错误日志
 *     4. update/all           → 遍历 channelHandlers → handler.handleMessage(response)
 * 
* *

心跳与断线检测(双层)

*
    *
  1. TCP 层:Java-WebSocket 自带 ping/pong 保活({@code connectionLostTimeout = 30s}), * 半开连接(对端静默丢包、无 FIN/RST)会在 30 秒内被库判定丢失并触发 onClose。
  2. *
  3. 应用层:{@code futures.ping}/{@code futures.pong}。10 秒未收到任何消息 → 发 ping; * 45 秒未收到任何消息 → 判定连接死亡,主动 close → onClose → 重连。
  4. *
* *

重连策略

* 无限次重试:5s → 10s → 20s → 40s → 60s(封顶),连接成功(onOpen)后归零。 * destroy() 后立即停止重连。 * *

线程安全

* 连接状态用 AtomicBoolean(isConnected, isConnecting, isInitialized, destroyed, reconnectInProgress)。 * 消息时间戳用 AtomicReference。心跳任务用 synchronized 保护。 * * @author Administrator */ @SuppressWarnings("ALL") @Slf4j public class GateKlineWebSocketClient { private static final String FUTURES_PING = "futures.ping"; private static final String FUTURES_PONG = "futures.pong"; /** 静默超过该秒数 → 发送 futures.ping */ private static final int HEARTBEAT_TIMEOUT = 10; /** 静默超过该秒数 → 判定连接死亡,主动断开重连 */ private static final int DEAD_TIMEOUT_SECONDS = 45; /** Java-WebSocket 库层 ping/pong 保活超时(秒),用于检测半开 TCP 连接 */ private static final int CONNECTION_LOST_TIMEOUT_SECONDS = 30; /** 重连初始延迟(毫秒) */ private static final long INITIAL_RECONNECT_DELAY_MS = 5000; /** 重连最大延迟(毫秒),指数退避封顶值 */ private static final long MAX_RECONNECT_DELAY_MS = 60000; /** WebSocket 地址,由 GateConfig 提供 */ private final String wsUrl; /** Java-WebSocket 客户端实例 */ private WebSocketClient webSocketClient; /** 心跳检测调度器 */ private ScheduledExecutorService heartbeatExecutor; /** 心跳超时 Future */ private volatile ScheduledFuture pongTimeoutFuture; /** 最后收到消息的时间戳(毫秒) */ private final AtomicReference lastMessageTime = new AtomicReference<>(System.currentTimeMillis()); /** 连接状态 */ private final AtomicBoolean isConnected = new AtomicBoolean(false); /** 连接中标记,防重入 */ private final AtomicBoolean isConnecting = new AtomicBoolean(false); /** 初始化标记,防重复 init */ private final AtomicBoolean isInitialized = new AtomicBoolean(false); /** 销毁标记:置位后停止一切重连 */ private final AtomicBoolean destroyed = new AtomicBoolean(false); /** 重连任务进行中标记,防止 onClose 并发触发多个重连循环 */ private final AtomicBoolean reconnectInProgress = new AtomicBoolean(false); /** 当前重连次数(连接成功后归零),仅用于日志 */ private final AtomicInteger reconnectAttempt = new AtomicInteger(0); /** 频道处理器列表,通过 addChannelHandler 注册 */ private final List channelHandlers = new ArrayList<>(); /** 重连等异步任务的缓存线程池(daemon 线程) */ private final ExecutorService sharedExecutor = Executors.newCachedThreadPool(r -> { Thread t = new Thread(r, "gate-ws-worker"); t.setDaemon(true); return t; }); /** * @param wsUrl Gate WebSocket 地址(由 {@link GateConfig#getWsUrl()} 提供) */ public GateKlineWebSocketClient(String wsUrl) { this.wsUrl = wsUrl; } /** * 注册频道处理器。需在 init() 前调用。 * * @param handler 实现了 {@link GateChannelHandler} 接口的频道处理器 */ public void addChannelHandler(GateChannelHandler handler) { channelHandlers.add(handler); } /** * 初始化:建立 WebSocket 连接 → 启动心跳检测。 * 使用 {@code AtomicBoolean} 防重入,同一实例只允许初始化一次。 */ public void init() { if (!isInitialized.compareAndSet(false, true)) { log.warn("[WS] 已初始化过,跳过重复初始化"); return; } connect(); startHeartbeat(); } /** * 销毁:置销毁标记(停止重连)→ 取消所有频道订阅 → 关闭 WebSocket 连接 → 关闭线程池。 * *

执行顺序

* 先取消订阅(等待 500ms 确保发送完成),再 closeBlocking 关闭连接, * 最后 shutdown 线程池。先关连接再关线程池,避免 onClose 回调中的重连任务访问已关闭的线程池。 */ public void destroy() { log.info("[WS] 开始销毁..."); destroyed.set(true); if (webSocketClient != null && webSocketClient.isOpen()) { for (GateChannelHandler handler : channelHandlers) { handler.unsubscribe(webSocketClient); } try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.warn("[WS] 取消订阅等待被中断"); } } if (webSocketClient != null && webSocketClient.isOpen()) { try { webSocketClient.closeBlocking(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.warn("[WS] 关闭连接时被中断"); } } if (pongTimeoutFuture != null) { pongTimeoutFuture.cancel(true); } shutdownExecutorGracefully(heartbeatExecutor); shutdownExecutorGracefully(sharedExecutor); log.info("[WS] 销毁完成"); } /** * 建立 WebSocket 连接。使用 SSLContext 配置 TLS 协议。 * *

连接成功回调

*
    *
  1. 设置 isConnected=true,isConnecting=false
  2. *
  3. 重连计数归零
  4. *
  5. 重置心跳计时器
  6. *
  7. 重置各 handler 订阅标记并重新订阅全部频道
  8. *
  9. 发送首次 ping
  10. *
* *

连接关闭回调

* 设置断连状态 → 取消心跳超时 → 异步触发无限指数退避重连。 * *

线程安全

* 使用 {@code AtomicBoolean.isConnecting} 防止并发重连; * finally 中保证失败路径下 isConnecting 一定被复位。 */ private void connect() { if (destroyed.get()) { log.info("[WS] 已销毁,不再建立连接"); return; } if (!isConnecting.compareAndSet(false, true)) { log.info("[WS] 连接进行中,跳过重复请求"); return; } try { SSLConfig.configureSSL(); System.setProperty("https.protocols", "TLSv1.2,TLSv1.3"); URI uri = new URI(wsUrl); if (webSocketClient != null) { try { webSocketClient.closeBlocking(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } webSocketClient = new WebSocketClient(uri) { @Override public void onOpen(ServerHandshake handshake) { log.info("[WS] 连接成功"); isConnected.set(true); isConnecting.set(false); reconnectAttempt.set(0); if (sharedExecutor != null && !sharedExecutor.isShutdown()) { for (GateChannelHandler handler : channelHandlers) { handler.setSubscribed(false); } resetHeartbeatTimer(); for (GateChannelHandler handler : channelHandlers) { handler.subscribe(webSocketClient); } sendPing(); } else { log.warn("[WS] 应用正在关闭,忽略连接成功回调"); } } @Override public void onMessage(String message) { lastMessageTime.set(System.currentTimeMillis()); handleMessage(message); resetHeartbeatTimer(); } @Override public void onClose(int code, String reason, boolean remote) { log.warn("[WS] 连接关闭, code:{}, reason:{}, remote:{}", code, reason, remote); isConnected.set(false); isConnecting.set(false); cancelPongTimeout(); scheduleReconnect(); } @Override public void onError(Exception ex) { log.error("[WS] 发生错误: {}", ex.toString()); isConnected.set(false); // 归一化处理:错误后主动关闭 socket,走 onClose → 重连 的路径 if (webSocketClient != null && webSocketClient.isOpen()) { try { webSocketClient.close(); } catch (Exception e) { log.warn("[WS] 错误后关闭连接失败", e); } } } }; webSocketClient.setConnectionLostTimeout(CONNECTION_LOST_TIMEOUT_SECONDS); webSocketClient.connect(); } catch (Exception e) { log.error("[WS] 建立连接失败: {}", e.toString()); // connect() 抛出异常时可能不会回调 onClose,这里主动调度重连 scheduleReconnect(); } finally { // 任何失败路径都要复位 isConnecting,否则后续重连会被永久跳过 if (!isConnected.get()) { isConnecting.set(false); } } } /** * 消息分发:先处理系统事件(pong/subscribe/error), * 再把 update/all 事件路由到各 channelHandler。 *

每个 handler 内部通过 channel 名称做二次匹配,匹配成功返回 true 则停止遍历。 */ private void handleMessage(String message) { try { JSONObject response = JSON.parseObject(message); String channel = response.getString("channel"); String event = response.getString("event"); if (FUTURES_PONG.equals(channel)) { log.debug("[WS] 收到 pong 响应"); cancelPongTimeout(); return; } if ("subscribe".equals(event)) { log.info("[WS] {} 订阅成功: {}", channel, response.getJSONObject("result")); for (GateChannelHandler handler : channelHandlers) { if (channel.equals(handler.getChannelName())) { handler.setSubscribed(true); break; } } return; } if ("unsubscribe".equals(event)) { log.info("[WS] {} 取消订阅成功", channel); return; } if ("error".equals(event)) { JSONObject error = response.getJSONObject("error"); log.error("[WS] {} 错误, code:{}, msg:{}", channel, error != null ? error.getInteger("code") : "N/A", error != null ? error.getString("message") : response.getString("msg")); return; } if ("update".equals(event) || "all".equals(event)) { for (GateChannelHandler handler : channelHandlers) { if (handler.handleMessage(response)) return; } } } catch (Exception e) { log.error("[WS] 处理消息失败: {}", message, e); } } /** * 检查所有已注册的频道是否都已收到订阅成功确认。 */ public boolean areAllSubscribed() { if (channelHandlers.isEmpty()) return false; for (GateChannelHandler h : channelHandlers) { if (!h.isSubscribed()) return false; } return true; } // ---- heartbeat ---- /** * 启动心跳检测器。 * 使用单线程 ScheduledExecutor,每 25 秒检查一次心跳超时。 */ private void startHeartbeat() { if (heartbeatExecutor != null && !heartbeatExecutor.isTerminated()) heartbeatExecutor.shutdownNow(); heartbeatExecutor = Executors.newSingleThreadScheduledExecutor(r -> { Thread t = new Thread(r, "gate-ws-heartbeat"); t.setDaemon(true); return t; }); heartbeatExecutor.scheduleWithFixedDelay(this::checkHeartbeatTimeout, 25, 25, TimeUnit.SECONDS); } /** * 重置心跳计时器:取消旧超时任务,提交新的 10 秒超时检测。 */ private synchronized void resetHeartbeatTimer() { cancelPongTimeout(); if (heartbeatExecutor != null && !heartbeatExecutor.isShutdown()) { pongTimeoutFuture = heartbeatExecutor.schedule(this::checkHeartbeatTimeout, HEARTBEAT_TIMEOUT, TimeUnit.SECONDS); } } /** * 心跳检查(由 25s 周期任务 + 每次收到消息后 10s 的一次性任务触发): *

    *
  1. 静默 ≥ {@link #DEAD_TIMEOUT_SECONDS} 秒 → 判定连接死亡(半开 TCP / 对端失联),主动断开触发重连;
  2. *
  3. 静默 ≥ {@link #HEARTBEAT_TIMEOUT} 秒 → 发送 futures.ping 探测。
  4. *
*/ private void checkHeartbeatTimeout() { if (!isConnected.get() || destroyed.get()) return; long silentMs = System.currentTimeMillis() - lastMessageTime.get(); if (silentMs >= DEAD_TIMEOUT_SECONDS * 1000L) { log.warn("[WS] 已 {} 秒未收到任何消息,判定连接死亡,主动断开重连", silentMs / 1000); if (webSocketClient != null) { try { webSocketClient.close(); } catch (Exception e) { log.warn("[WS] 主动断开失败", e); } } return; } if (silentMs >= HEARTBEAT_TIMEOUT * 1000L) { sendPing(); } } /** * 发送应用层 futures.ping 消息。 */ private void sendPing() { try { if (webSocketClient != null && webSocketClient.isOpen()) { JSONObject pingMsg = new JSONObject(); pingMsg.put("time", System.currentTimeMillis() / 1000); pingMsg.put("channel", FUTURES_PING); webSocketClient.send(pingMsg.toJSONString()); log.debug("[WS] 发送 ping 请求"); } } catch (Exception e) { log.warn("[WS] 发送 ping 失败", e); } } /** * 取消心跳超时检测任务。 */ private synchronized void cancelPongTimeout() { if (pongTimeoutFuture != null && !pongTimeoutFuture.isDone()) pongTimeoutFuture.cancel(true); } // ---- reconnect ---- /** * 调度重连任务。用 {@code reconnectInProgress} 防并发:onClose 可能被触发多次, * 但同一时刻只允许一个重连循环在跑。 */ private void scheduleReconnect() { if (destroyed.get()) { log.info("[WS] 已销毁,跳过重连调度"); return; } if (sharedExecutor == null || sharedExecutor.isShutdown() || sharedExecutor.isTerminated()) { log.warn("[WS] 线程池已关闭,不执行重连"); return; } if (!reconnectInProgress.compareAndSet(false, true)) { log.info("[WS] 已有重连任务进行中,跳过重复调度"); return; } sharedExecutor.execute(() -> { try { reconnectWithBackoff(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.info("[WS] 重连任务被中断"); } catch (Exception e) { log.error("[WS] 重连失败", e); } finally { reconnectInProgress.set(false); } }); } /** * 无限指数退避重连:5s → 10s → 20s → 40s → 60s(封顶),直到连接成功或 destroy()。 * connect() 为阻塞调用:返回后若 isConnected 未置位则视为失败,继续下一轮。 * * @throws InterruptedException 线程被中断(destroy 时) */ private void reconnectWithBackoff() throws InterruptedException { long delayMs = INITIAL_RECONNECT_DELAY_MS; while (!destroyed.get() && !isConnected.get()) { int attempt = reconnectAttempt.incrementAndGet(); try { Thread.sleep(delayMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.info("[WS] 重连等待被中断,停止重连"); return; } if (destroyed.get() || isConnected.get()) return; log.info("[WS] 开始第 {} 次重连", attempt); try { connect(); } catch (Exception e) { log.warn("[WS] 第 {} 次重连失败: {}", attempt, e.toString()); } delayMs = Math.min(delayMs * 2, MAX_RECONNECT_DELAY_MS); } if (isConnected.get()) { log.info("[WS] 重连成功,共尝试 {} 次", reconnectAttempt.get()); } else { log.info("[WS] 已销毁,停止重连"); } } /** * 优雅关闭线程池:先 shutdown,等待 5 秒,超时则 shutdownNow 强制中断。 * * @param executor 需要关闭的线程池 */ private void shutdownExecutorGracefully(ExecutorService executor) { if (executor == null || executor.isTerminated()) return; try { executor.shutdown(); if (!executor.awaitTermination(5, TimeUnit.SECONDS)) executor.shutdownNow(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); executor.shutdownNow(); } } }