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')
相关推荐
鬼手点金4 小时前
opencode-rules 插件
java·开发语言·前端·人工智能·chrome·git·github
柳杉4 小时前
借助 Codex 和可视化工作流,从调研到完整实现,3D 大屏竟然可以这么简单
前端·ai编程·数据可视化
孟健5 小时前
从5000亿Token到83%字节匹配:AI逆向工程与重构的工程闭环实践
后端·架构·ai编程
IMPYLH5 小时前
HTML 的 <wbr> 元素
前端·html
Dovis(誓平步青云)5 小时前
常用命令藏得太深?怎么做一个快捷命令坞
运维·前端·自动化·音视频·prometheus
码事漫谈5 小时前
丢得静悄悄,谈一谈 Agent 的记忆是怎么没的
后端
迪飞特科技5 小时前
智慧物业平台实战:Spring Boot 整合小程序实现报修巡检费用全流程
spring boot·后端·小程序
考虑考虑5 小时前
@JsonInclude注解使用
spring boot·后端·spring
IT_陈寒6 小时前
Vite的这个问题坑了我三天,原来如此解决!
前端·人工智能·后端
网易云信6 小时前
大模型能上岗做销售吗?我们让 12 款国产模型交了 420 份作业
前端·后端