覆盖文档:Gateways, Exception Filters (WS), Pipes (WS), Guards (WS), Interceptors (WS), Adapters 前置知识:第9课(注入作用域与生命周期) 源码重点:
packages/websockets/核心模块,@SubscribeMessage注册机制,Gateway 实例化流程
一、WebSocket 网关
[基础] 本节面向首次使用 NestJS WebSocket 的读者。
1.1 什么是 Gateway
在 NestJS 中,Gateway 是处理 WebSocket 连接的核心组件,类似于 HTTP 中的 Controller。Gateway 通过 @WebSocketGateway() 装饰器标记,由框架自动扫描和注册。
npm i @nestjs/websockets @nestjs/platform-socket.io
npm i -D @types/socket.io
1.2 创建第一个 Gateway
import {
WebSocketGateway,
SubscribeMessage,
MessageBody,
ConnectedSocket,
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
@WebSocketGateway()
export class EventsGateway {
@WebSocketServer()
server: Server;
@SubscribeMessage('events')
handleEvent(
@MessageBody() data: { name: string },
@ConnectedSocket() client: Socket,
): { event: string; data: any } {
return { event: 'events', data: { echo: data.name } };
}
}
关键装饰器:
| 装饰器 | 用途 | 等价 HTTP |
|---|---|---|
@WebSocketGateway() | 标记 Gateway 类 | @Controller() |
@SubscribeMessage('event') | 订阅消息事件 | @Get() / @Post() |
@MessageBody() | 提取消息体 | @Body() |
@ConnectedSocket() | 获取客户端 Socket | @Req() |
@WebSocketServer() | 获取 Server 实例 | 无 |
1.3 三种响应模式
Gateway 方法支持三种响应模式:
@WebSocketGateway()
export class EventsGateway {
@WebSocketServer()
server: Server;
// 模式一:返回值(自动发回客户端)
@SubscribeMessage('ping')
handlePing(@MessageBody() data: any) {
return { event: 'pong', data };
}
// 模式二:手动发送(可发给任意客户端)
@SubscribeMessage('broadcast')
handleBroadcast(
@MessageBody() data: any,
@ConnectedSocket() client: Socket,
) {
// 发给除发送者外的所有人
client.broadcast.emit('notification', data);
// 或发给所有人(包括发送者)
this.server.emit('notification', data);
}
// 模式三:Observable(异步多值响应)
@SubscribeMessage('stream')
handleStream(@MessageBody() data: any): Observable<WsResponse<number>> {
return from([1, 2, 3]).pipe(
map(item => ({ event: 'stream', data: item })),
);
}
}
响应流程图:
客户端 Gateway 其他客户端
│ │ │
│── emit('ping', data) ──→│ │
│ │── return { event, data } │
│←── on('pong', data) ────│ │
│ │ │
│── emit('broadcast') ───→│ │
│ │── client.broadcast.emit ─→│
│ │ │←─ on('notification')
│ │ │
│── emit('stream') ──────→│ │
│←── on('stream', 1) ─────│ Observable 逐个发送 │
│←── on('stream', 2) ─────│ │
│←── on('stream', 3) ─────│ │
1.4 生命周期钩子
Gateway 提供三个生命周期钩子接口:
import {
OnGatewayInit,
OnGatewayConnection,
OnGatewayDisconnect,
} from '@nestjs/websockets';
@WebSocketGateway()
export class EventsGateway
implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect
{
private readonly logger = new Logger(EventsGateway.name);
// 服务器初始化完成后调用
afterInit(server: Server) {
this.logger.log('WebSocket Gateway 已初始化');
}
// 客户端连接时调用
handleConnection(client: Socket, ...args: any[]) {
this.logger.log(`客户端连接: ${client.id}`);
}
// 客户端断开时调用
handleDisconnect(client: Socket) {
this.logger.log(`客户端断开: ${client.id}`);
}
}
生命周期钩子接口:
| 接口 | 方法 | 触发时机 |
|---|---|---|
OnGatewayInit | afterInit(server) | WebSocket 服务器初始化完成 |
OnGatewayConnection | handleConnection(client, ...args) | 新客户端连接 |
OnGatewayDisconnect | handleDisconnect(client) | 客户端断开连接 |
1.5 注册 Gateway
Gateway 必须在模块的 providers 中注册:
@Module({
providers: [EventsGateway, EventsService],
})
export class EventsModule {}
注意:Gateway 注册在
providers中而非controllers中。Gateway 在概念上是 Provider,但由框架的SocketModule特殊处理,自动识别带有@WebSocketGateway()元数据的类。
1.6 默认端口行为
// 默认:共享 HTTP 服务器端口(如 3000)
@WebSocketGateway()
// 指定独立端口
@WebSocketGateway(8080)
// 指定端口 + 选项
@WebSocketGateway(8080, {
cors: { origin: '*' },
transports: ['websocket'],
})
默认情况下,Gateway 共享 HTTP 服务器的端口,WebSocket 握手通过 HTTP Upgrade 机制完成。
二、Socket.IO 与 ws 平台
[中阶] 本节面向需要选择 WebSocket 实现并进行高级配置的读者。
2.1 Socket.IO(默认平台)
NestJS 默认使用 Socket.IO 作为 WebSocket 平台,它在原生 WebSocket 之上提供了丰富的高级特性:
┌──────────────────────────────────────────────┐
│ Socket.IO │
│ │
│ ┌───────────────┐ ┌────────────────────┐ │
│ │ Rooms │ │ Namespaces │ │
│ │ (房间分组) │ │ (命名空间隔离) │ │
│ └───────────────┘ └────────────────────┘ │
│ ┌───────────────┐ ┌────────────────────┐ │
│ │ Auto-reconnect│ │ Fallback │ │
│ │ (自动重连) │ │ WebSocket→Polling │ │
│ └───────────────┘ └────────────────────┘ │
│ │
│ 基于原生 WebSocket │
└──────────────────────────────────────────────┘
Rooms(房间):
@SubscribeMessage('joinRoom')
handleJoinRoom(
@ConnectedSocket() client: Socket,
@MessageBody() room: string,
) {
client.join(room);
this.server.to(room).emit('userJoined', { userId: client.id });
}
@SubscribeMessage('roomMessage')
handleRoomMessage(
@ConnectedSocket() client: Socket,
@MessageBody() payload: { room: string; message: string },
) {
// 发送给房间内除自己外的所有人
client.to(payload.room).emit('message', {
from: client.id,
text: payload.message,
});
}
Namespaces(命名空间):
// 聊天命名空间
@WebSocketGateway({ namespace: 'chat' })
export class ChatGateway {
@SubscribeMessage('message')
handleMessage(@MessageBody() data: string) {
return { event: 'message', data };
}
}
// 通知命名空间
@WebSocketGateway({ namespace: 'notifications' })
export class NotificationsGateway {
@SubscribeMessage('subscribe')
handleSubscribe(@MessageBody() topics: string[]) {
return { event: 'subscribed', data: topics };
}
}
客户端连接不同命名空间:
const chatSocket = io('http://localhost:3000/chat');
const notifSocket = io('http://localhost:3000/notifications');
chatSocket.emit('message', 'Hello!');
notifSocket.emit('subscribe', ['order', 'payment']);
2.2 ws 平台(轻量级)
ws 是一个轻量级的原生 WebSocket 实现,没有 Socket.IO 的额外封装:
npm i @nestjs/platform-ws
// main.ts
import { WsAdapter } from '@nestjs/platform-ws';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new WsAdapter(app));
await app.listen(3000);
}
// 使用 ws 平台的 Gateway
import { WebSocketGateway, SubscribeMessage, MessageBody } from '@nestjs/websockets';
import { WebSocket } from 'ws';
@WebSocketGateway(8080)
export class WsEventsGateway {
@SubscribeMessage('events')
handleEvent(@MessageBody() data: any, @ConnectedSocket() client: WebSocket) {
client.send(JSON.stringify({ event: 'events', data }));
}
}
2.3 Socket.IO vs ws 对比
| 特性 | Socket.IO | ws |
|---|---|---|
| 包大小 | 较大(含客户端库) | 极小 |
| 传输层 | WebSocket + HTTP long-polling 回退 | 纯 WebSocket |
| Rooms / Namespaces | 内置支持 | 无,需自行实现 |
| 自动重连 | 内置 | 需自行实现 |
| 二进制数据 | 支持 | 支持 |
| 浏览器 WebSocket API | 不兼容(需 socket.io-client) | 兼容原生 new WebSocket() |
| 适用场景 | 需要高级特性的应用 | 追求极致轻量和性能 |
| 多实例扩展 | Redis Adapter 支持 | 需自行实现 |
2.4 Gateway 配置选项
@WebSocketGateway({
// 命名空间
namespace: 'chat',
// CORS 配置
cors: {
origin: ['https://example.com'],
credentials: true,
},
// 传输方式
transports: ['websocket', 'polling'],
// 连接超时
connectTimeout: 45000,
// 每条消息最大大小
maxHttpBufferSize: 1e6, // 1MB
// 心跳间隔
pingInterval: 25000,
pingTimeout: 20000,
})
export class ChatGateway {}
三、WebSocket 增强器
[中阶] 本节面向需要在 WebSocket 中复用 HTTP 增强器的读者。
3.1 统一增强器模型
NestJS 的 Guard、Pipe、Interceptor、ExceptionFilter 在 WebSocket 上下文中以相同方式工作,但有一些关键差异:
HTTP 请求管线 WebSocket 消息管线
┌──────────┐ ┌──────────┐
│ Guard │ ←→ 相同接口 ←→ │ Guard │
├──────────┤ ├──────────┤
│ Pipe │ ←→ 相同接口 ←→ │ Pipe │
├──────────┤ ├──────────┤
│Interceptor│ ←→ 相同接口 ←→ │Interceptor│
├──────────┤ ├──────────┤
│ Handler │ │ Handler │
├──────────┤ ├──────────┤
│ Filter │ ←→ 接口相同 ←→ │ Filter │
│ │ 异常类型不同 │ WsException│
└──────────┘ └──────────┘
3.2 WebSocket Guards
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
@Injectable()
export class WsAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// 切换到 WebSocket 上下文
const client = context.switchToWs().getClient();
const token = client.handshake?.auth?.token
?? client.handshake?.headers?.authorization;
// 验证 token
return this.validateToken(token);
}
private validateToken(token: string): boolean {
// token 验证逻辑
return !!token;
}
}
// 使用
@WebSocketGateway()
@UseGuards(WsAuthGuard)
export class EventsGateway {
@SubscribeMessage('secret')
handleSecret(@MessageBody() data: any) {
return { event: 'secret', data: 'authorized data' };
}
}
3.3 WebSocket Pipes
@WebSocketGateway()
export class EventsGateway {
// 使用内置 ValidationPipe
@SubscribeMessage('createItem')
@UsePipes(new ValidationPipe({
// 关键:自定义 exceptionFactory 抛出 WsException
exceptionFactory: (errors) => {
return new WsException(
errors
.map(e => Object.values(e.constraints ?? {}).join(', '))
.join('; '),
);
},
}))
handleCreate(@MessageBody() dto: CreateItemDto) {
return { event: 'itemCreated', data: dto };
}
}
重要:默认
ValidationPipe抛出BadRequestException(HTTP 异常)。在 WebSocket 中必须通过exceptionFactory改为抛出WsException,否则客户端收不到正确的错误信息。
3.4 WebSocket Interceptors
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class WsLoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(WsLoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const client = context.switchToWs().getClient();
const data = context.switchToWs().getData();
const pattern = context.getHandler().name;
this.logger.log(`[${client.id}] ${pattern} 收到: ${JSON.stringify(data)}`);
const now = Date.now();
return next.handle().pipe(
tap((response) => {
this.logger.log(
`[${client.id}] ${pattern} 响应: ${Date.now() - now}ms`,
);
}),
);
}
}
3.5 WebSocket Exception Filters
HTTP 异常过滤器使用 HttpException,WebSocket 异常过滤器使用 WsException:
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
@Catch(WsException)
export class WsExceptionFilter extends BaseWsExceptionFilter {
catch(exception: WsException, host: ArgumentsHost) {
const client = host.switchToWs().getClient();
const error = exception.getError();
const errorResponse = {
event: 'exception',
data: {
status: 'error',
message: typeof error === 'string' ? error : (error as any).message,
timestamp: new Date().toISOString(),
},
};
client.emit('exception', errorResponse);
}
}
// 捕获所有异常(包括非 WsException)
@Catch()
export class WsAllExceptionsFilter extends BaseWsExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const client = host.switchToWs().getClient();
const message = exception instanceof WsException
? exception.getError()
: '服务器内部错误';
client.emit('exception', {
status: 'error',
message,
});
}
}
WsException 与 HttpException 的核心差异:
| 特性 | HttpException | WsException |
|---|---|---|
| 传输方式 | HTTP 响应 | Socket emit('exception') 事件 |
| 状态码 | 有(200/400/500...) | 无 |
| 默认行为 | 返回 JSON 响应 | 发送 exception 事件给客户端 |
| 继承 | Error | Error |
| 客户端监听 | HTTP 状态码 | socket.on('exception', handler) |
3.6 增强器绑定方式
// 方法级
@SubscribeMessage('events')
@UseGuards(WsAuthGuard)
@UseInterceptors(WsLoggingInterceptor)
@UseFilters(WsExceptionFilter)
handleEvent() {}
// Gateway 级(整个 Gateway 生效)
@WebSocketGateway()
@UseGuards(WsAuthGuard)
@UseInterceptors(WsLoggingInterceptor)
@UseFilters(WsExceptionFilter)
export class EventsGateway {}
// 全局级(通过 APP_GUARD 等 token)
@Module({
providers: [
{ provide: APP_GUARD, useClass: WsAuthGuard },
{ provide: APP_FILTER, useClass: WsAllExceptionsFilter },
],
})
四、适配器与多实例部署
[高阶] 本节面向需要自定义 WebSocket 适配器或部署多实例集群的读者。
4.1 WebSocketAdapter 接口
NestJS 通过 WebSocketAdapter 接口抽象 WebSocket 实现,使得底层库可以自由替换:
// @nestjs/common 中的接口定义
interface WebSocketAdapter<
TServer = any,
TClient = any,
TOptions = any,
> {
create(port: number, options?: TOptions): TServer;
bindClientConnect(server: TServer, callback: Function): void;
bindMessageHandlers(
client: TClient,
handlers: WsMessageHandler[],
transform: (data: any) => Observable<any>,
): void;
close(server: TServer): void;
}
4.2 自定义适配器
继承 IoAdapter 扩展默认行为:
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { INestApplicationContext } from '@nestjs/common';
export class CustomIoAdapter extends IoAdapter {
constructor(private app: INestApplicationContext) {
super(app);
}
createIOServer(port: number, options?: ServerOptions) {
const server = super.createIOServer(port, {
...options,
cors: {
origin: ['https://example.com'],
credentials: true,
},
// 自定义 Socket.IO 选项
connectionStateRecovery: {
maxDisconnectionDuration: 2 * 60 * 1000, // 2 分钟
skipMiddlewares: true,
},
});
return server;
}
}
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useWebSocketAdapter(new CustomIoAdapter(app));
await app.listen(3000);
}
4.3 Redis 适配器 — 多实例水平扩展
当应用部署在多个实例时,Socket.IO 的连接状态默认只存在于单个进程中。使用 Redis 适配器可以在多实例间同步消息:
npm i @socket.io/redis-adapter redis
import { IoAdapter } from '@nestjs/platform-socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
import { ServerOptions } from 'socket.io';
export class RedisIoAdapter extends IoAdapter {
private adapterConstructor: ReturnType<typeof createAdapter>;
async connectToRedis(): Promise<void> {
const pubClient = createClient({
url: process.env.REDIS_URL ?? 'redis://localhost:6379',
});
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
this.adapterConstructor = createAdapter(pubClient, subClient);
}
createIOServer(port: number, options?: ServerOptions) {
const server = super.createIOServer(port, options);
server.adapter(this.adapterConstructor);
return server;
}
}
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const redisAdapter = new RedisIoAdapter(app);
await redisAdapter.connectToRedis();
app.useWebSocketAdapter(redisAdapter);
await app.listen(3000);
}
多实例架构图:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Instance 1 │ │ Instance 2 │ │ Instance 3 │
│ Socket.IO │ │ Socket.IO │ │ Socket.IO │
│ + Redis Adapter │ + Redis Adapter │ + Redis Adapter
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────┬───────┘────────────────────┘
│
┌─────┴─────┐
│ Redis │
│ Pub/Sub │
└────────────┘
▲
│
消息在所有实例间同步
4.4 Sticky Sessions
Socket.IO 使用 HTTP long-polling 作为降级传输时,同一客户端的多次 HTTP 请求必须落在同一个实例上。这需要负载均衡器配置 Sticky Sessions(会话粘滞):
客户端 ──→ 负载均衡器(Nginx / ALB)
│
├── 基于 Cookie/IP 路由 ──→ Instance 1
│ (该客户端所有请求)
├── 其他客户端 ──→ Instance 2
└── 其他客户端 ──→ Instance 3
Nginx 配置示例:
upstream websocket_cluster {
ip_hash; # 基于 IP 的 sticky session
server 127.0.0.1:3001;
server 127.0.0.1:3002;
server 127.0.0.1:3003;
}
server {
location /socket.io/ {
proxy_pass http://websocket_cluster;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
提示:如果仅使用
transports: ['websocket'](禁用 polling 回退),则不需要 sticky sessions。
五、源码解读:WebSocket 模块
[资深] 本节面向希望深入理解 Gateway 注册和消息分发机制的读者。
5.1 核心模块结构
packages/websockets/
├── adapters/
│ └── ws-adapter.ts # AbstractWsAdapter 基类
├── constants.ts # 元数据 key 常量
├── context/
│ ├── ws-context-creator.ts # WebSocket 上下文创建器(管线组装)
│ ├── exception-filters-context.ts # WS 异常过滤器上下文
│ └── ws-proxy.ts # 异常安全代理
├── decorators/
│ ├── socket-gateway.decorator.ts # @WebSocketGateway()
│ ├── subscribe-message.decorator.ts # @SubscribeMessage()
│ ├── message-body.decorator.ts # @MessageBody()
│ ├── connected-socket.decorator.ts # @ConnectedSocket()
│ └── gateway-server.decorator.ts # @WebSocketServer()
├── errors/
│ └── ws-exception.ts # WsException
├── gateway-metadata-explorer.ts # Gateway 方法元数据扫描器
├── socket-module.ts # SocketModule(Gateway 注册入口)
├── socket-server-provider.ts # 服务器实例管理
├── sockets-container.ts # 所有 WebSocket 服务器的容器
└── web-sockets-controller.ts # Gateway 到服务器的连接控制器
5.2 @WebSocketGateway() 装饰器
文件位置:packages/websockets/decorators/socket-gateway.decorator.ts
export function WebSocketGateway(portOrOptions?, options?) {
const isPortInt = Number.isInteger(portOrOptions);
let [port, opt] = isPortInt ? [portOrOptions, options] : [0, portOrOptions];
opt = opt || {};
return (target: object) => {
// 标记为 Gateway
Reflect.defineMetadata(GATEWAY_METADATA, true, target);
// 存储端口
Reflect.defineMetadata(PORT_METADATA, port, target);
// 存储配置选项
Reflect.defineMetadata(GATEWAY_OPTIONS, opt, target);
};
}
通过三个元数据 key 完成 Gateway 的标记,后续由 SocketModule 扫描识别。
5.3 @SubscribeMessage() 注册机制
文件位置:packages/websockets/decorators/subscribe-message.decorator.ts
export const SubscribeMessage = <T = string>(message: T): MethodDecorator => {
return (target, key, descriptor) => {
// 标记为消息处理器
Reflect.defineMetadata(MESSAGE_MAPPING_METADATA, true, descriptor.value);
// 存储消息模式
Reflect.defineMetadata(MESSAGE_METADATA, message, descriptor.value);
return descriptor;
};
};
5.4 Gateway 发现与注册流程
文件位置:packages/websockets/socket-module.ts
export class SocketModule {
public register(container, applicationConfig, ...) {
// 遍历所有模块的 providers
const modules = container.getModules();
modules.forEach(({ providers }, moduleName) =>
this.connectAllGateways(providers, moduleName),
);
}
public connectGatewayToServer(wrapper, moduleName) {
const { instance, metatype } = wrapper;
// 检查是否有 GATEWAY_METADATA
const metadataKeys = Reflect.getMetadataKeys(metatype);
if (!metadataKeys.includes(GATEWAY_METADATA)) {
return; // 不是 Gateway,跳过
}
// 初始化适配器(懒加载 IoAdapter)
if (!this.isAdapterInitialized) {
this.initializeAdapter();
}
// 连接 Gateway 到 WebSocket 服务器
this.webSocketsController.connectGatewayToServer(instance, metatype, ...);
}
}
完整流程:
NestFactory.create(AppModule)
│
├── DependenciesScanner.scan() 扫描所有模块
├── InstanceLoader.create() 实例化所有 Provider
│
└── SocketModule.register() 遍历所有 Provider
│
├── 检查 GATEWAY_METADATA 是否为 Gateway
├── initializeAdapter() 懒加载 IoAdapter
│ └── 默认加载 @nestjs/platform-socket.io
│
└── WebSocketsController
.connectGatewayToServer()
│
├── GatewayMetadataExplorer
│ .explore(instance) 扫描 @SubscribeMessage 方法
│
├── WsContextCreator
│ .create(handler) 为每个 handler 组装管线
│ └── Guard → Pipe → Interceptor → Handler → Filter
│
└── server.on('connection') 绑定连接事件
└── bindMessageHandlers(client, handlers)
5.5 WsException 实现
文件位置:packages/websockets/errors/ws-exception.ts
export class WsException extends Error {
constructor(private readonly error: string | object) {
super();
this.initMessage();
}
public initMessage() {
if (isString(this.error)) {
this.message = this.error;
} else if (isObject(this.error) && isString(this.error.message)) {
this.message = this.error.message;
} else {
// 从类名生成消息:WsException → "Ws Exception"
this.message = this.constructor.name.match(/[A-Z][a-z]+|[0-9]+/g).join(' ');
}
}
}
关键设计:WsException 不继承 HttpException,因为 WebSocket 不使用 HTTP 状态码。getError() 返回原始错误对象,由异常过滤器决定如何发送给客户端。
六、实时通信架构
[架构] 本节面向技术负责人和架构师。
6.1 WebSocket 典型应用场景
| 场景 | 通信模式 | 技术要点 |
|---|---|---|
| 即时聊天 | 双向 + Rooms | 消息持久化、已读回执、离线消息 |
| 实时通知 | 服务端推送 | 精准推送(用户级/角色级)、消息去重 |
| 协同编辑 | 双向 + 冲突解决 | OT/CRDT 算法、光标同步 |
| 实时游戏 | 高频双向 | 低延迟、状态同步、帧锁定 |
| 实时看板 | 服务端推送 | 数据聚合、增量更新 |
| 金融行情 | 服务端高频推送 | 数据压缩、限流、断线恢复 |
6.2 连接管理策略
┌────────────────────────────────────────────────────┐
│ 连接生命周期管理 │
│ │
│ 连接建立 │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ 握手 │ → │ 认证 │ → │ 就绪 │ │
│ └──────┘ └──────┘ └──────┘ │
│ │
│ 连接维持 │
│ ┌──────────┐ ┌──────────┐ │
│ │ 心跳检测 │ ←→ │ 超时断开 │ │
│ │ ping/pong │ │ 30s 无响应│ │
│ └──────────┘ └──────────┘ │
│ │
│ 断线恢复 │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 检测断线 │ → │ 指数退避 │ → │ 恢复状态 │ │
│ │ │ │ 重连 │ │ 补发消息 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└────────────────────────────────────────────────────┘
认证最佳实践:
@WebSocketGateway()
export class AuthenticatedGateway implements OnGatewayConnection {
async handleConnection(client: Socket) {
const token = client.handshake.auth?.token;
if (!token) {
client.emit('exception', { message: '缺少认证令牌' });
client.disconnect();
return;
}
try {
const user = await this.authService.verifyToken(token);
// 将用户信息附加到 socket
client.data.user = user;
// 加入用户专属房间
client.join(`user:${user.id}`);
} catch {
client.emit('exception', { message: '认证失败' });
client.disconnect();
}
}
}
6.3 水平扩展策略
| 策略 | 实现 | 适用场景 |
|---|---|---|
| 单实例 | 默认 Socket.IO | 开发环境、小规模 |
| Redis Adapter | @socket.io/redis-adapter | 中等规模、多实例 |
| Redis Streams | 自定义 Adapter | 需要消息持久化 |
| NATS / Kafka | 自定义 Adapter | 大规模、微服务架构 |
6.4 WebSocket vs SSE vs Long Polling 选型
需要双向实时通信?
├── 是 → 高频消息(> 10 msg/s)?
│ ├── 是 → ws 平台(原生 WebSocket,低开销)
│ └── 否 → Socket.IO(自动重连 + Rooms + 回退)
└── 否 → 仅服务端推送?
├── 是 → SSE(简单、HTTP 原生、自动重连)
└── 否 → 低频查询?
├── 是 → 短轮询(最简实现)
└── 否 → 长轮询(兼容性最好)
6.5 消息协议设计
建议统一消息格式:
// 请求消息
interface WsRequest<T = any> {
event: string; // 事件名
data: T; // 负载
requestId?: string; // 请求 ID(用于请求-响应匹配)
}
// 响应消息
interface WsResponse<T = any> {
event: string; // 事件名
data: T; // 负载
requestId?: string; // 关联请求 ID
timestamp: number; // 服务器时间戳
}
// 错误消息
interface WsError {
event: 'exception';
data: {
code: string; // 错误码
message: string; // 错误信息
requestId?: string;
};
}
七、课后实践
练习 1:基础聊天 Gateway(基础)
// 要求:
// 1. 创建 ChatGateway,实现 @SubscribeMessage('message')
// 2. 接收消息后广播给所有连接的客户端
// 3. 实现 OnGatewayConnection/OnGatewayDisconnect,记录连接日志
// 4. 在模块中注册 Gateway
练习 2:房间聊天(中阶)
// 要求:
// 1. 实现 joinRoom / leaveRoom / roomMessage 三个消息处理器
// 2. joinRoom 将客户端加入指定房间
// 3. roomMessage 仅发送给同房间内的其他客户端
// 4. leaveRoom 时通知房间内其他成员
练习 3:WebSocket 认证守卫(中阶)
// 要求:
// 1. 实现 WsAuthGuard,从 handshake.auth.token 中提取和验证 token
// 2. 实现 WsValidationPipe,使用 ValidationPipe 但 exceptionFactory 抛出 WsException
// 3. 实现 WsExceptionFilter,捕获 WsException 并发送结构化错误消息
// 4. 将三者应用到 Gateway
练习 4:Redis 适配器部署(高阶)
// 要求:
// 1. 实现 RedisIoAdapter,使用 @socket.io/redis-adapter
// 2. 在 main.ts 中注册适配器
// 3. 启动两个 NestJS 实例(不同端口),验证消息跨实例同步
// 4. 使用 Nginx 配置 sticky sessions 和 WebSocket 代理
练习 5:源码阅读(资深)
打开 packages/websockets/ 目录,回答:
SocketModule.register()如何从所有 Provider 中筛选出 Gateway?GatewayMetadataExplorer.explore()如何扫描@SubscribeMessage()方法?AbstractWsAdapter的create和bindMessageHandlers分别在什么时机被调用?WsContextCreator如何为每个消息处理器组装 Guard/Pipe/Interceptor 管线?- 为什么
WsException不继承HttpException?
八、本课知识点总结
| 知识点 | 要点 |
|---|---|
| Gateway 基础 | @WebSocketGateway() + @SubscribeMessage() + @MessageBody() |
| 响应模式 | 返回值自动响应 / client.emit() 手动响应 / Observable 多值响应 |
| 生命周期 | OnGatewayInit / OnGatewayConnection / OnGatewayDisconnect |
| Socket.IO | Rooms、Namespaces、自动重连、polling 回退 |
| ws 平台 | 轻量级、原生 WebSocket API、WsAdapter 切换 |
| 增强器 | Guard/Pipe/Interceptor/Filter 与 HTTP 接口相同,异常用 WsException |
| 自定义适配器 | 继承 IoAdapter,重写 createIOServer() |
| 多实例扩展 | @socket.io/redis-adapter + Sticky Sessions |
| 源码入口 | packages/websockets/socket-module.ts → Gateway 发现与注册 |
| 架构选型 | 双向通信用 WebSocket,单向推送用 SSE,低频用轮询 |
下一课预告:第十七课将进入微服务领域,学习 NestJS 内置的 7 种传输层(TCP、Redis、NATS、MQTT、Kafka、gRPC、RabbitMQ)、消息模式(请求-响应 / 事件驱动)以及微服务间通信设计。