介绍
传统的 HTTP 通信遵循请求-响应模式。客户端发起一个请求,服务器返回一个响应。每次通信都是由客户端发起的,服务器在收到请求后作出响应,而不能主动推送数据
在客户端需要实时获取结果的场景下,请求-响应模式模式是不合适的,因为这意味着客户端需要不断地轮询。更好的做法是服务端生成结果之后主动推送给客户端
SSE(Server-Sent Events)协议是基于 HTTP 的扩展,允许服务器在客户端不发起新请求的情况下,主动向客户端推送数据,直到连接被关闭
WebSocket 协议是通过 HTTP 握手来建立连接的,但连接建立后它就从 HTTP 协议切换到 WebSocket 协议,这时不再遵循 HTTP 的请求-响应模式,而是使用全双工的通信模式
传统 HTTP 通信 | SSE | WebSocket 协议 | |
---|---|---|---|
通信方向 | 客户端到服务器(单向) | 服务器到客户端(单向) | 客户端与服务器(双向) |
连接模式 | 短连接,每次请求-响应后关闭 | 长连接,服务器持续发送数据 | 持久连接,随时双向通信 |
协议标准 | HTTP/1.0, HTTP/1.1, HTTP/2 | 基于 HTTP/1.1 | 始于 HTTP/HTTPS,后切换协议 |
状态保持 | 无状态 | 无状态 | 保持状态 |
断线重连 | 需要客户端重新发起请求 | 自动重连机制,浏览器会尝试重新连接 | 需要手动实现或使用库来处理 |
SSE 较于 WebSocket 更轻量。如果不需要和服务端动态交互,只是希望服务端在有数据的时候推过来,可以使用 SSE 协议
SSE 一般只能传输文本,而 WebSocket 支持传输二进制数据
客户端发起一个标准 HTTP 请求来开启 SSE 会话,要求请求头包含 Accept: text/event-stream
字段,相当于告诉服务端:期望接收 SSE 消息流
服务端返回携带 Content-Type: text/event-stream
字段的响应头,标志着 SSE 连接成功建立。连接会保持开放状态,服务端后续可以随时通过此连接向客户端发送数据
每次返回的数据都由若干个 message,每个 message 内部有若干行组成,每一行都是下述格式中的一种
格式 | 含义 |
---|---|
data: xxxx\n |
数据 |
id: xxxx\n |
数据编号 |
event: xxxx\n |
自定义事件 |
retry: xxxx\n |
重连 |
: xxxx\n |
注释 |
每个 message 之间用 \n\n
分隔
示例
data: {\n
data: "name": "li",\n
data: "age", 24\n
data: }\n\n
返回了一个 message,有四行,每行都是传输了一小段数据
: This is a comment\n\n
data: This is a message\n\n
data: This is another message\n
data: with two lines \n\n
返回了三个 message
- 第一个 message 有一行,是一个注释
- 第二个 message 有一行,传输了一段数据
- 第三个 message 有两行,传输了一段数据
event: foo\n
data: a foo event\n\n
data: an unnamed event\n\n
event: usermessage\n
data: {"name": "li", "age": 24}\n\n
返回了三个 message
- 第一个 message 有两行,第一行表示触发
foo
事件,第二行传输了一段数据 - 第二个 message 有一行,传输了一段数据(不指明事件名称默认触发的是
message
事件) - 第三个 message 有两行,第一行表示触发
bar
事件,第二行传输了一段数据
代码示例
服务端代码:
var http = require("http");
http.createServer(function (request, response) {
if (request.url === "/stream") {
response.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
});
// 发送一条 message,有一行,指定浏览器重新发起连接的时间间隔
response.write("retry: 1000\n\n");
// 发送一条 message,有两行
// 第一行表示触发 hello 事件,第二行传递一段数据
response.write("event: hello\n");
response.write("data: 0\n\n");
let count = 0;
// 每隔 500ms 发送一条 message,有一行,传递一段数据
interval = setInterval(function () {
count += 1;
response.write("data: " + count + "\n\n");
}, 500);
request.socket.addListener("close", function () {
clearInterval(interval);
}, false);
}
}).listen(4684, "127.0.0.1");
将上述代码保存为 server.js
,使用 node 运行,会在本机的 4684 端口打开一个 HTTP 服务。
先运行服务端,再打开下述客户端的网页,就能看到每隔 500ms 接收到一次服务端的数据
客户端代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>SSE client example</title>
</head>
<body>
<pre id="example"></pre>
<script>
const pre = document.querySelector('pre');
const source = new EventSource('http://127.0.0.1:4684/stream');
source.onopen = (event) => { pre.textContent += "open:\n"; };
source.onerror = (event) => { pre.textContent += "error.\n"; };
// hello 事件
source.addEventListener("hello", (event) => {
pre.textContent += `hello ${event.data}\n`;
}, false);
// message 事件
source.onmessage = (event) => {
pre.textContent += `${event.data}\n`;
};
</script>
</body>
</html>