引言
在众多 Web 通信技术 中,HTTP 是最基础且广泛应用的技术之一。它基于简明的 请求-响应模式,客户端发起请求,服务端返回响应,适用于绝大多数 Web 应用。HTTP 的无状态性简化了交互模式,但在需要持续状态或实时交互的场景中,其效率受到限制。
Server-Sent Events (SSE) 在 HTTP 上实现了一种单向持续数据流机制,服务端可以通过text/event-stream
内容类型向客户端发送更新。这种模式允许客户端发起单次请求后,服务端多次推送数据,适合实时数据场景。
进一步地,WebSocket 支持全双工通信,在客户端与服务端建立连接后,任意时刻均可发起数据交换,极适合高实时性要求的场景,如在线聊天和实时监控。WebSocket 的连接建立涉及一次特定的握手过程:
- 客户端发送握手请求:通过 HTTP 请求,包括
Upgrade: websocket
和Sec-WebSocket-Key
。 - 服务器响应:使用客户端的
Sec-WebSocket-Key
计算Sec-WebSocket-Accept
,回应状态码101 Switching Protocols
,完成协议切换。 - 数据传输:升级连接后,客户端与服务器通过二进制帧进行直接通信。
本文将由浅入深讲解 WebSocket,介绍如何使用原生 WebSocket API 和 socket.io
库,以提高实时通信应用开发的效率。特别是在 NestJS 框架中的实现方式,为实际应用提供参考。
原生 WebSocket API 的使用
浏览器的原生 WebSocket API 遵循 WebSocket 协议,定义于 Web 标准中,允许开发者在客户端应用中直接使用,无需依赖第三方库。基本使用步骤如下:
-
创建连接:通过
WebSocket
实例与服务器建立连接。const socket = new WebSocket('ws://example.com/socketserver');
注意:原生 WebSocket 使用
ws://
或wss://
协议进行实时通信。 -
事件监听:监听连接生命周期的不同事件,如
onopen
、onmessage
、onerror
和onclose
。我们有两种主要方式:直接赋值和使用事件监听器。直接赋值方式允许我们为每个事件指定一个处理函数,而事件监听器方式则提供了更大的灵活性,允许为同一事件绑定多个监听函数。后者的优势在于其扩展性和动态性,可以轻松添加或移除监听器,适用于复杂的应用场景。下面是两种方式的示例代码:直接赋值方式:
socket.onopen = function(event) { console.log('连接已打开'); }; socket.onmessage = function(event) { console.log('收到消息:', event.data); }; socket.onerror = function(event) { console.error('发生错误'); }; socket.onclose = function(event) { console.log('连接已关闭'); };
事件监听器方式:
socket.addEventListener('open', function (event) { console.log('连接已打开'); }); socket.addEventListener('message', function (event) { console.log('收到消息:', event.data); }); socket.addEventListener('error', function (event) { console.error('发生错误'); }); socket.addEventListener('close', function (event) { console.log('连接已关闭'); });
-
发送消息:使用
send
方法向服务器发送数据。socket.send('Hello, server!');
-
关闭连接:使用
close
方法结束通信。socket.close();
虽然原生 WebSocket API 提供了实现基础实时通信的全部功能,但在开发复杂的实时应用时,我们可能会遇到一些挑战,比如心跳检测、处理网络断连重连、兼容不同浏览器或网络环境、以及实现复杂的消息分发逻辑等。
引入 socket.io
面对复杂实时应用的挑战,socket.io
库凭借其丰富的功能集合和卓越的开发体验,显著优化了构建复杂实时应用的过程。在架构上,socket.io-client
专为前端设计,而其后端对应部分则由 socket.io
库负责
简化连接和事件处理
socket.io
提供了简洁的API,简化了服务器连接和事件处理。使用socket.on
方法监听服务器事件并作出响应:
const io = require('socket.io-client');
const socket = io('http://example.com');
socket.on('connect', () => {
console.log('连接已建立');
});
socket.on('event', data => {
console.log('收到事件:', data);
});
socket.on('disconnect', () => {
console.log('连接已断开');
});
高级功能
socket.io
时, 支持房间和命名空间,允许服务器向所有客户端或特定组的客户端广播消息,极大地简化了复杂通信逻辑的实现。
兼容性和后备机制
可以看到与上文原生 API 使用 ws:// 或 wss:// 不同,socket.io
初始使用 http:// 或 https:// 。
const io = require('socket.io-client');
const socket = io('http://example.com');
这是因为它在建立连接时进行一个兼容性更强的 HTTP 握手过程。这样做使得 socket.io 能够在不同环境下自动选择最佳的传输方式(包括 WebSocket),并且在 WebSocket 不可用时退回到其他后备传输机制,如轮询,从而确保通信的可靠性和灵活性。
NestJS 中的 WebSocket 实现
接着我们就用 NestJS 实现后端代码。在 NestJS 中,WebSocket 通信可通过socket.io
或ws
库实现。NestJS 默认支持socket.io
,使用ws
则需切换适配器:
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter(app));
因此本文将以socket.io
为例进行介绍。在NestJS 中实现 WebSocket 其实很简单。
1. 安装所需包
通过运行以下命令安装必要的包来开始:
npm i --save @nestjs/websockets @nestjs/platform-socket.io
2. 生成资源
使用 NestJS CLI 生成与 WebSocket 相关的资源:
nest g resource aaa
当提示选择传输层时,选择 WebSocket
,并在询问是否生成 CRUD 入口点时选择 Yes
:
这将自动生成 CRUD 操作相关的消息处理逻辑,生成的代码如下:
如上图所示,@WebSocketGateway
装饰器用于声明一个 WebSocket 网关,它可以接受端口号和其他配置选项。
@WebSocketGateway(80, { /* options */ })
消息的订阅和处理可以通过@SubscribeMessage
和@MessageBody
装饰器实现。同时,使用@ConnectedSocket
可以获取到客户端实例:
@SubscribeMessage('events')
handleEvent(@MessageBody() data: string, @ConnectedSocket() client: Socket): string {
client.emit('onMessage', data);
return 'Event handled';
}
具体的CRUD操作将在AaaService
中实现。例如,这是在 AaaService
中实现的 create
方法。
export class AaaService {
create(createAaaDto: CreateAaaDto) {
return 'This action adds a new aaa';
}
}
3. 前端集成
为了在前端与 WebSocket 服务器进行交互,我们可以利用以下 HTML 和 JavaScript 代码。 首先,通过 CDN 引入 socket.io-client
库。
接着,连接上 app.listen 的那个端口(默认 http://localhost:3000 )。 通过调用 emit
方法发布消息,服务器上对应的 handleEvent()
方法将被触发。同时,为了接收服务器响应的消息,设置一个监听器来处理从服务器发回的数据:
<html>
<head>
<script src="https://cdn.socket.io/4.3.2/socket.io.min.js"></script>
<script>
const socket = io('http://localhost:3000');
socket.emit('createAaa', { name: 'Cici' }, response => {
console.log('createAaa', response);
});
</script>
</head>
<body></body>
</html>
异步响应处理
在 NestJS 中,异步响应可以通过 async
方法或利用 RxJS 中的 Observable
来实现。
RxJS 是一个专注于异步事件处理的库,它通过一系列的操作符(operators)来简化异步逻辑的处理。数据通过被称为 observable 的数据源开始流动,并经过一系列操作符(如
map
和filter
)处理后,最终到达接收者。
示例:使用 RxJS Observable
import { SubscribeMessage, MessageBody } from '@nestjs/websockets';
import { Observable, from } from 'rxjs';
import { map } from 'rxjs/operators';
@SubscribeMessage('events')
onEvent(@MessageBody() data: unknown): Observable<WsResponse<number>> {
const event = 'events';
const response = [1, 2, 3];
return from(response).pipe(
map(data => ({ event, data })),
);
}
此示例中,onEvent
方法创建了一个从数组 [1, 2, 3]
发出数据的 Observable
,每个元素经过 map
操作后,作为异步响应返回。
示例:自定义 Observable
来输出异步数据流
以下是使用 setTimeout
来模拟异步数据流的示例,展示了如何在不同时间点向客户端发送不同的数据:
import { SubscribeMessage } from '@nestjs/websockets';
import { Observable } from 'rxjs';
@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.complete();
}, 5000);
});
}
生命周期管理
NestJS WebSocket 网关提供了生命周期钩子,例如 OnGatewayInit
, OnGatewayConnection
, 和 OnGatewayDisconnect
,允许开发者在网关初始化、客户端连接及断开时执行自定义逻辑。
import { WebSocketGateway, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
import { Server } from 'socket.io';
@WebSocketGateway()
export class AaaGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
afterInit(server: Server) {
// 初始化逻辑
}
handleConnection(client: Server, ...args: any[]) {
// 连接逻辑
}
handleDisconnect(client: Server) {
// 断开连接逻辑
}
}
这些生命周期钩子提供了对 WebSocket 网关行为的细粒度控制,使得管理连接的生命周期变得直接而清晰。
参考资料
- WebSocket – Web API 接口参考 | MDN (mozilla.org)
- NestJS官方文档 – Websockets
- 掘金小册 – Nest 通关秘籍
- CSDN博客 – WebSocket 学习
- 掘金文章 – 使用 Nest 集成 websocket 能力
原文链接:https://juejin.cn/post/7340479361219870757 作者:ayyo_cici