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();
};
}
本文来自博客园,作者:Sadness_sa,转载请注明原文链接:https://www.cnblogs.com/sadness-sa/p/18715044

浙公网安备 33010602011771号