客户端即时消息通讯

168 阅读7分钟

一、客户端实现数据的实时推送方案

#轮询(Polling)WebsocketSSE
通信协议httptcphttp
触发方式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=21、连接中或者重连中2、已连接3、连接已关闭
  1. SSE的工作原理

一般来说HTTP协议是要客户端先请求服务器,服务器才能响应给客户端,无法做到服务器主动推送信息。但是,有一种变通方法,就是服务器向客户端声明,接下来要发送的是流信息(event-streaming)。

也就是说,发送的不是一次性的数据包,而是一个数据流,会连续不断地发送过来。这时,客户端不会关闭连接,会一直等着服务器发过来的新的数据流,视频播放就是这样的例子。本质上,这种通信就是以流信息的方式,完成一次用时很长的下载。

SSE 就是利用这种机制,使用流信息向客户端推送信息。

0

          1. 客户端请求建立事件流类型的连接,具体如下:

0

res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', // 是否允许允许跨域 })

          1. 服务端响应,并将Response Headers Content-Type设置为text/event-stream,数据将以这种类型传送,服务端有数据就会发送给客户端

0

响应格式:

  • event 事件类型
  • data 消息的有效载荷,可以是普通字符串类型,或者是JSON对象
  • id 可以用来标记消息序号
  • retry 连接意外断开后,浏览器重新发起连接的时间间隔
  • \n\n 用来分割每条消息边界

0

  1. SSE的主要特点包括:
    1. 简单易用:SSE使用基于文本的数据格式,如纯文本、JSON等,使得数据的发送和解析都相对简单。
    2. 单向通信:SSE支持服务器向客户端的单向通信,服务器可以主动推送数据给客户端,而客户端只能接收数据。
    3. 实时性:SSE建立长时间的连接,使得服务器可以实时地将数据推送给客户端,而无需客户端频繁地发起请求。
  1. 应用场景

根据具体的业务需求和场景,选择SSE或WebSocket取决于您的实际需求。如果您只需要服务器向客户端单向推送数据,并且希望保持简单易用和兼容性好,那么SSE是一个不错的选择。如果您需要实现双向通信,或者需要更高级的功能和控制,那么WebSocket可能更适合您的需求。

  1. 进行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个就无法再连接了
  1. 浏览器支持,见下图

0

这些注意点将有助于您正确和高效地使用SseEmitter进行SSE实时数据推送。根据具体的应用需求,您可以根据实际情况进行调整和优化。

请记住,在实际应用中,确保服务器的稳定性、安全性和性能是至关重要的。在处理SSE连接时,您可以进行适当的限流和安全控制,以防止滥用和恶意连接的出现。

  1. 客户端代码
  • 封装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);