SSE(server-sent events)
SSE全称server-sent events,翻译过来是服务端发送事件,通常的http请求,客户端请求,服务端返回响应,一次只能返回一个值;SSE使用的http协议,客户端请求后,服务端可以多次返回响应,返回的http Content-Type:text/event-stream,客户端请求后,仍然保持http链接不关闭,服务端可多次发送响应,客户端监听事件,获取响应,这适用新闻实时更新、股票实时更新等服务端推送到客户端的场景。在chatgpt中提问问题后,答案不是一下子出来的,而是断断续续出来的,就是用的SSE技术。
SSE与websocket的异同
SSE | websocket |
---|---|
基于http协议的 | 基于websocket协议 |
单向的,只能服务端---->客户端 | 双向的,服务端<------>客户端 |
消息有id、event、data、retry等字段 | 不限格式 |
- 普通http请求
- SSE
实践
SSE通信分为客户端(通常是浏览器端)和服务端,客户端用来发送请求,监听事件,关闭链接等。服务端,就是普通的http服务,区别就是Content-Type:text/event-stream
客户端
客户端使用EventSource API来实现通信,
- 创建EventSource实例
const evtSource = new EventSource("ssedemo.php");
如果是跨域,则
const evtSource = new EventSource("//api.example.com/ssedemo.php", {
withCredentials: true,
});
- 监听服务端推送的消息
evtSource.onmessage = (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
newElement.textContent = `message: ${event.data}`;
eventList.appendChild(newElement);
};
- 监听自定义的事件,使用addEventListener方法,监听事件为ping的
evtSource.addEventListener("ping", (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
const time = JSON.parse(event.data).time;
newElement.textContent = `ping at ${time}`;
eventList.appendChild(newElement);
});
- 监听错误
evtSource.onerror = (err) => {
console.error("EventSource failed:", err);
};
- 关闭,注意,关闭只能从客户端发起。
evtSource.close();
服务端
服务端跟正常的http 服务差不多,除了响应类型是text/event-stream
-
Event Stream格式
event: 事件名称,如果不写事件名,则被客户端的onMessage
监听;有事件名,则被对应的addEventListener
监听。
data: 消息的数据字段
id: 事件的id
retry: 断线重连的等待时间,单位毫秒 -
只发数据
//冒号开头的是注释,可用于心跳
: this is a test stream
data: some text
data: another message
data: with two lines
- 命名事件
event: userconnect
data: {"username": "bobby", "time": "02:33:48"}
event: usermessage
data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."}
event: userdisconnect
data: {"username": "bobby", "time": "02:34:23"}
event: usermessage
data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}
也可以一会发数据消息、一会发命名事件消息
springboot支持SSE
在spring framework文档中,springmvc中ResponseBodyEmitter 、SseEmitter (ResponseBodyEmitter的子类)归属为异步请求(适用异步返回请求值),异步请求细分为Http Streaming(适用返回多个值的情况)
- java端代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Slf4j
@RestController
@RequestMapping("question")
public class AskQuestionController {
@GetMapping(value = "/askQuestion", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter askQuestion() {
final SseEmitter sseEmitter = new SseEmitter();
final ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(() -> {
for (int i = 0; i < 100; i++) {
String data = getCurDateStr();
try {
sseEmitter.send(SseEmitter.event().id(String.valueOf(i+1)).data(data).name("custom_event_name"));
} catch (IOException e) {
e.printStackTrace();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
return sseEmitter;
}
private static String getCurDateStr() {
final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return simpleDateFormat.format(new Date());
}
}
- 客户端代码
<button onclick="startSSE()">开始</button>
<button onclick="closeSSE()">关闭</button>
内容是:
<ul id="list">
</ul>
<script type="text/javascript">
let evtSource;
function startSSE() {
evtSource = new EventSource("/question/askQuestion");
evtSource.addEventListener("custom_event_name", (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
newElement.textContent = `收到的数据: ${event.data}`;
eventList.appendChild(newElement);
});
}
function closeSSE() {
evtSource.close()
}
</script>
-
效果,用了原生EventSource在dev tools的EventStream中显示事件数据
-
客户端自动重连
问题
- 在chrome devtools中的EventStream中看不到数据
- 这是因为devtools只支持原生的EventSource,不支持EventSource Polyfill
- Event does not show up in the Chrome EventStream subtab inside the Network tab when viewing the eventstream connection
- why did not found the chatgpt event stream data in google chrome devtools
- EventSource API不支持POST方法,只能GET,但可以不使用EventSource API,用sse.js来实现SSE POST
var source = new SSE("get_message.php", {payload: 'Hello World'});
总结
SSE是一种规范而不是一种新协议,它使用http通信,返回的Content-Type:text/event-stream
,客户端通过监听事件不断获取数据,链接断掉后会自动重连,接收完数据后由客户端发起关闭连接。
参考
-
Using server-sent events
-
Can Server Sent Events (SSE) with EventSource pass parameter by POST
-
How to pass POST parameters with HTML SSE?