mcpField
N
nestjs-websocket

nestjs-websocket

github.com/claude-dev-suite开发4646 次安装2 天前更新
代码仓库

关于 nestjs-websocket

为 NestJS 应用添加 WebSocket 功能的参考技能,涵盖网关设置、身份验证、房间管理、错误处理及扩展指导。

主要功能

  1. 01WebSocket 网关设置
  2. 02模块注册
  3. 03JWT 身份验证守卫
  4. 04房间与事件处理
  5. 05Redis 适配器扩展

适用场景

  1. 01为 NestJS 应用添加实时事件
  2. 02验证 WebSocket 连接
  3. 03向房间广播消息
  4. 04准备 WebSocket 服务进行水平扩展

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/claude-dev-suite/claude-dev-suite --skill nestjs-websocket

安装后在 Claude Code 中输入/nestjs-websocket确认已加载。

详细介绍

NestJS WebSocket 集成

深入了解:使用 technology: nestjs 的 mcp__documentation__fetch_docs 获取全面的文档。

网关设置

// events.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  OnGatewayConnection,
  OnGatewayDisconnect,
  ConnectedSocket,
  MessageBody,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({
  cors: {
    origin: process.env.CORS_ORIGINS?.split(',') || [],
    credentials: true,
  },
  namespace: '/events',
})
export class EventsGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  handleConnection(client: Socket) {
    console.log(`Client connected: ${client.id}`);
  }

  handleDisconnect(client: Socket) {
    console.log(`Client disconnected: ${client.id}`);
  }

  @SubscribeMessage('message')
  handleMessage(
    @ConnectedSocket() client: Socket,
    @MessageBody() payload: { room: string; data: any },
  ) {
    this.server.to(payload.room).emit('message', payload.data);
    return { event: 'message', data: 'Message sent' };
  }
}

模块注册

// events.module.ts
import { Module } from '@nestjs/common';
import { EventsGateway } from './events.gateway';

@Module({
  providers: [EventsGateway],
  exports: [EventsGateway],
})
export class EventsModule {}

// app.module.ts
@Module({
  imports: [EventsModule],
})
export class AppModule {}

身份验证守卫

// ws-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class WsAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const client: Socket = context.switchToWs().getClient();
    const token = client.handshake.auth?.token ||
                  client.handshake.headers?.authorization?.split(' ')[1];

    if (!token) {
      throw new WsException('Unauthorized');
    }

    try {
      const payload = await this.jwtService.verifyAsync(token);
      client.data.user = payload;
      return true;
    } catch {
      throw new WsException('Invalid token');
    }
  }
}

// Usage in gateway
@UseGuards(WsAuthGuard)
@SubscribeMessage('protected-event')
handleProtectedEvent(@ConnectedSocket() client: Socket) {
  const user = client.data.user;
  return { user };
}

房间管理

@WebSocketGateway()
export class RoomsGateway {
  @WebSocketServer()
  server: Server;

  @SubscribeMessage('join-room')
  handleJoinRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() roomId: string,
  ) {
    client.join(roomId);
    client.to(roomId).emit('user-joined', { id: client.id });
    return { event: 'joined', room: roomId };
  }

  @SubscribeMessage('leave-room')
  handleLeaveRoom(
    @ConnectedSocket() client: Socket,
    @MessageBody() roomId: string,
  ) {
    client.leave(roomId);
    client.to(roomId).emit('user-left', { id: client.id });
  }

  // Broadcast to room from service
  broadcastToRoom(roomId: string, event: string, data: any) {
    this.server.to(roomId).emit(event, data);
  }

  // Get clients in room
  async getClientsInRoom(roomId: string): Promise<string[]> {
    const sockets = await this.server.in(roomId).fetchSockets();
    return sockets.map((s) => s.id);
  }
}

异常处理

// ws-exception.filter.ts
import { Catch, ArgumentsHost } from '@nestjs/common';
import { BaseWsExceptionFilter, WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';

@Catch()
export class WsExceptionFilter extends BaseWsExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const client: Socket = host.switchToWs().getClient();

    const error = exception instanceof WsException
      ? exception.getError()
      : { message: 'Internal server error' };

    client.emit('error', {
      status: 'error',
      message: typeof error === 'string' ? error : (error as any).message,
    });
  }
}

// Apply globally or per gateway
@UseFilters(new WsExceptionFilter())
@WebSocketGateway()
export class EventsGateway {}

使用 Redis 适配器进行扩展

// main.ts
import { IoAdapter } from '@nestjs/platform-socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';

export class RedisIoAdapter extends IoAdapter {
  private adapterConstructor: ReturnType<typeof createAdapter>;

  async connectToRedis(): Promise<void> {
    const pubClient = createClient({ url: process.env.REDIS_URL });
    const subClient = pubClient.duplicate();

    await Promise.all([pubClient.connect(), subClient.connect()]);

    this.adapterConstructor = createAdapter(pubClient, subClient);
  }

  createIOServer(port: number, options?: any) {
    const server = super.createIOServer(port, options);
    server.adapter(this.adapterConstructor);
    return server;
  }
}

// Bootstrap
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const redisIoAdapter = new RedisIoAdapter(app);
  await redisIoAdapter.connectToRedis();
  app.useWebSocketAdapter(redisIoAdapter);

  await app.listen(3000);
}

事件监听器

@Component
@RequiredArgsConstructor
@Slf4j
export class WebSocketEventListener {
  constructor(private messagingTemplate: SimpMessagingTemplate) {}

  @EventListener
  handleWebSocketConnectListener(event: SessionConnectedEvent) {
    log.info('New WebSocket connection established');
  }

  @EventListener
  handleWebSocketDisconnectListener(event: SessionDisconnectEvent) {
    const headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
    const username = headerAccessor.getSessionAttributes().get('username');

    if (username != null) {
      log.info(`User disconnected: ${username}`);
      const leaveMessage = ChatMessage.builder()
        .type(ChatMessage.MessageType.LEAVE)
        .sender(username)
        .build();
      messagingTemplate.convertAndSend('/topic/public', leaveMessage);
    }
  }
}

在服务中注入网关

// notifications.service.ts
@Injectable()
export class NotificationsService {
  constructor(private eventsGateway: EventsGateway) {}

  async notifyUser(userId: string, notification: Notification) {
    this.eventsGateway.server
      .to(`user:${userId}`)
      .emit('notification', notification);
  }

  async broadcastAnnouncement(message: string) {
    this.eventsGateway.server.emit('announcement', { message });
  }
}

反模式

反模式不足之处正确做法
不使用命名空间所有事件混杂在一起使用命名空间进行隔离
连接时缺少身份验证未经授权的访问实现 WsAuthGuard
不清理房间内存泄漏处理断开连接事件
在处理程序中执行同步的繁重操作阻塞事件循环正确使用 async/await
生产环境中缺少 Redis 适配器无法水平扩展使用 Redis 适配器

快速故障排除

症状原因解决方法
CORS 错误缺少 CORS 配置为网关添加 cors 选项
身份验证始终失败握手中没有令牌检查客户端身份验证设置
未收到消息命名空间错误确认命名空间一致
房间广播失败客户端不在房间内确认已调用 join-room
扩展问题没有 Redis 适配器实现 RedisIoAdapter

生产环境检查清单

  • 正确配置 CORS
  • JWT 身份验证守卫
  • 用于水平扩展的 Redis 适配器
  • 用于错误处理的异常过滤器
  • 正确处理断开连接
  • 对消息进行速率限制
  • 配置心跳/ping
  • 记录连接/断开连接日志

不适用本技能的情况

  • React WebSocket 客户端 → 使用 react-websocket
  • Spring WebSocket → 使用 spring-websocket
  • 原始 HTTP 长轮询 → 使用标准控制器

参考文档

评论

支持 Markdown