Nest 使用SSE 服务端推送实现"协同"

缘起

一些涉及到 "实时更新"的场景,使用 "websocket" | "SSE",老生常谈的问题了,这篇文章会用一个通俗、简单的例子,来解释在 Nodejs 框架 "Nest" 中使用 SSE 技术完成状态同步。

Demo 完整代码

Why SSE ?

  • 基于HTTP协议,不需要额外的协议
  • 单向通信,实时性:SSE 提供了实时的数据推送机制,服务器可以在任何时候向客户端发送数据。当有新数据可用时,服务器会立即将其推送给客户端,而不需要客户端主动请求。
  • 自动重连、客户端连接、断开
  • 事件驱动:SSE 使用基于事件的模型。服务器可以定义不同类型的事件,并将数据作为事件发送给客户端。客户端可以通过监听特定类型的事件来处理接收到的数据。

协议

yml 复制代码
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

协议很简单本质上还是 http 请求

  • 网图侵删 👇

HTTP

SSE

场景

  • 设想一个场景,类似语雀、金山文档等这些在线编辑的文档软件,他们是如何实现多人状态下编辑内容同步的呢?例如金山文档 Excel 实时的展示 每个人正在编辑的单元格 ?

我们抽象一下例如:有2个接口:

  1. postLocation,接口负责推送当前编辑的位置
  2. getLocation, 获取最新的位置

这种情况下,可以实现的方式很多 比较常见的:"轮询"、"webSocket"、"SSE"; 我们可以理解 "切换单元格"是一件很频繁的事情,只有在状态更新的时候再去触发推送,避免不必要的带宽浪费; 且 getLocation 不需要 "双向通信" 只需要实时的获取数据就可以了,相信你们心里已经有答案了 那就是 SSE.

正文

接下来我们简单的通过假代码实现一个 SSE,本文建立在有一定的 前端技术基础和 nodejs nest 技术基础上。

server

我们先来实现一个简单的 SSE

ts 复制代码
import { Controller, Post, Body, Sse } from '@nestjs/common';
import { Observable, interval, map } from 'rxjs';

@Controller('location')
export class LocationtController {
  @Sse('sse')
  sse(): Observable<MessageEvent> {
    return interval(1000).pipe(
      map((_) => ({ data: { hello: 'world' } } as MessageEvent)),
    );
  }

  @Post()
  addList(@Body() body): any {
    console.log('当前位置信息');
    console.log(body);
    return 'ok';
  }
}

Client

ini 复制代码
const ev = new EventSource('xxx/location/sse');

ev.onmessage = (e) => {
  console.log(e.data)
};

这个时候你就可以看到 控制台不断的输出 hello word 这个键值对,

这个示例完成后我们可以在 浏览器 network 看到 每秒打印 "hello: 'world'", 你可能注意到了这里的 Observable<MessageEvent>, 这个稍后解释。

  • 我们把代码改写一下创建一个一个 mapList 来储存所有人的位置
ts 复制代码
import { Controller, Post, Body, Sse } from '@nestjs/common';
import { Observable, interval, map } from 'rxjs';

@Controller('location')
export class LocationtController {
  private mapList = [];

  @Sse('sse')
  sse() {
    return this.mapList;
  }

  @Post()
  addList(@Body() body): any {
    this.mapList = [body, ...this.mapList];
    return 'ok';
  }
}

打开客户端,链接 SSE 一开始一开始我们接受到的是[], 请求下 post请求 传递一个 { "x": 2, "y": 2 } 我们会发现数据是这样子的 👇🏻 :

👇🏻

数据 从 [] => [{"x":2,"y":2}]

但是主要为什么 SSE 请求那么多,而且每次都是 ID: 1

这时候我们来看 Observable<MessageEvent> 的解释

ts 复制代码
/**
   * @constructor
   * @param {Function} subscribe the function that is called when the Observable is
   * initially subscribed to. This function is given a Subscriber, to which new values
   * can be `next`ed, or an `error` method can be called to raise an error, or
   * `complete` can be called to notify of a successful completion.
   */
  constructor(subscribe?: (this: Observable<T>, subscriber: Subscriber<T>) => TeardownLogic) {
    if (subscribe) {
      this._subscribe = subscribe;
    }
  }
  
  • MessageEvent
ts 复制代码
interface MessageEvent<T = any> {
    /**
     * @deprecated
     *
     * [MDN Reference](https://developer.mozilla.org/docs/Web/API/MessageEvent/initMessageEvent)
     */
    initMessageEvent(type: string, bubbles?: boolean, cancelable?: boolean, data?: any, origin?: string, lastEventId?: string, source?: MessageEventSource | null, ports?: Iterable<MessagePort>): void;
}

再结合文档 👇🏻

arduino 复制代码
**WARNING**

Server-Sent Events routes must return an `Observable` stream.

大概意思就是: SSE 服务器端事件发送路径必须返回Observable流,可是我们明明可以直接返回静态数据呀!

接下来看 Observable 说:Observable 赋值一个订阅者 接收 subscribe 为参数,通过 next 或者 error 调用,complete 完成。

大致理解下来就是在使用 SSE 的过程中,是通过 stream 也就是浏来通信的,ID是用于客户端的状态跟踪,通过为每个事件指定唯一的标识符。

这下哦我们可以理解了,因为我们每次返回静态数据所以 每次都会创建一个新的 stream 且 ID 也默认每次都是第一个了

  • 接下来我们要把代码改写一下解决:
  1. SSE 多个的问题
  2. SSE 在 send 数据的时候 只有在 addList 被调用的时候,而不是一直朝 客户端推送
  • 解决思路👇🏻

  • 使用发布订阅来控制 SSE 的推送时机(当然实现的方式有很多种,这里用 nodeevents)

  • SSE 发送数据处在一个流中

  • 完整代码:

ts 复制代码
import { Controller, Post, Body, Sse } from '@nestjs/common';
import { Observable, interval, map } from 'rxjs';
import * as EventEmitter from 'events';

const myEmitter = new EventEmitter();

@Controller('location')
export class LocationtController {
  private mapList = [];

  @Sse('sse')
  sse(): Observable<MessageEvent> {
    return new Observable<any>((observer) => {
      myEmitter.on('send', (data: any) => {
        this.mapList = [data, ...this.mapList];
        observer.next({ data: this.mapList });
      });
    });
  }

  @Post()
  addList(@Body() body): any {
    myEmitter.emit('send', body);
    return 'ok';
  }
}

以上代码,只不过是通过一个简单的例子来 更好的理解 SSE,实际上我们的 语雀、金山文档不可能把数据存在 class 里面,也不会那么简单的实现。

Demo 完整代码

相关推荐
FuckTheWindows2 小时前
深入理解WebSocket接口:如何使用C++实现行情接口
websocket·行情接口·level2行情·实时行情
星辰tsy3 小时前
webSocket
websocket
MardaWang4 小时前
HarmonyOS开发,遇到 Object.assign(this, source)报错怎么解决?
typescript·harmonyos
m0_748254096 小时前
Nginx WebSocket 长连接及数据容量配置
运维·websocket·nginx
数据掘金9 小时前
实时股票行情接口与WebSocket行情接口的应用
websocket·行情接口·实时行情接口
是纯一呀1 天前
WebSocket(WS)协议系列(一)基本概念
网络·websocket·网络协议
CodeMonkey陈1 天前
springboot使用webSocket
spring boot·python·websocket
IT、木易1 天前
TypeScript跟js,es6这些的区别
javascript·typescript·es6
孟陬1 天前
持续改善 React 代码的 SOLID 原则(附带 hooks 详细案例)适用于高级前端
react.js·设计模式·typescript
李二。1 天前
TypeScript学习:初学
typescript