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')
