AI大模型的文本流如何持续吐到前端,实时通信的技术 SSE(Server-Sent Events) 认知

写在前面


  • 没接触过 SSE(Server-Sent Events),AI大模型出来之后,一直以为文本流是用 WebSocket 做的
  • 偶然看到返回到报文格式是 text/event-stream,所以简单认知,整理笔记
  • 博文内容涉及 SSE 认知,以及对应的 Demo
  • 理解不足小伙伴帮忙指正 😃,生活加油

99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式

持续分享技术干货,感兴趣小伙伴可以关注下 _


好奇大模型回答的文本流推送的是一句句话,还是一个个分词,然后看了下,才发现用的并不是 WebSocket ,是 SSE

下面是部分请求头,我们可以看到,请求报文类型为 content-type:text/event-stream; charset=utf-8

access-control-allow-credentials:true
content-type:text/event-stream; charset=utf-8
date:Sat, 15 Feb 2025 02:58:53 GMT
server:elb
strict-transport-security:max-age=31536000; includeSubDomains; preload
x-content-type-options:nosniff
x-ds-trace-id:20bd419739717b9d60a3224ab65b3620

text/event-stream; charset=utf-8 是一种 Server-Sent Events(SSE)MIME 类型。SSE 是一种允许服务器向客户端推送实时更新的技术,它基于 HTTP 协议,适用于需要服务端单向、实时数据传输的场景,如股票行情、新闻推送、社交媒体更新等。

通过下面的截图也可以看到,实际返回的是分词数据

deepseek 的

在这里插入图片描述

腾讯元宝 的

在这里插入图片描述

SSE 认知

SSEHTML5 规范的一部分,具体的规范文档可以在 W3C(万维网联盟)的官方网站上看到

查看规范以及对应的 API 文档,可以查看 W3C 官网对应的内容

https://htmlspecs.com/#server-sent-events

实际的代码使用 Demo 以及浏览器兼容问题,可以 Mozilla 官网看到

https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events

用一句话讲 ,SSE 即使服务器能够通过 HTTP 或使用专用的服务器推送协议向网页推送数据,引入了EventSource 接口,该API 包括创建一个EventSource 对象并注册一个事件监听器。

var source = new EventSource('updates.cgi');
source.onmessage = function (event) {
  alert(event.data);
};

在服务器端,通过 MIME 类型为text/event-stream 报文类型,下面是一个 Demo,实际可能需要考虑更多,比如异常处理等等

package com.example.ssestock;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;

import java.io.IOException;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

@RestController
public class StockController {

    private final ExecutorService executor = Executors.newCachedThreadPool();
    private final Random random = new Random();

    @GetMapping("/stock-price")
    public SseEmitter streamStockPrice() {
        SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); // 设置超时时间为最大值
        executor.execute(() -> {
            try {
                // 发送初始连接事件
                emitter.send(SseEmitter.event().name("connectionEstablished").data("连接已建立"));

                while (true) {
                    double price = 100 + random.nextDouble() * 10; // 生成100到110之间的随机价格
                    String message = String.format("data: %.2f\n\n", price);
                    emitter.send(SseEmitter.event().name("priceUpdate").data(price));

                    // 模拟偶尔的错误事件
                    if (random.nextDouble() < 0.05) { // 5% 的概率发送错误事件
                        emitter.send(SseEmitter.event().name("error").data("价格数据获取失败"));
                    }

                    Thread.sleep(1000); // 每秒发送一次
                }
            } catch (IOException | InterruptedException e) {
                emitter.completeWithError(e);
            }
        });

        return emitter;
    }
}

可以使用不同的事件类型来区分事件。上面是一个包含 "priceUpdate" 和 "error" 两种事件类型的流,默认的事件类型是 "message"。事件流始终以 UTF-8 解码。无法指定其他字符编码。

在客户端,监听对应的事件即可

const sse = new EventSource("/api/v1/stock-price");

/*
 * 这将仅监听类似下面的事件
 *
 * event: priceUpdate
 * data: useful data
 * id: someid
 */
sse.addEventListener("priceUpdate", (e) => {
  console.log(e.data);
});

/*
 * 同理,以下代码将监听具有字段 `event: error` 的事件
 */
sse.addEventListener("error", (e) => {
  console.log(e.data);
});

/*
 * “message”事件是一个特例,因为它可以捕获没有 event 字段的事件,
 * 以及具有特定类型 `event:message` 的事件。
 * 它不会触发任何其他类型的事件。
 */
sse.addEventListener("message", (e) => {
  console.log(e.data);
});

事件流请求可以像普通 HTTP 请求一样使用 HTTP 301 和 307 重定向进行重定向。如果连接关闭,客户端将重新连接;可以通过使用 HTTP 204 无内容响应代码来告知客户端停止重新连接。

需要注意的是:

当不使用 HTTP/2 时,服务器发送事件(SSE)受到打开连接数的限制,这个限制是对于浏览器的,并且设置为非常低的数字(6),打开多个选项卡时可能会特别痛苦。在 Chrome 和 Firefox 中,这个问题已被标记为“不会修复”。这个限制是每个浏览器和域名的,这意味着你可以在所有标签页中打开 6 个 SSE 连接到 www.example1.com,以及另外 6 个 SSE 连接到 www.example2.com(来源:Stackoverflow)。当使用 HTTP/2 时,最大并发 HTTP 流的数量是由服务器和客户端协商的(默认为 100)。

SSE 可以做什么

先看看 缺点:

SSE(Server-Sent Events)的缺点主要包括:

  1. 单向通信:SSE仅支持服务器向客户端的单向通信,客户端无法主动向服务器发送数据
  2. 浏览器并发限制:HTTP/1 浏览器对单个域名的EventSource连接数有限制(通常为6个)
  3. 仅支持文本数据:SSE只能传输UTF-8文本,不支持二进制数据(如图片、音频、视频流),限制了其在多媒体应用中的使用。

优点:

  • 基于 HTTP 协议:直接复用现有 HTTP 基础设施,无需额外协议(如 WebSocket 的 ws:// 或 wss://),也无需处理复杂的握手和协议升级。
  • 浏览器原生支持:通过 EventSource API 直接使用,无需引入第三方库
  • 低带宽消耗:相比 WebSocket 的帧头开销,SSE 的协议头更简单,适合高频小数据量推送(如实时日志、状态更新)。
  • 内置重连机制:连接中断时,浏览器会自动尝试重新连接,开发者无需手动处理。
  • 支持历史事件 ID:可通过 last-event-id 请求头实现断点续传,避免数据丢失。
  • 连接复用:一个 HTTP 连接支持多次数据推送,减少频繁建立连接的开销(对比短轮询)。
  • 无双向通信风险:由于 SSE 是单向的(服务器→客户端),减少了客户端主动攻击的入口面。

所以选择 SSE 的场景:

当需要服务器向客户端持续推送数据,且无需客户端频繁回传时,SSE 是比 WebSocket 更简单高效的解决方案。同时相比 http 轮询的方式,节省了资源,实现长链接复用,对于分布式的场景,可以考虑使用 MQ 或则 Redis 实现分布式广播。

常见应用:

  • 实时通知:如邮件提醒、社交媒体动态更新。
  • 数据流监控:服务器日志流、IoT 设备状态推送,大模型的问答分词数据流。
  • 实时数据展示:股票行情、新闻推送、赛事比分。

当然 SSE 主要面向 B/S 架构,是浏览器实现服务器推送的轻量级方案。非浏览器场景下 SSE 可用但非最优,对 C/S 架构或服务间通信,需权衡实现成本与需求(如单向性、HTTP 兼容性)。若需双向通信或高性能数据传输,建议选择 WebSocket、gRPC 或 MQTT

博文部分内容参考

© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 😃


https://developer.mozilla.org/zh-CN/docs/Web/API/Server-sent_events

https://htmlspecs.com/#server-sent-events


© 2018-至今 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/970987.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

计时器任务实现(保存视频和图像)

下面是一个简单的计时器任务实现&#xff0c;可持续地每秒保存一幅图像&#xff0c;也可持续地每60秒保存一个视频&#xff0c;图像和视频均以当前时间命名&#xff1a; TimerTask类的实现如下&#xff1a; class TimerTask { public:TimerTask(const std::string& path):…

力扣19题——删除链表的倒数第 N 个结点

#题目 #代码 //定义虚拟头结点ListNode curnew ListNode(0,head); //定义两个指针指向虚拟头结点ListNode lcur;ListNode rcur;for(int i0;i<n;i){rr.next;}while(r!null){rr.next;ll.next;} //l.next就是我们要删除的那个元素if(l.next!null){l.nextl.next.next;}return c…

网络工程师 (42)IP地址

一、定义与功能 IP地址是IP协议提供的一种统一的地址格式&#xff0c;它为互联网上的每一个网络和每一台主机分配一个逻辑地址&#xff0c;以此来屏蔽物理地址的差异。这种地址分配方式确保了用户在连网的计算机上操作时&#xff0c;能够高效且方便地从众多计算机中选出自己所需…

记忆力训练day19

万能字母组合编码法 所有的文字和字母的背后都有画面 练的不是记单词&#xff0c;练的是注意力给到单词&#xff0c;出什么画面&#xff0c;然后画面与画面之间进行连接 拆的过程就是找熟词的过程 要关注自己的回忆路径是什么&#xff1f;也就是你是怎么回忆起来的&#xff0c…

flutter image_cropper插件安装后 打包apk 报错命名空间问题

本篇文章主要讲解&#xff0c;Flutter安装完新依赖打包apk报错 A problem occurred configuring project ‘:image_cropper’. 命名空间问题的解决办法及原因说明。 日期&#xff1a;2025年2月15日 作者&#xff1a;任聪聪 一、报错现象&#xff1a; 报文信息&#xff1a; FAI…

八、SPI读写XT25数据

8.1 SPI 简介 SPI&#xff08;Serial Peripheral Interface&#xff0c;串行外设接口&#xff09;是一种同步串行通信协议&#xff0c;广泛用于嵌入式系统中连接微控制器与外围设备&#xff0c;如传感器、存储器、显示屏等。 主要特点 1. 全双工通信&#xff1a;支持同时发送…

kibana es 语法记录 elaticsearch

目录 一、认识elaticsearch 1、什么是正向索引 2、什么是倒排索引 二、概念 1、说明 2、mysql和es的对比 三、mapping属性 1、定义 四、CRUD 1、查看es中有哪些索引库 2、创建索引库 3、修改索引库 4、删除索引库 5、新增文档 6、删除文档 5、条件查询 一、认识…

三、Unity基础(主要框架)

一、Unity场景概念 如果把游戏运行过程理解成表演&#xff0c;那么场景就是舞台&#xff1b; 场景本质上是一个配置文件&#xff0c;这个配置文件决定了场景中有哪些东西&#xff1b; 二、Scene和Game窗口 1、Scene 滚轮缩放、拖动 单独选中也可以 最下面这个是全能工具…

pdf文档提取信息

目录 一、前言二、核心代码说明1、PyPDF2提取文本2、pdfplumber提取文本和表格3、fitz提取文本和图片4、fitz按页提取图片一、前言 本博客文章介绍pdf的文本、图片、表格等信息提取的技术方案对比。目前比较熟知的是pdfplumber 、PyPDF2 、fitz(PyMuPDF)。 它们之间对比如下 …

Git指南-从入门到精通

代码提交和同步命令 流程图如下&#xff1a; 第零步: 工作区与仓库保持一致第一步: 文件增删改&#xff0c;变为已修改状态第二步: git add &#xff0c;变为已暂存状态 bash $ git status $ git add --all # 当前项目下的所有更改 $ git add . # 当前目录下的所有更改 $ g…

我们来学HTTP/TCP -- 三次握手?

三次握手 题记三次呼叫结语 题记 来&#xff0c;我们来演示下川普王和普京帝会面了 哎呦&#xff01;你好你好&#xff0c;握手…哎嗨&#xff01;侬好侬好&#xff0c;握手…欧嘿呦玛斯&#xff0c;握手… 抓狂啊&#xff01;作孽啊!!! 不说人话啊! 关键的是&#xff0c;“三…

kubectl top输出与Linux free命令不一致原因?

当你在 Kubernetes 集群中使用 kubectl top 命令查看资源使用情况时&#xff0c;可能会发现与在节点上直接运行 Linux free 命令得到的结果不一致。这种不一致可能源于多个原因&#xff0c;以下是一些关键因素&#xff1a; MobaXterm中文版下载&#xff1a; https://pan.quark…

【设计模式】【行为型模式】迭代器模式(Iterator)

&#x1f44b;hi&#xff0c;我不是一名外包公司的员工&#xff0c;也不会偷吃茶水间的零食&#xff0c;我的梦想是能写高端CRUD &#x1f525; 2025本人正在沉淀中… 博客更新速度 &#x1f44d; 欢迎点赞、收藏、关注&#xff0c;跟上我的更新节奏 &#x1f3b5; 当你的天空突…

论文解读之DeepSeek R1

今天带来DeepSeek R1的解读 一、介绍 deepseek主打复杂推理任务&#xff0c;如数学、代码任务。 R1以预训练过的V1-base初始化&#xff0c;主要发挥了RL在长思维链上的优势&#xff0c;R1-Zero直接RL而在前置步骤中不进行SFT&#xff0c;即缺少了有监督的指令微调阶段&#…

Linux:用 clang 编译带 sched_ext 功能内核

文章目录 1. 前言2. 编译过程2.1 准备内核源代码2.2 安装编译工具2.3 配置、编译、运行2.3.1 配置2.3.2 编译2.3.3 运行 3. 参考资料 1. 前言 限于作者能力水平&#xff0c;本文可能存在谬误&#xff0c;因此而给读者带来的损失&#xff0c;作者不做任何承诺。 2. 编译过程 …

FPGA之​​​​​​​​​​​​​​HRBANK与HOBANK有什么区别?

在FPGA设计中&#xff0c;HP Bank&#xff08;High-Performance Bank&#xff09;与HR Bank&#xff08;High-Range Bank&#xff09;是针对I/O电气特性划分的不同区域&#xff0c;二者的主要区别在于支持的电压范围、信号速率以及应用场景。以下是具体对比&#xff1a; 核心区…

Ubuntu 下 nginx-1.24.0 源码分析 - ngx_ssl_init 函数

#if (NGX_OPENSSL)ngx_ssl_init(log); #endif objs/ngx_auto_config.h 中 #ifndef NGX_OPENSSL #define NGX_OPENSSL 1 #endif 所以这个条件编译成立 NGX_OPENSSL 是一个宏定义&#xff0c;用于控制与 OpenSSL 相关的功能是否被启用 若用户通过./configure参数&#xff08;如-…

pandas(13 Caveats Gotchas和SQL比较)

前面内容&#xff1a;pandas(12 IO工具和稀松数据) 目录 一、Caveats警告 & Gotchas预见 1.1 在Pandas中使用if/Truth语句 1.2 位运算布尔 1.3 isin操作 1.4 重新索引reindex和 loc&iloc 使用注意事项 1.5 loc和iloc 二、Python Pandas 与SQL的比较 2.1 数…

MongoDB 7 分片副本集升级方案详解(下)

#作者&#xff1a;任少近 文章目录 1.4 分片升级1.5 升级shard11.6 升级shard2,shard31.7 升级mongos1.8重新启用负载均衡器1.9 推荐MongoDB Compass来验证数据 2 注意事项&#xff1a; 1.4 分片升级 使用“滚动”升级从 MongoDB 7.0 升级到 8.0&#xff0c;即在其他成员可用…

洛谷 P2894 USACO08FEB Hotel 题解

题意 第一行输入 n , m n,m n,m&#xff0c; n n n 代表有 n n n 个房间 ( 1 ≤ n ≤ 50 , 000 ) (1\leq n \leq 50,000) (1≤n≤50,000)&#xff0c;编号为 1 ∼ n 1 \sim n 1∼n&#xff0c;开始都为空房&#xff0c; m m m 表示以下有 m m m 行操作 ( 1 ≤ m < 50…