Server-sent events
Server-sent events 简称 SSE,是一种服务端向客户端推送数据的协议,与 websocket 类似,服务端可以随时向客户端推送数据,且可以多次推送数据,但与 websocket 不同的是,数据只能在一个方向上传输,即服务端到客户端。客户端在建立连接时可以通过 url、query string 传递少量参数,但建立连接后,客户端无法再通过 SSE 通道向服务端发送数据,SSE 通道只允许服务端向客户端发送数据。
协议详情
SSE 的 MIME 类型为 text/event-stream,也就是说 request header accept=text/event-stream,response header content-type=text/event-stream。
SSE 的事件流是一个简单的文本数据流,浏览器在解析事件流时遵循以下规则:
每条消息以一行或多行文本构成,每条消息以双换行 \n\n 分隔。若最后一个块没有以双换行 \n\n 结尾,则会被丢弃。
data: This is the first message.
event: userconnect
id: 1
data: {"username": "bobby", "time": "02:33:48"}
retry: 3000
data: some text
event: add
data: 73857293
以下事件流会被合并为 data: YHOO\n+2\n10。
data: YHOO
data: +2
data: 10
第二个data 会被丢弃,因为没有以双换行 \n\n 结尾:
data: This is the first message.
data: This is the second message
事件流每行以第一个冒号 : 分割为 fieldName 和 fieldValue。
如果 fieldValue 包含前导空格,只会删除第一个空格。
如果一行不包含冒号,则整行被视为 fieldName,fieldValue 为一个空字符串。
id: 10
id
若一行以冒号 : 开头会被视为注释,不会触发事件,注释行可用于防止连接超时;服务器可以定期发送注释以保持连接活跃。
: this is a test stream
: ping
每条消息都包含以下字段的某种组合。
-
event是事件类型,若不包含此字段,则默认为 message。 -
id是lastEventId,在连接断开后重连时会作为 request headerLast-Event-ID=lastEventId自动发送给服务端。设置
lastEventId后,若想取消可以发送id为空的消息:data: first event id: 1 data:second event id -
data是消息的数据字段,如果收到多个以data:开头的连续行时,会将多个fieldValue使用\n连接起来作为一个fieldValue。 -
retry重新连接时间,单位为毫秒。 -
所有其他字段名都将被忽略。
以下流触发两个事件:第一个块触发事件,data 设置为空字符串。中间块触发一个事件,data 设置为一个换行符 \n。最后一个块被丢弃,因为它后面没有双换行 \n\n。
data
data
data
data:
以下流中前两个块触发相同的事件,data 设置为 ok,第三个块触发不同的事件,data 设置为 ok:
data:ok
data: ok
data: ok
以下流不会触发任何事件,因为其被视为注释:
: ping
代码
前端 JS
浏览器可以使用 EventSource 接收事件:
js
const evtSource = new EventSource("http://localhost:8080/sse");
当与事件源建立连接时, open 事件会被触发。
js
// addEventListener version
evtSource.addEventListener("open", (e) => {
console.log("The connection has been established.");
});
// onopen version
evtSource.onopen = (e) => {
console.log("The connection has been established.");
};
当与事件源的连接未能打开时,error 事件会被触发。
js
// addEventListener version
evtSource.addEventListener("error", (e) => {
console.log("An error occurred while attempting to connect.");
});
// onerror version
evtSource.onerror = (e) => {
console.log("An error occurred while attempting to connect.");
};
当收到 event 字段为空的事件时,message 事件会被触发:
js
// addEventListener version
evtSource.addEventListener("message", (e) => {
const newElement = document.createElement("li");
newElement.textContent = `message: ${e.data}`;
eventList.appendChild(newElement);
});
// onmessage version
evtSource.onmessage = (event) => {
const newElement = document.createElement("li");
const eventList = document.getElementById("list");
// event.data 为数据
newElement.textContent = `message: ${event.data}`;
eventList.appendChild(newElement);
};
当收到 event 字段不为空的事件时,自定义事件会被触发:
js
// 监听 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);
});
默认情况下,如果客户端和服务器之间的连接关闭,连接将在 retry 毫秒后重新启动。通过 .close() 方法可以终止连接。
js
evtSource.close();
后端 spring
spring 提供了 SseEmitter 来发送 SSE 消息,produces 必须为 MediaType.TEXT_EVENT_STREAM_VALUE,即 text/event-stream。
可以使用 SseEmitter.send() 发送 String;也可以发送对象,SseEmitter 会使用 jackson 将对象序列化为 json 后再发送。且 SseEmitter 会自动在开头添加 data: 。
可以使用 SseEmitter.event() 获取 SseEmitter.SseEventBuilder,使用其构造如前面所述的多字段消息。
java
package com.example.feigntest;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.Set;
@RestController
public class SseController {
@GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter handle() {
SseEmitter emitter = new SseEmitter();
new Thread(() -> {
try {
emitter.send("some text");
emitter.send(new User(1, "tom"));
SseEmitter.SseEventBuilder builder = SseEmitter.event()
// 注释
.comment("comment")
// event 字段
.name("userconnect")
// data 字段
.data("some data")
// id 字段
.id("1")
// retry 字段
.reconnectTime(5000);
Set<ResponseBodyEmitter.DataWithMediaType> event = builder.build();
emitter.send(event);
// 关闭连接
emitter.complete();
} catch (IOException e) {
// ignore
}
}).start();
return emitter;
}
}
record User(int id, String name) {
}
代码最终发送的事件流如下:
data:some text
data:{"id":1,"name":"tom"}
:comment
event:userconnect
data:some data
id:1
retry:5000
也支持返回 ResponseEntity<SseEmitter> 以自定义 http status、header 等。
java
@GetMapping(path = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public ResponseEntity<SseEmitter> handle() {
SseEmitter emitter = new SseEmitter();
// ...
return ResponseEntity
.status(HttpStatus.OK)
.header("X-TENANT-ID", "1")
.body(emitter);
}