第十六课:WebSocket 实时通信

4 阅读13分钟

覆盖文档: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}`);
  }
}

生命周期钩子接口:

接口方法触发时机
OnGatewayInitafterInit(server)WebSocket 服务器初始化完成
OnGatewayConnectionhandleConnection(client, ...args)新客户端连接
OnGatewayDisconnecthandleDisconnect(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.IOws
包大小较大(含客户端库)极小
传输层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 的核心差异

特性HttpExceptionWsException
传输方式HTTP 响应Socket emit('exception') 事件
状态码有(200/400/500...)
默认行为返回 JSON 响应发送 exception 事件给客户端
继承ErrorError
客户端监听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/ 目录,回答:

  1. SocketModule.register() 如何从所有 Provider 中筛选出 Gateway?
  2. GatewayMetadataExplorer.explore() 如何扫描 @SubscribeMessage() 方法?
  3. AbstractWsAdaptercreatebindMessageHandlers 分别在什么时机被调用?
  4. WsContextCreator 如何为每个消息处理器组装 Guard/Pipe/Interceptor 管线?
  5. 为什么 WsException 不继承 HttpException

八、本课知识点总结

知识点要点
Gateway 基础@WebSocketGateway() + @SubscribeMessage() + @MessageBody()
响应模式返回值自动响应 / client.emit() 手动响应 / Observable 多值响应
生命周期OnGatewayInit / OnGatewayConnection / OnGatewayDisconnect
Socket.IORooms、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)、消息模式(请求-响应 / 事件驱动)以及微服务间通信设计。