是什么
SSE ( Server-Sent Events )是 HTML5 提出基于 HTTP 协议的 WebSocket 轻量代替方案,用来从服务端实时推送数据到浏览器端
严格地说,HTTP 协议是没有办法做服务器推送的,但是当服务器向客户端声明接下来要发送流信息时,客户端就会保持连接打开、一直等着接收服务器发过来的新的数据流。这种通信就是以流信息的方式,完成一次用时很长的通讯
应用场景
在 Web 应用中,浏览器和服务器之间使用的是请求 / 响应的交互模式。导致服务器端产生的数据变化不能及时地通知浏览器,而是需要等到下次请求发出时才能被浏览器获取
SSE 适用更新频繁、 低延迟、对数据实时性要求很高的场景。
怎么用
服务端
var http = require("http");
var fs = require("fs");
var url = require("url");
var path = require("path");
http
.createServer((req, res) => {
var urlObj = url.parse(req.url);
var urlPathname = urlObj.pathname;
var filePathname = path.join(__dirname, "/index.html");
if (urlPathname === "/") {
fs.readFile(filePathname, (err, data) => {
// 错误处理
if (err) {
res.writeHead(404, { "Content-Type": "text/plain" });
res.write("404 - NOT FOUND");
res.end();
} else {
res.writeHead(200, { "Content-Type": "text/html" });
res.write(data);
res.end();
}
});
}
if (urlPathname === "/sse") {
// 服务器声明接下来发送的是事件流
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
});
// 发送消息
setInterval(() => {
res.write("event: slide\n"); // 可自定义事件类型,默认为 message
res.write(`id: ${+new Date()}\n`); // 消息 ID
res.write("data: 7\n"); // 消息数据
res.write("retry: 10000\n"); // 重连时间
res.write("\n\n"); // 消息结束
}, 3000);
// 发送注释保持长连接
setInterval(() => {
res.write(": \n\n");
}, 12000);
}
})
.listen(2000);
直接访问服务
浏览器端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<!-- todo 增加图标 -->
<!-- <link rel="shortcut icon" type="image/x-icon" href="https://unpkg.byted-static.com/latest/byted/arco-config/assets/favicon.ico"> -->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Elkeid</title>
<script>
if (window.EventSource) {
// 创建 EventSource 对象连接服务器
const source = new EventSource('http://localhost:2000/sse');
// 连接成功后会触发 open 事件
source.addEventListener('open', () => {
console.log('Connected');
}, false);
// 服务器发送信息到客户端时,如果没有 event 字段,默认会触发 message 事件
source.addEventListener('message', e => {
console.log(`data: ${e.data}`);
}, false);
// 自定义 EventHandler,在收到 event 字段为 slide 的消息时触发
source.addEventListener('slide', e => {
console.log(`data: ${e.data}`); // => data: 7
}, false);
// 连接异常时会触发 error 事件并自动重连
source.addEventListener('error', e => {
if (e.target.readyState === EventSource.CLOSED) {
console.log('Disconnected');
} else if (e.target.readyState === EventSource.CONNECTING) {
console.log('Connecting...');
}
}, false);
} else {
console.error('Your browser doesn't support SSE');
}
</script>
</head>
<body>
<div id="root"></div>
</body>
</html>
创建 EventSource 对象连接服务器
核心
通讯协议
-
请求头
- 带上 Accept: text/event-stream
-
响应头
-
带上 Content-type: text/events-stream(通讯协议是基于纯文本的简单协议
-
带上 Transfer-Encoding: chunked
-
-
响应格式
-
event 事件类型
-
data 消息的有效载荷,可以是普通字符串类型,或者是JSON对象
-
id 可以用来标记消息序号,便于重连后能恢复数据流,重连时会带上Last-Event-ID来帮助恢复事件流
-
retry 连接意外断开后,浏览器重新发起连接的时间间隔
-
\n\n 用来分割每条消息边界
-
浏览器端 EventSource 对象
- 浏览器端提供事件监听接口
- 对“事件流”数据格式响应内容解析,触发对应监听的事件
-
SSE所定义的“事件流”格式,让不支持的SSE的浏览器,可以通过XHR模拟来优雅降级
对比 服务端推送方案
简易轮询
短轮询指的是浏览器每隔一段时间向浏览器发送http请求,服务器在收到请求后,不论是否有数据更新,都直接进行响应
优点
实现简单、兼容性好
缺点
- 轮询间隔
轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务器端的负担。
- 无用的网络传输
当客户端按固定频率向服务器发起请求,数据可能并没有更新,造成浪费带宽、服务器资源
- 页面可能会出现假死
用 setTimeout 模拟轮询执行不精确,一旦遇到页面有大量任务或者返回时间特别耗时,页面就会出现 假死
长轮询
长轮询指的是在 HTTP 请求的过程中,若服务端数据并没有更新,那么则将这个连接挂起,直到服务器推送新的数据或者超时返回后,客户端再发起新的请求
优点
解决了短轮询场景频繁、无用网络请求导致服务器资源浪费的情景
缺点
- 需要服务器并发能力
保持连接也会消耗资源,服务器所能承载的 TCP 连接数有限,不太适用于客户端数量太多的情况
- 服务器长时间没有响应,链接会超时
SSE
优点
- 基于 HTTP / HTTPS 协议,改造成本很小,直接运行于现有的代理服务器和认证技术。
- 相比轮询,客户端只需连接一次,Server 就可以定时推送
- 天然支持断线重连:通过约定 Last-Event-ID 字段实现
- SSE支持比较灵活、支持自定义发送的消息类型
缺点
- 浏览器兼容性(浏览器 EventSource对象 的兼容性
- 长链接请求,浏览器对连接数的限制
-
相比较 websocket,只支持单向推送
-
SSE 只支持纯文本
事件流仅仅是一个简单的文本数据流, 文本只能使用 UTF-8 格式编码
Websocket
WebSocket是 Html5 定义的一个新协议,与传统的 HTTP 协议不同,WebSocket 使用的是套接字连接,基于 TCP 协议。该协议可以实现服务器与客户端之间全双工通信
优点
真正的全双工通信,强大和灵活、不会有多余的资源损耗
缺点
- 浏览器兼容性问题
- 对于普遍使用 HTTP 协议的项目,使用 websocket 有一些成本
总结
从兼容性角度考虑:短轮询 > 长轮询 > 长连接SSE > WebSocket
从性能方面考虑:WebSocket > 长连接SSE > 长轮询 > 短轮询
相关HTTP请求头
Connection: keep-alive
HTTP一次请求完成后,建立的TCP连接是持久的,不会关闭。使得对同一个服务器的后续请求可以继续在该连接上完成
Accept / Content-Type: text/event-stream
告诉服务端 / 客户端 实际返回的内容类型(应该)为 text/event-stream
Transfer-Encoding: chunked
分块编码的好处是,在返回客户端前不必生成完整的内容,因为它允许将内容作为分块进行流式处理,并明确地发出内容结尾的信号,从而使连接可用于下一个HTTP请求/响应。
在头部加入 Transfer-Encoding: chunked 之后,就代表这个报文采用了分块编码。这时,报文中的实体需要改为用一系列分块来传输。
每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的 CRLF。
最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。
扩展:流式渲染
CSR
SSR
bigPipe
它将网页分解成称为 pagelets 的小块,然后分块传输到浏览器端,进行渲染。它可以有效地提升首屏渲染时间。
实际使用体验
Server-Sent Events 其本质是 HTTP GET 请求,在实际使用时有如下诉求
- 后端会根据请求头里的 token 校验身份
- 前端需要传入(可能很大的)payload,用 GET 拼接在 querystring 里不太合适,希望使用 POST 放到 data 里
缺陷
原生 EventSource 只支持 get 请求
stackoverflow.com/questions/3…
原生 EventSource 不支持额外请求头
不能带特定请求头,无法满足一些 JWT 鉴权场景
stackoverflow.com/questions/2…
原生 EventSource 兼容性
解决方案:通过 XHR 模拟、增强 EventSource
SSE所定义的“事件流”格式,让不支持的 SSE 的浏览器,可以通过 XHR 模拟来优雅降级
且通过 XHR 还可以增强 SSE 不支持的特性
github.com/Yaffle/Even…
优点
- 支持自定义 headers
- 兼容性比原生更好,能覆盖到 IE8 以上
缺点
- 同原生,默认 GET、不支持 POST,且只支持 query-string 传 payload
- 为了兼容 IE,需要服务端配合做一些改造
更多说明
github.com/mpetazzoni/…
优点
- 支持自定义 headers
- 支持跨域
- 支持 POST,且支持 payload
缺点
- 兼容性类比 XHR,且 IE 应该不太行
HTTP 最大连接数限制
断开链接会触发一次 error 事件
总结
-
如果不考虑浏览器兼容性(IE),SSE 还是个不错的选择,且是 HTML5标准之一
-
相比 轮询,性能更好
-
相比 websocket,基于 HTTP 协议,改造成本很小,直接运行于现有的代理服务器和认证技术。
-
-
考虑到 HTTP 最大连接数问题建议升到 HTTP2。否则确实需要考虑到多 tab 的情况。
参考
使用服务器发送事件 - Web API 接口参考 | MDN
HTML5中的SSE(服务器推送技术) - HelloWorld开发者社区
Web端即时通讯技术盘点:短轮询、Comet、Websocket、SSE-网页端IM开发/专项技术区 - 即时通讯开发者社区!