主题
WebSocket Socket.IO Resource
本页对应 nest g resource chat 后生成的 src/chat。选择 WebSocket Gateway 后,CLI 保留 Module、Service、DTO、Entity,但将 HTTP Controller 换成 Gateway;每个 CRUD 动作变为由 @SubscribeMessage() 订阅的 Socket.IO 事件。
bash
nest g resource chatbash
src/chat
├── dto
│ ├── create-chat.dto.ts
│ └── update-chat.dto.ts
├── entities
│ └── chat.entity.ts
├── chat.gateway.ts
├── chat.module.ts
└── chat.service.tsmermaid
sequenceDiagram
participant C as Socket.IO Client
participant G as ChatGateway
participant S as ChatService
participant R as Repository
C->>G: emit("chat:create", payload)
G->>S: create(payload)
S->>R: 写入消息
R-->>S: ChatMessage
S-->>G: ChatMessage
G-->>C: emit("chat:created", message)生成代码的结构
| HTTP Resource | WebSocket Resource | 职责 |
|---|---|---|
UsersController | ChatGateway | 接收外部输入并委托 Service |
@Get()、@Post() | @SubscribeMessage('event') | 映射请求或事件 |
@Param()、@Body() | @MessageBody()、@ConnectedSocket() | 提取输入与客户端连接 |
HTTP 状态码、HttpException | 事件响应、WsException | 传达处理结果或错误 |
参考源码的 @WebSocketGateway() 未传配置,使用默认命名空间和当前 HTTP 服务端口。main.ts 的 app.setGlobalPrefix('api') 只影响 HTTP 路由,不会为 Socket.IO 事件加 api 前缀。
完整的聊天事件示例
1. DTO 与消息实体
ts
// dto/create-chat.dto.ts
import { IsString, IsUUID, MaxLength, MinLength } from 'class-validator'
export class CreateChatDto {
@IsUUID()
roomId!: string
@IsString()
@MinLength(1)
@MaxLength(2000)
content!: string
}ts
// entities/chat.entity.ts
export class ChatMessage {
id!: number
roomId!: string
content!: string
createdAt!: Date
}全局 ValidationPipe 同样可以用于 Gateway;本例也在 Gateway 内显式注册,保证该模块独立时仍校验事件负载。
2. Service:与传输层无关
ts
import { Injectable } from '@nestjs/common'
import { CreateChatDto } from './dto/create-chat.dto'
import { ChatMessage } from './entities/chat.entity'
@Injectable()
export class ChatService {
private readonly messages: ChatMessage[] = []
private nextId = 1
create(input: CreateChatDto): ChatMessage {
const message: ChatMessage = {
id: this.nextId++,
...input,
createdAt: new Date(),
}
this.messages.push(message)
return message
}
findByRoom(roomId: string): ChatMessage[] {
return this.messages.filter((message) => message.roomId === roomId)
}
}Service 不依赖 Socket 或 Server。它可以被 Gateway、HTTP Controller、队列消费者或测试直接复用;持久化实现替换内存数组即可。
3. Gateway:连接、房间、事件
ts
import { UsePipes, ValidationPipe } from '@nestjs/common'
import {
ConnectedSocket,
MessageBody,
SubscribeMessage,
WebSocketGateway,
WebSocketServer,
WsException,
} from '@nestjs/websockets'
import type { Server, Socket } from 'socket.io'
import { ChatService } from './chat.service'
import { CreateChatDto } from './dto/create-chat.dto'
@WebSocketGateway({
namespace: 'chat',
cors: { origin: ['http://localhost:5173'] },
})
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }))
export class ChatGateway {
@WebSocketServer()
private server!: Server
constructor(private readonly chatService: ChatService) {}
@SubscribeMessage('chat:join')
join(@MessageBody('roomId') roomId: string, @ConnectedSocket() client: Socket) {
if (!roomId) throw new WsException('roomId 不能为空')
client.join(roomId)
client.emit('chat:joined', { roomId })
}
@SubscribeMessage('chat:create')
create(@MessageBody() input: CreateChatDto) {
const message = this.chatService.create(input)
this.server.to(message.roomId).emit('chat:created', message)
}
@SubscribeMessage('chat:list')
list(@MessageBody('roomId') roomId: string, @ConnectedSocket() client: Socket) {
client.emit('chat:list:result', this.chatService.findByRoom(roomId))
}
}@SubscribeMessage()的字符串是事件名,不是 URL。@MessageBody()取得客户端emit()的负载;按字段提取可写成@MessageBody('roomId')。@ConnectedSocket()取得当前连接,用于加入房间或读取已认证用户。@WebSocketServer()注入 Socket.IOServer,server.to(roomId).emit()广播给房间内全部客户端。- 使用
client.emit()仅回复发起事件的连接;使用server.emit()则广播给所有连接。
如果不需要命名空间,去掉 namespace: 'chat',客户端连接根命名空间即可。cors.origin 生产环境必须替换为明确前端来源,不能设置为任意来源并携带凭据。
4. Module
ts
import { Module } from '@nestjs/common'
import { ChatGateway } from './chat.gateway'
import { ChatService } from './chat.service'
@Module({
providers: [ChatGateway, ChatService],
})
export class ChatModule {}Gateway 是 Provider,必须出现在 providers 中;它不属于 controllers。AppModule 导入 ChatModule 后,Nest 才会创建 Gateway 并注册事件处理器。
5. Socket.IO 客户端
浏览器客户端安装 socket.io-client:
bash
pnpm add socket.io-clientts
import { io } from 'socket.io-client'
const socket = io('http://localhost:3000/chat')
socket.on('connect', () => {
socket.emit('chat:join', { roomId: 'd933c8be-73a9-4e99-8fb1-5c21e8792f20' })
socket.emit('chat:create', {
roomId: 'd933c8be-73a9-4e99-8fb1-5c21e8792f20',
content: 'Hello, Socket.IO!',
})
})
socket.on('chat:created', (message) => {
console.log(message)
})
socket.on('exception', (error) => {
console.error(error)
})/chat 是命名空间,不是 HTTP 全局前缀。Socket.IO 使用自己的握手与事件协议;它不是原生 WebSocket API 的简单别名,客户端必须使用兼容的 Socket.IO 客户端。
上线前补充
- 在握手阶段验证令牌,并将已认证用户挂到
socket.data;事件中还需校验房间成员资格。 - 限制每条消息长度、事件频率和单连接房间数;高频事件需要限流与背压策略。
- 多实例部署时使用兼容的 Socket.IO 适配器共享房间与广播状态,不能只依赖进程内内存。
- 存储聊天记录、审计日志和异常格式;不要将鉴权信息或敏感负载写入日志。
