Server-sentevents实时获取服务端数据技术详解

2023-02-09 12:09:20
目录
正文WebSocket vs 轮询 vs SSE使用场景使用方式浏览器兼容性简单封装第三方库eventsourceevent-source-polyfill不足之处FAQ总结

正文

实时获取服务端的数据,大家第一时间想到的是轮询和>

SSE 是基于 http 协议的服务器推送技术,数据只能从服务端到客户端。服务端把序列化后的数据发送给客户端, 整个过程持续不断直至连接关闭

WebSocket>

下面是 WebSocket、轮询和 SSE 的功能对比

    SSE 和轮询使用 HTTP 协议,现有的服务器软件都支持。WebSocket 是一个独立协议SSE 属于轻量级的 WebSocket,使用简单;WebSocket 使用相对复杂,轮询使用简单SSE 默认支持断线重连,WebSocket 需要自己实现断线重连SSE 一般只用来传送文本,二进制数据需要编码后传送,WebSocket 默认支持传送二进制数据SSE 支持自定义发送的消息类型WebSocket 支持双向推送消息,SSE 是单向的轮询性能开销大、轮询时间久导致客户端及时更新数据

    使用场景

    基于服务端单向的向客户端推送信息的特性,SSE>

      Sass 平台的消息通知信息流网站实时更新数据

      使用方式

      下面讲解如何在客户端使用>

        创建一个 EventSource 实例,向服务器发起连接
        const evtSource = new EventSource();
        
          自定义事件

          对于自定义事件,服务端和客户端一定要保持事件名一致。服务端通过自定义事件发送数据, 就会触发自定义事件。SSE 默认支持 message 事件,下面以 message 事件为例

          evtSource.addEventListener("message", (event) => {
            let payload;
            try {
              payload = JSON.parse(event.data); // <--- event.data 需要反序列化
              console.log("receiving data...", payload);
            } catch (error) {
              console.error("failed to parse payload from server", error);
            }
          });
          

          自定义事件的回调函数接收 event 对象,event.data 存着服务端发给客户端的数据但是需要反序列化

          可以通过 Chrome Devtool 工具查看 eventsource 通信情况,如图所示

          EventSource

            1 - 自定义事件名,服务端和客户端需要保持一致2 - EventStream Tab,数据都在这里3 - 服务端推送给客户端的数据
              错误处理

              如果连接发生错误,就会触发 error 事件

              evtSource.addEventListener("error", (err) => {
                console.error("EventSource failed:", err);
              });
              
                关闭连接

                SSE 提供 close 方法,用来关闭 SSE 连接

                evtSource.close();
                

                浏览器兼容性

                通过>

                Server-sent events

                除了 IE 浏览器不支持,其它现代浏览器都支持,所以放心大胆在项目中使用 SSE

                简单封装

                在平常的工作中,每次写>

                当我们决定写 SSE 的 SDK 时,首先想到使用面向对象(OOP)进行封装。根据 SSE 的特性,那么库需要实现 subscribeunsubscribe 两个方法。通过确定 SSE 库使用方式,根据使用方式确定 SDK 的实现。我们可以在代码中这样使用,如下所示

                // SSESdk 实例化
                const SSE = new SSESdk(url, options);
                // 订阅来自服务端的消息
                SSE.subscribe("message", (data) => {
                  console.log("receive message from server", data);
                });
                // 取消订阅
                SSE.unsuscribe();
                

                我们要封装的库对外仅仅提供 subscribeunsubscribe 两个 Api,非常方便开发人员使用。 subscribe 用来订阅来自服务端的消息, unsubscribe 用来取消订阅,关闭 SSE 连接,通过使用形式可以看出,使用 ES6 中的类语法。接下来我们先确定 SSE SDK 的大体结构

                class SSEClient {
                  constructor() {}
                  subscribe(type, handler) {}
                  unsunscribe() {}
                }
                

                SSEClient 类中有三个方法需要实现,通过 constructor 接受可配置的参数,比如 SSE 建立连接失败后的重试次数和重试时间。 subscribe 接收一个与后端保持一致的事件名和一个回调函数。unsunscribe 不需要传递任何参数,调用 unsunscribe 方法关闭 SSE 连接

                // SSE-client.js
                class SSEClient {
                  constructor(url) {
                    this.url = url;
                    this.es = null;
                  }
                  subscribe(type, handler) {
                    this.es = new EventSource(url);
                    this.es.addEventListener("open", () => {
                      console.log("server sent event connect created");
                    });
                    this.es.addEventListener(type, (event) => {
                      let payload;
                      try {
                        payload = JSON.parse(event.data);
                        console.log("receiving data...", payload);
                      } catch (error) {
                        console.error("failed to parse payload from server", error);
                      }
                      if (typeof handler === "function") {
                        handler(payload);
                      }
                    });
                    this.es.addEventListener("error", () => {
                      console.error("EventSource connection failed for subscribe.Retry");
                    });
                  }
                  unsunscribe() {
                    if (this.es) {
                      this.es.close();
                    }
                  }
                }
                

                就这样实现了一个简单的 SSE SDK。首先根据 url 参数创建一个 SSEClient 实例,当调用 subscribe 方法时,才会根据传入的 url 建立 SSE 连接,然后监听对应的事件,一旦 连接建立成功,后端向客户端发送数据,就可以从 handler 方法中拿到数据

                这个库仅仅实现了非常基本的功能,代码封装上存在很多问题。比如 es 的事件全部杂糅在 subscribe 方法中、缺少 SSE 连接建立失败的重试等等功能。接下来我们对刚刚实现的 SSEClient SDK 进行优化

                const defaultOptions = {
                  retry: 5,
                  interval: 3 * 1000,
                };
                class SSEClient {
                  constructor(url, options = defaultOptions) {
                    this.url = url;
                    this.es = null;
                    this.options = options;
                    this.retry = options.retry;
                    this.timer = null;
                  }
                  _onOpen() {
                    console.log("server sent event connect created");
                  }
                  _onMessage(handler) {
                    return (event) => {
                      this.retry = options.retry;
                      let payload;
                      try {
                        payload = JSON.parse(event.data);
                        console.log("receiving data...", payload);
                      } catch (error) {
                        console.error("failed to parse payload from server", error);
                      }
                      if (typeof handler === "function") {
                        handler(payload);
                      }
                    };
                  }
                  _onError(type, handler) {
                    return () => {
                      console.error("EventSource connection failed for subscribe.Retry");
                      if (this.es) {
                        this._removeAllEvent(type, handler);
                        this.unsunscribe();
                      }
                      if (this.retry > 0) {
                        this.timer = setTimeout(() => {
                          this.subscribe(type, handler);
                        }, this.options.interval);
                      } else {
                        this.retry--;
                      }
                    };
                  }
                  _removeAllEvent(type, handler) {
                    this.es.removeEventListener("open", this._onOpen);
                    this.es.removeEventListener(type, this._onMessage(handler));
                    this.es.removeEventListener("error", this._onError(type, handler));
                  }
                  subscribe(type, handler) {
                    this.es = new EventSource(url);
                    this.es.addEventListener("open", this._onOpen);
                    this.es.addEventListener(type, this._onMessage(handler));
                    this.es.addEventListener("error", this._onError(type, handler));
                  }
                  unsunscribe() {
                    if (this.es) {
                      this.es.close();
                      this.es = null;
                    }
                    if (this.timer) {
                      clearTimeout(this.timer);
                    }
                  }
                }
                

                我们将 SSEClient 中的三个事件方法分别提取为三个私有方法,_onOpen 方法在 event 触发 open 时调用,向控制台输出链接已经创建。 _onMessage 方法在后端向前端发送数据时触发,负责解析数据,并调用 handler 方法。_onError 方法在 SSE 发生错误时触发, 会在控制台输出错误的提示,根据开发者传入的重试次数,先关闭上一次的 SSE 链接,取消所有的事件监听,关闭定时器, 再开启递归调用 subscribe 方法进行重连, 一旦重连成功,重试次数恢复为设定的重试次数,如果超过重试次数依旧没有连接成功,那么 SSE 会彻底终止。需要开发人员排查具体原因

                一个可以用在项目上的简单 SSE SDK 封装完

                第三方库

                SSE>headers 传递 Authorization token。虽然可以把 token 放在 url 上 解决不能传 token 的问题,但是又会引发 token 安全隐患。所以社区里有使用 xhrfetch 模拟原生 Server-sent events 的功能,解决不能 通过 headers 传递 Authorization token 的问题。主要有两个第三方库,分别是 eventsourceevent-source-polyfill, 下面笔者详细讲述这两个库的使用

                eventsource

                此库是>

                yarn add eventsource
                # Or npm install eventsource
                

                然后从 eventsource 中导出 EventSource 类,然后实例化得到 es 实例

                import EventSource from "eventsource";
                const eventSourceInitDict = { headers: { authorization: "Bearer token" } };
                const es = new EventSource(url, eventSourceInitDict);
                es.addEventListener("message", (event) =&gt; {
                  console.log("receiving data from server:", JSON.parse(event.data));
                });
                

                eventsource 的实现用到了一些 node 标准库。分别是 httpshttp。 笔者将 eventsource 的部分源码列在下面。

                // eventsource.js 源码如下
                const https = require("https");
                const http = require("http");
                

                然而,浏览器环境并不支持 httpshttp 标准库。所以当我们在浏览器环境中使用 eventsource 时,需要做一些额外的工作。下面以 webpack5 为例子讲解解决办法

                  需要在 webpack 配置文件中添加 node-polyfill-webpack-plugin 插件
                  yarn add node-polyfill-webpack-plugin -D
                  

                  然后在 webpack 配置文件使用该插件

                  // 项目中的 webpack 配置文件,比如 webpack.config.js
                  const NodePolyfillPlugin = require("node-polyfill-webpack-plugin");
                  module.exports = {
                    // Other rules...
                    plugins: [new NodePolyfillPlugin()],
                  };
                  
                    或者在 webpackcallback 中对使用的库进行单独的配置
                    module.exports = {
                      // other configuration ...
                      resolve: {
                        fallback: {
                          https: false,
                          http: false,
                        },
                      },
                    };
                    

                    做完上面的步骤后,eventsource 可以在浏览器中正常运行

                    如果不想改动 webpack 的配置,那么可以试试 event-source-polyfill 这个库

                    event-source-polyfill

                    event-source-polyfill>EventSourcePolyfill 替换原生的 EventSource

                    import { EventSourcePolyfill } from "event-source-polyfill";
                    var es = new EventSourcePolyfill(url, {
                      headers: {
                        authorization: "Bearer token",
                      },
                    });
                    es.addEventListener("message", (event) => {
                      console.log("receiving data from server:", JSON.parse(event.data));
                    });
                    

                    不足之处

                    eventsource>event-source-polyfill 只是在一定的程度上解决了 Authorization token 的问题,但它们也存在问题。 这两个库提供的 close 方法只能关闭处于 pending 状态的 SSE 连接,因为 fetch 一旦从 pending 变为 resolvedreject, 其结果无法改变。当频繁的断开 SSE 连接和建立新 SSE 连接时,旧的 SSE 连接实际上并没有关闭,系统里会存在多个 SSE 连接,这样会带来很大的性能开销

                    FAQ

                      SSE>

                      可以将数据放入 url 中,断开当前的 SSE 连接,根据新 url 重新建立 SSE 连接

                      总结

                      本篇文章讲述一种服务端向客户端推送信息的技术、它比>WebSocket 更简单更轻量化,比轮询性能好。 简单介绍 Server-sent events 的技术原理和使用场景,并进行简单的封装,方便日常在项目中使用。推荐使用 eventsourceevent-source-polyfill 第三方库解决不能通过 headers 传递 Authorization token 的问题。

                      参考链接 Server-sent events

                      以上就是Server-sent events实时获取服务端数据技术详解的详细内容,更多关于Server-sent events的资料请关注易采站长站其它相关文章!