📌 面试答不上的问题
1️⃣ SSE(Server-Sent Events)原理以及各端实现方式
SSE(服务器发送事件)是一种基于 HTTP 的 单向通信 技术,服务器可以持续向客户端推送消息,但客户端不能主动向服务器发送数据(只能通过 HTTP 请求发送)。
🔹 工作原理
- 客户端通过
EventSource发起 HTTP 请求 (长连接)。 - 服务器返回
text/event-stream格式的数据,持续推送消息。 - 连接保持打开状态,直到客户端关闭或服务器断开连接。
🔹 SSE 连接状态
SSE 连接状态由 EventSource.readyState 属性表示
| 状态 | 描述 | 值 |
|---|---|---|
CONNECTING | 连接正在进行中,尚未建立连接。 | 0 |
OPEN | 连接已建立并且可以接收消息。 | 1 |
CLOSED | 连接已关闭,客户端不再接收消息,不能重新连接。 | 2 |
🔹 SSE 相关事件
-
open事件
当连接成功时触发,一旦连接建立,服务器和客户端之间的连接将保持打开,直到关闭或网络断开。可以在事件中执行连接建立后的操作。const eventSource = new EventSource("/sse/chat"); eventSource.onopen = () => { console.log("连接成功!可以开始接收数据了"); }; -
message事件
当服务器通过text/event-stream协议发送数据时,这个事件会被触发。可以从event.data中获取服务器发送的数据。eventSource.onmessage = (event) => { console.log("接收到的数据:", event.data); }; -
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封装
主要步骤:
connectChatSSE(data):这是对话的 SEE 请求方法,传入data参数。request:request是一个封装了请求发送逻辑的工具函数,能够处理不同类型的请求(如 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,适用于单向数据流(如实时通知、股票推送)。
❌ 只支持服务器到客户端推送,无法双向通信。
❌ 仅支持文本数据(不支持二进制)。