JAVA原生Servlet支持SSE

SSE原理

(MDN)SSE文档

SSE非常轻量,当框架有严格的超时时间,但某个业务处理非常耗时,可以用它绕过超时限制,或者需要大批量推流时,都可以使用它来做.

SSE其实类似于文件下载,但是有特定的格式以让EventSource正常解析.

必要响应头: Content-Type: text/event-stream.

常用报文格式:

plain 复制代码
event: 事件1名称
data: 事件1消息

event: 事件2名称
data: 事件2消息

后端代码

直接从HttpServletResponse中调用startAsync创建异步上下文,并在其他线程中国呢使用这个异步上下文推送消息

java 复制代码
package local.my.demo.controller;

import cn.hutool.core.date.DateUtil;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.concurrent.SynchronousQueue;

@Controller
public class DemoController {

    static class SseMsg {
        String event;
        String data;
        public SseMsg(String event, String data) {
            this.event = event;
            this.data = data;
        }
    }


    @RequestMapping(value = "/sse/test",method = RequestMethod.GET)
    public void syncOrderPubStatusWithParamCore(HttpServletResponse resp, HttpServletRequest req){
        resp.setContentType(MediaType.TEXT_EVENT_STREAM_VALUE);
        resp.setCharacterEncoding(StandardCharsets.UTF_8.name());
        AsyncContext aCtx = req.startAsync(req,resp);
        //单次消息间隔时长,超时将断开
        aCtx.setTimeout(1000*60*10);
        long[] lastSendTime = {0};
        boolean[] live = {true};
        SynchronousQueue<SseMsg> queue = new SynchronousQueue<>();
        new Thread(()->{
            for(;;){
                if (live[0]){
                    //每隔一会儿发送一次保活消息避免超时
                    long l = System.currentTimeMillis();
                    if (l - lastSendTime[0] > 1500){
                        queue.offer(new SseMsg("live",String.valueOf(l)));
                    }
                }else{
                    break;
                }
            }
        },"保活").start();
        new Thread(()->{
            try {
                for(;;){
                    SseMsg take = queue.take();
                    PrintWriter writer = aCtx.getResponse().getWriter();
                    writer.print("event: ");
                    writer.print(take.event);
                    writer.print("\n");
                    writer.print("data: ");
                    writer.print(take.data);
                    writer.print("\n\n");
                    writer.flush();
                    lastSendTime[0] = System.currentTimeMillis();
                    if ("stop".equals(take.event)){
                        aCtx.complete();
                        live[0] = false;
                        break;
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        },"发送").start();
        new Thread(()->{
            try{
                String max = aCtx.getRequest().getParameter("max");
                int maxInt = max==null||max.isEmpty()?10:Integer.parseInt(max);
                for (int i = 0; i < Math.max(maxInt, 10); i++) {
                    queue.offer(new SseMsg("msg","模拟SSE"+i+" "+ DateUtil.format(new Date(),"HH:mm:ss")));
                    //模拟处理耗时
                    Thread.sleep(i%8==0?5000:500);
                }
                queue.offer(new SseMsg("stop","stop"));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        },"业务").start();
    }
}

前端代码

监听不同的消息种类,有不同的处理方式,以分别处理正常业务消息和其他消息

js 复制代码
function newSSE(url) {
    const evtSource = new EventSource(url, {withCredentials: true})
    evtSource.addEventListener('live', function (event) {
          var data = event.data;
          //解析保活消息
          console.log("保活: "+data);
        }, false);
    evtSource.addEventListener('msg', function (event) {
          var data = event.data;
          //解析正常业务消息
          console.log("消息: "+data);
        }, false);
    evtSource.onerror = function (event) {
        console.log("close evtSource")
        evtSource.close()
    };
}

var sseHost = 'http://127.0.0.1:8080'
newSSE(sseHost+'/sse/test?max=300')
相关推荐
uzong22 分钟前
Harness Engineering 是什么?一场新的 AI 范式已经开始
人工智能·后端·架构
农夫山泉不太甜1 小时前
Tauri v2 实战代码示例
前端
唐叔在学习1 小时前
Python桌面端应用最小化托盘开发实践
后端·python·程序员
yuhaiqiang1 小时前
被 AI 忽悠后,开始怀念搜索引擎了?
前端·后端·面试
红色石头本尊1 小时前
1-umi-前端工程化搭建
前端
真夜2 小时前
关于对echart盒子设置百分比读取的宽高没有撑开盒子解决方案
前端
二闹2 小时前
Python文件读取三巨头你该选择哪一个?
后端·python
楠木6852 小时前
RAG 资料库 Demo 完整开发流程
前端·ai编程
肠胃炎2 小时前
挂载方式部署项目
服务器·前端·nginx
苏三说技术2 小时前
推荐几个牛逼的AI Agent项目
后端