服务端推送长连接,SSE

182 阅读3分钟

前言

一提到「长连接」或者「服务端消息推送」,很多人脑子里第一反应就是 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 有时候更适合