什么是 SSE
SSE,即 Server-Sent Events,是一种基于 HTTP 的服务器向客户端推送消息的方式。它的特点是一种单向连接,因此无法将事件从客户端发送到服务器。
因为 Ai 生成的时候,生成的句子都是由一个一个的语句逐渐生成拼接起来的,所以为了减少用户在客户端的等待消息的时间,将生成的语句通过 SSE 马上就推送到客户端,而不是等待完全生成完成之后再返回消息,这样用户在客户端就可以实时的看到生成的结果,减少等待的时间。
特点是:
- 简单设置: SSE 易于实现,只需要标准HTTP连接
- 单向通信: SSE 允许服务器向客户端推送数据,而不需要客户端的任何操作
- 基于文本: SSE 以纯文本形式发送数据,使其易于理解和使用
豆包 Ai 使用 SSE 接收消息的例子
梳理一下豆包从提问到回答的全过程
-
输入消息,点击回车后,豆包发送了一个 POST 请求,其中Header
Content-Type的值为application/json,表明请求体是一个 JSON ,同时在请求体中携带了当前经过编码的消息msToken。请求体内容
-
发送消息之后,豆包返回了 Header 的
Content-Type为text/event-stream的 response ,这个 MIME 为text/event-stream类型,非常重要,服务端通过此字段告诉浏览器返回的是一个数据流,数据流会通过不断的接收数据返回过来。 -
事件流发送完毕,返回
event:done表示结束
让我们来总结一下
-
浏览器是如何区分这是一个
SSE通过返回 Header
Content-Type为text/event-stream来表明自己返回的是一个数据流。 -
SSE返回的数据流本质上是一个文本如豆包返回的数据流
... id:678 event:pb data:CAEScgoPMTIyOTI5NzUwMTU0MjQyEg8xMjI5Mjk3NTAxNTQ0OTgaDzEyMzAyNTEzNzgzNzgyNjoPMTIzMDI1MTM3ODM3MzE0UAFYAmIOeyJzdWdnZXN0IjpbXX1yDwoLaW5wdXRfc2tpbGwSAHgBgAGmBYgBATAB event:done data:服务端每一次发送的信息,由若干个 `message` 组成,每个`message`之间用`\n\n`分隔。每个`message`内部由若干行组成,每一行都是如下格式。[field]: value\n上面的
field可以取四个值。data event id retry各个字段的解释
标识所述事件类型的字符串。如果指定了该字符串,则将在浏览器上向指定事件名称的侦听器发送事件;网站源代码应使用来
addEventListener()侦听命名事件。onmessage如果未为消息指定事件名称,则调用处理程序。消息的数据字段。当
EventSource收到以 开头的多个连续行时data:,它会将它们连接起来,并在每行之间插入换行符。结尾的换行符会被删除。用于设置对象的最后一个事件 ID 值的事件 ID 。
重新连接时间。如果与服务器的连接丢失,浏览器将等待指定的时间,然后再尝试重新连接。这必须是一个整数,以毫秒为单位指定重新连接时间。如果指定非整数值,则忽略该字段。
特别的,在前面只加上冒号,代表一段注释
: this is a test stream
使用 Fetch Post 请求处理 SSE
背景:实现一个大模型的聊天请求
前端封装了一个使用 fetch 发送 post ,并且处理消息的 sendMessage 请求
export async function sendMessage(
// aiId
botId: string,
// 发送消息
message: string,
// 聊天历史
history: {
content: string;
}[],
) {
const response = await fetch(`//xxx/send-message`, {
method: 'POST', // 使用 post 方法
headers: {
Authorization: getAuthorization(), // 根据情况,带上登录态
Accept: 'text/event-stream', // 可写可不写,写上明确表明需要返回数据流
'Content-Type': 'application/json;charset=UTF-8', // 默认是 text/plain 必须设置为 application/json,不然后端无法解析出 body 内容
},
// body 是一个字符串,指定 Content-Type 以表明内容格式,
body: JSON.stringify({
botId,
msg: message,
history,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let done: boolean;
do {
const { done: currentDone, value } = await reader.read();
done = currentDone;
if (done) return;
const text = decoder.decode(value);
console.log(text);
// 自行解析
} while (!done);
}
-
代码解释
通过 featch 方法发送了一个
POST请求,并且设置 Header 的Content-Type为application/json,在body中携带了必传的参数。接着,
featch完成之后得到response,response.body是一个 ReadableStream 对象,ReadableStream对象提供了便捷的工具去读取数据流,如通过await response.body.getReader().read()可以获取到文本内容。
一般,在实际使用时,sendMessage 函数需要加上一个接收消息的参数,用来显示在页面上
function receiver(message: ChatResponse) {
const element = getElementById('id1');
element.innerHtml = element.innerHtml + message.content;
}
async function sendMessage(
botId: string,
message: string,
history: {
content: string;
}[],
receiver: (chatMessage: ChatResponse[]) => void,
) {
const response = await fetch(`//xxx/send-message`, {
method: 'POST',
headers: {
Authorization: getAuthorization(), // 根据需要,带上登录态
Accept: 'text/event-stream',
'Content-Type': 'application/json;charset=UTF-8',
},
body: JSON.stringify({
botId,
msg: message,
history,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let done: boolean;
do {
const { done: currentDone, value } = await reader.read();
done = currentDone;
if (done) return;
let text = decoder.decode(value);
// text中可能有多条 data
// 总感觉应该有更规范的方法,解析内容
if (text.includes('data: [DONE]')) {
text = text.replace(/data: [DONE]/g, '');
if (!text) return;
}
const jsonText = `[${text.replace(/data: /g, ',')}]`.replace(/^[,/, '[');
receiver(JSON.parse(jsonText));
console.log(JSON.parse(jsonText));
} while (!done);
}
sendMessage('bot1', '你是谁', [{history: '你好'}], receiver);
使用原生 EventSource 请求
当然,也可以通过原生的 EventSource 来发送消息,这样就可以直接通过订阅消息 onMeeage 来处理消息,缺点是扩展性比较差,比如无法发起 POST 请求等。
const source = new EventSource(url, { withCredentials: true });
source.onmessage = function (event) {
const data = event.data;
...
};
...
source.close();
That's all.欢迎指正~