【后端】nestjs 的 SSE 案例

640 阅读2分钟

简介

SSE 是 server side event 的缩写,它给予了服务端主动向客户端推送的能力。和 web socket 不同的是,你并不需要新的协议和新的服务来支持。它完全附属于你的 http 服务器,比 web socket 更加轻量,如果你没有双向通信的需求,大可直接使用 SSE。

案例

首先,客户端要通过 EventSource 对象订阅 SSE 的路由,所以,服务端 controller 这边要有一个 subscribe 路由函数。

import { Controller, Get, Req, Res, Sse } from '@nestjs/common';

// controller function
  @Sse('subscribe')
  async subscribe(@Req() req, @Res() res: Response) {
    const address = req.socket.remoteAddress;
    const port = req.socket.remotePort;
    return this.nestSseService.subscribe();
  }

这里的重点是,你需要使用 Sse 装饰器。

subscribe 的具体逻辑,需要放到 subscribe 函数中:

subscribe() {
    return fromEvent(this.eventEmitter, 'push_data').pipe(
      map((data) => {
        return data as MessageEvent;
      }),
    );
  }

注意,带 Sse 装饰器的 controller 的返回对象必须是一个 observable 对象,当检测到有 push_data 对象的时候,把消息发送给 client。

我们还需要有触发发送消息的 controller

import { EventEmitter2 } from '@nestjs/event-emitter';

 // controller
  @Get('sse')
  send() {
    this.nestSseService.emit({ data: { num: 123 } });
  }
  
  // service
  constructor(private readonly eventEmitter: EventEmitter2) {}
  
  emit(data: EmitSSEData) {
    this.eventEmitter.emit('push_data', data);
  }

this.eventEmitter.emit 方法触发一个事件,并发送与这个事件相对应的数据,fromEvent 方法感知到了这个事件,并把这个事件通过 SSE 的方式推送给了前端。

当然,这种实现方式所有的 client 都会得到 SSE 的推送消息,但有的时候,我们只需要把消息给特定的 client,这个时候,我们应该怎么办。

这个时候我们就需要在服务端记录一个 clientId 和 连接映射的 mapping。我们想对哪个 client 发送消息,就找对应那个 clientId 的连接。

于是,我们修改代码

  @Sse('subscribe')
  async subscribe(@Req() req, @Res() res: Response) {
    const address = req.socket.remoteAddress;
    const port = req.socket.remotePort;
    return this.nestSseService.subscribe(`${address}:${port}`, res, req);
  }
  
  @Get('send_to_client')
  sendToClient(@Query() query) {
    const { client_id } = query;
    this.nestSseService.sendToClient(client_id, { data: 'test' });
  }

  
// service
  subscribe(clientId: string, res: Response, req: any) {
    this.addClient(clientId, res, req);
    return fromEvent(this.eventEmitter, 'push_data').pipe(
      map((data) => {
        return data as MessageEvent;
      }),
    );
  }
  
  addClient(clientId: string, res: Response, req: any) {
    if (this.clients.get(clientId)) {
      return;
    }
    this.clients.set(clientId, res);

    req.connection.on(
      'close',
      () => {
        if (this.clients.get(clientId)) {
          this.clients.delete(clientId);
        }
      },
      false,
    );
  }
  
  sendToClient(clientId: string, data: any) {
    const client = this.clients.get(clientId);
    if (client) {
      client.write(`data: ${JSON.stringify(data)}\n\n`);
    }
  }

addClient 要做两个事情

  1. 把 clientId 和 res 放到键值对里。
  2. 在客户端断开连接以后,删除键值对里面的响应体键值对防止内存泄露

sendClient 即根据 clientId 选出特定的响应体,只对特定的 client 进行 SSE 推送。