一、前言
HTTP 请求-响应模型是 Web 应用的主要通信模型,在这种模型中由客户端向服务器发起请求,服务器处理请求,要进行此通信,服务端和客户端必须先建立连接,并且当请求-响应周期结束时,连接将关闭。
这种模型对于早期的 Web 应用程序来说已经足够了,因为早期的网站只显示静态内容。但随着网络的发展,出现了允许服务器向客户端发送数据而无需客户端首先请求的需求。比如说实时聊天、多人协作、新闻消息、股票行情等。
对于这些场景传统的 HTTP 请求-响应模式并不适合,因为这种模式需要频繁建立和断开连接,造成大量的网络开销和资源浪费。因此,为了解决这种实时推送数据的问题,出现了 SSE、WebSocket 等消息推送技术。比如当前很火的 ChatGPT 的消息回复就使用了 SSE:

二、SSE 简介
Server-Sent Events (SSE) 是一种实现服务器向客户端推送事件的技术,它通过 HTTP 协议提供了一种简单的、轻量级的服务器端推送技术,可以实时地将服务器端数据推送给客户端浏览器,所以具有以下优点:
- 兼容性好:SSE 是使用 HTTP 协议进行通信的,因此所有基于 HTTP 协议的客户端都可以使用 SSE 接收数据。
- 简单易用:相比 Websocket,SSE 的 API 更加简单且易于使用,比如不需要额外的心跳检测机制。
- 无需建立双向连接:相比 Websocket,SSE 只需要在客户端与服务器之间建立一个单向的连接,而不是双向的连接。
三、SSE VS Websocket
websocket 是一种独立于 http 协议的持久化协议,这是相对于 http 这种 非持久 的协议来说的,它可以在用户的浏览器和服务器之间打开交互式通信会话,使用此 API,可以向服务器发送消息并接收服务器的消息。从效果和使用场景来看与 SSE 很相似,但是他们还是有比较多的不同之处:
| SSE | Websocket |
|---|---|
| 基于 HTTP 协议 | 独立的、基于 TCP 的全双工通信协议 |
| 单工,只能由服务端向客户端发送消息 | 全双工,双端可以同时接收和发送消息 |
| 内置重连机制 | 不支持,需要手动实现 |
| 仅支持UTF-8数据传输 | 支持二进制和UTF-8数据传输 |
| 支持自定义事件类型 | 不支持自定义事件类型 |
| 连接数 HTTP/1.1 6 个,HTTP/2 可协商(默认 100) | 连接数无限制 |
| 可以使用 JavaScript 进行 www.npmjs.com/package/eve… | 不能使用 JavaScript 进行 polyfill,socket.io/zh-CN/docs/… |
四、SSE 基本使用
1、服务端实现
1.1、处理 HTTP 请求
SSE 本质就是浏览器发起 HTTP 请求,服务端应该能够接收到该请求,并正确地响应,具体来说,服务端需要发送以下 HTTP 头部信息:
- Content-Type:设置为
text/event-stream,表示返回的内容是 SSE 格式的数据。 - Cache-Control:设置为
no-cache,禁止客户端缓存响应。 - Connection:设置为
keep-alive,保持长连接。
1.2、返回消息格式
EventStream(事件流)仅仅是一个简单的文本数据流,文本应该使用 UTF-8 格式的编码,每条消息由多个字段组成,这些字段包涵(event、data、id、retry),每个字段由字段名,一个冒号,以及字段值组成,比如 data: {name: ‘xx’} 。
每条消息后面都由一个空行作为分隔符(\n\n),以冒号开头的行为注释行,会被忽略,如下所示:
: this is explanatory note\n\n // 这是注释行
data: {name: 'message1'}\n\n // 这是第一条消息
event: event1\n // 这是第二条消息
data: {name: 'message2'}\n\n
event: event2\n // 这是第三条消息
data: {name: 'message3-1'}\n
data: {name: 'message3-2'}\\n
-
⚠️ 注意:
- 除上述四个字段外,其他所有字段都会被忽略。
- 如果一行字段中不包含冒号,则整行文本将被视为字段名,字段值为空。
- 注释行可以用来防止链接超时,服务端可以定期向浏览器发送一条消息注释行,以保持连接不断。
event
事件类型。如果指定了该字段,则在客户端接收到该条消息时,会在当前的EventSource对象上触发一个事件,事件类型就是该字段的字段值,你可以使用addEventListener() 方法在当前 EventSource对象上监听任意类型的命名事件,如果该条消息没有event字段,则会触发onmessage 属性上的事件处理函数。
服务端返回两条消息:
data: {name: 'message1'}\n\n // 这是第一条消息
event: custom-event\n // 这是第二条消息
data: {name: 'message2'}\n\n
浏览器接受这两条消息:
const evtSource = new EventSource('<http://xxx>')
// 监听自定义事件
evtSource.addEventListener('custom-event', (event) => {
console.log(event.data); // {name: 'message2'}
})
// 监听原生事件
evtSource.addEventListener('onmessage', (event) => {
console.log(event.data); // {name: 'message1'}
})
id
事件ID,用于标识事件的唯一标识符。它允许服务器为每个事件分配一个唯一的标识符,并将该标识符发送给客户端。使用事件 ID 可以轻松实现事件追踪功能,通过为每个事件分配唯一的 id,客户端可以追踪已接收的事件,如果发生断连,浏览器会把收到的最后一个事件ID放到 HTTP Header Last-Event-Id 中进行重连,以便在重新连接时,服务器可以发送丢失的事件或仅发送更新的事件。
const express = require('express');
const app = express();
const port = 8000;
const eventData = [];
let id = 0;
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Access-Control-Allow-Origin', '*')
const eventId = req.headers['Last-Event-Id'];
if (eventId) {
// 从数组中取出数据并发送给客户端
}
const intervalId = setInterval(() => {
const data = new Date().toLocaleString();
res.write(`id: ${id}\n`)
res.write(`data: ${data}\n\n`)
eventData.push({id, data})
id++;
}, 2000)
res.on('close', () => {
console.log('Client closed connected')
})
})
app.listen(port, () => {
console.log('run server on 8000')
})
对于 websocket 也有相应的事件追踪实现方案,可以参考 socket.io 的实现。
data
消息数据。数据内容只能以一个字符串的文本形式进行发送,如果需要发送一个对象时,需要将该对象以一个 JSON 格式的字符串的形式进行发送。在浏览器接收到该字符串后,再把它还原为一个 JSON 对象。
retry
重连时间。整数值,单位 ms,如果与服务器的连接丢失,浏览器将等待指定时间,然后尝试重新连接,如果该字段不是整数值,会被忽略。
2、客户端
在浏览器端,可以使用 JavaScript 的 EventSource API 创建 EventSource 对象监听服务器发送的事件。一旦建立连接,服务器就可以发送事件消息,浏览器则可以通过监听 EventSource 对象的 原生事件 onmessage、onopen 和 onerror 来处理这些消息。
2.1、建立连接
EventSource 接受两个参数:
-
url
url 表示事件源,一旦 EventSource 对象被创建后,浏览器立即开始对该 url 地址发送过来的事件进行监听。
-
options
options 是一个可选的对象,包含 withCredentials 属性,表示是否发送凭证(cookie、HTTP认证信息等)到服务端,默认为 false。
2.2、监听事件
EventSource 对象本身继承自 EventTarget 接口,因此可以使用 addEventListener() 方法来监听事件。
-
open事件连接刚打开时被调用。
eventSource.addEventListener('open', (event) => { console.log('Connection opened') }) -
当接收到服务器发送的消息时触发。该事件对象的 data 属性包含了服务器发送的消息内容,这里要注意的是如果发送的是自定义事件消息,该事件将不会被触发。
eventSource.addEventListener('message', (event) => { console.log('Received message: ' + event.data); }) -
error事件当发生错误时被调用,并且在此对象上派发
[error](<https://developer.mozilla.org/zh-CN/docs/Web/API/HTMLElement/error_event>)事件。eventSource.addEventListener('error', function(event) { console.log('Error occurred: ' + event.event); }) -
自定义事件
EventSource 除了为我们提供原生事件还支持自定义事件,事件的名称就是服务器返回的消息中
event字段指定的事件名称。eventSource.addEventListener('custom-event', function(event) { console.log('custom event message: ' + event.event); })
2.3、关闭连接
EventSource 提供了 close 方法用于关闭连接,如果连接已经被关闭,此方法不会再进行任何操作。
// 关闭连接
eventSource.close()
3、注意事项
3.1、浏览器兼容
目前大多数现代浏览器都支持 SSE,但在旧版浏览器中可能会有一些限制或不支持。
对于不支持 EventSource 的浏览器,可以使用 polyfill 实现。
3.2、连接数最大限制
在非 HTTP / 2 的场景下使用 SSE(server-sent events)会受到最大连接数的限制,这在打开各种选项卡时特别麻烦,因为该限制是针对每个浏览器的,并且被设置为一个非常低的数字(6)。该问题在 Chrome 和 Firefox 中被标记为“无法解决”。
使用 HTTP / 2 时,HTTP 同一时间内的最大连接数由服务器和客户端之间协商(默认为 100),但是升级到 HTTP / 2 需要先接入 HTTPS,如果我们做的是 ToB 的产品,这一点是需要重点考虑的,因为很可能客户就不支持 HTTPS。
3.3、消息传输质量
SSE 本身不具备保证消息传递的质量,比如断连重连后消息不会重发,这个都需要开发者额外去实现。
五、案例实践
1、服务端
const express = require('express');
const app = express();
const port = 8000;
let id = 0;
app.get('/events', (req, res) => {
console.log(req.headers['Last-Event-Id']);
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Access-Control-Allow-Origin', '*')
const intervalId = setInterval(() => {
const data = new Date().toLocaleString();
res.write(`id: ${id}\n`)
res.write(`data: ${data}\n\n`)
id++;
}, 2000)
res.on('close', () => {
console.log('Client closed connected')
})
})
app.listen(port, () => {
console.log('run server on 8000')
})
2、客户端
<!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>
<div id="messages"></div>
<script src="./client.js"></script>
</body>
<script>
const eventSource = new EventSource(`http://localhost:8000/events`)
function updateMessage (message) {
const list = document.getElementById('messages');
const item = document.createElement('p');
item.textContent = message;
list.appendChild(item);
}
eventSource.onmessage = function(event) {
updateMessage(event.data);
}
eventSource.onerror = function() {
updateMessage('Server closed connection')
eventSource.close();
}
</script>
</html>
六、总结
Server-Sent Events (SSE) 是一种实现服务器向客户端推送事件的技术,它通过 HTTP 协议提供了一种简单的、轻量级的服务器端推送技术,自带断连重连,支持事件追踪,但是也存在一些缺陷,连接数限制、没有消息重发机制等。
SSE 适用于简单的单向服务推送的场景,比如新闻更新、消息通知,因为它兼容性好,对技术要求低,而对于一些需要双向通信、高实时行的场景下,Websocket 会更合适。