我们最常用的网络协议是 HTTP,它是一问一答的模式,客户端发送请求,服务端返回响应。
有时候也会用 Server Sent Event,它是基于 HTTP 的,客户端发送请求,服务端返回 text/event-stream 类型的响应,可以多次返回数据。
但是 HTTP 不能服务端向客户端推送数据,SSE 适合一次请求之后服务端多次推送数据的场景。
类似聊天室这种,需要实时的双向通信的场景,还是得用 WebSocket。
在 Nest 里实现 WebSocket 的服务还是很简单的。
我们创建个项目:
nest new nest-websocket
进入项目,安装用到的包:
npm i --save @nestjs/websockets @nestjs/platform-socket.io
然后创建个 websocket 模块:
nest g resource aaa
生成的代码很容易看懂:
@WebSocketGateWay 声明这是一个处理 weboscket 的类。
默认的端口和 http 服务 app.listen 的那个端口一样。
然后 @SubscribeMessage 是指定处理的消息。
通过 @MessageBody 取出传过来的消息内容。
分别声明了 find、create、update、remove 这些 CRUD 的消息类型。
具体的实现在 AaaService 里:
然后我们加一下客户端代码,跑起来试试。
添加 pages/index.html
<html>
<head>
<script src="https://cdn.socket.io/4.3.2/socket.io.min.js" integrity="sha384-KAZ4DtjNhLChOB/hxXuKqhMLYvx3b5MlT55xPEiNmREKRzeEm+RVPlTnAn0ajQNs" crossorigin="anonymous"></script>
<script>
const socket = io('http://localhost:3000');
socket.on('connect', function() {
console.log('Connected');
socket.emit('findAllAaa', response =>
console.log('findAllAaa', response),
);
socket.emit('findOneAaa', 1, response =>
console.log('findOneAaa', response),
);
socket.emit('createAaa', {name: 'guang'},response =>
console.log('createAaa', response),
);
socket.emit('updateAaa',{id: 2, name: 'dong'},response =>
console.log('updateAaa', response),
);
socket.emit('removeAaa', 2,response =>
console.log('removeAaa', response),
);
});
socket.on('disconnect', function() {
console.log('Disconnected');
});
</script>
</head>
<body></body>
</html>
这段代码也比较容易看懂,就是用 socket.io 来连接 ws 服务端。
connect 之后,分别发送 find、remove、update 等消息。
然后在 main.ts 里支持下这个 pages 静态目录的访问:
import { NestApplication, NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.useStaticAssets('pages');
await app.listen(3000);
}
bootstrap();
把服务跑起来:
npm run start:dev
浏览器访问下 http://localhost:3000
可以看到,CRUD 方法都有了正确的响应。
在 Nest 里写 WebSocket 服务就这么简单。
那如果响应接受和返回消息不想用同样的名字呢?
这样:
分别指定 event 和 data。
这时候原来的代码就收不到 findAll 返回的消息了:
因为返回的消息是 guang,可以加一下这个事件的监听:
socket.on('guang', function(data) {
console.log('guang', data);
});
这样就收到消息了:
那如果我不是马上发送消息,而是过几秒再发呢?
这就要返回 rxjs 的 Observer 了:
@SubscribeMessage('findAllAaa')
findAll() {
return new Observable((observer) => {
observer.next({ event: 'guang', data: { msg: 'aaa'} });
setTimeout(() => {
observer.next({ event: 'guang', data: { msg: 'bbb'} });
}, 2000);
setTimeout(() => {
observer.next({ event: 'guang', data: { msg: 'ccc'} });
}, 5000);
});
}
测试下:
可以看到,2s、5s 的时候,收到了服务端传过来的消息。
有这些就足够用了,websocket 是用来双向实时通信的。
当然,如果你想用具体平台的 api,也可以注入实例。
安装 socket.io(Nest 默认使用 socket.io 包实现 WebSocket 功能)
npm install socket.io
@SubscribeMessage('findOneAaa')
findOne(@MessageBody() id: number, @ConnectedSocket() server: Server) {
server.emit('guang', 666);
return this.aaaService.findOne(id);
}
这样也可以,但是和具体的平台耦合了,不建议这样写。
除了 @ConnectedSocket 装饰器注入实例,也可以用 @WebSocketServer 注入实例:
@WebSocketServer()
server: Server;
@SubscribeMessage('createAaa')
create(@MessageBody() createAaaDto: CreateAaaDto) {
this.server.emit('guang', 777);
return this.aaaService.create(createAaaDto);
}
同样,也是不建议用的。
此外,服务端也有 connected、disconnected 等生命周期函数:
@WebSocketGateway()
export class AaaGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect{
handleDisconnect(client: Server) {
}
handleConnection(client: Server, ...args: any[]) {
}
afterInit(server: Server) {
}
}
分别实现 OnGatewayInit、OnGatewayConnection、OnGatewayDisconnect 接口。
在生命周期函数里可以拿到实例对象。
案例代码上传了小册仓库。
总结
这节我们学习了 Nest 实现 WebSocket 服务。
需要用到 @nestjs/websockets 和 @nestjs/platform-socket.io 包。
涉及到这些装饰器:
@WebSocketGateWay:声明这是一个处理 weboscket 的类。
@SubscribeMessage:声明处理的消息。
@MessageBody:取出传过来的消息内容。
@WebSocketServer:取出 Socket 实例对象
@ConnectedSocket:取出 Socket 实例对象注入方法
客户端也是使用 socket.io 来连接。
如果想异步返回消息,就通过 rxjs 的 Observer 来异步多次返回。
整体来说,Nest 里用 WebSocket 来做实时通信还是比较简单的。