第 21 章:WebSocket
学习目标
- 集成 @nestjs/websockets + socket.io
- 写 Gateway 和事件处理
- 实现房间和广播
- 避开 3 个 WebSocket 坑
一、安装
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io二、最简 Gateway
// 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 };
}
}逐行拆解:
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 => ...) 触发注册:
@Module({
providers: [ChatGateway], // Gateway 当 Provider 注入(跟 Service 一样)
})
export class ChatModule {}
// ↑ 必须放 providers 里,NestJS DI 才能 new 它
// 否则 @WebSocketServer() server 拿不到值三、前端连接
// 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));逐行拆解:
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 上:
// 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。
// 默认行为(socket.io 自动协商)
io('http://localhost:3000');
// 浏览器内部:
// ① 先发起 HTTP 长轮询 GET /socket.io/?transport=polling(保活)
// ② 探测成功后才升级成 WebSocket 协议
//
// 优点:兼容老浏览器、不支持 ws 的网络也能用
// 缺点:握手多一步(慢一点点)
// 强制只用 WebSocket(更快)
io('http://localhost:3000', { transports: ['websocket'] });
// 浏览器:直接发 WebSocket 握手三种传输方式对比:
| transport | 协议 | 何时用 |
|---|---|---|
polling | HTTP 长轮询 | 兜底(老浏览器/企业代理禁用 ws) |
websocket | WebSocket | ✅ 现代浏览器首选 |
['polling', 'websocket'] | 先 polling 再升级 | socket.io 默认(兼容性好) |
实战选择:
// 内部应用/服务对服务 → 强制 websocket(快)
io(url, { transports: ['websocket'] });
// 面向公网用户 → 保留 polling 兜底(默认即可)
io(url); // 不传 transport 配置对应配置也要改后端(如果你强制 websocket):
// 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) │四、命名空间
默认命名空间 = /:
@WebSocketGateway()
// ↑ 省略 namespace,等价于 namespace: '/'对应前端三种连法(都连到同一个 / 命名空间):
io('http://localhost:3000');
io('/');
io('http://localhost:3000/');显式命名空间:
@WebSocketGateway({ namespace: '/chat' })
export class ChatGateway {}对应前端两种连法:
io('http://localhost:3000/chat'); // 完整 URL
io('/chat'); // 相对路径(推荐)命名空间 vs 房间:
| 概念 | 维度 | 比喻 |
|---|---|---|
| 命名空间 | Gateway 级别(一个 Gateway 一个) | 不同的 App(微信、QQ) |
| 房间 | 客户端级别(一个 Gateway 多个) | 一个 App 里的不同群 |
实战选择:
// 大型项目 → 命名空间拆分业务
@WebSocketGateway({ namespace: '/chat' }) class ChatGateway
@WebSocketGateway({ namespace: '/notification' }) class NotificationGateway
// 小项目 → 1 个 Gateway + 内部分房间
@WebSocketGateway() class ChatGateway
// 内部 client.join('r1')/client.join('r2')五、生命周期
@WebSocketGateway()
export class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
handleConnection(client: Socket) {
console.log('connected', client.id);
}
handleDisconnect(client: Socket) {
console.log('disconnect', client.id);
}
}六、房间(Room)
@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 个客户端(沉默失败,没报错)内部维护一张表:
client.join(data.room)
// 服务端内部维护:
// room 'r1' → [client_id 'abc', client_id 'def', ...]
// ↑↑↑↑↑
// 房间里有哪些客户端server.to vs client.to 区别:
// 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
@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)
@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?
// ✅ 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 共享
// 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;
}
}
}// 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();
}
}// 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 两个来源:
// 后端取 token 的两个位置
const token =
client.handshake.auth?.token // ① 前端 auth(推荐)
?? client.handshake.headers.authorization; // ② 前端 header// 前端 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 | 兼容老代码 |
记忆口诀:
@UseGuardsHTTP 专属,WS 用不上 → 手写handleConnection- 多 Gateway 抽
WsAuthService共享 → 实战推荐 - 前端 token 用
auth字段(socket.io 专门支持) - 成功后挂到
client.data.user(后续 @SubscribeMessage 用@ConnectedSocket() client拿)
九、从外部触发推送
@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 disconnectDevTools → Network → WS 帧里能看到 ping/pong 包。
断线自动重连(掉线后自动 reconnect):
正在连着 → 突然网断了
↓
socket 立刻触发 disconnect
↓
socket.io 自动开始重连(默认无限次)
↓
重连成功 → 触发 reconnect 事件两套机制的关系:
| 机制 | 触发方 | 频率 | 失败时 |
|---|---|---|---|
| 心跳 ping/pong | server → browser | 25s | 25s 没回 = 判定掉线 |
| 重连 | 客户端 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 探活;一旦探活失败,客户端靠重连拉起新连接。
客户端可选:
const socket = io(url, {
reconnection: true, // 默认 true,自动重连
reconnectionAttempts: 5, // 最多重试几次(默认 Infinity 无限)
reconnectionDelay: 1000, // 第一次等 1s 再重试
// 后面按"指数退避":1s → 2s → 4s → 5s → 5s...(封顶 reconnectionDelayMax: 5000)
});监听重连事件:
socket.on('reconnect', (n) => console.log(`第 ${n} 次重连成功`));
socket.on('reconnect_failed', () => toast.error('连接断开,请刷新'));实战坑:重连后要重发 join
// 重连后,服务端 client.data.user 没了,房间订阅也没了
// 必须重发 join 否则收不到房间消息
socket.on('reconnect', () => {
socket.emit('join', { room: currentRoom }); // 重新进房间
});心跳机制(默认就有,不用配):
server 每 25s 发 ping → 客户端自动回 pong
→ 25s 没回 = 掉线,socket.io 触发重连默认参数:
| 配置 | 默认 | 改不改 |
|---|---|---|
reconnection | true | 默认就好 |
reconnectionAttempts | Infinity | 建议设 5~10(无限重连是浪费) |
reconnectionDelay | 1000 | 默认就好 |
reconnectionDelayMax | 5000 | 大流量服务可调到 10000 |
十一、实战:聊天室
后端:
// 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(),
});
}
}// chat/chat.module.ts
@Module({
providers: [ChatGateway],
})
export class ChatModule {}// app.module.ts
@Module({
imports: [ChatModule],
})
export class AppModule {}前端:
<!-- 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>演示步骤:
# 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 即可发 |
动手练习
- 回声:写 Gateway,客户端发
ping服务端回pong - 房间:写
join/msg事件,房间内广播 - JWT:在
handleConnection校验 token,失败 disconnect
下一章:第 22 章:微服务 →