📌 面试答不上的问题 - SSE通信

203 阅读3分钟

📌 面试答不上的问题

1️⃣ SSE(Server-Sent Events)原理以及各端实现方式

SSE(服务器发送事件)是一种基于 HTTP 的 单向通信 技术,服务器可以持续向客户端推送消息,但客户端不能主动向服务器发送数据(只能通过 HTTP 请求发送)。

🔹 工作原理

  1. 客户端通过 EventSource 发起 HTTP 请求 (长连接)
  2. 服务器返回 text/event-stream 格式的数据,持续推送消息。
  3. 连接保持打开状态,直到客户端关闭或服务器断开连接。

🔹 SSE 连接状态

SSE 连接状态由 EventSource.readyState 属性表示

状态描述
CONNECTING连接正在进行中,尚未建立连接。0
OPEN连接已建立并且可以接收消息。1
CLOSED连接已关闭,客户端不再接收消息,不能重新连接。2

🔹 SSE 相关事件

  1. open 事件
    当连接成功时触发,一旦连接建立,服务器和客户端之间的连接将保持打开,直到关闭或网络断开。可以在事件中执行连接建立后的操作。

    const eventSource = new EventSource("/sse/chat");
    eventSource.onopen = () => {
      console.log("连接成功!可以开始接收数据了");
    };
    
  2. message 事件
    当服务器通过 text/event-stream 协议发送数据时,这个事件会被触发。可以从 event.data 中获取服务器发送的数据。

    eventSource.onmessage = (event) => {
      console.log("接收到的数据:", event.data);
    };
    
  3. error 事件
    当连接发生错误时触发。常见错误包括网络断开或服务器关闭连接。在 error 事件中,通常会尝试重连(如果支持重连)。可以在这里处理连接失败或恢复等逻辑。

    eventSource.onerror = (err) => {
      console.error("连接发生错误:", err);
    };
    

📌 实现方式

🔹 服务器端(Node.js 示例)
const express = require('express');
const app = express();

app.get('/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  setInterval(() => {
    res.write(`data: ${JSON.stringify({ time: new Date().toISOString() })}\n\n`);
  }, 1000);
});

app.listen(3000, () => console.log('SSE Server running on port 3000'));
🔹 客户端(浏览器)
const eventSource = new EventSource('/events');

eventSource.onmessage = (event) => {
  console.log('Received:', event.data);
};

eventSource.onerror = () => {
  console.log('Connection closed, retrying...');
};
🔹 Uniapp(仅微信小程序支持)

流式请求(SSE)

  • 通过 enableChunked 参数判断是否使用流式响应。
  • 监听 onChunkReceived 处理服务器返回的流式数据。

数据处理工具

  • uint8ArrayToText()arrayBufferToString() 处理服务器返回的流式数据。

📌 片段有点长 还请耐心阅读

API - uni.request封装

主要步骤:

  1. connectChatSSE(data) :这是对话的 SEE 请求方法,传入 data 参数。
  2. requestrequest 是一个封装了请求发送逻辑的工具函数,能够处理不同类型的请求(如 HTTP 请求和 SEE 请求)。通过 enableChunked: true 来表示这次请求是一个 SEE 流式请求,微信那边也需要这个请求配置。
// 流式请求
async function chunkedRequest(options = {}) {
  if (!options?.enableChunked) return;
  options.complete = () => ""; // 避免默认回调
  const requestTask = uni.request(options);
  if (typeof options?.TaskCallBack === "function") {
    requestTask.onChunkReceived((res) => options.TaskCallBack(res));
  }
  return requestTask;
}

// WebSocket 请求
async function socketRequest(options = {}) {
  if (!options?.isWebSocket) return;
  const requestTask = uni.connectSocket({
    url: `${options.url}?token=${store.state.user.tokenData}`,
    header: options?.header || {},
    method: options?.method || "GET",
    complete: () => "",
  });
  return requestTask;
}

// 统一请求封装
export default async function request(options = {}) {
  try {
    options = await interceptorsRequest(options); // 请求拦截
    if (options?.enableChunked) return chunkedRequest(options);
    if (options?.isWebSocket) return socketRequest(options);
    const [err, response] = await uni.request(options);
    return interceptorsResponse(err, response, options); // 响应拦截
  } catch (e) {
    return Promise.reject(e);
  }
}

// 对话 API - SSE
function connectChatSSE(data) {
  return request({
      method: "POST",
      url: "/sse/chat",
      data: { msg: data?.msg || "", type: data?.type || "ALI" },
      enableChunked: true,
      header: { Authorization: data?.Authorization || "" },
      TaskCallBack: data?.TaskCallBack || "",
    })
}
组件内调用API

注明:小程序使用流式请求返回的数据类型是 字节数组 ArrayBuffer,需要做转码处理。 developers.weixin.qq.com/miniprogram…

浏览器处理转码可以使用

const uint8Array = new Uint8Array(event.data);
const text = new TextDecoder('utf-8').decode(uint8Array);

UniApp组件内使用

// SSE 连接处理 - 组件内使用
async function connectSee() {
  console.log("connectSee start");
  const [, connect] = await connectChatSSE({ msg: "你好" });
  if (!connect?.onChunkReceived) return;

  const chunkTaskFun = (res) => {
    const resText = uint8ArrayToText(res?.data) || "";
    if (resText.includes("第}10")) {
      console.log("移除");
      this.connectTask.abort(); // 终止请求
      connect.offChunkReceived(chunkTaskFun);
      this.connectTask = null;
    }
    this.currentContent += resText;
    console.log("connectTask", resText);
  };

  connect.onChunkReceived(chunkTaskFun);
  this.connectTask = connect;
}
工具类 - 处理数据编码
// Uint8Array 转字符串
export const uint8ArrayToText = (uint8Array) => {
  if (!uint8Array) return "";
  return arrayBufferToString(uint8Array);
};

// ArrayBuffer 转字符串
export const arrayBufferToString = (arr) => {
  if (typeof arr === "string") {
    return arr;
  }

  // 将 ArrayBuffer 转换为 Uint8Array 以处理每个字节
  var uint8Array = new Uint8Array(arr);
  var str = "";

  // 遍历 Uint8Array 并处理每个字节
  for (var i = 0; i < uint8Array.length; i++) {
    if (uint8Array[i]) {
      // 将当前字节转换为二进制表示
      var binary = uint8Array[i].toString(2);
      // 检查字节的开头是否是 UTF-8 多字节字符
      var match = binary.match(/^1+?(?=0)/);

      if (match && binary.length === 8) {
        // 处理多字节 UTF-8 字符
        var byteLength = match[0].length;
        var store = uint8Array[i].toString(2).slice(7 - byteLength);

        // 拼接多字节字符的剩余字节
        for (var j = 1; j < byteLength; j++) {
          if (uint8Array[i + j]) {
            store += uint8Array[i + j].toString(2).slice(2);
          }
        }
        // 将二进制字符串转换为字符
        str += String.fromCharCode(parseInt(store, 2));
        // 跳过多字节字符的其他字节
        i += byteLength - 1;
      } else {
        // 如果是单字节字符,直接转换为字符
        str += String.fromCharCode(uint8Array[i]);
      }
    }
  }
  return str;
};

📌 特点

✅ 轻量级,基于 HTTP 1.1,适用于单向数据流(如实时通知、股票推送)。
❌ 只支持服务器到客户端推送,无法双向通信。
❌ 仅支持文本数据(不支持二进制)。