7.3 WebSocket 网关与实时通信 HTTP 有个先天局限:永远客户端先开口,服务端无法主动推送。进度条、协作白板、在线客服、行情推送——这些场景要的是双向通道。Nest.js 给 WebSocket 的答案叫网关(Gateway):一个类同时挂着连接生命周期、房间管理与消息处理,写法与控制器同样同构。本节实现带鉴权的网关、房间广播与服务端主动推送。 网关的骨架 网关用装饰器声明它的一切。
HTTP 有个先天局限:永远客户端先开口,服务端无法主动推送。进度条、协作白板、在线客服、行情推送——这些场景要的是双向通道。Nest.js 给 WebSocket 的答案叫网关(Gateway):一个类同时挂着连接生命周期、房间管理与消息处理,写法与控制器同样同构。本节实现带鉴权的网关、房间广播与服务端主动推送。
网关用装饰器声明它的一切。一个订单进度推送网关的骨架:
import { WebSocketGateway, SubscribeMessage, ConnectedSocket, MessageBody, WebSocketServer, } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; @WebSocketGateway({ namespace: '/orders', cors: { origin: '*' } }) export class OrdersGateway { @WebSocketServer() server!: Server; // 服务端句柄:广播与房间操作都靠它 @SubscribeMessage('joinOrder') onJoin( @ConnectedSocket() client: Socket, @MessageBody() body: { orderId: string }, ) { client.join(`order:${body.orderId}`); // 进房间:只收这个订单的进度 return { event: 'joined', data: body.orderId }; } @SubscribeMessage('ping') onPing() { return { event: 'pong', data: Date.now() }; } }
对照控制器读这份骨架:@WebSocketGateway 相当于 @Controller,@SubscribeMessage 相当于方法装饰器,@ConnectedSocket 与 @MessageBody 相当于参数装饰器——第 2 章的"声明式登记"风格原样平移到了双工通道。网关也是标准 Provider,可注入服务、可被注入,挂在模块里参与容器装配。
房间是 socket 库的核心抽象:连接加入房间,服务端可向"某房间全体"或"全体除某人"发消息。这解决了推送的第一难题——广播不是越大越好,是越准越好。

广播的三种粒度按需选用:server.to(房间).emit(...) 房间内全体;server.emit(...) 全体(慎用,等于全站弹窗);client.broadcast.to(房间).emit(...) 房间内除自己(协作编辑的"别人正在改"提示就用它)。
实时通道不能成为权限体系的旁门——HTTP 有守卫把门,WebSocket 的入口在握手。写法是给网关配一个握手函数:
@WebSocketGateway({ namespace: '/orders' }) export class OrdersGateway implements OnGatewayConnection { async handleConnection(client: Socket) { const token = client.handshake.auth?.token; // 握手带的令牌 try { const payload = this.jwt.verify(token); // 复用 6.1 的密钥与策略 client.data.user = payload; // 身份挂在连接上 client.join(`tenant:${payload.tenantId}`); // 租户级房间 自动加入 } catch { client.disconnect(true); // 证件无效 立即断开 } } async handleDisconnect(client: Socket) { // 清理:从跟踪表移除 打点在线时长 } }
鉴权之外的收获是把身份存进连接——此后每条消息的处理都能从连接上取身份,不必每条重新验签。租户房间在握手时自动加入,则是数据级权限在实时通道的落点:向租户房间推送即天然只达本租户的连接。
网关最独特的用法:业务服务在任意时刻调 server 句柄推送。为避免业务层反向依赖网关,用 2.3 节的事件解耦——服务发领域事件,网关订阅并转发:
@Injectable() export class OrderProgressListener { constructor( private readonly events: EventEmitter2, private readonly gateway: OrdersGateway, ) {} @OnEvent('order.status.changed') handle(evt: { orderId: string; status: string }) { this.gateway.server .to(`order:${evt.orderId}`) .emit('orderProgress', { orderId: evt.orderId, status: evt.status }); } }
业务代码只管喊"状态变了",谁在听、怎么推送、推给哪个房间,全部收在网关侧——推送是表现层的事,不该渗进业务层,这是薄窗口原则在实时通道的翻版。
网关的三类高发问题:连不上——先查命名空间与路径是否两端一致,再查握手鉴权逻辑(最常见的"连接秒断"就是被自己的 handleDisconnect 了);消息不来——确认进了正确的房间(join 是否成功)、事件名两端拼写一致;多实例失灵——房间广播只在本实例生效,多实例部署必须换 Redis 适配器把广播转投共享频道,这在 8.3 节部署时一并落地。还有一条经验边界:不是所有"实时感"都需要 WebSocket——三十秒轮询对低频场景足够且便宜,别为炫技引入长连接的运维复杂度。
跨车间协作的三条通道全部打通:同步请求应答、异步事件、实时推送。最后一章进验收区——缓存限流、测试体系、容器化部署与可观测性,整机出厂前的最后一道关。