前言
一提到「长连接」或者「服务端消息推送」,很多人脑子里第一反应就是 WebSocket。确实,WebSocket 很适合做聊天室、游戏对战这种双向通信的场景。
但其实,除了它,还有一个更轻量的方案: 👉 SSE(Server-Sent Events,服务端推送事件)。
它的特点是:
- 单向通道:只能 服务端 → 客户端,不能反过来。
- 基于 HTTP 协议,不需要额外协议。
- 实现起来比 WebSocket 简单很多。
一个典型的例子就是 ChatGPT 的「打字机效果」:它就是服务端不断往浏览器推消息,浏览器一点点显示。
接下来,我们就用代码来看看 SSE 怎么玩。
第一种方式:用 EventSource
浏览器自带了一个 API 叫 EventSource,专门用来接收 SSE 消息。
后端代码(Node.js + Express)
// 模拟文章内容
const article = `警告:当不使用 HTTP/2 时.....。`
app.get('/chat_typing', (req, res) => {
// 开启 SSE 通道
res.setHeader('Content-Type', 'text/event-stream')
let index = 0
const timerId = setInterval(() => {
const data = article[index] // 取出一个字符
index++
if (data) {
// 发送给前端,注意 \n\n 是必须的,表示一条消息结束
res.write(`data: ${data}\n\n`)
} else {
res.end() // 结束
clearInterval(timerId)
}
}, 100) // 每 100 毫秒推送一个字符
})
这里的关键点就是:
Content-Type要写成text/event-stream,表示这是 SSE。- 每次推送消息,都要用
data: 内容\n\n的格式。
前端代码(Vue3 示例)
<script setup>
import { ref } from 'vue'
const article = ref('')
let source
// 开启 SSE
const OpenSSE = () => {
source = new EventSource('http://localhost:3000/chat_typing')
source.addEventListener('message', (e) => {
article.value += e.data // 拼接输出
})
}
// 关闭 SSE
const CloseSSE = () => {
source.close()
}
</script>
<template>
<div>
<button @click="OpenSSE">开启SSE</button>
<button @click="CloseSSE">关闭SSE</button>
<div>{{ article }}</div>
</div>
</template>
点击按钮,就能看到文字被「打字机式」输出。
第二种方式:用 fetch + 流
EventSource 有个限制:
- 它只能用 GET 请求
- 不能很方便地传参数
如果你想用 POST(比如传上下文、带 token),那就需要另一种方式:fetch + 流式解析。
后端改造
app.post('/chat_typing', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.write(`data: 这是服务器推送的数据\n\n`)
})
前端用 fetch + ReadableStream
let abort
// 开启 SSE(POST)
const OpenSSE = async () => {
abort = new AbortController()
const res = await fetch('http://localhost:3000/chat_typing', {
method: 'POST',
signal: abort.signal, // 用来终止请求
})
const reader = res.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
console.log(decoder.decode(value)) // 输出推送内容
}
}
// 关闭 SSE
const CloseSSE = () => {
abort.abort()
}
这里我们手动解析返回的流:
getReader()用来一点点读数据TextDecoder把二进制转成字符串abort.abort()就能关闭连接
这种方式更灵活,支持 POST、带参数、带请求头。
SSE 适合什么场景?
- 实时消息提醒(通知、公告)
- 进度更新(比如文件上传进度、训练任务状态)
- 聊天对话的单向输出(比如 AI 输出回答)
如果你只需要 单向推送,SSE 简直不要太省心!
总结
- WebSocket:适合双向通信,功能更强大。
- SSE:适合服务端单向推送,轻量简单。
EventSource:开箱即用,适合简单场景。fetch + 流:更灵活,支持 POST、带参数。
以后别再觉得「长连接 = WebSocket」,其实 SSE 有时候更适合。