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 协议。
*
* 连接成功回调
*
* - 设置 isConnected=true,isConnecting=false
* - 重连计数归零
* - 重置心跳计时器
* - 重置各 handler 订阅标记并重新订阅全部频道
* - 发送首次 ping
*
*
* 连接关闭回调
* 设置断连状态 → 取消心跳超时 → 异步触发无限指数退避重连。
*
* 线程安全
* 使用 {@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 的一次性任务触发):
*
* - 静默 ≥ {@link #DEAD_TIMEOUT_SECONDS} 秒 → 判定连接死亡(半开 TCP / 对端失联),主动断开触发重连;
* - 静默 ≥ {@link #HEARTBEAT_TIMEOUT} 秒 → 发送 futures.ping 探测。
*
*/
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(); }
}
}