SSE知识盘点

5/16/2023

#前言

不知道有人是否与我一致,想到SSE脑海中的印象就是服务器能够向客户端推送消息,但客户端不能通过该方法返回消息,而有了WebSocket之后似乎就没有必要用到SSE了,那真的是这样吗?那就来一次知识盘点吧,看看它与WebSocket的差异!

#介绍

SSEServer-Sent Events的简称,是一种允许服务器实时地将事件推送到客户端的技术,同时能够让客户端与服务器保持一个持久的HTTP连接。它是单项通信,只能从服务端发送到客户端。

有如下几个特点:

通过一个长连接低延迟交付

高效的浏览器消息解析,不会出现无限缓冲;

自动跟踪最后看到的消息及自动重新连接;

消息通知在客户端以 DOM 事件形式呈现。

#建立 SSE

在客户端创建一个SSE应用程序也很简单,只需要简单的一行,例如:

const sse = new EventSource('/api/sse');
1

语法也就两个参数

new EventSource(url, options);
1

#url

远端服务器资源路径,该url参数值应该符合正常的http接口格式,那一个正常的连接是怎么样的内,如图所示:

success

有几个疑问点值得注意

说明:下面如果没有特殊标明,一律使用Chrome进行测试

#疑问一

如果不使用当前的域名(服务器相关接口开放),会发生什么现象?

直接上图,如下图所示:

跨域

从图中可以发现,与使用AJAX请求在浏览器中表现一致,浏览器会提示存在跨域,连接失败

#疑问二

如果这个连接地址是错误的或者没有这个资源路径会有什么现象?

直接上图,如下图所示:

同域情况:
error

如果是找不到资源路径的情况下不会发生重连

跨域情况:
error

而在跨域情况下ChromeEdge会发生自动重连现象,而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;
    }
});
1
2
3
4
5
6
7
8
9
10
11
  1. Chrome 版本 112.0.5615.138(正式版本)(64 位)5s左右

chrome reconnect

  1. Microsoft Edge版本 113.0.1774.35 (正式版本) (64 位)5s左右

edge reconnect

  1. FireFox 版本 113.0.1firefox比较特殊,如果是同域,那么与chrome一致,如果是跨域,那么如下图所示,直接报错,不发生重连

firefox reconnect

为什么会表现不一致呢?在规范中有这么一句话:

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();
}
1
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>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  1. Chrome:3s左右
    image
  2. FireFox:5s左右
    image

因此:Chrome重连时间存在两种情况

#options

连接的配置项,目前只有一个配置项withCredentials

#withCredentials

布尔值,默认为false,表示在跨域时是否携带凭证(credentials)

因此这个值需要在跨域的时候才需要设置

未设置时:
no cookie

设置这个属性后
cookie

#实例属性

#readyState

当前SSE的连接状态

  • 0(EventSource.CONNECTING): 连接中
  • 1(EventSource.OPEN): 已连接
  • 2(EventSource.CLOSED): 已关闭
const source = new EventSource('/sse');

source.addEventListener('open', () => {
    console.log(source.readyState); // 1
});
1
2
3
4
5

#url

创建SSE实例对象的URL注意这个返回的url如果是同域时会自动补充前面的域名,如果是默认端口号也会隐藏掉,例如:

  1. 自动补全
const source = new EventSource('sse');

source.addEventListener('open', () => {
    console.log(source.url); // http://localhost:3000/sse
});
1
2
3
4
5
  1. 默认端口不显示
const source = new EventSource('http://localhost:80/sse', {
   withCredentials: true
});

source.addEventListener('open', () => {
   console.log(source.url); // http://localhost/sse
});
1
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);
});
1
2
3
4
5
6
7

#实例方法

#close

关闭SSE,与WebSocket不同的是,这个实例方法不需要传递任何参数

const source = new EventSource('sse');

source.addEventListener('open', () => {
   source.close();
});
1
2
3
4
5

#事件

和其他事件一致,有两种声明方式

addEventListener('open', (event) => {});

onopen = (event) => {};
1
2
3

#open

SSE处于open状态,即readyState1时触发

该打开事件参数为普通的Event(opens new window)

#message

SSE接收到消息时触发并且消息类型为message

这个消息事件参数与WebSocketmessage一致,可以看另一个篇文章WebSocket知识盘点(opens new window)中的介绍

依次打印出的结果:
image

这里需要着重介绍一下lastEventId

这个字段是用来标识返回的消息,如果在某一刻浏览器与服务器之间的服务断开了,那么在重新连接后浏览器会把断开前最后一个消息标识传递给服务器,例如

image

如果一开始设置了标识2,但是后面的消息没有设置标识了,那么如果这个时候断开重连,那这个lastEventId是什么呢?

经过测试发现,后面没有设置的标识默认会是最后设置的那一个标识,也就是2,因此重连时lastEventId也为2

#error

SSE发生了一些异常导致不能正常连接时触发

该关闭事件参数为普通的Event(opens new window)

#自定义事件

SSE支持自定义事件,只要服务器设置了相应的事件类型,那么SSE支持设置自定义事件

// server.js
client.send({ event: 'update' });

// 原生
res.write('event: update\n')
1
2
3
4
5
// browser
const source = new EventSource('sse');
source.addEventListener('update', (e) => {
   console.log(e.data);
});
1
2
3
4
5

#其他

后端返回给前端的消息一共存在四个键值对,eventdataidretry,例如Node中:

res.write('event: update\n')
res.write('data: test\n')
res.write('id: 1\n')
res.write('retry: 8000\n')
1
2
3
4

前三个其实已经介绍过了,这里着重介绍retry

#retry

这个字段用于设置重连时间,正如之前疑问三中所说,规范自定义了模糊的几秒,具体实现不同浏览器可能存在差异,因此如果加了这个字段后,那么重连时间能够做到一致

res.write('retry: 8000\n')
1

#差异总结

以下是SSEWebSocket的差异总结

#不同点

  1. SSE利用的是HTTP连接,因此使用的协议也就是HTTP,而WebSocket主要(仍然要经历HTTP阶段,需要经历一次握手)使用的是自己的协议
  2. SSE是一次性的单工传输,只能从服务端发送到客户端,而WebSocket是可复用的全双工传输,客户端和服务端都可以发送消息给对方
  3. SSEWebSocket都存在实例方法close,但WebSokcet由于是双向通信,多了一个send实例方法
  4. SSE具备断开重连机制,而WebSocket没有
  5. SSE实例调用close方法没有参数,不可以配置,而WebSocket可以配置参数
  6. SSE支持自定义事件,而WebSocket不支持
  7. SSE没有close事件能够被监听,而WebSocket可以通过addEventListener可以监听到
  8. SSE服务器响应的格式为text/event-stream,返回的内容只为string,而WebSocket返回的格式有多种,stringblob...
  9. SSE直接利用cookie来实现权限控制的效果,WebSocket权限控制需要另外设置
  10. SSE无法选择要发送的对象,而WebSocket可以通过控制逻辑实现选择性发送,进而实现多播和广播

#相同点

  1. 实例属性获取url返回的规则相同
  2. 都存在实例方法close,同样存在openerror事件

#参考文章

#关联阅读

Last Updated:5/25/2024, 2:23:06 AM