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

### 微信小程序实现 SSE 的解决方案 尽管微信小程序本身并未原生支持 Server-Sent Events (SSE),但可以通过模拟 HTTP 长轮询或自定义协议来实现类似的流式数据传输功能。以下是通过 UniApp `uni.request` 方法实现的一个简单 SSE 客户端的思路[^1]。 #### 1. 使用 `uni.request` 模拟 SSE 行为 由于微信小程序不支持标准的 EventSource API,可以利用 `uni.request` 来持续监听来自服务器的消息推送。具体方法如下: - **设置请求参数** 将 `dataType` 设置为 `"text"` 或 `"arraybuffer"`,以便能够接收未处理的数据流。 - **保持连接状态** 在每次接收到消息后重新发起新的请求,从而形成一种伪长连接的效果。 ```javascript let eventSource; function initEventSource() { if (!eventSource) { eventSource = uni.request({ url: 'https://your-server-endpoint/sse', // 替换为目标地址 method: 'GET', dataType: 'text', // 接收纯文本形式的数据 success(res) { const data = res.data; handleIncomingData(data); // 处理传入的数据 }, fail(err) { console.error('Request failed:', err); setTimeout(initEventSource, 5000); // 如果失败则重试 } }); } } // 数据解析函数 function handleIncomingData(rawData) { let lines = rawData.split('\n'); lines.forEach(line => { if (line.startsWith('data:')) { // 判断是否为有效数据行 let payload = line.substring(5).trim(); try { let parsedData = JSON.parse(payload); console.log('Received message:', parsedData); updateUI(parsedData); // 更新界面逻辑 } catch (e) { console.warn('Invalid JSON received:', payload); } } }); } ``` 上述代码片段展示了如何使用 `uni.request` 构建一个简易版的 SSE 客户端。需要注意的是,在实际应用中可能还需要考虑断线重连机制以及性能优化等问题。 --- #### 2. WebSocket 替代方案 如果目标平台允许,则推荐优先采用 WebSocket 技术替代传统的 SSE 方案。WebSocket 提供了真正的双向通信能力,并且兼容性更好。对于大多数实时应用场景来说,它是一个更优的选择。 创建 WebSocket 连接的方式非常直观: ```javascript const socket = new wx.connectSocket({ url: 'wss://your-websocket-url' }); wx.onSocketOpen(() => { console.log('Connected to server.'); }); wx.onSocketMessage((res) => { console.log('Received from server:', res.data); }); ``` 虽然此方式并非严格意义上的 SSE 实现,但在许多情况下足以满足业务需求。 --- #### 3. 跨域问题与安全性考量 无论是选择基于 HTTP 的长轮询还是切换到 WebSocket 协议,都需要特别注意跨域资源共享(CORS)策略配置以及身份验证措施的安全设计。确保仅授权合法用户访问敏感资源是非常重要的一步。 --- ### 总结 综上所述,在当前环境下无法直接依赖于传统浏览器环境下的 EventSource 对象完成 SSE 功能开发工作;然而借助框架所提供的工具集(如UniApp中的`uni.request`),仍然有可能构建出具备相似特性的解决方案。与此同时,评估项目具体情况之后选用更为合适的通讯手段也是值得提倡的做法之一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值