Skip to content
第 21 章 后端 ⏱ 13 分钟阅读

第 21 章:WebSocket ​

学习目标 ​

  • 集成 @nestjs/websockets + socket.io
  • 写 Gateway 和事件处理
  • 实现房间和广播
  • 避开 3 个 WebSocket 坑

一、安装 ​

bash
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io

二、最简 Gateway ​

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

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
  @WebSocketServer() server: Server;

  @SubscribeMessage('msg')
  handleMessage(
    @MessageBody() data: { room: string; text: string },
    @ConnectedSocket() client: Socket,
  ) {
    this.server.to(data.room).emit('msg', {
      from: client.id,
      text: data.text,
      ts: Date.now(),
    });
    return { ok: true };
  }
}

逐行拆解:

typescript
import { WebSocketGateway, SubscribeMessage, MessageBody, ConnectedSocket } from '@nestjs/websockets';
// 4 个装饰器,跟 Controller 装饰器思路一样:
//   @WebSocketGateway → 跟 @Controller 一样,标这是一个网关类
//   @SubscribeMessage → 跟 @Get/@Post 一样,标一个事件处理函数
//   @MessageBody      → 跟 @Body 一样,拿客户端发的数据
//   @ConnectedSocket  → 拿当前客户端的 Socket 对象

import { Server, Socket } from 'socket.io';
// socket.io 提供的类型:
//   Server → 整个 WS 服务器实例(用 server.emit 推给所有人)
//   Socket → 单个连接(代表一个浏览器/客户端)

@WebSocketGateway({ cors: { origin: '*' } })
// ↑ 标这是 WS 网关类
//   cors: { origin: '*' } → 允许所有跨域来源(浏览器连 WS 用)
//                                ↑ 没这行浏览器会报 CORS 错

export class ChatGateway {
  @WebSocketServer() server: Server;
  // ↑ 注入完整 server 实例(用来 emit,跟 HTTP 里 @Inject 注入 Service 一样)

  @SubscribeMessage('msg')
  // ↑ 监听客户端发的 'msg' 事件(字符串可以随便起)
  //   客户端 socket.emit('msg', data) 就会触发这函数

  handleMessage(
    @MessageBody() data: { room: string; text: string },
    // 拿客户端发的数据(就是 emit 第二参数)
    //   data = { room: 'r1', text: 'hello' } 举例

    @ConnectedSocket() client: Socket,
    // 拿这个客户端的连接对象
    //   client.id   → 唯一连接 ID(类似 session)
    //   client.data → 自定义存数据(后面讲)

  ) {
    this.server.to(data.room).emit('msg', {
      //   server.to('r1')     → 拿到 'r1' 房间
      //    .emit('msg', {...}) → 推 'msg' 事件 + 数据给 r1 里所有人
      from: client.id,                                            // 谁发的
      text: data.text,                                            // 内容
      ts: Date.now(),                                             // 时间戳
    });
    // ↑ 这一行 = 服务器主动推 "msg" 事件给房间内所有客户端

    return { ok: true };
    // ↑ 返回值会作为 "ack" 回给发消息的客户端(可选)
  }
}

触发流程:

1. 前端连上 ws://localhost:3000
2. 前端:socket.emit('msg', { room: 'r1', text: 'hello' })
3. NestJS:@SubscribeMessage('msg') 命中 → handleMessage 执行
4. handleMessage 里 server.to('r1').emit(...) → 推回 r1 房间所有人
5. r1 里所有客户端 socket.on('msg', payload => ...) 触发

注册:

typescript
@Module({
  providers: [ChatGateway],                  // Gateway 当 Provider 注入(跟 Service 一样)
})
export class ChatModule {}
// ↑ 必须放 providers 里,NestJS DI 才能 new 它
//   否则 @WebSocketServer() server 拿不到值

三、前端连接 ​

javascript
// browser
import { io } from 'socket.io-client';

const socket = io('http://localhost:3000', { transports: ['websocket'] });

socket.emit('msg', { room: 'r1', text: 'hello' });
socket.on('msg', payload => console.log(payload));

逐行拆解:

javascript
import { io } from 'socket.io-client';
// ↑ 浏览器端的 socket.io 客户端库
//   (对应后端 import { Server } from 'socket.io')
//   浏览器用 npm 装:socket.io-client

const socket = io('http://localhost:3000', { transports: ['websocket'] });
//   ① 'http://localhost:3000' = 后端服务地址
//   ② io() 创建连接,返回一个 socket 对象
//   ③ 第二参数 = 配置对象

socket.emit('msg', { room: 'r1', text: 'hello' });
//   ↑ 发 'msg' 事件(对应后端 @SubscribeMessage('msg'))
//   ↑ 第二个参数是数据 → 后端 @MessageBody() 拿到

socket.on('msg', payload => console.log(payload));
//   ↑ 监听 'msg' 事件(server.to(room).emit('msg', {...}) 会触发这)
//   ↑ payload = 后端 push 的数据

io() 的地址从哪来?

默认就开在 HTTP 同一个端口 —— NestJS 在 NestFactory.create() 时自动给 socket.io 装载 ws 服务,绑定在 HTTP server 上:

typescript
// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  //                                       ↑
  //             这里创建时,NestJS 自动给 AppModule
  //             里所有 @WebSocketGateway 类装载 socket.io
  //             监听同一个 3000 端口

  await app.listen(3000);
}

所以前端连这个地址:

浏览器 → ws://localhost:3000
              ↑ 跟 HTTP 端口一样(不是另开一个)

默认挂载点(/socket.io/ 是 socket.io 协议的固定路径,跟 HTTP 路由无关):

用途地址
HTTP API(Express)GET http://localhost:3000/users
socket.io 连接GET http://localhost:3000/socket.io/?EIO=4&transport=websocket 然后升级成 ws

transports: ['websocket'] 是什么?

socket.io 默认会先 HTTP 轮询、再升级 WebSocket。这个选项强制只用 WebSocket。

javascript
// 默认行为(socket.io 自动协商)
io('http://localhost:3000');
// 浏览器内部:
//   ① 先发起 HTTP 长轮询 GET /socket.io/?transport=polling(保活)
//   ② 探测成功后才升级成 WebSocket 协议
//
// 优点:兼容老浏览器、不支持 ws 的网络也能用
// 缺点:握手多一步(慢一点点)

// 强制只用 WebSocket(更快)
io('http://localhost:3000', { transports: ['websocket'] });
// 浏览器:直接发 WebSocket 握手

三种传输方式对比:

transport协议何时用
pollingHTTP 长轮询兜底(老浏览器/企业代理禁用 ws)
websocketWebSocket✅ 现代浏览器首选
['polling', 'websocket']先 polling 再升级socket.io 默认(兼容性好)

实战选择:

javascript
// 内部应用/服务对服务 → 强制 websocket(快)
io(url, { transports: ['websocket'] });

// 面向公网用户 → 保留 polling 兜底(默认即可)
io(url);   // 不传 transport 配置

对应配置也要改后端(如果你强制 websocket):

typescript
// chat.gateway.ts
@WebSocketGateway({
  cors: { origin: '*' },
  transports: ['websocket'],                  // ← 跟前端对应
})
export class ChatGateway {}

完整连接流程:

浏览器                                    后端(NestJS + socket.io)
  │                                                │
  │ ① 握手 HTTP                                    │
  │   GET /socket.io/?EIO=4&transport=websocket ───→│ 解析参数
  │ ←───────────────────────── 101 Switching ──────│
  │                                                │
  │ ② WebSocket 建好(socket 对象可用)              │
  │                                                │
  │ ③ socket.emit('msg', {room,text}) ────────────→│ @SubscribeMessage('msg')
  │                                                │   handleMessage() 执行
  │                                                │
  │ ←──── server.to(room).emit('msg', {...}) ──────│ 推给房间
  │                                                │
  │ ④ socket.on('msg', payload => ...) 触发         │
  │   console.log(payload)                          │

四、命名空间 ​

默认命名空间 = /:

typescript
@WebSocketGateway()
//                ↑ 省略 namespace,等价于 namespace: '/'

对应前端三种连法(都连到同一个 / 命名空间):

javascript
io('http://localhost:3000');
io('/');
io('http://localhost:3000/');

显式命名空间:

typescript
@WebSocketGateway({ namespace: '/chat' })
export class ChatGateway {}

对应前端两种连法:

javascript
io('http://localhost:3000/chat');               // 完整 URL
io('/chat');                                     // 相对路径(推荐)

命名空间 vs 房间:

概念维度比喻
命名空间Gateway 级别(一个 Gateway 一个)不同的 App(微信、QQ)
房间客户端级别(一个 Gateway 多个)一个 App 里的不同群

实战选择:

typescript
// 大型项目 → 命名空间拆分业务
@WebSocketGateway({ namespace: '/chat' })        class ChatGateway
@WebSocketGateway({ namespace: '/notification' }) class NotificationGateway

// 小项目 → 1 个 Gateway + 内部分房间
@WebSocketGateway()                              class ChatGateway
//   内部 client.join('r1')/client.join('r2')

五、生命周期 ​

typescript
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
  handleConnection(client: Socket) {
    console.log('connected', client.id);
  }

  handleDisconnect(client: Socket) {
    console.log('disconnect', client.id);
  }
}

六、房间(Room) ​

typescript
@SubscribeMessage('join')
joinRoom(
  @MessageBody() data: { room: string },
  @ConnectedSocket() client: Socket,
) {
  client.join(data.room);
  return { joined: data.room };
}

@SubscribeMessage('leave')
leaveRoom(
  @MessageBody() data: { room: string },
  @ConnectedSocket() client: Socket,
) {
  client.leave(data.room);
}

@SubscribeMessage('msg')
send(@MessageBody() data: { room: string; text: string }, @ConnectedSocket() client: Socket) {
  // 广播给房间内所有人(包括自己)
  this.server.to(data.room).emit('msg', { from: client.id, text: data.text });
  // 排除自己:client.to(data.room).emit(...)
}

⚠️ 坑 1:this.server.emit() 广播给所有人(包括没加入房间的)→ 用 to(room) 限定。

client.join(room) vs server.to(room):

操作谁执行作用类比
client.join(room)客户端(单个 socket)加入房间(订阅)"我进 r1 群"
server.to(room)服务器(发消息时)拿到房间里的客户端(推送)"给 r1 群所有人发消息"

必须先 join,后 to(否则 server.to 找不到人):

① 客户端 socket.emit('join', { room: 'r1' })
   → 后端:client.join('r1')                    // 这个 socket 加入 r1
   → r1 房间里有 1 个客户端 ✅

② 客户端 socket.emit('msg', { room: 'r1', text: 'hi' })
   → 后端:this.server.to('r1').emit('msg', {...})
   → 给 r1 房间所有客户端推 msg → 推送 1 个客户端 ✅

③ 没 join 就直接 msg
   → server.to('r1').emit(...)
   → r1 房间是空的 → 推 0 个客户端(沉默失败,没报错)

内部维护一张表:

typescript
client.join(data.room)
// 服务端内部维护:
//   room 'r1' → [client_id 'abc', client_id 'def', ...]
//                          ↑↑↑↑↑
//                 房间里有哪些客户端

server.to vs client.to 区别:

typescript
// server.to(room) = 给房间内所有客户端发(包括自己)
this.server.to(data.room).emit('msg', payload);

// client.to(room) = 给房间内其他人发(排除自己)
client.to(data.room).emit('msg', payload);

场景对照(A 在 r1、B 在 r1、C 不在 r1):

触发方式A 收到B 收到C 收到
server.to('r1').emit(...)✅✅❌
client.to('r1').emit(...)❌(排自己)✅❌

完整对话流程:

=== 用户 A 加入 ===
前端:
  socket.emit('join', { room: 'r1' });

后端:
  @SubscribeMessage('join')
  joinRoom(@MessageBody() data, @ConnectedSocket() client) {
    client.join(data.room);                          // A 进 r1
                                                    // r1 房间: [A]
    return { joined: 'r1' };
  }

=== 用户 B 加入 ===
前端:
  socket.emit('join', { room: 'r1' });

后端:
  client.join('r1');                                 // B 进 r1
                                                    // r1 房间: [A, B]

=== A 发消息 ===
前端:
  socket.emit('msg', { room: 'r1', text: 'hello' });

后端:
  @SubscribeMessage('msg')
  send(@MessageBody() data, @ConnectedSocket() client) {
    this.server.to(data.room).emit('msg', payload);
    //  ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
    //  to('r1') = 拿 [A, B] → 给它们都发 msg
  }

→ A 浏览器 console.log 收到 payload    ✅(包括自己)
→ B 浏览器 console.log 收到 payload    ✅

记忆口诀:

动词视角时机
client.join(room)入群(我加入)连接建立时或切换房间
client.leave(room)退群退出房间
server.to(room).emit发群消息(含自己)广播给所有人
client.to(room).emit发群消息(排除自己)广播给别人

七、注入 Service ​

typescript
@WebSocketGateway()
export class ChatGateway {
  constructor(private readonly messagesService: MessagesService) {}

  @SubscribeMessage('msg')
  async handle(@MessageBody() data: any, @ConnectedSocket() client: Socket) {
    const saved = await this.messagesService.create(data);
    this.server.to(data.room).emit('msg', saved);
  }
}

八、认证(JWT) ​

typescript
@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway implements OnGatewayConnection {
  async handleConnection(client: Socket) {
    const token = client.handshake.auth?.token ?? client.handshake.headers.authorization;
    if (!token) { client.disconnect(); return; }
    try {
      const payload = await this.jwt.verifyAsync(token.replace('Bearer ', ''));
      client.data.user = payload;
    } catch {
      client.disconnect();
    }
  }
}

⚠️ 坑 2:不验权直接放行 → 任何人都能连 WS,房间广播泄露。


WS 为什么不能用 @UseGuards?

typescript
// ✅ HTTP 这样写(NestJS 支持)
@Controller('users')
@UseGuards(JwtAuthGuard)              // Guard 自动拦截 HTTP 请求
@Get('me')
me() {}

// ❌ WS 不能这么写
@WebSocketGateway()                   // WS 没有"路径"概念,Guard 没匹配目标
@UseGuards(...)                       // 强行写也没意义
class ChatGateway {}

// 原因:
//   - HTTP Guard 拦截"路径 + 方法"
//   - WS 没有路径概念,只有"连接 + 事件"
//   - NestJS 没给 WS 定义标准的 @UseGuards 用法
// → 必须手写 handleConnection 里验

实战方案 B(推荐):抽 Service,多 Gateway 共享

typescript
// ws/ws-auth.service.ts
@Injectable()
export class WsAuthService {
  constructor(private readonly jwt: JwtService) {}

  async authenticate(client: Socket): Promise<boolean> {
    const token = client.handshake.auth?.token
               ?? client.handshake.headers.authorization;
    if (!token) return false;

    try {
      const payload = await this.jwt.verifyAsync(token.replace('Bearer ', ''));
      client.data.user = payload;                    // 挂到 client.data
      return true;
    } catch {
      return false;
    }
  }
}
typescript
// chat/chat.gateway.ts
@Injectable()
export class ChatGateway implements OnGatewayConnection {
  constructor(private readonly auth: WsAuthService) {}

  async handleConnection(client: Socket) {
    const ok = await this.auth.authenticate(client);
    if (!ok) return client.disconnect();
  }
}
typescript
// notification/notification.gateway.ts(也用同一个)
@Injectable()
export class NotificationGateway implements OnGatewayConnection {
  constructor(private readonly auth: WsAuthService) {}

  async handleConnection(client: Socket) {
    const ok = await this.auth.authenticate(client);
    if (!ok) return client.disconnect();
  }
}

WsAuthService 在 CommonModule 的 providers 里声明一次就行,所有 Gateway 通过 DI 拿。


前端 token 两个来源:

typescript
// 后端取 token 的两个位置
const token =
  client.handshake.auth?.token                  // ① 前端 auth(推荐)
  ?? client.handshake.headers.authorization;    // ② 前端 header
javascript
// 前端 A:auth(socket.io 专门支持,✅ 推荐)
const socket = io('/chat', {
  auth: { token: localStorage.getItem('token') }
  //               ↑ 随握手一起发,socket.io 自动放 handshake.auth
});

// 前端 B:extraHeaders(走 HTTP 头)
const socket = io('/chat', {
  extraHeaders: { authorization: 'Bearer xxx' }
  //                          ↑ 进 handshake.headers.authorization
});

两种方式对比:

前端写法后端取适用
auth: { token }client.handshake.auth.token✅ 推荐
extraHeaders: { authorization }client.handshake.headers.authorization兼容老代码

记忆口诀:

  • @UseGuards HTTP 专属,WS 用不上 → 手写 handleConnection
  • 多 Gateway 抽 WsAuthService 共享 → 实战推荐
  • 前端 token 用 auth 字段(socket.io 专门支持)
  • 成功后挂到 client.data.user(后续 @SubscribeMessage 用 @ConnectedSocket() client 拿)

九、从外部触发推送 ​

typescript
@Injectable()
export class NotificationsService {
  constructor(@Inject(ChatGateway) private readonly gw: ChatGateway) {}

  notify(userId: number, text: string) {
    this.gw.server.to(`user:${userId}`).emit('notify', { text });
  }
}

⚠️ 坑 3:Gateway 不导出时注入不进来 → Gateway 必须在 providers 里声明。

十、心跳与重连 ​

socket.io 默认就内置两套机制:心跳(25s 探活) + 断线自动重连。

心跳(判定"连接还活着吗"):

server → 浏览器: ping              (每 25s 一次)
浏览器 → server: pong              (浏览器自动回,不用写代码)
       ↓
25s 内 server 没收到 pong = 判定掉线
                   ↓
         触发 socket disconnect

DevTools → Network → WS 帧里能看到 ping/pong 包。

断线自动重连(掉线后自动 reconnect):

正在连着 → 突然网断了
   ↓
socket 立刻触发 disconnect
   ↓
socket.io 自动开始重连(默认无限次)
   ↓
重连成功 → 触发 reconnect 事件

两套机制的关系:

机制触发方频率失败时
心跳 ping/pongserver → browser25s25s 没回 = 判定掉线
重连客户端 socket掉线后自动1s→2s→4s→5s 一直重

实战时间线:

13:00:00   浏览器 ←→ server    连接正常(每 25s ping/pong)
13:05:30   网络突然断了
13:05:30   server 25s 没收到 pong → 判定掉线
13:05:30   浏览器 socket 收到 disconnect
13:05:31   socket.io 自动开始重连(1s 后第一次)
13:05:35   重连成功
13:05:35   浏览器 socket.on('reconnect', ...) 触发

为什么需要这两套? WS 不像 HTTP 有"请求-响应"自动断流,WS 连上后双方都不知道对面还活着没,必须靠 ping/pong 探活;一旦探活失败,客户端靠重连拉起新连接。

客户端可选:

javascript
const socket = io(url, {
  reconnection: true,           // 默认 true,自动重连
  reconnectionAttempts: 5,       // 最多重试几次(默认 Infinity 无限)
  reconnectionDelay: 1000,       // 第一次等 1s 再重试
  // 后面按"指数退避":1s → 2s → 4s → 5s → 5s...(封顶 reconnectionDelayMax: 5000)
});

监听重连事件:

javascript
socket.on('reconnect', (n) => console.log(`第 ${n} 次重连成功`));
socket.on('reconnect_failed', () => toast.error('连接断开,请刷新'));

实战坑:重连后要重发 join

javascript
// 重连后,服务端 client.data.user 没了,房间订阅也没了
// 必须重发 join 否则收不到房间消息
socket.on('reconnect', () => {
  socket.emit('join', { room: currentRoom });     // 重新进房间
});

心跳机制(默认就有,不用配):

server 每 25s 发 ping → 客户端自动回 pong
→ 25s 没回 = 掉线,socket.io 触发重连

默认参数:

配置默认改不改
reconnectiontrue默认就好
reconnectionAttemptsInfinity建议设 5~10(无限重连是浪费)
reconnectionDelay1000默认就好
reconnectionDelayMax5000大流量服务可调到 10000

十一、实战:聊天室 ​

后端:

typescript
// chat/chat.gateway.ts
@WebSocketGateway({ namespace: '/chat' })              // 命名空间 /chat
export class ChatGateway {
  @WebSocketServer() server: Server;

  @SubscribeMessage('join')
  join(
    @MessageBody() { room }: { room: string },
    @ConnectedSocket() client: Socket,
  ) {
    client.join(room);                                 // 加 socket.io 房间(自带维护)
    this.server.to(room).emit('system', `${client.data.user?.username ?? client.id} 加入`);
  }

  @SubscribeMessage('msg')
  msg(
    @MessageBody() data: { room: string; text: string },
    @ConnectedSocket() client: Socket,
  ) {
    this.server.to(data.room).emit('msg', {
      from: client.data.user?.username ?? client.id,
      text: data.text,
      ts: Date.now(),
    });
  }
}
typescript
// chat/chat.module.ts
@Module({
  providers: [ChatGateway],
})
export class ChatModule {}
typescript
// app.module.ts
@Module({
  imports: [ChatModule],
})
export class AppModule {}

前端:

html
<!-- index.html -->
<div id="app">
  <input id="token" placeholder="JWT token">
  <input id="room" placeholder="房间名" value="r1">
  <button onclick="connect()">连接</button>

  <div id="log" style="height: 300px; overflow-y: scroll; border: 1px solid #ccc"></div>

  <input id="input" placeholder="说点什么...">
  <button onclick="send()">发送</button>
</div>

<script src="https://cdn.socket.io/4.7.0/socket.io.min.js"></script>
<script>
let socket;

function connect() {
  const token = document.getElementById('token').value;
  if (!token) return alert('请先填 JWT token');

  // ① 连 '/chat' 命名空间,带 token 走握手
  socket = io('/chat', {
    transports: ['websocket'],
    auth: { token },                                  // → 后端 client.handshake.auth.token
  });

  socket.on('connect', () => {
    const room = document.getElementById('room').value;
    socket.emit('join', { room });                    // ② 加入房间
    log('✅ 已连上,加入房间 ' + room);
  });

  socket.on('disconnect', () => log('❌ 断开'));
  socket.on('reconnect', () => {
    const room = document.getElementById('room').value;
    socket.emit('join', { room });                    // ③ 重连后重发 join(房间订阅丢了)
    log('🔁 重连成功');
  });

  socket.on('system', msg => log(`📢 ${msg}`));
  socket.on('msg', payload => {
    log(`💬 ${payload.from}: ${payload.text}`);
  });
}

function send() {
  const text = document.getElementById('input').value;
  const room = document.getElementById('room').value;
  if (!socket || !text) return;
  socket.emit('msg', { room, text });                 // ④ 发消息
  document.getElementById('input').value = '';
}

function log(msg) {
  const div = document.getElementById('log');
  div.innerHTML += msg + '<br>';
  div.scrollTop = div.scrollHeight;
}
</script>

演示步骤:

bash
# 1. 起 NestJS 服务
pnpm start

# 2. 拿 JWT(用之前章节的 /auth/login)
TOKEN=$(curl -s -X POST http://localhost:3000/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"tom","password":"123456"}' | jq -r '.access_token')

# 3. 浏览器开两个标签页
#    标签 A:粘 token1 → 房间 r1 → 发 "hello"
#    标签 B:粘 token2 → 房间 r1 → 看到 A 发的消息 + 自己发的也收到

流程图:

标签 A                              NestJS                              标签 B
  │── handshake auth.token ───────────→                                   │
  │                          handleConnection:                          │
  │                          client1.data.user = { id:1 }              │
  │── emit 'join' {room:'r1'} ───────→                                   │
  │                       server.to(r1).emit('system','A 加入')          │
  │ ←─────────────────────────────────────────────────────────────────→  │
  │                       (A、B 都收到 system)                         │
  │                                   │ ←────── handshake TOKEN2 ──────  │
  │                                   │ client2.data.user = { id:2 }    │
  │                                   │ ←────── emit 'join' {r1} ──────  │
  │ ←─────────────────────────────────────────────────────────────────→  │
  │                       'system': B 加入(A、B 都收到)                 │
  │── emit 'msg' {room:'r1','hi'} ───→                                   │
  │                  server.to(r1).emit('msg', {from:A, text:'hi'})     │
  │ ←─────────────────────────────────────────────────────────────────→  │
  │                       (A、B 都看到 'A: hi')                          │

实战要点:

关键说明
from: client.data.user?.username ?? client.id优先用登录用户名,未登录时退化成 client.id(类似 #AbCdEf)
重连后 socket.emit('join', { room })服务端的 client.join() 没持久化,重连后要重发
server.to(room) 不发服务端的 socket只是推给客户端,服务端没"自己"
client.to(room) 排除发消息的人实战"正在输入"提示用

十二、本章小结 ​

要点关键
装饰器@WebSocketGateway + @SubscribeMessage
注入@WebSocketServer 拿 Server
房间client.join/leave + server.to(room).emit
认证handleConnection 校验 JWT
命名空间namespace: '/chat'
推送任意 Service 持有 Gateway 即可发

动手练习 ​

  1. 回声:写 Gateway,客户端发 ping 服务端回 pong
  2. 房间:写 join/msg 事件,房间内广播
  3. JWT:在 handleConnection 校验 token,失败 disconnect

下一章:第 22 章:微服务 →

本站基于 VitePress 构建 · 由 StackHub 团队维护