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')
相关推荐
前端一课2 分钟前
【前端每天一题】🔥 第 25 题:什么是 Virtual DOM?它的优缺点是什么?Diff 算法是如何工作的?
前端·面试
前端一课3 分钟前
【前端每天一题】第 23 题:闭包(Closure)与作用域链(详细 + 面试模板 + 速记卡)
前端·面试
前端一课5 分钟前
【前端每天一题】🔥第 22 题:HTTP vs HTTPS、TCP vs UDP 的区别
前端·面试
前端一课7 分钟前
第 26 题:浏览器与 Node.js 的事件循环有什么区别?
前端·面试
前端一课9 分钟前
【前端每天一题】🔥 第 24 题:Virtual DOM 中 diff 算法的核心流程(详细版
前端·面试
掘金0114 分钟前
根据提供的表格动态渲染多个表单,每个配置项包含 label、prop、type 和 placeholder 等属性。
前端
用户44455436542615 分钟前
自定义viewgroup
前端
稚辉君16 分钟前
Gemini永久会员 01不等概率随机到01等概率随机
后端
ohyeah21 分钟前
用 Coze 打造你的教育智能客服:从想法到前端集成的完整实践
前端·coze·trae
雨雨雨雨雨别下啦24 分钟前
【从0开始学前端】 Git版本控制系统
前端·git