Spring AI-流式编程

1. 流式编程

1.1 SSE协议介绍

HTTP协议本身设计为无状态的请求-响应模式,严格来说,是无法做到服务器主动推送消息到客户端,但 通过Server-Sent Events(服务器发送事件,简称SSE)技术可实现流式传输,允许服务器主动向浏览器推送数据流。

也就是说,服务器向客户端声明,接下来要发送的是流消息(streaming),这时客户端不会关闭连接,会一直等待服务器发送过来新的数据流。

SSE(Server-Sent Events)是一种基于HTTP的轻量级实时通信协议,浏览器通过内置的EventSource API接受并处理这些实时事件。

核心特点

基于HTTP协议

复用标准HTTP/HTTPS协议,无需额外端口或协议,兼容性好且易于部署

单向通信机制

SSE仅支持服务器向客户端的单向数据推送,客户端通过普通HTTP请求建立连接后,服务器可持续发送数据流,但客户端无法通过同一连接向服务器发送数据。

自动重连机制

支持断线重连,连接中断时,浏览器会自动尝试重新连接(支持 retry 字段指定重连间隔)

自定义消息类型

客户端发起请求后,服务器保持连接开放,响应头设置 Content-Type: text/event-stream,标识为事件流格式,持续推送事件流。

数据格式

服务器向浏览器发送SSE数据,需要设置必要的HTTP头信息

java 复制代码
 Content-Type: text/event-stream;charset=utf-8
 Connection: keep-alive

每一次发送的消息,由若干个message组成,每个message之间由\n\n分割,每个message内部由若干行组成,每一行都是如下的格式:

java 复制代码
[field]: value\n

Field可以取值为:

data[必需]:数据内容

event[非必需]:表示自定义的事件类型,默认是message事件

id[非必需]:数据标识符,相当于每一条数据的编号

retry[非必需]:指定浏览器重新发起连接的时间间隔

除此之外,还可以有冒号:开头的行,表示注释。

数据示例:

java 复制代码
event: foo\n
data: a foo event\n\n
data: an unnamed event\n\n
event: end\n
data: a bar event\n\n

服务端实现

java 复制代码
@Slf4j
@RestController
@RequestMapping("/sse")
public class SseController {
    @RequestMapping("/data")
    public void data(HttpServletResponse  response) throws IOException, InterruptedException {
        log.info("发送请求:data");
        response.setContentType("text/event-stream;charset=utf-8");
        PrintWriter printWriter=response.getWriter();
        for (int i = 0; i < 10; i++){
            String s = "data: " + new Date() + "\n\n";
            printWriter.write(s);
            printWriter.flush();
            Thread.sleep(1000);
        }
    }

测试接口:127.0.0.1:8080/sse/data

客户端API

java 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>SSE</title>
</head>
<body>
<div id="sse"></div>
<script>
  let eventSource = new EventSource('/sse/data');
  eventSource.onmessage = function(event){
    document.getElementById("sse").innerHTML= event.data;
  }
  // eventSource.addEventListener("foo", (event)=>{
  //     document.getElementById("sse").innerHTML= event.data;
  // });
  // eventSource.addEventListener("end", (event)=>{
  //   document.getElementById("sse").innerHTML= event.data;
  //     eventSource.close();
  // });
</script>
</body>
</html>

1.2 Spring 中SSE实现

Spring 4.2 开始就已经支持SSE,从Spring 5开始我们可以使用WebFlux更优雅的实现SSE协议。Flux 是WebFlux的核心API。

可以把Flux想象成一条传送带

异步传送:数据像快递包裹一样逐个到达,不用等全部到齐。

灵活加工:支持途中修改数据(如过滤/转换)

弹性控制:接收方可以调速(背压价值)

快速使用

1.创建Flux:创建一个Flux,并有数据源。

2.处理数据:使用操作符对数据进行处理。

3.订阅数据:订阅Flux来消费数据,触发数据流动。

示例:

java 复制代码
public class FluxDemoTest {
    public static void main(String[] args) throws InterruptedException {
        Flux<String> flux=Flux.just("Apple","Banana","Cherry","Pear")
                .delayElements(Duration.ofSeconds(1));
        flux.map(String::toUpperCase).map(s->s+"-").subscribe(System.out::println);
        Thread.sleep(5000);
    }
}

创建Flux

使用Flux.just(...)创建一个指定元素的Flux

java 复制代码
Flux<String> flux=Flux.just("Apple","Banana","Cherry","Pear")

自动重连代码练习

java 复制代码
@RequestMapping("/retry")
    public void retry(HttpServletResponse  response) throws IOException{
        log.info("发送请求:retry");
        response.setContentType("text/event-stream;charset=utf-8");
        PrintWriter printWriter=response.getWriter();
        //告诉浏览器如果断开连接2s后重传
        String s="retry: 2000\n\n";
        s += "data: " + new Date() + "\n\n";
        printWriter.write(s);
        printWriter.flush();
    }

测试接口:http://127.0.0.1:8080/index.html

测试结果:

每两秒重新发一次消息

自定义事件:

java 复制代码
@RequestMapping("/event")
    public void event(HttpServletResponse  response) throws IOException, InterruptedException {
        log.info("发起请求: event");
        response.setContentType("text/event-stream;charset=utf-8");
        PrintWriter writer=response.getWriter();
        for (int i = 0; i < 10; i++){
            //自定义事件类型
            String s = "event: foo\n";
            s += "data: " + new Date() + "\n\n";
            writer.write(s);
            writer.flush();
            Thread.sleep(1000L);
        }
    }

测试接口:http://127.0.0.1:8080/index.html

测试结果:

有明确结束信号的持续数据推送流:

java 复制代码
@RequestMapping("/end")
    public void end(HttpServletResponse  response) throws IOException, InterruptedException {
        log.info("发起请求: end");
        response.setContentType("text/event-stream;charset=utf-8");
        PrintWriter writer=response.getWriter();
        for (int i = 0; i < 10; i++){
            String s = "event: foo\n";
            s += "data: " + new Date() + "\n\n";
            writer.write(s);
            writer.flush();
            Thread.sleep(1000L);
        }
        //定义事件,表示当前流传输结束
        writer.write("event: end\ndata: EOF\n\n");
        writer.flush();
    }

流式响应接口:

需求:每隔一秒向客户端输出当前时间。

java 复制代码
@RequestMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public  Flux<String> stream(){
        return Flux.interval(Duration.ofSeconds(1)).map(s->new Date().toString());
    }
java 复制代码
<div id="sse"></div>
<script>
 let eventSource = new EventSource("/sse/stream");
 eventSource.onmessage = function(event){
 document.getElementById("sse").innerHTML = event.data;
 }
</script>

常见的操作符:

|---------------|------------------|---------------------------------------------|
| 操作 | 作用 | 示例代码片段 |
| map() | 元素一对一转换 | .map(String::toUpperCase) |
| filter() | 条件过滤 | .filter(s -> s.length() > 5) |
| take() | 限制元素数量 | .take(2) //只取前2个元素 |
| merge() | 合并多个Flux(不保证顺序) | Flux.merge(Flux.just("A"), Flux.just("B")) |
| concat() | 顺序拼接多个Flux(保证顺序) | Flux.concat(Flux.just("A"), Flux.just("B")) |
| delayElements | 延迟元素发射 | .delayElements(Duration.ofSeconds(1)) |

相关推荐
canonical_entropy5 小时前
对《DDD本质论》一文的解读
后端·架构·领域驱动设计
码事漫谈5 小时前
我用亲身经历告诉你,为什么程序员千万别不把英语当回事
后端
码事漫谈5 小时前
C++ const 用法全面总结与深度解析
后端
haogexiaole5 小时前
Java高并发常见架构、处理方式、api调优
java·开发语言·架构
间彧5 小时前
分布式单例模式在微服务架构中的实际应用案例
后端
间彧5 小时前
分布式系统中保证单例唯一性的Java解决方案
后端
间彧5 小时前
为什么避免在单例中保存上下文状态
后端
间彧5 小时前
单例模式防御反射与序列化攻击的意义与实践
后端
EnCi Zheng5 小时前
@ResponseStatus 注解详解
java·spring boot·后端