Appearance
SSE
什么是 SSE
SSE(Server Sent Event) 既是服务端发送消息,是一种轻量级长连接协议,和 websocket 不同,socket 可以双端接受发送消息,而 SSE 只能由服务端发送消息,并且 SSE 自动会重连.
从“服务端主动向浏览器实时推送消息”这一点来看,该 API 与 WebSockets API 有一些相似之处。但是,该 API 与 WebSockers API 的不同之处在于:
Server-Sent Events API | WebSockets API |
---|---|
基于 HTTP 协议 | 基于 TCP 协议 |
单工,只能服务端单向发送消息 | 全双工,可以同时发送和接收消息 |
轻量级,使用简单 | 相对复杂 |
内置断线重连和消息追踪的功能 | 不在协议范围内,需手动实现 |
文本或使用 Base64 编码和 gzip 压缩的二进制消息 | 类型广泛 |
支持自定义事件类型 | 不支持自定义事件类型 |
连接数 HTTP/1.1 6 个,HTTP/2 可协商(默认 100) | 连接数无限制 |
服务器发送事件(SSE)受到打开连接数的限制,这个限制是对于浏览器的,并且设置为非常低的数字(6),打开多个选项卡时可能会特别痛苦。
SSE 应用场景:ChatGPT.
ChatGPT 这种需要耗费大量时间计算才能得到最终结果,不适用与 Ajax,因为 Ajax 只能一次请求响应一次结果,也就是说要想得到完整数据,就必须等待 GPT 把全部的计算完成才能返回,中间可能会耗费很多时间.
SSE 浏览器上使用
使用浏览器提供的EventSource
来完成 SSE 通信,简单易用.通过addEventListener
来实现自定义事件的监听,回调可以接受一个参数,这个参数就是服务端响应的数据.
EventSource
内置了三种事件:
open
:连接成功就触发
error
:出错或关闭时触发
message
:服务端响应的 SSE 报文数据中 event 字段为message
或无 evetn 字段就触发该事件回调。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="X-UA-Compatible"
content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>你好</h1>
<button>开启连接</button>
<button>断开连接</button>
<div class="list"></div>
<script>
const [start, end] = document.getElementsByTagName("button");
const list = end.nextElementSibling;
let eventSource = null;
const startSSE = () => {
eventSource = new EventSource("/stream");
eventSource.addEventListener("customEvent", (event) => {
console.log(event);
const li = document.createElement("li");
li.innerText = JSON.stringify(event.data);
list.append(li);
});
eventSource.onopen = () => {
console.log("SSE连接成功!");
};
eventSource.onerror = () => {
console.log("SSE连接失败!");
};
};
const closeSSE = () => {
if (eventSource) {
eventSource.close();
console.log("关闭连接!");
}
};
start.onclick = startSSE;
end.onclick = closeSSE;
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
http-equiv="X-UA-Compatible"
content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>你好</h1>
<button>开启连接</button>
<button>断开连接</button>
<div class="list"></div>
<script>
const [start, end] = document.getElementsByTagName("button");
const list = end.nextElementSibling;
let eventSource = null;
const startSSE = () => {
eventSource = new EventSource("/stream");
eventSource.addEventListener("customEvent", (event) => {
console.log(event);
const li = document.createElement("li");
li.innerText = JSON.stringify(event.data);
list.append(li);
});
eventSource.onopen = () => {
console.log("SSE连接成功!");
};
eventSource.onerror = () => {
console.log("SSE连接失败!");
};
};
const closeSSE = () => {
if (eventSource) {
eventSource.close();
console.log("关闭连接!");
}
};
start.onclick = startSSE;
end.onclick = closeSSE;
</script>
</body>
</html>
SSE 服务端上使用
SSE 的响应头部的设置
首先 SSE 协议的报文必须要设置的响应头部为content-type
cache-control
connection
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
SSE API 规定推送事件流的 MIME 类型为
text/event-stream
。必须指定浏览器不缓存服务端发送的数据,以确保浏览器可以实时显示服务端发送的数据。
SSE 是一个一直保持开启的 TCP 连接,所以 Connection 为 keep-alive。
SSE 报文消息格式
EventStream(事件流)为 UTF-8
格式编码的文本
或使用 Base64 编码和 gzip 压缩的二进制消息。
每条消息由一行或多行字段(event
、id
、retry
、data
)组成,每个字段组成形式为:字段名:字段值
。字段以行为单位,每行一个(即以 \n
结尾)。以冒号
开头的行为注释行,会被浏览器忽略。
每次推送,可由多个消息组成,每个消息之间以空行分隔(即最后一个字段以\n\n
结尾)。
📢 注意:
- 除上述四个字段外,其他所有字段都会被忽略。
- 如果一行字段中不包含冒号,则整行文本将被视为字段名,字段值为空。
- 注释行可以用来防止链接超时,服务端可以定期向浏览器发送一条消息注释行,以保持连接不断。
1. event
事件类型。如果指定了该字段,则在浏览器收到该条消息时,会在当前 EventSource
对象(见 4)上触发一个事件,事件类型就是该字段的字段值。可以使用 addEventListener
方法在当前 EventSource
对象上监听任意类型的命名事件。
如果该条消息没有 event
字段,则会触发 EventSource
对象 onmessage
属性上的事件处理函数。如果接收消息中有一个 event 字段,触发的事件与 event 字段的值相同。如果不存在 event 字段,则将触发通用的 message
事件
2. id
事件 ID。事件的唯一标识符,浏览器会跟踪事件 ID,如果发生断连,浏览器会把收到的最后一个事件 ID 放到 HTTP Header Last-Event-Id
中进行重连,作为一种简单的同步机制。
例如可以在服务端将每次发送的事件 ID 值自动加 1,当浏览器接收到该事件 ID 后,下次与服务端建立连接后再请求的 Header 中将同时提交该事件 ID,服务端检查该事件 ID 是否为上次发送的事件 ID,如果与上次发送的事件 ID 不一致则说明浏览器存在与服务器连接失败的情况,本次需要同时发送前几次浏览器未接收到的数据。
3. retry
重连时间。整数值,单位 ms,如果与服务器的连接丢失,浏览器将等待指定时间,然后尝试重新连接。如果该字段不是整数值,会被忽略。
当服务端没有指定浏览器的重连时间时,由浏览器自行决定每隔多久与服务端建立一次连接(一般为 30s)。
4. data
消息数据。数据内容只能以一个字符串的文本形式进行发送,如果需要发送一个对象时,需要将该对象以一个 JSON 格式的字符串的形式进行发送。在浏览器接收到该字符串后,再把它还原为一个 JSON 对象。
SSE 报文示例
如下事件流示例,共发送了 4 条消息,每条消息间以一个空行\n\n
作为分隔符。每条消息中的字段以\n
分割。
第一条仅仅是个注释,因为它以冒号开头。
第二条消息只包含一个 data 字段,值为 'this is second message'。
第三条消息包含两个 data 字段,其会被解析为一个字段,值为 'this is third message part 1\nthis is third message part 2'。
第四条消息包含完整四个字段,指定了事件类型为 'server-time',事件 id 为 '1',重连时间为 '30000'ms,消息数据为 JSON
格式的 '{"text": "this is fourth message", "time": "12:00:00"}'。
js
// 注释
: this is first message\n\n
// 消息1
data: this is second message\n\n
// 消息2
data: this is third message part one\n
data this is third message part two\n\n
// 消息3
event: server-time\n
id: 1\n
retry: 30000\n
data: {"text": "this is fourth message", "time": "2023-04-09 12:00:00"}\n\n
// 注释
: this is first message\n\n
// 消息1
data: this is second message\n\n
// 消息2
data: this is third message part one\n
data this is third message part two\n\n
// 消息3
event: server-time\n
id: 1\n
retry: 30000\n
data: {"text": "this is fourth message", "time": "2023-04-09 12:00:00"}\n\n
示例代码如下
js
function getStream(req, res) {
res.setHeader("Content-type", "text/event-stream"); // 开启sse协议
res.setHeader("Cache-Control", "no-cache"); // 避免浏览器缓存
res.setHeader("Connection", "keep-alive"); // sse是一个一直保持开启的 TCP 连接,所以 Connection 为 keep-alive
let id = 1;
// 当请求断开时,结束响应
req.on("close", () => {
id = 0;
clearInterval(timer);
res.end();
});
const timer = setInterval(() => {
// 自定义事件名称customEvent
res.write(`event: customEvent\n`);
res.write(`id: ${id}\n`);
res.write(`retry: 30000\n`);
res.write(`data: ${JSON.stringify({ content: "你好!" })}\n\n`);
id++;
// 响应10次内容就结束本次HTTP传输
if (id >= 10) {
clearInterval(timer);
res.end();
}
}, 1000);
}
function getStream(req, res) {
res.setHeader("Content-type", "text/event-stream"); // 开启sse协议
res.setHeader("Cache-Control", "no-cache"); // 避免浏览器缓存
res.setHeader("Connection", "keep-alive"); // sse是一个一直保持开启的 TCP 连接,所以 Connection 为 keep-alive
let id = 1;
// 当请求断开时,结束响应
req.on("close", () => {
id = 0;
clearInterval(timer);
res.end();
});
const timer = setInterval(() => {
// 自定义事件名称customEvent
res.write(`event: customEvent\n`);
res.write(`id: ${id}\n`);
res.write(`retry: 30000\n`);
res.write(`data: ${JSON.stringify({ content: "你好!" })}\n\n`);
id++;
// 响应10次内容就结束本次HTTP传输
if (id >= 10) {
clearInterval(timer);
res.end();
}
}, 1000);
}
SSE 推送房间
基本步骤
- 服务端维护房间 id 的表,用来发送对应消息。
- 客户端发送 HTTP 请求,服务端在表中添加一个房间号 id,并返回给客户端
- 客户端创建 SSE 请求,开启长连接
- 客户端的
EventSource
需要监听房间号事件,服务端可以在需要推送的时候响应该事件的消息,这样客户端就能接收到数据了。 - 客户端发送 HTTP 请求,携带上房间号 id,服务器通过 id 删除表中的房间号。成功后客户端断开连接。