#前言
不知道有人是否与我一致,想到SSE
脑海中的印象就是服务器能够向客户端推送消息,但客户端不能通过该方法返回消息,而有了WebSocket
之后似乎就没有必要用到SSE
了,那真的是这样吗?那就来一次知识盘点吧,看看它与WebSocket
的差异!
#介绍
SSE
是Server-Sent Events
的简称,是一种允许服务器实时地将事件推送到客户端的技术,同时能够让客户端与服务器保持一个持久的HTTP
连接。它是单项通信,只能从服务端发送到客户端。
有如下几个特点:
通过一个长连接低延迟交付
高效的浏览器消息解析,不会出现无限缓冲;
自动跟踪最后看到的消息及自动重新连接;
消息通知在客户端以 DOM 事件形式呈现。
#建立 SSE
在客户端创建一个SSE
应用程序也很简单,只需要简单的一行,例如:
const sse = new EventSource('/api/sse');
语法也就两个参数
new EventSource(url, options);
#url
远端服务器资源路径,该url
参数值应该符合正常的http
接口格式,那一个正常的连接是怎么样的内,如图所示:
有几个疑问点值得注意
说明:下面如果没有特殊标明,一律使用Chrome
进行测试
#疑问一
如果不使用当前的域名(服务器相关接口开放),会发生什么现象?
直接上图,如下图所示:
从图中可以发现,与使用AJAX
请求在浏览器中表现一致,浏览器会提示存在跨域,连接失败
#疑问二
如果这个连接地址是错误的或者没有这个资源路径会有什么现象?
直接上图,如下图所示:
同域情况:
如果是找不到资源路径的情况下不会发生重连
跨域情况:
而在跨域情况下Chrome
、Edge
会发生自动重连现象,而Firfox
则不会,这里我在自己的电脑上测试一下时间间隔是多大:
使用的测试代码为:
const sse = new EventSource('http://xxx:8080/api');
let timer = 0;
sse.addEventListener('error', () => {
if (!timer) {
timer = Date.now();
} else {
const newTimer = Date.now();
console.log(newTimer - timer);
timer = newTimer;
}
});
2
3
4
5
6
7
8
9
10
11
Chrome 版本 112.0.5615.138(正式版本)(64 位)
:5s
左右
Microsoft Edge版本 113.0.1774.35 (正式版本) (64 位)
:5s
左右
FireFox 版本 113.0.1
:firefox
比较特殊,如果是同域,那么与chrome
一致,如果是跨域,那么如下图所示,直接报错,不发生重连
为什么会表现不一致呢?在规范中有这么一句话:
When a user agent is to fail the connection, the user agent must queue a task which, if the readyState attribute is set to a value other than CLOSED, sets the readyState attribute to CLOSED and fires an event named error at the EventSource object. Once the user agent has failed the connection, it does not attempt to reconnect.
大致意思就是:当用户代理需要关闭连接时,用户代理必须排队执行一个任务,该任务在readyState
属性设置为非CLOSED
值时,将readyState
属性设置为CLOSED
,并在EventSource
对象上触发一个名为error
的事件。一旦用户代理关闭了连接,它就不会尝试重新连接。
其实在error
事件中打印readyState
能够发现端倪,Chrome
的表现还是为0
,而FireFox
则为2
,因此两者是否重连取决于对于连接失败的请求的状态是否设置为CLOSED
有关
#疑问三
SSE
能够自动重连,那么这个时间是多少?是疑问二的5s
吗?
规范是否有对这个重连时间进行定义呢?这里可以查看HTML规范(opens new window)
A reconnection time, in milliseconds. This must initially be an implementation-defined value, probably in the region of a few seconds.
重连时间:以毫秒为单位。这最初必须是一个实现定义的值,可能在几秒钟内。
这句话意思是应当先设置一个由具体实现(例如浏览器或其他客户端)所定义的时间值,作为SSE
重新连接的间隔。虽然此值会因不同实现而有所差别,但通常会在几秒钟的范围内,因此规范本身并没有确定默认的重连时间间隔是多少,而是将这个初始值留给具体实现(例如浏览器)来设定。
使用如下代码进行测试:
// server.js
const express = require('express');
const SSE = require('sse');
const app = express();
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
const server = app.listen(3000, () => {
console.log('Server listening on port 3000');
});
const sse = new SSE(server);
sse.on('connection', (client) => {
console.log('Client connected');
sendEvent(client, 1);
client.on('close', () => {
console.log('Client disconnected');
});
});
function sendEvent(client, count) {
client.send({ event: 'message', data: 'Sample data: ' + count });
client.close();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!-- 主要代码 -->
<script>
const source = new EventSource('/sse');
let timer = 0;
source.addEventListener('message', (event) => {
const div = document.createElement('div');
div.textContent = event.data;
document.body.appendChild(div);
});
source.addEventListener('error', () => {
if (!timer) {
timer = Date.now();
} else {
const newTimer = Date.now();
console.log(newTimer - timer);
timer = newTimer;
}
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Chrome
:3s
左右FireFox
:5s
左右
因此:Chrome
重连时间存在两种情况
#options
连接的配置项,目前只有一个配置项withCredentials
#withCredentials
布尔值,默认为false
,表示在跨域时是否携带凭证(credentials)
因此这个值需要在跨域的时候才需要设置
未设置时:
设置这个属性后
#实例属性
#readyState
当前SSE
的连接状态
- 0(EventSource.CONNECTING): 连接中
- 1(EventSource.OPEN): 已连接
- 2(EventSource.CLOSED): 已关闭
const source = new EventSource('/sse');
source.addEventListener('open', () => {
console.log(source.readyState); // 1
});
2
3
4
5
#url
创建SSE
实例对象的URL
,注意这个返回的url
如果是同域时会自动补充前面的域名,如果是默认端口号也会隐藏掉,例如:
- 自动补全
const source = new EventSource('sse');
source.addEventListener('open', () => {
console.log(source.url); // http://localhost:3000/sse
});
2
3
4
5
- 默认端口不显示
const source = new EventSource('http://localhost:80/sse', {
withCredentials: true
});
source.addEventListener('open', () => {
console.log(source.url); // http://localhost/sse
});
2
3
4
5
6
7
#withCredentials
是否携带凭证,这个在new
一个对象时第二个参数配置相关,默认为false
const source = new EventSource('http://localhost:80/sse', {
withCredentials: true
});
source.addEventListener('open', () => {
console.log(source.withCredentials);
});
2
3
4
5
6
7
#实例方法
#close
关闭SSE
,与WebSocket
不同的是,这个实例方法不需要传递任何参数
const source = new EventSource('sse');
source.addEventListener('open', () => {
source.close();
});
2
3
4
5
#事件
和其他事件一致,有两种声明方式
addEventListener('open', (event) => {});
onopen = (event) => {};
2
3
#open
当SSE
处于open
状态,即readyState
为1
时触发
该打开事件参数为普通的Event(opens new window)
#message
当SSE
接收到消息时触发并且消息类型为message
这个消息事件参数与WebSocket
中message
一致,可以看另一个篇文章WebSocket知识盘点(opens new window)中的介绍
依次打印出的结果:
这里需要着重介绍一下lastEventId
这个字段是用来标识返回的消息,如果在某一刻浏览器与服务器之间的服务断开了,那么在重新连接后浏览器会把断开前最后一个消息标识传递给服务器,例如
如果一开始设置了标识2
,但是后面的消息没有设置标识了,那么如果这个时候断开重连,那这个lastEventId
是什么呢?
经过测试发现,后面没有设置的标识默认会是最后设置的那一个标识,也就是2
,因此重连时lastEventId
也为2
#error
当SSE
发生了一些异常导致不能正常连接时触发
该关闭事件参数为普通的Event(opens new window)
#自定义事件
SSE
支持自定义事件,只要服务器设置了相应的事件类型,那么SSE
支持设置自定义事件
// server.js
client.send({ event: 'update' });
// 原生
res.write('event: update\n')
2
3
4
5
// browser
const source = new EventSource('sse');
source.addEventListener('update', (e) => {
console.log(e.data);
});
2
3
4
5
#其他
后端返回给前端的消息一共存在四个键值对,event
、data
、id
、retry
,例如Node
中:
res.write('event: update\n')
res.write('data: test\n')
res.write('id: 1\n')
res.write('retry: 8000\n')
2
3
4
前三个其实已经介绍过了,这里着重介绍retry
#retry
这个字段用于设置重连时间,正如之前疑问三中所说,规范自定义了模糊的几秒,具体实现不同浏览器可能存在差异,因此如果加了这个字段后,那么重连时间能够做到一致
res.write('retry: 8000\n')
#差异总结
以下是SSE
与WebSocket
的差异总结
#不同点
SSE
利用的是HTTP
连接,因此使用的协议也就是HTTP
,而WebSocket
主要(仍然要经历HTTP
阶段,需要经历一次握手)使用的是自己的协议SSE
是一次性的单工传输,只能从服务端发送到客户端,而WebSocket
是可复用的全双工传输,客户端和服务端都可以发送消息给对方SSE
与WebSocket
都存在实例方法close
,但WebSokcet
由于是双向通信,多了一个send
实例方法SSE
具备断开重连机制,而WebSocket
没有SSE
实例调用close
方法没有参数,不可以配置,而WebSocket
可以配置参数SSE
支持自定义事件,而WebSocket
不支持SSE
没有close
事件能够被监听,而WebSocket
可以通过addEventListener
可以监听到SSE
服务器响应的格式为text/event-stream
,返回的内容只为string
,而WebSocket
返回的格式有多种,string
、blob
...SSE
直接利用cookie
来实现权限控制的效果,WebSocket
权限控制需要另外设置SSE
无法选择要发送的对象,而WebSocket
可以通过控制逻辑实现选择性发送,进而实现多播和广播
#相同点
- 实例属性获取
url
返回的规则相同 - 都存在实例方法
close
,同样存在open
、error
事件
#参考文章
- MDN(opens new window)
- 《High Performance Browser Networking》(opens new window)
- HTML SSE规范(opens new window)