简介
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 要做两个事情
- 把 clientId 和 res 放到键值对里。
- 在客户端断开连接以后,删除键值对里面的响应体键值对防止内存泄露
sendClient 即根据 clientId 选出特定的响应体,只对特定的 client 进行 SSE 推送。