这节我们把 websocket 的后端部分写一下。
首先支持下跨域访问:

我们按照这个事件来设计服务端:
服务端事件
| 方向 | 事件 | 说明 |
|---|---|---|
| → 客户端 | worldRoster | { players: { userId, username }[] },当前已在房里的其他人 |
| → 客户端 | playerJoined | 有人进房 |
| → 客户端 | playerLeft | 有人离开 |
| → 客户端 | playerState | 他人状态;由下面 state 转发,并带上 userId、username |
| 客户端 → | state | 任意可序列化对象(如 position),会合并进 playerState 发给其他人 |
worldRoster 是服务端给客户端推送当前的所有玩家
playerJoined 是推送有人进入房间的消息
playerState 是某个用户的 state 更新
state 是客户端给服务端发的更新某个人 state 的逻辑
按照这个来写一下 websocket 的逻辑:
创建 src/world.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { WorldGateway } from './world.gateway';
@Module({
imports: [
JwtModule.register({
secret: process.env.JWT_SECRET ?? 'open-world-dev-secret',
signOptions: { expiresIn: '7d' },
}),
],
providers: [WorldGateway],
})
export class WorldModule {}
实现这个 WorldGateway
import { JwtService } from '@nestjs/jwt';
import {
OnGatewayConnection,
OnGatewayDisconnect,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
const WORLD_ROOM = 'world';
@WebSocketGateway({
namespace: '/world',
cors: { origin: '*' },
})
export class WorldGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer()
server: Server;
constructor(private readonly jwtService: JwtService) {}
async handleConnection(client: Socket) {
const token = this.extractToken(client);
if (!token) {
client.disconnect(true);
return;
}
try {
const payload = await this.jwtService.verifyAsync<{
sub: number;
username: string;
}>(token);
client.data.userId = payload.sub;
client.data.username = payload.username;
await client.join(WORLD_ROOM);
const sockets = await this.server.in(WORLD_ROOM).fetchSockets();
const others = sockets
.filter((s) => s.id !== client.id)
.map((s) => ({
userId: s.data.userId as number,
username: s.data.username as string,
}));
client.emit('worldRoster', { players: others });
client.to(WORLD_ROOM).emit('playerJoined', {
userId: payload.sub,
username: payload.username,
});
} catch {
client.disconnect(true);
}
}
handleDisconnect(client: Socket) {
const userId = client.data?.userId as number | undefined;
if (userId != null) {
this.server.to(WORLD_ROOM).emit('playerLeft', { userId });
}
}
@SubscribeMessage('state')
handleState(client: Socket, payload: unknown) {
if (client.data.userId == null) return;
const extra =
payload && typeof payload === 'object' && !Array.isArray(payload)
? (payload as Record<string, unknown>)
: {};
client.to(WORLD_ROOM).emit('playerState', {
userId: client.data.userId as number,
username: client.data.username as string,
...extra,
});
}
private extractToken(client: Socket): string | undefined {
const auth = client.handshake.auth as { token?: string } | undefined;
if (auth?.token && typeof auth.token === 'string') return auth.token;
const q = client.handshake.query?.token;
if (typeof q === 'string') return q;
if (Array.isArray(q) && typeof q[0] === 'string') return q[0];
return undefined;
}
}
首先,客户端向服务端发的消息是这样监听:

收到 state 消息后,拿到 payload 里的信息,通过 playerState 事件广播到房间内的所有 client
建立连接的时候还要做一下鉴权:
取出 jwt 的 token,校验下,拿到其中的 username 的信息,广播给所有的客户端 playerJoined 事件

理解逻辑就行,细节不用关注,AI 时代主要还是理解思路。
在 AppModule 引入这个模块:

这样,ws 的服务端部分就写好了。
安装下用到的依赖:
pnpm install @nestjs/websockets socket.io
总结
这节实现了 ws 的服务端,设计了 4 个事件:
worldRoster 所有的 player
playerJoined 新的 player 加入
playerState 某个 player 的状态
state player 的 state 更新
下节基于这 4 个实现来实现前端和后端的 ws 通信。