一、客户端实现数据的实时推送方案
| # | 轮询(Polling) | Websocket | SSE |
| 通信协议 | http | tcp | http |
| 触发方式 | client(客户端) | client、server(客户端、服务端) | client、server(客户端、服务端) |
| 优点 | 兼容性好容错性强,实现简单 | 全双工通讯协议,性能开销小、安全性高,可扩展性强 | 实现简便,开发成本低 |
| 缺点 | 安全性差,占较多的内存资源与请求数 | 传输数据需要进行二次解析,增加开发成本及难度 | 只适用高级浏览器(对浏览器有要求) |
| 延迟 | 非实时,延迟取决于请求间隔 | 实时 | 非实时,默认3秒延迟,延迟可自定义 |
| 适用场景 | WebSocket适用于需要实时双向通信的场景,如聊天应用、多人协同编辑等 | SSE适用于服务器向客户端实时推送数据的场景,如股票价格更新、新闻实时推送等 |
二、SSE介绍:
EventSource 是服务器推送的一个网络事件接口。一个EventSource实例会对HTTP服务开启一个持久化的连接,以text/event-stream 格式发送事件, 会一直保持开启直到被要求关闭;
Server-Sent Events(SSE)是一种用于实现服务器向客户端实时推送数据的Web技术。与传统的轮询和长轮询相比,SSE提供了更高效和实时的数据推送机制,SSE基于HTTP协议,允许服务器将数据以事件流(Event Stream)的形式发送给客户端。客户端通过建立持久的HTTP连接,并监听事件流,可以实时接收服务器推送的数据。
| 响应类型 | Content-Type: text/event-stream | 状态码200 |
| 消息格式 | data:消息文本 (utf-8格式) | |
| 支持跨域 | EventSource | 服务器设置Access-Control-Allow-Origin |
| 重新连接 | 自动重连 | 1、Content-Type 不正常不重连2、状态码非301、307、200、204(No Content)不重连3、关闭连接后需重新初始化实例 |
| 关闭连接 | 客户端关闭,eventSource.close() | 1、浏览器主动关闭2、服务器返回204状态吗关闭 |
| 消息ID | 客户端接收:eventSource.lastEventId服务端接受:header中增加Last-Event-ID | |
| 连接状态 | EventSource.CONNECTING =0EventSource.OPEN=1EventSource.CLOSED=2 | 1、连接中或者重连中2、已连接3、连接已关闭 |
- SSE的工作原理
一般来说HTTP协议是要客户端先请求服务器,服务器才能响应给客户端,无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(event-streaming)。
也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。
SSE 就是利用这种机制,使用流信息向客户端推送信息。
-
-
-
-
- 客户端请求建立事件流类型的连接,具体如下:
-
-
-
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', // 是否允许允许跨域 })
-
-
-
-
- 服务端响应,并将Response Headers Content-Type设置为text/event-stream,数据将以这种类型传送,服务端有数据就会发送给客户端
-
-
-
响应格式:
- event 事件类型
- data 消息的有效载荷,可以是普通字符串类型,或者是JSON对象
- id 可以用来标记消息序号
- retry 连接意外断开后,浏览器重新发起连接的时间间隔
- \n\n 用来分割每条消息边界
- SSE的主要特点包括:
-
- 简单易用:SSE使用基于文本的数据格式,如纯文本、JSON等,使得数据的发送和解析都相对简单。
- 单向通信:SSE支持服务器向客户端的单向通信,服务器可以主动推送数据给客户端,而客户端只能接收数据。
- 实时性:SSE建立长时间的连接,使得服务器可以实时地将数据推送给客户端,而无需客户端频繁地发起请求。
- 应用场景
根据具体的业务需求和场景,选择SSE或WebSocket取决于您的实际需求。如果您只需要服务器向客户端单向推送数据,并且希望保持简单易用和兼容性好,那么SSE是一个不错的选择。如果您需要实现双向通信,或者需要更高级的功能和控制,那么WebSocket可能更适合您的需求。
- 进行SSE实时数据推送注意点:
- 异步处理:由于SSE是基于长连接的机制,推送数据的过程是一个长时间的操作。为了不阻塞服务器线程,推荐使用异步方式处理SSE请求。您可以在控制器方法中使用@Async注解或使用CompletableFuture等异步编程方式。
- 超时处理:SSE连接可能会因为网络中断、客户端关闭等原因而发生超时。为了避免无效的连接一直保持在服务器端,您可以设置超时时间并处理连接超时的情况。可以使用SseEmitter对象的setTimeout()方法设置超时时间,并通过onTimeout()方法处理连接超时的逻辑。
- 异常处理:在实际应用中,可能会出现一些异常情况,如网络异常、推送数据失败等。您可以使用SseEmitter对象的completeWithError()方法将异常信息发送给客户端,并在客户端通过eventSource.onerror事件进行处理。
- 并发性能:SSE的并发连接数可能会对服务器的性能造成影响。如果需要处理大量的并发连接,可以考虑使用线程池或其他异步处理方式,以充分利用服务器资源。
- 客户端兼容性:虽然大多数现代浏览器都支持SSE,但仍然有一些旧版本的浏览器不支持。IE都不支持、EDGE 79以后的版本豆支持
- 浏览器连接数的限制:大多数浏览器对于同一个IP或域名都有连接数量限制(比如chrome上限是6个),对于普通的HTTP请求,连接一下又断开了这没什么问题,但 Server-sent events 是长连接,超过6个就无法再连接了
- 浏览器支持,见下图
这些注意点将有助于您正确和高效地使用SseEmitter进行SSE实时数据推送。根据具体的应用需求,您可以根据实际情况进行调整和优化。
请记住,在实际应用中,确保服务器的稳定性、安全性和性能是至关重要的。在处理SSE连接时,您可以进行适当的限流和安全控制,以防止滥用和恶意连接的出现。
- 客户端代码
- 封装EventSourceClient
export default class EventSourceClient { constructor(url) { this.url = url; this.eventSource = null; } // 建立连接 connection(openCallback, messageCallback, errorCallback) { this.eventSource = new EventSource(this.url); this.eventSource.onopen = openCallback; this.eventSource.onmessage = messageCallback; this.eventSource.onerror = function (e) { if (this.readyState == EventSource.CONNECTING) { console.log(Reconnecting (readyState=${this.readyState})...); } else { errorCallback && errorCallback(e) } }; } // 断开连接 disconnect() { this.eventSource && this.eventSource.close(); } addAction(action, fn) { this.eventSource && this.eventSource.addEventListener(action, fn); } }
- 使用EventSourceClient
- 服务端 node
let http = require('http'); function onDigits(req, res) { // 设置请求头 res.writeHead(200, { 'Cache-Control': 'no-cache', // 支持跨域请求 "Access-Control-Allow-Origin": "*", // 返回类型为text/event-stream 'Content-Type': 'text/event-stream; charset=utf-8', }); let i = 0; let timer = setInterval(write, 1000); write(); function write() { i++; if (i == 4) { res.write('event: bye\ndata: bye-bye\n\n'); clearInterval(timer); res.end(); return; } res.write('data: ' + i + '\n\n'); } } function accept(req, res) { if (req.url == '/digits') { onDigits(req, res); } } http.createServer(accept).listen(8080);