SSE 本质
HTTP 协议本身是基于请求-响应模型的,这意味着客户端发起请求后,服务器响应该请求,然后通信就结束了。按照传统的 HTTP 模型,服务器无法主动向客户端发送信息。然而,通过将 HTTP 响应保持打开状态并发送数据流,我们可以实现类似于服务器主动推送的效果。
在流信息或流媒体(Streaming)的上下文中,服务器开始响应 HTTP 请求后,不是发送一个完整的响应后就关闭连接,而是保持连接打开状态,并持续发送数据片段。这些数据片段可以是文件的一部分、实时生成的数据或任何连续的数据流。客户端接收并处理这些连续的数据片段,而不需要每次需要新数据时都发起一个新的 HTTP 请求。
长轮询是流信息的一种特殊形式,客户端发送 HTTP 请求到服务器,服务器保持请求打开,直到有新数据可发送。发送数据后,连接关闭,客户端立即再次发起新的请求。这种方式虽然可以实现实时通信,但效率较低,因为每次数据传输结束后都需要重新建立连接。
SSE 通过持久 HTTP 连接高效实现单向实时数据流,适合简单推送如新闻和股票,且自带重连;而 WebSocket 支持复杂的双向交互,如在线游戏和聊天,能处理多种数据类型,但需自设重连策略。简言之,SSE 优化单向通信,WebSocket 强化双向互动。
总的来说,SSE 主要有以下的特点:
-
SSE 使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议。
-
SSE 属于轻量级,使用简单;WebSocket 协议相对复杂。
-
SSE 默认支持断线重连,WebSocket 需要自己实现。
-
SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据。
-
SSE 支持自定义发送的消息类型。
客户端 API
EventSource 对象允许浏览器从服务器接收通过 HTTP 协议发送的 SSE(Server-Sent Events)实时更新。
使用 SSE 时,浏览器首先生成一个 EventSource 实例,向服务器发起连接。
js
const eventSource = new EventSource(url);
EventSource 实例会自动监听来自服务器的消息,并通过事件来处理这些消息。可以为 EventSource 实例添加事件监听器来响应服务器发送的不同类型的事件。
-
message 事件:这是最常见的事件类型,用于接收服务器发送的不确定类型的消息。
-
open 事件:当与服务器的连接成功建立时触发。
-
error 事件:当连接出错或无法建立连接时触发。
js
eventSource.onmessage = function (event) {
console.log("新消息:", event.data);
};
eventSource.onopen = function () {
console.log("连接成功");
};
eventSource.onerror = function () {
console.log("连接错误.");
};
另外 close 方法用于关闭 SSE 连接。
js
eventSource.close();
EventSource 实例的 readyState 属性,表明连接的当前状态。该属性只读,可以取以下值:
-
0:相当于常量 EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。
-
1:相当于常量 EventSource.OPEN,表示连接已经建立,可以接受数据。
-
2:相当于常量 EventSource.CLOSED,表示连接已断,且不会重连。
服务端实现
服务器向浏览器发送的 Server-Sent Events(SSE)数据必须是 UTF-8 编码的文本。SSE 协议是基于文本的,它要求传输的数据使用 UTF-8 编码,这确保了数据的国际化和兼容性,允许消息内容包含任何 Unicode 字符。
具有如下的 HTTP 头信息:
数据内容用 data 字段表示。
text
data: message\n\n
如果数据很长,可以分成多行,最后一行用\n\n 结尾,前面行都用\n 结尾。
text
data: begin message\n
data: continue message\n\n
下面是一个发送 JSON 数据的例子。
text
data: {\n
data: "foo": "bar",\n
data: "baz", 555\n
data: }\n\n
id 字段用于设置事件的唯一标识符。这对于客户端在断开连接后重连时,请求从上次接收的最后一个事件之后继续接收事件很有用。
客户端可以在重连时发送 Last-Event-ID 头,其中包含上次接收到的事件 ID,从而允许服务器只发送新的事件。
event 字段用于指定事件的类型。客户端可以根据事件类型来决定如何处理接收到的数
这允许一个 SSE 连接用于传输多种类型的消息,而客户端可以通过监听特定类型的事件来进行响应。
服务器可以用 retry 字段,指定浏览器重新发起连接的时间间隔。
makefile
retry: 10000\n
两种情况会导致浏览器重新发起连接:一种是时间间隔到期,二是由于网络错误等原因,导致连接出错。
实现打字 🐔 效果
ts
import { Controller, Sse } from "@nestjs/common";
import { Observable } from "rxjs";
@Controller("events")
export class EventsController {
@Sse("sse")
typing(): Observable<any> {
const text = "PHP是世界上最好的语言吗";
return new Observable<any>((observer) => {
let currentIndex = 0;
const intervalId = setInterval(() => {
if (currentIndex < text.length) {
// 发送正常消息
observer.next({
data: JSON.stringify({
data: text.substring(0, ++currentIndex),
isEnd: false,
}),
});
} else {
// 发送结束信号
observer.next({ data: JSON.stringify({ data: "", isEnd: true }) });
clearInterval(intervalId);
observer.complete();
}
}, 100);
});
}
}
在上面的代码中,我们使用了 @Sse
装饰器在 EventsController 控制器中定义了一个名为 typing 的 SSE 端点。当客户端向/events/sse 发起 GET 请求时,将触发这个 typing 方法。@Sse 装饰器标记的方法预期返回一个 Observable 对象,这个对象用于向客户端推送消息。
typing 方法内部创建了一个 Observable 对象,这个对象用 setInterval 函数定时每 100 毫秒向客户端发送一段文本的增长部分,模拟打字效果。
并且定义了一段初始文字,然后逐字发送这段文本,每次发送时文本长度增加一个字符,直到整个文本被发送完毕。
当整个文本发送完毕后(即 currentIndex 等于文本长度时),再发送一个消息,其 isEnd 标记为 true,表示消息发送完毕。随后清除定时器并调用 observer.complete()结束 Observable 流。
html
<body>
<div id="message"></div>
<script>
const eventSource = new EventSource(
"http://localhost:8080/api/v1/events/sse"
);
eventSource.onmessage = function (event) {
const { data, isEnd, ...rest } = JSON.parse(event.data);
console.log(event);
if (!isEnd) {
document.getElementById("message").innerHTML = data;
} else {
console.log("Message complete");
eventSource.close();
}
};
</script>
</body>
上面这段前端代码就没啥好说的了,之前的已经讲过了,当接收到 isEnd 为 true 的时候则中断连接。
具体效果如下图所示:
参考链接
总结
SSE (Server-Sent Events) 技术是基于 HTTP 协议的轻量级实时通信解决方案。它主要优点包括服务端推送功能、自动断线重连机制以及其轻量级特性。然而,SSE 也存在一些局限性,例如无法支持双向通信、同时打开的连接数量有限制,以及仅支持 GET 请求等。
在 Web 应用中,SSE 能够有效实现股票市场数据实时更新、日志信息推送、实时显示聊天室人数等功能。但是,SSE 并非适用于所有实时数据传输场景。对于那些对高并发、高吞吐量以及极低延迟有严格要求的应用场景,WebSocket 技术可能是更好的选择。相对而言,在对系统资源消耗有较低要求的轻量级推送场景中,SSE 显示出了其独特的优势。因此,在选择实时数据更新方案时,开发者需要针对应用的具体需求和场景特点做出恰当的选择。