阅读视图

发现新文章,点击刷新页面。

一文读懂 SSE

什么是SSE

SSE(Server Sent Events)是一种基于HTTP的轻量级实时通信技术,允许服务端主动向客户端推送数据,适用于需要进行实时数据流的场景。

从表面看,SSE 和 WebSocket 似乎都能实现“双向通信” —— 毕竟数据能在客户端和服务器之间流动。

双向通信的错觉

实际上,SSE 的通信模式是 单向的(服务器 → 客户端),而所谓的“双向”需要额外配合其他技术(如客户端通过普通 HTTP 请求发送数据)。

每次客户端建立 EventSource 时(GET请求),携带自定义参数,服务端收到请求后,解析参数,只要当前的 EventSource 没有关闭,服务端就能持续通过这个通道向客户端发送数据。

流程如下:

sse流程.png

从上图可以看出,整个数据传输的过程,除了客户端开头主动建立 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,可以清晰地看到每一次服务器返回的数据:

image.png

  • 多行文本
res.write(`data: 这是一条普通消息\n\n`)
res.write(`data: 这是一条普通消息\n\n`)

浏览器查看结果:

image.png

  • JSON字符串:
 res.write(`data: ${JSON.stringify({foo: "hello world"})}\n\n`);

浏览器查看结果:

image.png

注意:收到的是JSON字符串,需要进行JSON解析

  • 自定义事件:

服务端:

  res.write(`event: custom event name\n`)
  res.write(`data: 这是一条消息\n\n`)

前端:

 eventSource.addEventListener("custom event name", e => {
    console.log(e.data); // 输出这是一条消息
});

浏览器查看结果:

image.png

数据遵循 SSE 协议格式,以data:开头,并且每条消息以两个换行符\n\n结尾。

实践

在阅读完上文内容后,我们对 SSE 有了初步的了解,下面就来看看 SSE 的应用。

在 deepseek 的问答中,就使用了 SSE 来传输结果:

屏幕录制2025-05-14 17.48.48.gif

仔细看这个请求,发现 deepseek 在使用 SSE 和 理论上的有区别:它使用了POST请求,而并不是用 EventSource API 来实现

image.png

这是一个典型的 POST 请求 + SSE 响应 的组合,是现代 AI 聊天服务的主流实现方式。

这种组合解决了两个核心需求:

  1. 数据量和数据格式

    • POST 请求允许通过请求体发送大量数据,比 GET 请求灵活。
  2. 实时响应

    • 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);
                }
            }
        }
    }

效果:

屏幕录制2025-05-17 18.44.01.gif

当然,在实际业务中并不会使用这么简单的数据格式,一般会使用自定义事件数据格式或者JSON字符串数据格式,在数据中包含各种类型,方便前端根据类型做出判断。

总结

SSE作为基于 HTTP 的轻量级实时通信技术,凭借其单向推送的核心特性,在服务器主动传输数据的场景中展现出独特优势。

❌