ResponseBodyEmitter与SseEmitter使用

目录

背景

  • 最近在接入阿里云百炼AI助手时,接触到ResponseBodyEmitter
  • 实现了流式返回,比较感兴趣,所以去了解了一下如何实现。

ResponseBodyEmitter

简介

  • Spring 框架提供的通用流式传输接口,支持分块传输编码(Chunked Encoding),允许逐步向客户端发送数据块,异步推送数据,而非一次性响应。返回ResponseBodyEmitter灵活性强,也可以自己构造标准的SSE返回。

核心代码实现

后端代码

java 复制代码
    private final ExecutorService executorService = Executors.newFixedThreadPool(2000);

    private final List<String> replyData = Arrays.asList("我是", "您的AI助手", "有什么可以帮您", "我是", "您的AI助手", "有什么可以帮您");
    /**
     * 返回ResponseBodyEmitter灵活性强,也可以自己构造标准的SSE返回
     * @param response
     * @return
     */
    @RequestMapping(value = "/responseBodyEmitter")
    @CrossOrigin
    public ResponseBodyEmitter responseBodyEmitter(HttpServletResponse response) {
        ResponseBodyEmitter emitter = new ResponseBodyEmitter(180000L);
        executorService.execute(() -> {
            try {
                for (String value : replyData) {
                    emitter.send(value.getBytes(java.nio.charset.StandardCharsets.UTF_8));
                    Thread.sleep(1000);
                }
                emitter.complete();
            } catch (Exception e) {
                log.error("其他的请求聊天异常 {}", e);
                emitter.completeWithError(e);
                throw new RuntimeException(e);
            }
        });
        return emitter;
    }

前端代码

  • 先来看百炼助手使用fetch

  • 我是使用Nuxt 3 框架,也用fetch API调用

javascript 复制代码
const fetchStream = async () => {
    const response = await fetch('http://127.0.0.1:8080/api/index/responseBodyEmitter'); // 替换为你的接口路径
    const reader = response.body.getReader();
    const decoder = new TextDecoder('utf-8');

    // 持续读取数据流
    while (true) {
        const { done, value } = await reader.read();
        if (done) {
            console.log('Stream completed');
            break;
        }
        const textChunk = decoder.decode(value, { stream: true });
        console.log('Received chunk:', textChunk);
        text.value += textChunk
  
    }
}


onMounted(()=>{
  console.log("onMounted")
  // 返回ResponseBodyEmitter 
  fetchStream()

})

运行效果

SseEmitter

  • 在了解ResponseBodyEmitter时又发现了SseEmitter。

简介

  • ResponseBodyEmitter的子类,为Server-Sent Events(SSE)协议设计,基于text/event-stream格式实现服务器到客户端的单向推送。自带重连机制。

核心代码实现

后端代码

java 复制代码
   private final List<String> replyData = Arrays.asList("我是", "您的AI助手", "有什么可以帮您", "我是", "您的AI助手", "有什么可以帮您");
     private final ExecutorService executorService = Executors.newFixedThreadPool(2000);

    @RequestMapping("/chat")
    @CrossOrigin
    public SseEmitter chat(String query) {
        SseEmitter emitter = new SseEmitter(180000L);

        executorService.execute(() -> {
            try {
                for (int i = 0; i < replyData.size(); i++) {
                    String value = replyData.get(i);
                    emitter.send(value.getBytes(java.nio.charset.StandardCharsets.UTF_8));
                    Thread.sleep(1000);
                }
                emitter.send(SseEmitter.event().name("end").data("[DONE]"));
                Thread.sleep(1000);
                emitter.complete();
            } catch (Exception e) {
                log.error("其他的请求聊天异常 {}", e);
                emitter.completeWithError(e);
                throw new RuntimeException(e);
            }
        });
        log.info("返回emitter");
        return emitter;
    }

前端代码

javascript 复制代码
//接收后台消息
const  receiveMessage = () =>{
  let eventSource  = new EventSource('http://127.0.0.1:8080/api/index/chat');
   
    eventSource.onopen = (event) =>{
      console.log("onopen ",event); 
    }
    //接收成功
    eventSource.onmessage = (event) => {
      console.log("onmessage ",event);
      
      text.value =  text.value + event.data;
    };

    
    
    eventSource.addEventListener('end', (event) => {
      console.log("服务器主动关闭连接");
      eventSource.close(); // 主动关闭连接
    });


    //接收失败
    eventSource.onerror = (error) => {
        console.error('SSE error:',eventSource.readyState, error);
        if (eventSource.readyState === EventSource.CLOSED) { 
          console.log("正常关闭"); // 应在此过滤已关闭状态
        } else {
          console.error("真实错误:");
        }
        // 如果不close,会自动重连
        eventSource.close()
    };

    
}



onMounted(()=>{

  console.log("onMounted")
  // SseEmitter 的方式
  receiveMessage()

})

运行效果

小程序

  • 小程序也支持SSE,核心代码如下,我使用的是uni-app-x

小程序代码

javascript 复制代码
let title = ref<string>("hello")
	onMounted(() => {
		console.log("onMounted")
		startStream()
	})

	const startStream = () => {
		const requestTask = wx.request({
			url: 'http://127.0.0.1:8080/api/index/chat', // 后端接口地址
			method: 'GET',
			// dataType: 'text',
			//responseType: 'stream', // 关键:设置响应类型为 stream
			enableChunked: true,
			success: (res) => {

			},
			fail: (err) => {
				console.error("请求失败:", err);
			}
		});
		const decoder = new TextDecoder('utf-8');

		requestTask.onChunkReceived(function (resp) {
			let data = decoder.decode(resp.data)
			console.log(data)
			title.value += data
		});
	}

运行效果

本文源码

总结

相关推荐
东阳马生架构3 天前
Disruptor—3.核心源码实现分析二
异步·队列·disruptor
码观天工7 天前
C#线程池核心技术:从原理到高效调优的实用指南
性能优化·c#·.net·线程·线程池·多线程·异步
救救孩子把11 天前
MCP本地高效与云端实时:stdio 与 HTTP+SSE 传输机制深度对比
网络·网络协议·http·sse·mcp·stdio
有梦想的攻城狮14 天前
spring中的@Async注解详解
java·后端·spring·异步·async注解
江小皮不皮15 天前
为何选择MCP?自建流程与Anthropic MCP的对比分析
人工智能·llm·nlp·aigc·sse·mcp·fastmcp
落霞的思绪1 个月前
Springboot集成SSE实现消息推送+RabbitMQ解决集群环境下SSE通道跨节点事件推送问题
spring boot·rabbitmq·sse
JaneYork1 个月前
vue前端SSE工具库|EventSource 替代方案推荐|PUSDN平行宇宙软件开发者网
sse·pusdn·流式请求·塔滋米·破该仔
码观天工1 个月前
解锁.NET 9性能优化黑科技:从内存管理到Web性能的最全指南
性能优化·c#·.net·内存管理·异步·.net 9