Loading

java okhttp3实现kook webstock连接及心跳检测

java okhttp3实现kook webstock连接及心跳检测

KookWebSocketClient

package cn.sadness.kookrob.service.websocket;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ZipUtil;
import cn.sadness.kookrob.config.KookConfigProperties;
import cn.sadness.kookrob.dto.Message;
import cn.sadness.kookrob.service.KookMessageService;
import cn.sadness.kookrob.utils.OkHttpUtil;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import okhttp3.*;
import okio.ByteString;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.*;

public class KookWebSocketClient {
    private static final Logger logger = LogManager.getLogger(KookWebSocketClient.class);
    private final OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
    private WebSocket webSocket;
    private String sessionId;
    private long lastSn = 0;
    private volatile boolean isConnected = false;
    private final Map<Long, JsonNode> messageBuffer = new ConcurrentHashMap<>();
    private ScheduledExecutorService heartbeatExecutor;
    private static final int MAX_RETRY_TIMES = 2;
    private static final int MAX_RESUME_TIMES = 2;
    private static final long HEARTBEAT_INTERVAL = 30000;
    private static final long HEARTBEAT_TIMEOUT = 6000;
    private Timer connectWithRetryTimer;
    private Timer scheduleResumeTimer;
    private KookConfigProperties kookConfigProperties;
    private KookMessageService kookMessageService;

    public KookWebSocketClient(KookConfigProperties kookConfigProperties, KookMessageService kookMessageService) {
        this.kookConfigProperties = kookConfigProperties;
        this.kookMessageService = kookMessageService;
    }

    @PostConstruct
    public void start() {
        connectWithRetry(0);
    }

    /**
     * 获取网关并连接ws
     *
     * @param retryCount
     */
    private void connectWithRetry(int retryCount) {
        try {
            String gateway = getGateway();
            connect(gateway, retryCount);
        } catch (Exception e) {
            logger.error("获取Gateway失败", e);
            if (retryCount < MAX_RETRY_TIMES) {
                long delay = (long) Math.pow(2, retryCount + 1);
                scheduleReconnect(delay, retryCount + 1);
            }
        }
    }

    /**
     * 获取wws
     *
     * @return
     * @throws IOException
     */
    private String getGateway() throws IOException {
        // 这里实现获取Gateway的逻辑
        // 实际应该调用KOOK的API获取WebSocket地址
        Message message = message = OkHttpUtil.sendGetRequest(你的网关url, 你的机器人token, null);
        ;
        logger.info(message);
        if (message.getCode() == 0) {
            // 在应用启动完成后建立 WebSocket 连接
            String url = message.getData().getString("url") + "&resume=1&sn=" + lastSn;
            if (ObjectUtil.isNotEmpty(sessionId)) {
                url += "&session_id=" + sessionId;
            }
            logger.info("KOOK websocket:::{}", url);
            return url;
        } else {
            logger.error("无法获取wss连接;code:::" + message.getCode());
        }
        return null;
    }

    private void connect(String gateway, int retryCount) {
        Request request = new Request.Builder()
                .url(gateway + (sessionId != null ? "?resume=1&sessionId=" + sessionId + "&sn=" + lastSn : ""))
                .build();

        webSocket = okHttpClient.newWebSocket(request, new WebSocketListener() {
            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                logger.info("WebSocket连接已建立");
                hello();
                cancelConnectWithRetryTimer();
            }

            @Override
            public void onMessage(WebSocket webSocket, ByteString bytes) {
                byte[] bytes1 = ZipUtil.unZlib(bytes.toByteArray());
                String str = new String(bytes1, StandardCharsets.UTF_8);
                handleMessage(str);
            }

            @Override
            public void onFailure(WebSocket webSocket, Throwable t, Response response) {
                logger.error("WebSocket连接失败;{}", response, t);
                stopHeartbeat();
                isConnected = false;
                if (retryCount < MAX_RETRY_TIMES) {
                    //重新连接
                    scheduleReconnect((long) Math.pow(2, retryCount + 1), retryCount + 1);
                }
            }
        });
    }

    private void handleMessage(String text) {
        logger.info("text:{}", text);
        try {
            JSONObject result = JSONObject.parse(text);
            Integer signal = result.getInteger("s");
            switch (signal) {
                case 0: // EVENT
                    handleEvent(result);
                    break;
                case 1: // HELLO
                    handleHello(result);
                    break;
                case 3: // PONG
                    handlePong();
                    break;
                case 5: // RECONNECT
                    handleReconnect(result);
                    break;
                case 6: // RESUME ACK   服务端通知客户端 resume 动作成功,中间所有离线消息已经全部发送成功
                    handleResumeAck(result);
                    break;
            }
        } catch (Exception e) {
            logger.error("消息处理失败", e);
        }
    }

    private void handlePong() {
        isConnected = true;
        //通了,就停止请求网关
        logger.info("PONG");
    }

    private void handleEvent(JSONObject result) {
        long sn = result.getLong("sn");
        if (sn <= lastSn) {
            return; // 忽略已处理的消息
        }
        lastSn = sn;
        //这里写你的消息处理
        kookMessageService.messageHandler(webSocket, result);
    }

    private void processMessage(JsonNode message) {
        lastSn = message.get("sn").asLong();
        // 实际的消息处理逻辑
        logger.info("处理消息: {}", message);
    }

    private void handleHello(JSONObject result) {
        JSONObject d = result.getJSONObject("d");
        Integer code = d.getInteger("code");
        if (0 == code) {
            sessionId = d.getString("session_id");
            isConnected = true;
            startHeartbeat();
        } else if (40100 == code) {
            logger.error("连接服务器异常缺少参数");
        } else if (40101 == code) {
            logger.error("连接服务器异常无效的token");
        } else if (40102 == code) {
            logger.error("连接服务器异常token验证失败");
        } else if (40103 == code) {
            logger.error("连接服务器异常token过期");
        } else {
            logger.error("连接服务器异常未知");
        }
        if (0 != code) {
            logger.error("连接验证失败: {}", code);
            reconnect();
        }
    }

    private void handleReconnect(JSONObject result) {
        logger.info("收到重连信号");
        JSONObject d = result.getJSONObject("d");
        if (40106 == d.getInteger("code")) {
            logger.error("resume 失败, 缺少参数");
        } else if (40107 == d.getInteger("code")) {
            logger.error("当前 session 已过期 (resume 失败, PING 的 sn 无效)");
        } else if (40108 == d.getInteger("code")) {
            logger.error("无效的 sn , 或 sn 已经不存在 (resume 失败, PING 的 sn 无效)");
        } else {
            logger.info("未知异常");
        }
        sessionId = null;
        lastSn = 0;
        messageBuffer.clear();
        reconnect();
    }

    private void handleResumeAck(JSONObject message) {
        logger.info("Resume确认成功");
        sessionId = message.getJSONObject("d").getString("session_id");
        cancelScheduleResumeTimer();
    }

    private void hello() {
        JSONObject ping = new JSONObject();
        ping.put("s", 2);
        ping.put("sn", lastSn);
        String message = ping.toJSONString();
        isConnected = false;
        logger.info("ping:{}", message);
        webSocket.send(message);
        logger.info("hello");
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                if (!isConnected) {
                    logger.info("hello没有回应");
                    webSocket.close(1000, "hello没有回应");
                    timer.cancel();
                }
            }
        }, 6000);
    }

    /**
     * 开始心跳检测
     */
    private void startHeartbeat() {
        logger.info("开始心跳检测");
        stopHeartbeat();
        heartbeatExecutor = Executors.newSingleThreadScheduledExecutor();
        heartbeatExecutor.scheduleAtFixedRate(this::sendHeartbeat,
                HEARTBEAT_INTERVAL,
                HEARTBEAT_INTERVAL + (long) (Math.random() * 10000 - 5000),
                TimeUnit.MILLISECONDS);
    }

    /**
     * 心跳
     */
    private void sendHeartbeat() {
        try {
            JSONObject ping = new JSONObject();
            ping.put("s", 2);
            ping.put("sn", lastSn);
            String message = ping.toJSONString();
            isConnected = false;
            logger.info("ping:{}", message);
            webSocket.send(message);
            logger.info("PING");
            // 设置超时检查
            heartbeatExecutor.schedule(() -> {
                if (!isConnected) {
                    logger.warn("心跳超时");
                    handleHeartbeatTimeout();
                }
            }, HEARTBEAT_TIMEOUT, TimeUnit.MILLISECONDS);
        } catch (Exception e) {
            logger.error("发送心跳失败", e);
        }
    }

    private void handleHeartbeatTimeout() {
        isConnected = false;
        stopHeartbeat();
        // 开始重试心跳
        retryHeartbeat(0);
    }

    /**
     * 重试心跳链接
     *
     * @param retryCount
     */
    private void retryHeartbeat(int retryCount) {
        if (retryCount < 2) {
            long delay = (long) Math.pow(2, retryCount + 1);
            scheduleReconnect(delay, retryCount);
        } else {
            // 心跳重试失败,尝试resume
            tryResume(0);
        }
    }

    /**
     * 尝试重启心跳,如果失败MAX_RESUME_TIMES(2)次就重新获取连接
     *
     * @param resumeCount
     */
    private void tryResume(int resumeCount) {
        if (resumeCount < MAX_RESUME_TIMES) {
            long delay = (long) Math.pow(2, resumeCount + 3); // 8, 16
            scheduleResume(delay, resumeCount + 1);
        } else {
            // Resume失败,重新获取Gateway
            connectWithRetry(0);
        }
    }

    /**
     * 重新连接
     *
     * @param seconds
     * @param retryCount
     */
    private void scheduleReconnect(long seconds, int retryCount) {
        if (connectWithRetryTimer == null) {
            connectWithRetryTimer = new Timer();
        }
        connectWithRetryTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                connectWithRetry(retryCount);
            }
        }, seconds * 1000);
    }

    /**
     * 重新恢复心跳
     *
     * @param seconds
     * @param resumeCount
     */
    private void scheduleResume(long seconds, int resumeCount) {
        if (scheduleResumeTimer == null) {
            scheduleResumeTimer = new Timer();
        }
        scheduleResumeTimer.schedule(new TimerTask() {
            @Override
            public void run() {
                sendResume(resumeCount);
            }
        }, seconds * 1000);
    }

    /**
     * 恢复心跳
     *
     * @param resumeCount
     */
    private void sendResume(int resumeCount) {
        try {
            JSONObject resume = new JSONObject();
            resume.put("s", 4);
            resume.put("sn", lastSn);
            String message = resume.toJSONString();
            webSocket.send(message);
        } catch (Exception e) {
            logger.error("发送Resume失败", e);
            tryResume(resumeCount);
        }
    }

    /**
     * 停止心跳检测
     */
    private void stopHeartbeat() {
        if (heartbeatExecutor != null) {
            heartbeatExecutor.shutdown();
            heartbeatExecutor = null;
        }
    }

    /**
     * 取消尝试恢复心跳任务
     */
    private void cancelScheduleResumeTimer() {
        logger.debug("取消尝试恢复心跳任务");
        if (scheduleResumeTimer != null) {
            scheduleResumeTimer.cancel();
            scheduleResumeTimer = null;
        }
    }

    /**
     * 取消重试连接任务
     */
    private void cancelConnectWithRetryTimer() {
        logger.debug("取消重试连接任务");
        if (connectWithRetryTimer != null) {
            connectWithRetryTimer.cancel();
            connectWithRetryTimer = null;
        }
    }

    private void reconnect() {
        stopHeartbeat();
        if (webSocket != null) {
            webSocket.close(1000, "Reconnecting");
        }
        connectWithRetry(0);
    }

    @PreDestroy
    public void stop() {
        stopHeartbeat();
        if (webSocket != null) {
            webSocket.close(1000, "Application shutdown");
        }
    }
}

OkHttpUtil

    public static Message sendGetRequest(String url, String token, Map<String, String> params) throws IOException {
        HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();

        // 添加请求参数
        if (params != null) {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
            }
        }

        String requestUrl = urlBuilder.build().toString();
        logger.info("GET请求URL={}; ToKen={};请求参数={}", requestUrl, token, params);

        Request request = new Request.Builder()
                .url(requestUrl)
                .header("Accept", "application/json")
                .header("Authorization", "Bot " + token)
                .build();

        try (Response response = client.newCall(request).execute()) {
            if (response.isSuccessful()) {
                String jsonResponse = response.body().string();
                logger.info(jsonResponse);
                response.close();
                return JSON.parseObject(jsonResponse, Message.class);
            } else {
                logger.error("Unexpected response code:{}", response.code());
                throw new IOException("Unexpected response code: " + response.code());
            }
        }
    }

springboot里使用

@Bean
public CommandLineRunner init() {
    return args -> {
        KookWebSocketClient client1 = new KookWebSocketClient(kookConfigProperties,kookMessageService);
        client1.start();
    };
}
posted @ 2025-02-14 11:43  Sadness_sa  阅读(172)  评论(0)    收藏  举报