refactor: improve webSocket connection handling and error recovery

This commit is contained in:
wonfen
2025-03-09 04:22:01 +08:00
parent c1a9de4d66
commit f18202a3a4
5 changed files with 391 additions and 101 deletions

View File

@@ -39,3 +39,224 @@ export const createSockette = (
},
});
};
/**
* 创建一个支持认证的WebSocket连接
* 使用标准的URL参数方式添加token
*
* 注意mihomo服务器对WebSocket的认证支持不佳使用URL参数方式传递token
*/
export const createAuthSockette = (
baseUrl: string,
secret: string,
opt: SocketteOptions,
maxError = 10,
) => {
// 确保baseUrl格式正确
let url = baseUrl;
if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
url = `ws://${url}`;
}
// 重试控制
let reconnectAttempts = 0;
const MAX_RECONNECT = maxError;
let reconnectTimeout: any = null;
let ws: WebSocket | null = null;
// 使用URL API解析和构建URL
try {
const urlObj = new URL(url);
// 添加token参数如果有secret
if (secret) {
urlObj.searchParams.delete("token");
urlObj.searchParams.append("token", secret);
}
url = urlObj.toString();
console.log(`[WebSocket] 创建连接: ${url.replace(secret || "", "***")}`);
} catch (e) {
console.error(`[WebSocket] URL格式错误: ${url}`, e);
if (opt.onerror) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onerror(
new ErrorEvent("error", { message: `URL格式错误: ${e}` } as any),
);
}
return createDummySocket();
}
function connect() {
try {
ws = new WebSocket(url);
ws.onopen = function (event) {
console.log(
`[WebSocket] 连接成功: ${url.replace(secret || "", "***")}`,
);
reconnectAttempts = 0; // 重置重连计数
if (opt.onopen) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onopen(event);
}
};
ws.onmessage = function (event) {
if (opt.onmessage) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onmessage(event);
}
};
ws.onerror = function (event) {
console.error(
`[WebSocket] 连接错误: ${url.replace(secret || "", "***")}`,
);
// 错误处理
if (reconnectAttempts < MAX_RECONNECT) {
scheduleReconnect();
} else if (opt.onerror) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onerror(event);
}
};
ws.onclose = function (event) {
console.log(
`[WebSocket] 连接关闭: ${url.replace(secret || "", "***")}, 代码: ${event.code}`,
);
// 如果不是正常关闭(1000, 1001),尝试重连
if (
event.code !== 1000 &&
event.code !== 1001 &&
reconnectAttempts < MAX_RECONNECT
) {
scheduleReconnect();
} else {
if (opt.onclose) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onclose(event);
}
// 如果已达到最大重试次数
if (reconnectAttempts >= MAX_RECONNECT && opt.onmaximum) {
console.error(
`[WebSocket] 达到最大重试次数: ${url.replace(secret || "", "***")}`,
);
const anyOpt = opt as any;
anyOpt.onmaximum(event);
}
}
};
} catch (error) {
console.error(`[WebSocket] 创建连接失败:`, error);
if (opt.onerror) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onerror(
new ErrorEvent("error", { message: `创建连接失败: ${error}` } as any),
);
}
}
}
function scheduleReconnect() {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
}
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(1.5, reconnectAttempts), 10000); // 指数退避最大10秒
console.log(
`[WebSocket] 计划重连 (${reconnectAttempts}/${MAX_RECONNECT}) 延迟: ${delay}ms`,
);
reconnectTimeout = setTimeout(() => {
console.log(
`[WebSocket] 尝试重连 (${reconnectAttempts}/${MAX_RECONNECT})`,
);
cleanup();
connect();
}, delay);
}
function cleanup() {
if (ws) {
// 移除所有事件监听器
ws.onopen = null;
ws.onmessage = null;
ws.onerror = null;
ws.onclose = null;
// 如果连接仍然打开,关闭它
if (
ws.readyState === WebSocket.OPEN ||
ws.readyState === WebSocket.CONNECTING
) {
try {
ws.close();
} catch (e) {
console.error("[WebSocket] 关闭连接时出错:", e);
}
}
ws = null;
}
// 清除重连计时器
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
}
// 创建一个类似Sockette的接口对象
const socketLike = {
ws,
close: () => {
console.log(
`[WebSocket] 手动关闭连接: ${url.replace(secret || "", "***")}`,
);
cleanup();
},
reconnect: () => {
cleanup();
connect();
},
json: (data: any) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
},
send: (data: string) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
},
open: connect,
};
// 立即连接
connect();
return socketLike;
};
// 创建一个空的WebSocket对象
function createDummySocket() {
return {
close: () => {},
reconnect: () => {},
json: () => {},
send: () => {},
open: () => {},
};
}