SSE客户端实现(微信小程序专用)

/**

 * 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();

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值