ai大模型流式输出------基于SSE协议的长连接实现
传统的http1.0请求开发,已经满足了我们日常的web开发。
一般请求就像下图这样子,客服端发起一个请求(触发),服务端做出一个响应(动作):
有时会有诸如实时刷新,实时显示的场景,我们往往是客户端定时发起请求,不断的尝试获取最新的数据。
但是每次请求都会创建并释放一个新的连接,这样对于需要频繁请求的场景,性能损耗太大,此外对于实时性响应的场景也很难评估轮询周期。轮询的周期短,很多查询结果其实并没有变化,增加了成本开销。轮询周期长,又不能实时的展示数据,周期值变成了一个经验值,而且不同场景都需要不断的调整。这属实不够友好。
于是http1.1协议对此进行了扩展,允许长连接的存在。今天要介绍的SSE协议,就属于http1.1下的新协议。
SSE全称为 Sever-Sent Event
指服务器端事件发送。当客户端请求成功后,服务端会依次将事件(其实就是响应信息),分多次发送到客户端。客户端只要接收事件(响应信息),做出相应的处理即可。
就像下图的样子:

比如K线增长图,实时热力图,各种增长曲线等等,都可以实时的,由后端主动将事件推送到前端,不再需要前端每次建立一个新的连接来请求。这种方式也称之为长连接。
除了SSE,像websocket 、TCP等都属于长连接的类型。依次连接可以多次交互。
SSE其实最初并不受重视,甚至很多人都不知道这个协议。如果是简单一点的话,通常直接多轮询几遍就解决问题了,如果是复杂一点的话,直接就使用websocket这样的重协议来处理了,功能也相对来说比较强大。但是自从交互大模型问世以后,大模型的流式对话往往能更高效的输出,这种流式输出的用户体验也更好。这种主要是侧重大模型响应的交互模式,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )反而使得SSE的优势又体现出来了。
下面我们看下如何在springboot中使用sse来开发:
由于springboot的封装,我们使用SSE开发变得异常简单,
核心思路是:
创建一个 SseEmitter 对象,返回给前端
这个SseEmitter类似于一个socket,我们只管向里边塞数据即可,
而前端在收到SseEmitter对象后,则只管从sseEmitter中取数据即可。(注意此处一般采用注册响应方式)
后端代码如下:
pom文件新增依赖:
1 <dependency>
2 <groupId>org.springframework.boot</groupId>
3 <artifactId>spring-boot-starter-web</artifactId>
4 </dependency>
controller类:
1 package com.example.demo.learnsse;
2
3 import lombok.extern.slf4j.Slf4j;
4 import org.springframework.http.MediaType;
5 import org.springframework.web.bind.annotation.CrossOrigin;
6 import org.springframework.web.bind.annotation.GetMapping;
7 import org.springframework.web.bind.annotation.RequestParam;
8 import org.springframework.web.bind.annotation.RestController;
9 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
10
11 import java.io.IOException;
12 import java.util.concurrent.TimeUnit;
13
14 /**
15 * @discription
16 */
17 @Slf4j
18 @RestController
19 @CrossOrigin(origins = "*")
20 public class SseController {
21
22
23 @GetMapping(value = "/learn/sseChat" , produces = {MediaType.TEXT_EVENT_STREAM_VALUE})
24 public SseEmitter chat(@RequestParam String name) throws IOException {
25 SseEmitter sseEmitter = new SseEmitter(360000L);
26 sseEmitter.onCompletion(() -> log.warn("sse complete!!!" + Thread.currentThread().getName()));
27 sseEmitter.onError(throwable -> {
28 log.warn("sse error " + Thread.currentThread().getName(), throwable);
29 });
30 sseEmitter.send("start");
31 Runnable r = () -> {
32 int i = 1;
33 try {
34 while (i <= 10) {
35 sseEmitter.send(Thread.currentThread().getName()+": the next index:" + i);
36 log.warn(Thread.currentThread().getName() + ":" + i);
37 i++;
38 TimeUnit.SECONDS.sleep(3);
39 }
40 sseEmitter.complete();
41 } catch (Exception e) {
42 log.warn("catch a ex", e);
43 sseEmitter.completeWithError(e);
44 }
45 };
46 Thread t = new Thread(r);
47 t.start();
48 log.warn("start return sse");
49 return sseEmitter;
50 }
51 }
我们可以不写前端,直接用浏览器或者命令行访问,
浏览器效果如下:
真实效果是一行行输出的
data:start data:Thread-2: the next index:1 data:Thread-2: the next index:2 data:Thread-2: the next index:3 data:Thread-2: the next index:4 data:Thread-2: the next index:5 data:Thread-2: the next index:6 data:Thread-2: the next index:7 data:Thread-2: the next index:8 data:Thread-2: the next index:9 data:Thread-2: the next index:10
日志输出如下:
2024-12-02 11:06:36.267 WARN 2032 --- [nio-8081-exec-4] com.example.demo.learnsse.SseController : sse complete!!!http-nio-8081-exec-4
2024-12-02 11:06:38.440 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:2
2024-12-02 11:06:41.442 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:3
2024-12-02 11:06:44.450 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:4
2024-12-02 11:06:47.458 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:5
2024-12-02 11:06:50.468 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:6
2024-12-02 11:06:53.471 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:7
2024-12-02 11:06:56.475 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:8
2024-12-02 11:06:59.483 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:9
2024-12-02 11:07:02.495 WARN 2032 --- [ Thread-2] com.example.demo.learnsse.SseController : Thread-2:10
2024-12-02 11:07:05.508 WARN 2032 --- [nio-8081-exec-5] com.example.demo.learnsse.SseController : sse complete!!!http-nio-8081-exec-5
这样一个简单的单次连接,服务器多次推送的示例就写完了。
当然你也可以写一个简短的前端代码,查看效果,注意此时涉及到跨域了,因此我们的java代码要使用注解@CrossOrigin(origins = "*") 来解决跨域,请看controller代码中红色字体
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>SSE Example</title>
5 </head>
6 <body>
7 <div id="events"></div>
8 <script>
9 const eventSource = new EventSource('http://127.0.0.1:8081/learn/sseChat?name=xx');
10
11 eventSource.onmessage = function(event) {
12 const newElement = document.createElement("div");
13 newElement.textContent = "New message: " + event.data;
14 document.getElementById("events").appendChild(newElement);
15 };
16
17 eventSource.onerror = function(error) {
18 console.error("Error:", error);
19 const newElement = document.createElement("div");
20 newElement.textContent = "error message: " + error;
21 document.getElementById("events").appendChild(newElement);
22 eventSource.close();
23 };
24
25 eventSource.onclose = function(event) {
26 const newElement = document.createElement("div");
27 newElement.textContent = "close message: " + event.data;
28 document.getElementById("events").appendChild(newElement);
29 eventSource.close();
30 };
31 </script>
32 </body>
33 </html>
我们在创建好SSE示例时,一般会设置以下几个回调方法:
onCompletion(Runnable callback):当异步请求完成时,我们会调用此方法注册的回调函数。
onError(Consumer<Throwable> callback) 当异步处理期间发生错误时,会调用该方法设置的回调函数
服务端发现任务结束时,主动知会客户端关闭连接:
complete():表示已经完成推送,通知客户端不再有新的事件发送。
completeWithError(Throwable ex) 表示由于发生了某个异常而结束推送。springmvc将通过异常处理机制传递该异常。
一般在对接大模型时,(防盗连接:本文首发自http://www.cnblogs.com/jilodream/ )我们除了完成SSE相关的注册,还会设置与大模型的连接,
一般的思路是这样的:
1、当前端发送请求提问来后端时,
2、我们首先创建一个SseEmitter,作为未来发送的套接字,
3、接着启动一个http连接,来请求大模型,
4、此时我们会使用Reactor-Mono之类的响应式编程框架,来回调处理大模型推送回来的数据。(其中Reactor部分的代码实现,由于篇幅有限,我会在后边的文章中讲解)
5、在Mono的每次回调到大模型推送回来的数据时,我们通过SseEmitter发送给前端
6、将第二步创建好的SseEmitter,返回给前端。
注意3/4/5步都是作为异步回调注册到mono中的。整体的结构图如下:

ai大模型流式输出------基于SSE协议的长连接实现的更多相关文章
- HttpURLConnection的流式输出的缺陷和解决方法
转自:http://www.mzone.cc/article/198.html 最近在用applet写文件上传控件的时候发现使用URLConnection来对服务器进行流式输出时的一些问题.我们通常要 ...
- 文件下载(StreamingHttpResponse流式输出)
文件下载(StreamingHttpResponse流式输出) HttpResponse会直接使用迭代器对象,将迭代器对象的内容存储成字符串,然后返回给客户端,同时释放内存.可以当文件变大看出这是一个 ...
- 网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接
本文原作者:“水晶虾饺”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.引言 好多小白初次接触即时通讯(比如:IM或者消息推送应用)时,总是不 ...
- 基于心跳的socket长连接
http://coach.iteye.com/blog/2024444 基于心跳的socket长连接 博客分类: http socket 案例: 心跳: socket模拟网页的报文连接某个网站,创建t ...
- 精讲RestTemplate第6篇-文件上传下载与大文件流式下载
本文是精讲RestTemplate第6篇,前篇的blog访问地址如下: 精讲RestTemplate第1篇-在Spring或非Spring环境下如何使用 精讲RestTemplate第2篇-多种底层H ...
- AI大模型学习了解
# 百度文心 上线时间:2019年3月 官方介绍:https://wenxin.baidu.com/ 发布地点: 参考资料: 2600亿!全球最大中文单体模型鹏城-百度·文心发布 # 华为盘古 上线时 ...
- 基于netty实现的长连接,心跳机制及重连机制
技术:maven3.0.5 + netty4.1.33 + jdk1.8 概述 Netty是由JBOSS提供的一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速 ...
- DJANGO的HTTPRESPONSE流式输出
在项目当中遇到的问题,网上有样例代码,但都不行,后来,发现在了1.5版本之后,新的STREAMHTTPRESPONSE对象, 搞定. from django.http import HttpRespo ...
- C#大文件流式压缩加解密
* * , CancellationToken token=default) { try { FileStream zipStream = new FileStream(writeFile, File ...
- 网络编程[第三篇]基于tcp协议实现远程连接
需要用到subprogress模块来远程控制cmd控制台程序来得到控制台的输出信息 一.服务端 —— 控制输出信息 import socket import subprocess #socket实例化 ...
随机推荐
- ZEGO 即构科技首发适配鸿蒙系统的 Express SDK 1.0 版本
2019年8月,华为在开发者大会上正式发布鸿蒙系统. HarmonyOS 鸿蒙系统是一款"面向未来".面向全场景(移动办公.运动健康.社交通信.媒体娱乐等)的分布式操作系统.在 ...
- 【论文解读】System 2 Attention提高大语言模型客观性和事实性
一.简要介绍 本文简要介绍了论文"System 2 Attention (is something you might need too) "的相关工作.基于trans ...
- DTO转VO工具
data工具,实现了对象拷贝 DTO -> VO 只需要实现一个类即可 data-utils data工具,实现了对象拷贝DTO -> VO 解决的问题 Mapstruct需要安插件!!! ...
- 【原创】解决NasCab掉进程,NasCab进程维护
最近对象吐槽家里服务器又连不上,看不了考研视频了. 我掏出手机一试,确实连不上.家里的服务器是Win11平台,用NasCab管理的视频文件,然后通过frpc做的内网穿透. 我们在外面的图书馆,连不上无 ...
- Java Pom两个模块需要互相引用怎么办
1. Java POM模块化是什么 在Java项目中,特别是在使用Maven作为构建工具时,"POM模块化"是一个重要的概念,它指的是将大型项目拆分成多个更小.更易于管理的模块(或 ...
- slab分配器正式被弃用,slub成为分配器唯一选择
在使用slab分配器进行内存分配时,可能会出现以下缺点: 内存碎片化.由于slab分配器需要将内存分成大小相同的块,如果分配不均衡或者对象大小不同,就容易导致内存碎片化. 性能下降.Slab分配器将内 ...
- 数据库周刊60丨3月国产数据库排行榜出炉;日本银行数据迁移失败致使业务宕机;阿里云RDS PG13发布;亚健康Oracle数据库故障定位;Redis最佳实践;MySQL查询优化……
热门资讯 1.2021年3月国产数据库排行榜:雏凤声清阿里三连 绝代双骄华为合璧 [摘要]2021年3月国产数据库流行度排行榜已出炉,在本月排行的前十名中,TiDB 仍然以领先第二名135分 的优势稳 ...
- 1001 Attention 和 Self-Attention 的区别(还不能区分我就真的无能为力了)
通过 pytorch 去构建一个 transformer 的框架 不是导包,不是调包侠 注意力机制是一个很宽泛(宏大)的一个概念,QKV 相乘就是注意力,但是他没有规定 QKV是怎么来的 通过一个查询 ...
- HDU-ACM 2024 Day2
T1004 a*b problem(HDU 7448) 不会. T1005 小塔的养成游戏之梦(HDU 7449) 不会. T1009 强攻计策(HDU 7453) 容易发现初始速度是多少对答案没有影 ...
- KubeSphere 社区双周报 | Java functions framework 支持 SkyWalking | 2023.8.4-8.17
KubeSphere 社区双周报主要整理展示新增的贡献者名单和证书.新增的讲师证书以及两周内提交过 commit 的贡献者,并对近期重要的 PR 进行解析,同时还包含了线上/线下活动和布道推广等一系列 ...