美文网首页
使用 SSE 代替轮询

使用 SSE 代替轮询

作者: 咆哮小狮子 | 来源:发表于2024-05-29 16:56 被阅读0次

    声明

    本人也在不断的学习和积累中,文章中有不足和误导的地方还请见谅,可以给我留言指正。希望和大家共同进步,共建和谐学习环境。

    背景

    公司开发Electron应用时,需要服务器向客户端主动推送消息,经过相关的搜索,发现了SSE可以取代WebSockets。那让我们就来学习一下SSE。

    🤔️ SSE 是啥

    SSE 全称是 Server-sent events(服务器发送事件),是服务器向客户端推送数据的一种方式。
    SSE 的本质是通过 HTTP 请求,不断发送 流信息(streaming),使得服务器向客户端推送信息。类似于视频流。
    他不是一次性的数据包,而是会一直等着服务端的推送。因此客户端不会关闭连接,等着服务端的不断推送。这样就实现了服务端向客户端的推送。

    概述

    Server-Sent Events 服务器推送事件,简称 SSE,是一种服务端实时主动向浏览器推送消息的技术。
    SSE 是 HTML5 中一个与通信相关的 API,主要由两部分组成:服务端与浏览器端的通信协议(HTTP 协议)及浏览器端可供 JavaScript 使用的 EventSource 对象。
    从“服务端主动向浏览器实时推送消息”这一点来看,该 API 与 WebSockets API 有一些相似之处。但是,该 API 与 WebSockers API 的不同之处在于:

    Server-Sent Events API WebSockets API
    基于 HTTP 协议 基于 TCP 协议
    单工,只能服务端单向发送消息 全双工,可以同时发送和接收消息
    轻量级,使用简单 相对复杂
    内置断线重连和消息追踪的功能 不在协议范围内,需手动实现
    文本或使用 Base64 编码和 gzip 压缩的二进制消息 类型广泛
    支持自定义事件类型 不支持自定义事件类型
    连接数 HTTP/1.1 6 个,HTTP/2 可协商(默认 100) 连接数无限制

    服务端实现

    1 协议

    SSE 本质是浏览器发起 http 请求,服务器在收到请求后,返回状态与数据,并附带以下 headers:

    Content-Type: text/event-stream
    Cache-Control: no-cache
    Connection: keep-alive
    
    • SSE API规定推送事件流的 MIME 类型为 text/event-stream
    • 必须指定浏览器不缓存服务端发送的数据,以确保浏览器可以实时显示服务端发送的数据。
    • SSE 是一个一直保持开启的 TCP 连接,所以 Connection 为 keep-alive。

    2 消息格式

    EventStream(事件流)为 UTF-8 格式编码的文本或使用 Base64 编码和 gzip 压缩的二进制消息。

    每条消息由一行或多行字段(eventidretrydata)组成,每个字段组成形式为:字段名:字段值。字段以行为单位,每行一个(即以 \n 结尾)。以冒号开头的行为注释行,会被浏览器忽略。

    每次推送,可由多个消息组成,每个消息之间以空行分隔(即最后一个字段以\n\n结尾)。

    📢 注意:

    • 除上述四个字段外,其他所有字段都会被忽略。
    • 如果一行字段中不包含冒号,则整行文本将被视为字段名,字段值为空。
    • 注释行可以用来防止链接超时,服务端可以定期向浏览器发送一条消息注释行,以保持连接不断。

    2.1 event

    事件类型。如果指定了该字段,则在浏览器收到该条消息时,会在当前 EventSource 对象(见 4)上触发一个事件,事件类型就是该字段的字段值。可以使用 addEventListener 方法在当前 EventSource 对象上监听任意类型的命名事件。

    如果该条消息没有 event 字段,则会触发 EventSource 对象 onmessage 属性上的事件处理函数。

    2.2 id

    事件ID。事件的唯一标识符,浏览器会跟踪事件ID,如果发生断连,浏览器会把收到的最后一个事件ID放到 HTTP Header Last-Event-Id 中进行重连,作为一种简单的同步机制。

    例如可以在服务端将每次发送的事件ID值自动加 1,当浏览器接收到该事件ID后,下次与服务端建立连接后再请求的 Header 中将同时提交该事件ID,服务端检查该事件ID是否为上次发送的事件ID,如果与上次发送的事件ID不一致则说明浏览器存在与服务器连接失败的情况,本次需要同时发送前几次浏览器未接收到的数据。

    2.3 retry

    重连时间。整数值,单位 ms,如果与服务器的连接丢失,浏览器将等待指定时间,然后尝试重新连接。如果该字段不是整数值,会被忽略。

    当服务端没有指定浏览器的重连时间时,由浏览器自行决定每隔多久与服务端建立一次连接(一般为 30s)。

    2.4 data

    消息数据。数据内容只能以一个字符串的文本形式进行发送,如果需要发送一个对象时,需要将该对象以一个 JSON 格式的字符串的形式进行发送。在浏览器接收到该字符串后,再把它还原为一个 JSON 对象。

    示例

    const express = require('express');
    const cors = require('cors'); // 允许跨域
    const app = express();
    const PORT = 3000;
    
    app.use(cors());
    app.use(express.static('public'));
    
    app.get('/events', function (req, res) {
        res.setHeader('Content-Type', 'text/event-stream');
        res.setHeader('Cache-Control', 'no-cache');
        res.setHeader('Connection', 'keep-alive');
        // res.setHeader('Access-Control-Allow-Origin', '*'); 也可以设置跨域
    
        let startTime = Date.now();
    
        const sendEvent = () => {
            // 检查是否已经发送了10秒
            if (Date.now() - startTime >= 10000) {
                res.write('event: close\ndata: {}\n\n'); // 发送一个特殊事件通知客户端关闭
                res.end(); // 关闭连接
                return;
            }
    
            // data:事件的数据。如果数据跨越多行,每行都应该以data:开始。
            // id:事件的唯一标识符。客户端可以使用这个ID来恢复事件流。
            // event:自定义事件类型。客户端可以根据不同的事件类型来执行不同的操作。
            // retry:建议的重新连接时间(毫秒)。如果连接中断,客户端将等待这段时间后尝试重新连接。
            const data = { message: 'Hello World', timestamp: new Date() };
            res.write(`data: ${JSON.stringify(data)}\n\n`);
    
            // 每隔2秒发送一次消息
            setTimeout(sendEvent, 2000);
        };
    
        sendEvent();
    });
    
    app.listen(PORT, () => {
        console.log(`Server running on http://localhost:${PORT}`);
    });
    

    浏览器 API

    在浏览器端,可以使用 JavaScript 的 EventSource API 创建 EventSource 对象监听服务器发送的事件。一旦建立连接,服务器就可以使用 HTTP 响应的 'text/event-stream' 内容类型发送事件消息,浏览器则可以通过监听 EventSource 对象的 onmessageonopenonerror 事件来处理这些消息。

    1 建立连接

    EventSource 接受两个参数:URLoptions
    URL 为 http 事件来源,一旦 EventSource 对象被创建后,浏览器立即开始对该 URL 地址发送过来的事件进行监听。
    options 是一个可选的对象,包含 withCredentials 属性,表示是否发送凭证(cookie、HTTP认证信息等)到服务端,默认为 false。

    const eventSource = new EventSource('http_api_url', { withCredentials: true })
    

    与 XMLHttpRequest 对象类型,EventSource 对象有一个 readyState 属性值,具体含义如下表:

    readyState 含义
    0 浏览器与服务端尚未建立连接或连接已被关闭
    1 浏览器与服务端已成功连接,浏览器正在处理接收到的事件及数据
    2 浏览器与服务端建立连接失败,客户端不再继续建立与服务端之间的连接

    可以使用 EventSource 对象的 close 方法关闭与服务端之间的连接,使浏览器不再建立与服务端之间的连接。

    // 初始化 eventSource 等省略
    // 关闭连接
    eventSource.close()
    

    2 监听事件

    EventSource 对象本身继承自 EventTarget 接口,因此可以使用 addEventListener() 方法来监听事件。EventSource 对象触发的事件主要包括以下三种:

    • open 事件:当成功连接到服务端时触发。

    • message 事件:当接收到服务器发送的消息时触发。该事件对象的 data 属性包含了服务器发送的消息内容。

    • error 事件:当发生错误时触发。该事件对象的 event 属性包含了错误信息。

    // 初始化 eventSource 等省略
    eventSource.addEventListener('open', function(event) {
      console.log('Connection opened')
    })
    
    eventSource.addEventListener('message', function(event) {
      console.log('Received message: ' + event.data);
    })
    
    // 监听自定义事件
    eventSource.addEventListener('xxx', function(event) {
      console.log('Received message: ' + event.data);
    })
    
    eventSource.addEventListener('error', function(event) {
      console.log('Error occurred: ' + event.event);
    })
    

    当然,也可以采用属性监听(onopenonmessageonerror)的形式。

    // 初始化 eventSource 等省略
    eventSource.onopen = function(event) {
      console.log('Connection opened')
    }
    
    eventSource.onmessage = function(event) {
      console.log('Received message: ' + event.data);
    }
    
    eventSource.onerror = function(event) {
      console.log('Error occurred: ' + event.event);
    })
    
    

    📢注意:

    EventSource 对象的属性监听只能监听预定义的事件类型(openmessageerror)。不能用于监听自定义事件类型。如果要实现自定义事件类型的监听,可以使用 addEventListener() 方法。

    示例

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <title>SSE示例</title>
    </head>
    
    <body>
        <h1>这是一个测试</h1>
        <div id="messages"></div>
    
    <script>
    
        // 同网址
        //const evtSource = new EventSource(url);
        // 跨域带上 cookie。 打开withCredentials属性,表示是否一起发送 Cookie。
        //const evtSource = new EventSource(url, { withCredentials: true });
    
        const evtSource = new EventSource('http://locahost:3000/events');
        const messages = document.getElementById('messages');
    
        evtSource.onmessage = function(event) {
            const newElement = document.createElement("p");
            const eventObject = JSON.parse(event.data);
                newElement.textContent = "消息: " + eventObject.message + " 时间:" + eventObject.timestamp;
                messages.appendChild(newElement);
        };
    </script>
    </body>
    </html>
    

    兼容性

    发展至今,SSE 已具有广泛的的浏览器兼容性,几乎除 IE 之外的浏览器均已支持。

    image.png

    对于不支持 EventSource 的浏览器,可以使用 polyfill 实现。判断浏览器是否支持 EventSource:

    if(typeof(EventSource) !== “undefined”) {
        // 支持
    } else {
        // 不支持,使用 polyfill
    }
    

    参考文档

    https://juejin.cn/post/7221125237500330039
    https://juejin.cn/post/7018726581157756965
    https://juejin.cn/post/7355666189475954725

    相关文章

      网友评论

          本文标题:使用 SSE 代替轮询

          本文链接:https://www.haomeiwen.com/subject/nkckqjtx.html