/**
* SSE客户端实现(微信小程序专用)
* 功能:
* 1. 支持Server-Sent Events协议
* 2. 自动重连机制
* 3. 心跳检测
* 4. 多设备登录处理
* 5. 二进制数据解码
*/
class SSEClient {
constructor() {
// 基础连接配置
this.baseUrl = 'http://xxxxx:xxxx'; // 服务器地址
this.task = null; // 当前请求任务对象
// 重连相关
this.reconnectTimer = null; // 重连定时器
this.reconnectCount = 0; // 当前重连次数
this.MAX_RECONNECT = 5; // 最大重连次数
// 心跳检测
this.heartbeatTimer = null; // 心跳定时器
this.lastMessageTime = 0; // 最后收到消息时间
this.HEARTBEAT_INTERVAL = 10000; // 心跳间隔(ms)
// 状态控制
this.isForceClosed = false; // 是否被强制关闭(如多设备登录)
this._isConnectionConfirmed = false; // 是否确认连接成功(收到数据)
this._connectionStartTime = 0; // 连接开始时间
this._isClosing = false; // 是否正在关闭中
this._requestLock = false; // 请求锁防止重复连接
this._pendingToken = null; // 等待处理的token
}
/**
* 建立SSE连接(主入口)
* @param {string} token - 身份验证令牌
*/
async connect(token) {
// 验证token有效性
if (!this._validateToken(token)) {
console.warn('[SSE] 无效的token,拒绝连接');
return;
}
// 加锁防止重复请求
if (this._requestLock) {
console.log('[SSE] 已有连接正在进行');
return;
}
this._requestLock = true;
try {
// 第一步:安全关闭旧连接
await this._safeClose();
// 第二步:建立新连接
await this._establishConnection(token);
} catch (error) {
console.error('[SSE] 连接流程异常:', error);
this._scheduleReconnect(token);
} finally {
this._requestLock = false;
}
}
/**
* 安全关闭当前连接
* @return {Promise} 关闭完成的Promise
*/
_safeClose() {
return new Promise((resolve) => {
if (!this.task) {
return resolve();
}
console.log('[SSE] 正在关闭旧连接...');
this._isClosing = true;
const cleanup = () => {
this._clearTimers();
this.task = null;
this._isClosing = false;
this._isConnectionConfirmed = false;
resolve();
};
try {
// 尝试中止请求
if (typeof this.task.abort === 'function') {
this.task.abort();
}
// 给予50ms时间确保abort生效
setTimeout(cleanup, 50);
} catch (e) {
console.warn('[SSE] 关闭异常:', e);
cleanup();
}
});
}
/**
* 实际建立SSE连接
* @param {string} token - 身份验证令牌
*/
async _establishConnection(token) {
// 重置强制关闭状态(如果需要)
if (this.isForceClosed) {
console.warn('[SSE] 重置强制关闭状态');
this.isForceClosed = false;
}
console.log('[SSE] 正在建立新连接...', token);
this._connectionStartTime = Date.now();
return new Promise((resolve, reject) => {
// 创建微信请求任务
this.task = wx.request({
url: `${this.baseUrl}/sseController/connect/${token}`,
method: 'GET',
responseType: 'text',
enableChunked: true, // 启用分块传输
success: (res) => {
if (this._isClosing) {
return reject(new Error('Connection cancelled'));
}
if (res.statusCode !== 200) {
console.error('[SSE] 连接异常,状态码:', res.statusCode);
return reject(new Error(`Invalid status: ${res.statusCode}`));
}
console.log('[SSE] 连接初步建立');
this.reconnectCount = 0; // 重置重连计数器
resolve();
},
fail: (err) => {
if (!this._isClosing) {
console.error('[SSE] 连接失败', err.errMsg);
reject(err);
}
}
});
// 监听响应头
this.task.onHeadersReceived(res => {
this._validateContentType(res.header);
});
// 监听数据流
this.task.onChunkReceived(res => {
this._processIncomingData(res.data);
});
});
}
/**
* 验证token有效性
* @param {string} token - 待验证的token
* @return {boolean} 是否有效
*/
_validateToken(token) {
return token && typeof token === 'string' && token.length > 10;
}
/**
* 验证响应内容类型
* @param {object} headers - 响应头
*/
_validateContentType(headers) {
const contentType = headers['Content-Type'] || headers['content-type'];
if (!contentType || !contentType.includes('text/event-stream')) {
console.error('[SSE] 非SSE协议:', contentType);
this.close();
}
}
/**
* 处理接收到的数据
* @param {ArrayBuffer|string} rawData - 原始数据
*/
async _processIncomingData(rawData) {
// 首次收到数据时确认连接成功
if (!this._isConnectionConfirmed) {
this._isConnectionConfirmed = true;
console.log('[SSE] 连接确认成功',
`耗时:${Date.now() - this._connectionStartTime}ms`);
}
// 更新最后消息时间(用于心跳检测)
this.lastMessageTime = Date.now();
try {
const messageStr = await this._decodeData(rawData);
const validMessages = this._extractValidMessages(messageStr);
for (const msg of validMessages) {
try {
const parsed = JSON.parse(msg);
this._handleMessage(parsed);
} catch (e) {
console.error('[SSE] 单条消息解析失败:', e.message);
}
}
} catch (e) {
console.error('[SSE] 数据处理失败:', e.message);
}
}
/**
* 解码原始数据
* @param {ArrayBuffer|string} rawData - 原始数据
* @return {Promise<string>} 解码后的字符串
*/
async _decodeData(rawData) {
if (rawData instanceof ArrayBuffer) {
return this._decodeArrayBuffer(rawData);
}
return this._forceUTF8Decoding(String(rawData));
}
/**
* 解码ArrayBuffer数据
* @param {ArrayBuffer} buffer - 二进制数据
* @return {Promise<string>} 解码后的字符串
*/
_decodeArrayBuffer(buffer) {
return new Promise((resolve) => {
try {
const uintArray = new Uint8Array(buffer);
let str = '';
const chunkSize = 8192; // 分块处理避免堆栈溢出
for (let i = 0; i < uintArray.length; i += chunkSize) {
const chunk = uintArray.subarray(i, Math.min(i + chunkSize, uintArray.length));
str += String.fromCharCode.apply(null, chunk);
}
resolve(decodeURIComponent(escape(str)));
} catch (e) {
console.error('[SSE] 二进制解码失败:', e);
resolve('[DECODE ERROR]');
}
});
}
/**
* 强制UTF-8解码
* @param {string} str - 待解码字符串
* @return {string} 解码结果
*/
_forceUTF8Decoding(str) {
try {
// 检测是否存在高字节字符
return /[\x80-\xFF]/.test(str) ?
decodeURIComponent(escape(str)) :
str;
} catch (e) {
return str;
}
}
/**
* 从原始字符串提取有效消息
* @param {string} rawStr - 原始字符串
* @return {Array<string>} 有效消息数组
*/
_extractValidMessages(rawStr) {
const messages = [];
// 匹配标准SSE格式:data: {...}\n\n
const sseFormatRegex = /(?:^|\n)data:(\{.*?\})(?:\n\n|$)/g;
let match;
while ((match = sseFormatRegex.exec(rawStr)) !== null) {
messages.push(match[1]);
}
// 兼容非标准JSON格式
if (messages.length === 0) {
const jsonObjRegex = /\{[\s\S]*?\}(?=\\?\n|$)/g;
while ((match = jsonObjRegex.exec(rawStr)) !== null) {
messages.push(match[0]);
}
}
return messages;
}
/**
* 处理解析后的消息
* @param {object} msg - 消息对象
*/
_handleMessage(msg) {
if (!msg || !msg.status) {
console.log('[SSE] 收到无状态消息', msg);
return;
}
console.log('[SSE] 处理状态:', msg.status);
switch(msg.status) {
case '1': // 多设备登录
this._handleForceLogout(msg.message || '您已在其他设备登录');
break;
case '2': // 登录失效
this._handleSessionExpired(msg.message || '登录状态已失效');
break;
case '9': // 心跳消息
this.lastMessageTime = Date.now();
break;
default:
console.log('[SSE] 未知状态消息', msg);
}
}
/**
* 处理强制下线
* @param {string} message - 提示消息
*/
_handleForceLogout(message) {
this.isForceClosed = true;
wx.removeStorageSync('token');
this.close().then(() => {
wx.showModal({
title: '下线通知',
content: message,
showCancel: false,
complete: () => wx.reLaunch({ url: '/pages/login/login' })
});
});
}
/**
* 处理会话过期
* @param {string} message - 提示消息
*/
_handleSessionExpired(message) {
this.isForceClosed = true;
wx.removeStorageSync('token');
this.close().then(() => {
wx.showModal({
title: '登录失效',
content: message,
showCancel: false,
complete: () => wx.reLaunch({ url: '/pages/login/login' })
});
});
}
/**
* 启动心跳检测
*/
_startHeartbeatCheck() {
this._clearHeartbeatTimer();
this.heartbeatTimer = setInterval(() => {
const timeElapsed = Date.now() - this.lastMessageTime;
if (timeElapsed > this.HEARTBEAT_INTERVAL * 1.5) {
console.error('[SSE] 心跳超时', timeElapsed);
if (!this.isForceClosed) {
this.close();
this._scheduleReconnect(this._pendingToken);
}
}
}, this.HEARTBEAT_INTERVAL / 2);
}
/**
* 安排重连
* @param {string} token - 身份验证令牌
*/
_scheduleReconnect(token) {
if (!token || this.isForceClosed) return;
this.reconnectCount++;
// 达到最大重连次数
if (this.reconnectCount > this.MAX_RECONNECT) {
console.error('[SSE] 达到最大重连次数');
wx.showToast({
title: '连接断开,请检查网络',
icon: 'none'
});
return;
}
// 指数退避算法计算延迟
const delay = Math.min(3000 * Math.pow(2, this.reconnectCount - 1), 30000);
console.log(`[SSE] ${delay}ms后第${this.reconnectCount}次重试`);
this.reconnectTimer = setTimeout(() => {
this.connect(token);
}, delay);
}
/**
* 关闭连接(公开方法)
* @return {Promise} 关闭完成的Promise
*/
close() {
if (this._isClosing) return Promise.resolve();
return this._safeClose();
}
/**
* 清理所有定时器
*/
_clearTimers() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
}
// 导出单例实例
export const sse = new SSEClient();