一文读懂 SSE
什么是SSE
SSE(Server Sent Events)是一种基于HTTP的轻量级实时通信技术,允许服务端主动向客户端推送数据,适用于需要进行实时数据流的场景。
从表面看,SSE 和 WebSocket 似乎都能实现“双向通信” —— 毕竟数据能在客户端和服务器之间流动。
双向通信的错觉
实际上,SSE 的通信模式是 单向的(服务器 → 客户端),而所谓的“双向”需要额外配合其他技术(如客户端通过普通 HTTP 请求发送数据)。
每次客户端建立 EventSource 时(GET请求),携带自定义参数,服务端收到请求后,解析参数,只要当前的 EventSource 没有关闭,服务端就能持续通过这个通道向客户端发送数据。
流程如下:
从上图可以看出,整个数据传输的过程,除了客户端开头主动建立 SEE 请求外,后续都是由服务端给客户端发送数据, 这种单向推送机制正是 SSE 的核心设计特点。
为了更清晰地理解 SSE,从以下几个维度对 SSE 和 WebSocket 进行对比:
特性 | SSE | WebSocket |
---|---|---|
协议基础 | HTTP 长连接 | 独立的 WebSocket 协议 |
连接开销 | 低(复用 HTTP ) | 中(需升级协议) |
通信模式 | 单向(服务器→客户端) | 双向通信 |
二进制支持 | 仅文本传输 | 支持文本与二进制数据 |
客户端 API | 浏览器内置 EventSource | 浏览器内置 WebSocket 对象 |
典型场景 | 服务器主动推送(如新闻推送、ai智能回复) | 双向交互(如聊天、协作) |
至于具体要选择 SSE 还是 WebSocket,应根据业务需求决定——是否需要真正的双向通信。
如何使用 SSE
客户端建立 SSE 连接
在客户端建立 SSE 连接十分简洁,借助浏览器内置的 EventSource API,只需数行代码,就能向服务器发起连接。
const eventSource = new EventSource('/xxxx?question=hi');
eventSource.onmessage = (event) => {
console.log('收到消息:', event.data);
};
上述代码中,通过 new EventSource
创建一个 SSE 实例,并指定连接的服务器地址,同时可以在 URL 中携带参数。
然后通过 onmessage
事件监听服务器推送的数据。
服务端响应
服务器端在响应 SSE 请求时,必须包含特定的响应头。
这些响应头包括:
-
Content-Type : text/event-stream; :必须将该响应头设置为
text/event-stream
,这是 SSE 协议规定的内容类型。 - Connection: keep-alive; :保持长连接,使得服务器能够持续向客户端推送数据。
以 express 框架为例,服务端的实现代码如下:
app.get('/xxxx', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Connection', 'keep-alive');
const question = decodeURIComponent(req.query.question); // 获取参数
res.write(`data: this is a test data\n\n`);
res.write('data: [DONE]\n\n');
});
在这段代码中,首先通过 res.setHeader
设置了必要的响应头,然后使用res.write
方法向客户端发送数据。
服务器发送的消息必须严格遵循 SSE 协议格式,常见的格式有以下几种:
- 单行文本:
res.write(`data: 这是一条普通消息\n\n`)
在浏览器开发者工具中的network
面板里查看 EventStream,可以清晰地看到每一次服务器返回的数据:
- 多行文本:
res.write(`data: 这是一条普通消息\n\n`)
res.write(`data: 这是一条普通消息\n\n`)
浏览器查看结果:
- JSON字符串:
res.write(`data: ${JSON.stringify({foo: "hello world"})}\n\n`);
浏览器查看结果:
注意:收到的是JSON字符串,需要进行JSON解析
- 自定义事件:
服务端:
res.write(`event: custom event name\n`)
res.write(`data: 这是一条消息\n\n`)
前端:
eventSource.addEventListener("custom event name", e => {
console.log(e.data); // 输出这是一条消息
});
浏览器查看结果:
数据遵循 SSE 协议格式,以data:
开头,并且每条消息以两个换行符\n\n
结尾。
实践
在阅读完上文内容后,我们对 SSE 有了初步的了解,下面就来看看 SSE 的应用。
在 deepseek 的问答中,就使用了 SSE 来传输结果:
仔细看这个请求,发现 deepseek 在使用 SSE 和 理论上的有区别:它使用了POST请求,而并不是用 EventSource API 来实现。
这是一个典型的 POST 请求 + SSE 响应 的组合,是现代 AI 聊天服务的主流实现方式。
这种组合解决了两个核心需求:
-
数据量和数据格式:
- POST 请求允许通过请求体发送大量数据,比 GET 请求灵活。
-
实时响应:
- SSE 流式响应允许服务器在生成回复的过程中逐步发送给客户端,提升用户体验。
下面笔者将介绍这种组合的实现方式。
服务端代码:
app.post('/api/chat/completion', async (req, res) => {
// 设置 SSE 响应头
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
try {
// 模拟 AI 生成回复
const replys =
"刚进入大学想成为前端开发者,建议从HTML/CSS/JavaScript基础学起,先做静态网页练习。大二重点学习React或Vue框架,掌握组件开发和状态管理,同时熟悉Git和Webpack等工具。大三开始做完整项目,学习TypeScript和性能优化,尝试参与开源或团队协作。大四前要完成3-5个高质量作品,部署上线并整理成作品集。平时多逛GitHub、掘金等技术社区,保持每周20小时以上的编码时间。找实习时重点准备前端面试题,包括CSS布局、JS原理和框架特性等。记住动手实践比只看教程更重要,遇到问题先自己调试再求助。保持对新技术的敏感度,但先深入掌握核心技能再扩展知识面。坚持每天写代码,毕业时就能达到初级前端工程师的水平。"
.split('');
for (const reply of replys) {
// 发送每个数据块
res.write(`data: ${reply}\n\n`);
// 模拟生成延迟
await new Promise(resolve => setTimeout(resolve, 20));
}
// 结束响应
res.end();
} catch (error) {
res.write(`data: error\n\n`);
res.end();
}
});
前端请求和处理结果:
const response = await fetch('http://localhost:3000/api/chat/completion', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
// 问题ID
// 问题等参数
.....
})
});
// 处理流式响应
const reader = response.body.getReader();
const decoder = new TextDecoder();
let accumulatedText = '';
const answerId = 'answer-' + Date.now();
chatContainer.innerHTML += `<p><strong>问:</strong>${question}</p>`;
chatContainer.innerHTML += `<p><strong>答:</strong><span id="${answerId}"></span></p>`;
// 清空输入框
questionInput.value = '';
const answerElement = document.getElementById(answerId);
while (true) {
const { done, value } = await reader.read();
if (done) break;
// 解码数据块
const chunk = decoder.decode(value, { stream: true });
// 解析每个 SSE 消息
const messages = chunk.split('\n\n');
for (const message of messages) {
if (message.startsWith('data: ')) {
try {
accumulatedText += message.substring(6);
answerElement.textContent = accumulatedText;
} catch (e) {
console.error('解析消息失败:', e);
}
}
}
}
效果:
当然,在实际业务中并不会使用这么简单的数据格式,一般会使用自定义事件数据格式或者JSON字符串数据格式,在数据中包含各种类型,方便前端根据类型做出判断。
总结
SSE作为基于 HTTP 的轻量级实时通信技术,凭借其单向推送的核心特性,在服务器主动传输数据的场景中展现出独特优势。