从零入门 NestJS:构建你的第一个 REST API(十)WebSocket 实时通信

779 阅读5分钟

WebSocket 实时通信

在现代 Web 应用中,实时通信能力(如聊天室、在线协作、实时通知等)越来越重要。WebSocket 协议可以实现客户端与服务端的双向、低延迟通信。NestJS 对 WebSocket 提供了原生支持,开发者可以通过 Gateway(网关)机制轻松实现实时功能。

WebSocket 基本原理

  • WebSocket 是一种持久化的全双工通信协议,允许服务端主动向客户端推送消息。
  • 与传统 HTTP 不同,WebSocket 连接建立后无需每次请求都重新握手,适合高频、低延迟的实时场景。
  • 常见应用:聊天室、在线游戏、实时通知、协同编辑等。

WebSocket 与 HTTP 的区别

特性HTTPWebSocket
连接方式短连接/单向长连接/双向
通信模式请求-响应任意时刻可推送
适用场景普通 API、页面加载实时消息、推送
性能有额外握手和头部更高效、低延迟

在 NestJS 中集成 WebSocket

NestJS 提供了 @nestjs/websockets 包,内置 Gateway(网关)机制,支持 Socket.IO 和原生 ws 两种驱动。

安装依赖

npm install --save @nestjs/websockets @nestjs/platform-socket.io socket.io

创建 Gateway(网关)

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

@WebSocketGateway({ namespace: '/chat', cors: true })
export class ChatGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  afterInit(server: Server) {
    console.log('WebSocket 网关已初始化');
  }

  handleConnection(client: Socket) {
    console.log(`客户端连接: ${client.id}`);
  }

  handleDisconnect(client: Socket) {
    console.log(`客户端断开: ${client.id}`);
  }

  @SubscribeMessage('sendMessage')
  handleMessage(@MessageBody() data: { user: string; message: string }, @ConnectedSocket() client: Socket) {
    // 广播消息到所有客户端
    this.server.emit('receiveMessage', data);
  }
}
  • @WebSocketGateway:声明一个 WebSocket 网关,可指定命名空间、端口、CORS 等参数。
  • @WebSocketServer:注入底层 Socket.IO Server 实例。
  • @SubscribeMessage:监听客户端事件。
  • emit:服务端主动推送事件。

在模块中注册 Gateway

// chat/chat.module.ts
import { Module } from '@nestjs/common';
import { ChatGateway } from './chat.gateway';

@Module({
  providers: [ChatGateway],
})
export class ChatModule {}

客户端连接示例

// 浏览器端 Socket.IO 客户端
const socket = io('http://localhost:3000/chat');
socket.on('connect', () => {
  console.log('已连接到 WebSocket 服务');
});
socket.emit('sendMessage', { user: '小明', message: 'Hello NestJS!' });
socket.on('receiveMessage', data => {
  console.log('收到消息:', data);
});

单聊、群聊与广播代码实例

WebSocket 支持多种消息推送模式,常见的有:

  • 单聊(点对点):只推送给指定用户
  • 群聊(房间):推送给同一房间内的所有用户
  • 广播:推送给所有在线用户

1. 单聊(点对点消息)

实现思路:服务端维护用户与 socket.id 的映射,发送消息时只推送给目标用户的 socket。

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

@WebSocketGateway({ namespace: '/chat', cors: true })
export class ChatGateway {
  @WebSocketServer()
  server: Server;

  // 用户ID与socket.id映射
  private userMap = new Map<string, string>();

  handleConnection(client: Socket) {
    const userId = client.handshake.query?.userId as string;
    if (userId) {
      this.userMap.set(userId, client.id);
    }
  }

  handleDisconnect(client: Socket) {
    // 移除断开用户
    for (const [userId, socketId] of this.userMap.entries()) {
      if (socketId === client.id) {
        this.userMap.delete(userId);
        break;
      }
    }
  }

  @SubscribeMessage('privateMessage')
  handlePrivateMessage(@MessageBody() data: { toUserId: string; message: string }, @ConnectedSocket() client: Socket) {
    const targetSocketId = this.userMap.get(data.toUserId);
    if (targetSocketId) {
      this.server.to(targetSocketId).emit('receivePrivateMessage', {
        from: client.id,
        message: data.message,
      });
    }
  }
}

2. 群聊(房间消息)

实现思路:用户加入房间,消息推送到房间内所有成员。

@SubscribeMessage('joinRoom')
handleJoinRoom(@MessageBody() data: { room: string }, @ConnectedSocket() client: Socket) {
  client.join(data.room);
  client.emit('joinedRoom', { room: data.room });
}

@SubscribeMessage('roomMessage')
handleRoomMessage(@MessageBody() data: { room: string; message: string }, @ConnectedSocket() client: Socket) {
  this.server.to(data.room).emit('receiveRoomMessage', {
    from: client.id,
    message: data.message,
    room: data.room,
  });
}

3. 广播(全局消息)

实现思路:服务端直接 emit 给所有连接的客户端。

@SubscribeMessage('broadcast')
handleBroadcast(@MessageBody() data: { message: string }) {
  this.server.emit('receiveBroadcast', {
    message: data.message,
  });
}

客户端调用示例

// 单聊
socket.emit('privateMessage', { toUserId: 'user2', message: '你好,user2!' });
socket.on('receivePrivateMessage', data => console.log('收到私聊:', data));

// 加入房间
socket.emit('joinRoom', { room: 'group1' });
socket.emit('roomMessage', { room: 'group1', message: '大家好!' });
socket.on('receiveRoomMessage', data => console.log('收到群聊:', data));

// 广播
socket.emit('broadcast', { message: '全体注意!' });
socket.on('receiveBroadcast', data => console.log('收到广播:', data));

实际项目中建议结合用户认证、消息持久化、离线消息等功能,提升聊天体验和安全性。

典型实战场景:聊天室/实时通知

  • 聊天室:用户发送消息,服务端通过 Gateway 广播到所有在线用户。
  • 实时通知:如订单状态变更、系统公告等,服务端主动推送到指定客户端。
  • 在线状态:可通过连接/断开事件统计在线人数。

最佳实践与常见坑点

  • 命名空间与事件名要规范,避免冲突。
  • 注意权限校验,如身份认证、消息过滤等。
  • 合理管理连接数,防止资源泄漏。
  • 服务端异常要捕获,避免影响所有连接。
  • 生产环境建议开启 CORS 和安全配置
  • 可结合 Redis 实现多节点广播,适合大规模分布式部署。

WebSocket 连接鉴权实践

在实际项目中,WebSocket 连接通常需要进行身份认证(如 JWT token 校验),以防止未授权用户建立连接或发送消息。NestJS 支持在 Gateway 的 handleConnection 钩子中实现自定义鉴权逻辑。

1. 通过 handleConnection 实现 token 鉴权

// chat/chat.gateway.ts
import { WebSocketGateway, WebSocketServer, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import * as jwt from 'jsonwebtoken';

@WebSocketGateway({ namespace: '/chat', cors: true })
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  @WebSocketServer()
  server: Server;

  async handleConnection(client: Socket) {
    // 假设前端通过 query 传递 token
    const token = client.handshake.query?.token as string;
    try {
      const payload = jwt.verify(token, 'your_jwt_secret'); // 替换为你的密钥
      // 可将用户信息挂载到 client 对象
      (client as any).user = payload;
      console.log(`用户已鉴权: ${payload.username}`);
    } catch (e) {
      console.log('WebSocket 鉴权失败,断开连接');
      client.disconnect();
    }
  }

  handleDisconnect(client: Socket) {
    console.log(`客户端断开: ${client.id}`);
  }
}

2. 客户端连接时传递 token

// 浏览器端
const socket = io('http://localhost:3000/chat', {
  query: { token: '用户登录后的JWT' }
});

3. 注意事项

  • token 建议用 HTTP Only Cookie 或 Secure Storage 存储,防止泄露。
  • 生产环境建议用 HTTPS,防止 token 被窃取。
  • 鉴权失败要及时断开连接,避免非法访问。
  • 如需更复杂的权限控制,可结合 Guard、角色等机制。

详细鉴权方案可参考 Socket.IO 官方文档NestJS WebSocket 安全最佳实践

WebSocket 让你的应用具备实时能力,建议多查阅 NestJS WebSocket 文档Socket.IO 官方文档 掌握更多高级用法。