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')
相关推荐
Sterting19 分钟前
Element-Plus 入门与整体布局
前端·vue.js·elementui
程序员黑豆7 小时前
Java 注释详解:单行、多行与文档注释的完整指南
java·前端·ai编程
剪刀石头布啊8 小时前
antd中可编辑表格中巧用useWatch实现联动高性能效果,以及定制延伸学习
前端
油丶酸萝卜别吃8 小时前
前端转全栈学习路线
前端·学习
浮生望8 小时前
前端路由进阶:History API原理与手写HistoryRouter
前端
陆枫Larry9 小时前
JavaScript 中的竞态是什么,为啥会有竟态?
前端
维克兜率天10 小时前
【维克】ARMA模型:AR与MA的完美结合
后端·restful
是未才10 小时前
从输入 URL 到页面返回:DNS、路由、TLS 与 HTTP 完整链路
java·后端·计算机网络
上海安当技术11 小时前
半天接入:USBKey RESTful API + C 动态库,Web 和 C/S 两套集成路径实战
前端·后端·restful·集成·usbkey
DevUI团队11 小时前
从“即兴创作”到“规格先行”,华为云码道(CodeArts)代码智能体持续深耕企业级规范驱动开发能力
前端·人工智能·后端