feature/websocket-unify-and-openapi-update #38
@@ -42,7 +42,6 @@ import {
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../auth/jwt_auth.guard';
|
||||
import { ZulipService } from './zulip.service';
|
||||
import { ZulipWebSocketGateway } from './zulip_websocket.gateway';
|
||||
import { CleanWebSocketGateway } from './clean_websocket.gateway';
|
||||
import {
|
||||
SendChatMessageDto,
|
||||
@@ -106,7 +105,7 @@ export class ChatController {
|
||||
// 注意:这里需要一个有效的 socketId,但 REST API 没有 WebSocket 连接
|
||||
// 这是一个限制,实际使用中应该通过 WebSocket 发送消息
|
||||
throw new HttpException(
|
||||
'聊天消息发送需要通过 WebSocket 连接。请使用 WebSocket 接口:ws://localhost:3000/game',
|
||||
'聊天消息发送需要通过 WebSocket 连接。请使用 WebSocket 接口:wss://whaletownend.xinghangee.icu',
|
||||
HttpStatus.BAD_REQUEST,
|
||||
);
|
||||
|
||||
@@ -318,7 +317,7 @@ export class ChatController {
|
||||
properties: {
|
||||
websocketUrl: {
|
||||
type: 'string',
|
||||
example: 'ws://localhost:3000/game',
|
||||
example: 'wss://whaletownend.xinghangee.icu/game',
|
||||
description: 'WebSocket 连接地址'
|
||||
},
|
||||
namespace: {
|
||||
@@ -347,7 +346,9 @@ export class ChatController {
|
||||
})
|
||||
async getWebSocketInfo() {
|
||||
return {
|
||||
websocketUrl: 'ws://localhost:3001',
|
||||
websocketUrl: 'wss://whaletownend.xinghangee.icu/game',
|
||||
protocol: 'native-websocket',
|
||||
path: '/game',
|
||||
namespace: '/',
|
||||
supportedEvents: [
|
||||
'login', // 用户登录
|
||||
|
||||
@@ -31,9 +31,12 @@ export class CleanWebSocketGateway implements OnModuleInit, OnModuleDestroy {
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
const port = 3001;
|
||||
const port = process.env.WEBSOCKET_PORT ? parseInt(process.env.WEBSOCKET_PORT) : 3001;
|
||||
|
||||
this.server = new WebSocket.Server({ port });
|
||||
this.server = new WebSocket.Server({
|
||||
port,
|
||||
path: '/game' // 统一使用 /game 路径
|
||||
});
|
||||
|
||||
this.server.on('connection', (ws: ExtendedWebSocket) => {
|
||||
ws.id = this.generateClientId();
|
||||
@@ -71,7 +74,7 @@ export class CleanWebSocketGateway implements OnModuleInit, OnModuleDestroy {
|
||||
});
|
||||
});
|
||||
|
||||
this.logger.log(`WebSocket服务器启动成功,端口: ${port}`);
|
||||
this.logger.log(`WebSocket服务器启动成功,端口: ${port},路径: /game`);
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
|
||||
@@ -47,7 +47,7 @@ export class WebSocketDocsController {
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
example: 'ws://localhost:3000/game',
|
||||
example: 'wss://whaletownend.xinghangee.icu/game',
|
||||
description: 'WebSocket 连接地址'
|
||||
},
|
||||
namespace: {
|
||||
@@ -92,8 +92,8 @@ export class WebSocketDocsController {
|
||||
getWebSocketDocs() {
|
||||
return {
|
||||
connection: {
|
||||
url: 'ws://localhost:3000/game',
|
||||
namespace: '/game',
|
||||
url: 'wss://whaletownend.xinghangee.icu/game',
|
||||
namespace: '/',
|
||||
transports: ['websocket', 'polling'],
|
||||
options: {
|
||||
timeout: 20000,
|
||||
@@ -262,52 +262,52 @@ export class WebSocketDocsController {
|
||||
examples: {
|
||||
javascript: {
|
||||
connection: `
|
||||
// 使用 Socket.IO 客户端连接
|
||||
const io = require('socket.io-client');
|
||||
// 使用原生 WebSocket 客户端连接
|
||||
const ws = new WebSocket('wss://whaletownend.xinghangee.icu/game');
|
||||
|
||||
const socket = io('ws://localhost:3000/game', {
|
||||
transports: ['websocket', 'polling'],
|
||||
timeout: 20000,
|
||||
forceNew: true,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 3,
|
||||
reconnectionDelay: 1000
|
||||
});
|
||||
|
||||
// 连接成功
|
||||
socket.on('connect', () => {
|
||||
console.log('连接成功:', socket.id);
|
||||
ws.onopen = function() {
|
||||
console.log('连接成功');
|
||||
|
||||
// 发送登录消息
|
||||
socket.emit('login', {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'login',
|
||||
token: 'YOUR_JWT_TOKEN_HERE'
|
||||
});
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
// 登录成功
|
||||
socket.on('login_success', (data) => {
|
||||
ws.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('收到消息:', data);
|
||||
|
||||
// 处理不同类型的消息
|
||||
if (data.t === 'login_success') {
|
||||
console.log('登录成功:', data);
|
||||
|
||||
// 发送聊天消息
|
||||
socket.emit('chat', {
|
||||
ws.send(JSON.stringify({
|
||||
t: 'chat',
|
||||
content: '大家好!',
|
||||
scope: 'local'
|
||||
});
|
||||
});
|
||||
|
||||
// 接收聊天消息
|
||||
socket.on('chat_render', (data) => {
|
||||
}));
|
||||
} else if (data.t === 'chat_render') {
|
||||
console.log('收到消息:', data.from, '说:', data.txt);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function(event) {
|
||||
console.log('连接关闭:', event.code, event.reason);
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
console.error('连接错误:', error);
|
||||
};
|
||||
`,
|
||||
godot: `
|
||||
# Godot WebSocket 客户端示例
|
||||
extends Node
|
||||
|
||||
var socket = WebSocketClient.new()
|
||||
var url = "ws://localhost:3000/game"
|
||||
var url = "wss://whaletownend.xinghangee.icu/game"
|
||||
|
||||
func _ready():
|
||||
socket.connect("connection_closed", self, "_closed")
|
||||
@@ -320,18 +320,18 @@ func _ready():
|
||||
print("连接失败")
|
||||
|
||||
func _connected(protocol):
|
||||
print("WebSocket 连接成功")
|
||||
# 发送登录消息
|
||||
var login_msg = {
|
||||
"type": "login",
|
||||
"token": "YOUR_JWT_TOKEN_HERE"
|
||||
}
|
||||
socket.get_peer(1).put_packet(JSON.print(login_msg).to_utf8())
|
||||
print("连接成功")
|
||||
|
||||
func _on_data():
|
||||
var packet = socket.get_peer(1).get_packet()
|
||||
var message = JSON.parse(packet.get_string_from_utf8())
|
||||
print("收到消息: ", message.result)
|
||||
|
||||
func _closed(was_clean_close):
|
||||
print("连接关闭")
|
||||
|
||||
func _error():
|
||||
print("连接错误")
|
||||
`
|
||||
}
|
||||
},
|
||||
|
||||
817
src/business/zulip/websocket_openapi.controller.ts
Normal file
817
src/business/zulip/websocket_openapi.controller.ts
Normal file
@@ -0,0 +1,817 @@
|
||||
/**
|
||||
* WebSocket OpenAPI 文档控制器
|
||||
*
|
||||
* 专门用于在OpenAPI/Swagger中展示WebSocket接口
|
||||
* 通过REST API的方式描述WebSocket的消息格式和交互流程
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-09
|
||||
*/
|
||||
|
||||
import { Controller, Get, Post, Body } from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiProperty,
|
||||
ApiExtraModels,
|
||||
} from '@nestjs/swagger';
|
||||
|
||||
// WebSocket 消息格式 DTO
|
||||
class WebSocketLoginRequest {
|
||||
@ApiProperty({
|
||||
description: '消息类型',
|
||||
example: 'login',
|
||||
enum: ['login']
|
||||
})
|
||||
type: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'JWT认证令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
})
|
||||
token: string;
|
||||
}
|
||||
|
||||
class WebSocketLoginSuccessResponse {
|
||||
@ApiProperty({
|
||||
description: '响应类型',
|
||||
example: 'login_success'
|
||||
})
|
||||
t: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '会话ID',
|
||||
example: '89aff162-52d9-484e-9a35-036ba63a2280'
|
||||
})
|
||||
sessionId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户ID',
|
||||
example: 'user_123'
|
||||
})
|
||||
userId: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户名',
|
||||
example: 'Player_123'
|
||||
})
|
||||
username: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '当前地图',
|
||||
example: 'whale_port'
|
||||
})
|
||||
currentMap: string;
|
||||
}
|
||||
|
||||
class WebSocketChatRequest {
|
||||
@ApiProperty({
|
||||
description: '消息类型',
|
||||
example: 'chat',
|
||||
enum: ['chat']
|
||||
})
|
||||
t: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '消息内容',
|
||||
example: '大家好!我刚进入游戏',
|
||||
maxLength: 1000
|
||||
})
|
||||
content: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '消息范围',
|
||||
example: 'local',
|
||||
enum: ['local', 'global']
|
||||
})
|
||||
scope: string;
|
||||
}
|
||||
|
||||
class WebSocketChatResponse {
|
||||
@ApiProperty({
|
||||
description: '响应类型',
|
||||
example: 'chat_render'
|
||||
})
|
||||
t: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '发送者用户名',
|
||||
example: 'Player_456'
|
||||
})
|
||||
from: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '消息内容',
|
||||
example: '欢迎新玩家!'
|
||||
})
|
||||
txt: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '是否显示气泡',
|
||||
example: true
|
||||
})
|
||||
bubble: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '消息范围',
|
||||
example: 'local'
|
||||
})
|
||||
scope: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '地图ID(本地消息时)',
|
||||
example: 'whale_port',
|
||||
required: false
|
||||
})
|
||||
mapId?: string;
|
||||
}
|
||||
|
||||
class WebSocketPositionRequest {
|
||||
@ApiProperty({
|
||||
description: '消息类型',
|
||||
example: 'position'
|
||||
})
|
||||
t: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'X坐标',
|
||||
example: 150
|
||||
})
|
||||
x: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'Y坐标',
|
||||
example: 400
|
||||
})
|
||||
y: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '地图ID',
|
||||
example: 'whale_port'
|
||||
})
|
||||
mapId: string;
|
||||
}
|
||||
|
||||
class WebSocketErrorResponse {
|
||||
@ApiProperty({
|
||||
description: '错误类型',
|
||||
example: 'error'
|
||||
})
|
||||
type: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误消息',
|
||||
example: '请先登录'
|
||||
})
|
||||
message: string;
|
||||
}
|
||||
|
||||
@ApiTags('websocket')
|
||||
@ApiExtraModels(
|
||||
WebSocketLoginRequest,
|
||||
WebSocketLoginSuccessResponse,
|
||||
WebSocketChatRequest,
|
||||
WebSocketChatResponse,
|
||||
WebSocketPositionRequest,
|
||||
WebSocketErrorResponse
|
||||
)
|
||||
@Controller('websocket-api')
|
||||
export class WebSocketOpenApiController {
|
||||
|
||||
@Get('connection-info')
|
||||
@ApiOperation({
|
||||
summary: 'WebSocket 连接信息',
|
||||
description: `
|
||||
获取WebSocket连接的基本信息和配置
|
||||
|
||||
**连接地址**: \`wss://whaletownend.xinghangee.icu/game\`
|
||||
|
||||
**协议**: 原生WebSocket (非Socket.IO)
|
||||
|
||||
**认证**: 需要JWT Token
|
||||
|
||||
**架构更新**:
|
||||
- ✅ 已从Socket.IO迁移到原生WebSocket
|
||||
- ✅ 统一使用 /game 路径
|
||||
- ✅ 支持地图房间管理
|
||||
- ✅ 实现消息广播机制
|
||||
`
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'WebSocket连接配置信息',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: {
|
||||
type: 'string',
|
||||
example: 'wss://whaletownend.xinghangee.icu/game',
|
||||
description: 'WebSocket服务器地址'
|
||||
},
|
||||
protocol: {
|
||||
type: 'string',
|
||||
example: 'native-websocket',
|
||||
description: '使用原生WebSocket协议'
|
||||
},
|
||||
authentication: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
required: { type: 'boolean', example: true },
|
||||
method: { type: 'string', example: 'JWT Token' },
|
||||
tokenFormat: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
issuer: { type: 'string', example: 'whale-town' },
|
||||
audience: { type: 'string', example: 'whale-town-users' },
|
||||
type: { type: 'string', example: 'access' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
supportedMaps: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: ['whale_port', 'pumpkin_valley', 'novice_village']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
getConnectionInfo() {
|
||||
return {
|
||||
url: 'wss://whaletownend.xinghangee.icu/game',
|
||||
protocol: 'native-websocket',
|
||||
path: '/game',
|
||||
port: {
|
||||
development: 3001,
|
||||
production: 'via_nginx_proxy'
|
||||
},
|
||||
authentication: {
|
||||
required: true,
|
||||
method: 'JWT Token',
|
||||
tokenFormat: {
|
||||
issuer: 'whale-town',
|
||||
audience: 'whale-town-users',
|
||||
type: 'access',
|
||||
requiredFields: ['sub', 'username', 'email', 'role']
|
||||
}
|
||||
},
|
||||
supportedMaps: [
|
||||
'whale_port',
|
||||
'pumpkin_valley',
|
||||
'novice_village'
|
||||
],
|
||||
features: [
|
||||
'实时聊天',
|
||||
'位置同步',
|
||||
'地图房间管理',
|
||||
'消息广播',
|
||||
'连接状态监控',
|
||||
'自动重连支持'
|
||||
],
|
||||
messageTypes: {
|
||||
clientToServer: [
|
||||
{
|
||||
type: 'login',
|
||||
description: '用户登录认证',
|
||||
required: ['type', 'token']
|
||||
},
|
||||
{
|
||||
type: 'chat',
|
||||
description: '发送聊天消息',
|
||||
required: ['t', 'content', 'scope']
|
||||
},
|
||||
{
|
||||
type: 'position',
|
||||
description: '更新位置信息',
|
||||
required: ['t', 'x', 'y', 'mapId']
|
||||
}
|
||||
],
|
||||
serverToClient: [
|
||||
{
|
||||
type: 'connected',
|
||||
description: '连接确认'
|
||||
},
|
||||
{
|
||||
type: 'login_success',
|
||||
description: '登录成功'
|
||||
},
|
||||
{
|
||||
type: 'login_error',
|
||||
description: '登录失败'
|
||||
},
|
||||
{
|
||||
type: 'chat_render',
|
||||
description: '接收聊天消息'
|
||||
},
|
||||
{
|
||||
type: 'position_update',
|
||||
description: '位置更新广播'
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
description: '通用错误消息'
|
||||
}
|
||||
]
|
||||
},
|
||||
connectionLimits: {
|
||||
maxConnections: 1000,
|
||||
sessionTimeout: 1800, // 30分钟
|
||||
heartbeatInterval: 30000 // 30秒
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@ApiOperation({
|
||||
summary: '用户登录 (WebSocket消息格式)',
|
||||
description: `
|
||||
**注意**: 这不是真实的REST API端点,而是WebSocket消息格式的文档展示
|
||||
|
||||
通过WebSocket发送此格式的消息来进行用户登录认证
|
||||
|
||||
**WebSocket连接后发送**:
|
||||
\`\`\`json
|
||||
{
|
||||
"type": "login",
|
||||
"token": "your_jwt_token_here"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**成功响应**:
|
||||
\`\`\`json
|
||||
{
|
||||
"t": "login_success",
|
||||
"sessionId": "uuid",
|
||||
"userId": "user_id",
|
||||
"username": "username",
|
||||
"currentMap": "whale_port"
|
||||
}
|
||||
\`\`\`
|
||||
`
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '登录成功响应格式',
|
||||
type: WebSocketLoginSuccessResponse
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: '登录失败响应格式',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
t: { type: 'string', example: 'login_error' },
|
||||
message: { type: 'string', example: 'Token验证失败' }
|
||||
}
|
||||
}
|
||||
})
|
||||
websocketLogin(@Body() loginRequest: WebSocketLoginRequest) {
|
||||
// 这个方法不会被实际调用,仅用于文档展示
|
||||
return {
|
||||
note: '这是WebSocket消息格式文档,请通过WebSocket连接发送消息',
|
||||
websocketUrl: 'wss://whaletownend.xinghangee.icu/game',
|
||||
messageFormat: loginRequest
|
||||
};
|
||||
}
|
||||
|
||||
@Post('chat')
|
||||
@ApiOperation({
|
||||
summary: '发送聊天消息 (WebSocket消息格式)',
|
||||
description: `
|
||||
**注意**: 这不是真实的REST API端点,而是WebSocket消息格式的文档展示
|
||||
|
||||
通过WebSocket发送聊天消息的格式
|
||||
|
||||
**发送消息**:
|
||||
\`\`\`json
|
||||
{
|
||||
"t": "chat",
|
||||
"content": "消息内容",
|
||||
"scope": "local"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**接收消息**:
|
||||
\`\`\`json
|
||||
{
|
||||
"t": "chat_render",
|
||||
"from": "发送者",
|
||||
"txt": "消息内容",
|
||||
"bubble": true,
|
||||
"scope": "local",
|
||||
"mapId": "whale_port"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**消息范围说明**:
|
||||
- \`local\`: 仅当前地图的玩家可见
|
||||
- \`global\`: 所有在线玩家可见
|
||||
`
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '聊天消息广播格式',
|
||||
type: WebSocketChatResponse
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: '发送失败响应',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
t: { type: 'string', example: 'chat_error' },
|
||||
message: { type: 'string', example: '消息内容不能为空' }
|
||||
}
|
||||
}
|
||||
})
|
||||
websocketChat(@Body() chatRequest: WebSocketChatRequest) {
|
||||
return {
|
||||
note: '这是WebSocket消息格式文档,请通过WebSocket连接发送消息',
|
||||
websocketUrl: 'wss://whaletownend.xinghangee.icu/game',
|
||||
messageFormat: chatRequest
|
||||
};
|
||||
}
|
||||
|
||||
@Post('position')
|
||||
@ApiOperation({
|
||||
summary: '位置更新 (WebSocket消息格式)',
|
||||
description: `
|
||||
**注意**: 这不是真实的REST API端点,而是WebSocket消息格式的文档展示
|
||||
|
||||
更新玩家位置信息,支持地图切换
|
||||
|
||||
**发送格式**:
|
||||
\`\`\`json
|
||||
{
|
||||
"t": "position",
|
||||
"x": 150,
|
||||
"y": 400,
|
||||
"mapId": "whale_port"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
**功能说明**:
|
||||
- 自动处理地图房间切换
|
||||
- 向同地图其他玩家广播位置更新
|
||||
- 支持实时位置同步
|
||||
`
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '位置更新成功,无特定响应消息'
|
||||
})
|
||||
websocketPosition(@Body() positionRequest: WebSocketPositionRequest) {
|
||||
return {
|
||||
note: '这是WebSocket消息格式文档,请通过WebSocket连接发送消息',
|
||||
websocketUrl: 'wss://whaletownend.xinghangee.icu/game',
|
||||
messageFormat: positionRequest
|
||||
};
|
||||
}
|
||||
|
||||
@Get('message-flow')
|
||||
@ApiOperation({
|
||||
summary: 'WebSocket 消息流程图',
|
||||
description: '展示WebSocket连接和消息交互的完整流程'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'WebSocket交互流程',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
connectionFlow: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
example: [
|
||||
'1. 建立WebSocket连接到 wss://whaletownend.xinghangee.icu/game',
|
||||
'2. 发送login消息进行认证',
|
||||
'3. 接收login_success确认',
|
||||
'4. 发送chat/position消息进行交互',
|
||||
'5. 接收其他玩家的消息广播'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
getMessageFlow() {
|
||||
return {
|
||||
connectionFlow: [
|
||||
'1. 建立WebSocket连接到 wss://whaletownend.xinghangee.icu/game',
|
||||
'2. 发送login消息进行认证',
|
||||
'3. 接收login_success确认',
|
||||
'4. 发送chat/position消息进行交互',
|
||||
'5. 接收其他玩家的消息广播'
|
||||
],
|
||||
messageTypes: {
|
||||
clientToServer: [
|
||||
'login - 用户登录认证',
|
||||
'chat - 发送聊天消息',
|
||||
'position - 更新位置信息'
|
||||
],
|
||||
serverToClient: [
|
||||
'connected - 连接确认',
|
||||
'login_success/login_error - 登录结果',
|
||||
'chat_sent/chat_error - 消息发送结果',
|
||||
'chat_render - 接收聊天消息',
|
||||
'position_update - 位置更新广播',
|
||||
'error - 通用错误消息'
|
||||
]
|
||||
},
|
||||
exampleSession: {
|
||||
step1: {
|
||||
action: '建立连接',
|
||||
client: 'new WebSocket("wss://whaletownend.xinghangee.icu/game")',
|
||||
server: '{"type":"connected","message":"连接成功","socketId":"ws_123"}'
|
||||
},
|
||||
step2: {
|
||||
action: '用户登录',
|
||||
client: '{"type":"login","token":"jwt_token"}',
|
||||
server: '{"t":"login_success","sessionId":"uuid","userId":"user_123","username":"Player","currentMap":"whale_port"}'
|
||||
},
|
||||
step3: {
|
||||
action: '发送消息',
|
||||
client: '{"t":"chat","content":"Hello!","scope":"local"}',
|
||||
server: '{"t":"chat_sent","messageId":137,"message":"消息发送成功"}'
|
||||
},
|
||||
step4: {
|
||||
action: '接收广播',
|
||||
server: '{"t":"chat_render","from":"Player","txt":"Hello!","bubble":true,"scope":"local","mapId":"whale_port"}'
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Get('testing-tools')
|
||||
@ApiOperation({
|
||||
summary: 'WebSocket 测试工具推荐',
|
||||
description: '推荐的WebSocket测试工具和示例代码'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '测试工具和示例代码',
|
||||
})
|
||||
getTestingTools() {
|
||||
return {
|
||||
onlineTools: [
|
||||
{
|
||||
name: 'WebSocket King',
|
||||
url: 'https://websocketking.com/',
|
||||
description: '在线WebSocket测试工具,支持消息发送和接收'
|
||||
},
|
||||
{
|
||||
name: 'WebSocket Test Client',
|
||||
url: 'https://www.websocket.org/echo.html',
|
||||
description: '简单的WebSocket回显测试'
|
||||
},
|
||||
{
|
||||
name: '内置测试页面',
|
||||
url: '/websocket-test',
|
||||
description: '项目内置的WebSocket测试界面,支持完整功能测试'
|
||||
}
|
||||
],
|
||||
codeExamples: {
|
||||
javascript: `
|
||||
// JavaScript WebSocket 客户端示例
|
||||
const ws = new WebSocket('wss://whaletownend.xinghangee.icu/game');
|
||||
|
||||
ws.onopen = function() {
|
||||
console.log('连接成功');
|
||||
// 发送登录消息
|
||||
ws.send(JSON.stringify({
|
||||
type: 'login',
|
||||
token: 'your_jwt_token_here'
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('收到消息:', data);
|
||||
|
||||
if (data.t === 'login_success') {
|
||||
// 登录成功,发送聊天消息
|
||||
ws.send(JSON.stringify({
|
||||
t: 'chat',
|
||||
content: 'Hello from JavaScript!',
|
||||
scope: 'local'
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function(event) {
|
||||
console.log('连接关闭:', event.code, event.reason);
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
console.error('WebSocket错误:', error);
|
||||
};
|
||||
`,
|
||||
python: `
|
||||
# Python WebSocket 客户端示例
|
||||
import websocket
|
||||
import json
|
||||
import threading
|
||||
|
||||
def on_message(ws, message):
|
||||
data = json.loads(message)
|
||||
print(f"收到消息: {data}")
|
||||
|
||||
if data.get('t') == 'login_success':
|
||||
# 登录成功,发送聊天消息
|
||||
ws.send(json.dumps({
|
||||
't': 'chat',
|
||||
'content': 'Hello from Python!',
|
||||
'scope': 'local'
|
||||
}))
|
||||
|
||||
def on_error(ws, error):
|
||||
print(f"WebSocket错误: {error}")
|
||||
|
||||
def on_close(ws, close_status_code, close_msg):
|
||||
print(f"连接关闭: {close_status_code} - {close_msg}")
|
||||
|
||||
def on_open(ws):
|
||||
print("连接成功")
|
||||
# 发送登录消息
|
||||
ws.send(json.dumps({
|
||||
'type': 'login',
|
||||
'token': 'your_jwt_token_here'
|
||||
}))
|
||||
|
||||
# 创建WebSocket连接
|
||||
ws = websocket.WebSocketApp("wss://whaletownend.xinghangee.icu/game",
|
||||
on_message=on_message,
|
||||
on_error=on_error,
|
||||
on_close=on_close,
|
||||
on_open=on_open)
|
||||
|
||||
# 启动连接
|
||||
ws.run_forever()
|
||||
`,
|
||||
nodejs: `
|
||||
// Node.js WebSocket 客户端示例
|
||||
const WebSocket = require('ws');
|
||||
|
||||
const ws = new WebSocket('wss://whaletownend.xinghangee.icu/game');
|
||||
|
||||
ws.on('open', function() {
|
||||
console.log('连接成功');
|
||||
|
||||
// 发送登录消息
|
||||
ws.send(JSON.stringify({
|
||||
type: 'login',
|
||||
token: 'your_jwt_token_here'
|
||||
}));
|
||||
});
|
||||
|
||||
ws.on('message', function(data) {
|
||||
const message = JSON.parse(data.toString());
|
||||
console.log('收到消息:', message);
|
||||
|
||||
if (message.t === 'login_success') {
|
||||
// 登录成功,发送聊天消息
|
||||
ws.send(JSON.stringify({
|
||||
t: 'chat',
|
||||
content: 'Hello from Node.js!',
|
||||
scope: 'local'
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('close', function(code, reason) {
|
||||
console.log(\`连接关闭: \${code} - \${reason}\`);
|
||||
});
|
||||
|
||||
ws.on('error', function(error) {
|
||||
console.error('WebSocket错误:', error);
|
||||
});
|
||||
`
|
||||
},
|
||||
testingSteps: [
|
||||
'1. 获取有效的JWT Token(通过 /auth/login 接口)',
|
||||
'2. 使用WebSocket客户端连接到 wss://whaletownend.xinghangee.icu/game',
|
||||
'3. 发送login消息进行认证',
|
||||
'4. 验证收到login_success响应',
|
||||
'5. 发送chat消息测试聊天功能',
|
||||
'6. 发送position消息测试位置更新',
|
||||
'7. 观察其他客户端的消息广播'
|
||||
],
|
||||
troubleshooting: {
|
||||
connectionFailed: [
|
||||
'检查网络连接是否正常',
|
||||
'验证WebSocket服务器是否启动',
|
||||
'确认防火墙设置允许WebSocket连接',
|
||||
'检查SSL证书是否有效(WSS连接)'
|
||||
],
|
||||
authenticationFailed: [
|
||||
'验证JWT Token是否有效且未过期',
|
||||
'检查Token格式是否正确',
|
||||
'确认Token包含必需的字段(sub, username, email, role)',
|
||||
'验证Token的issuer和audience是否匹配'
|
||||
],
|
||||
messageFailed: [
|
||||
'确认已完成登录认证',
|
||||
'检查消息格式是否符合API规范',
|
||||
'验证必需字段是否都已提供',
|
||||
'检查消息内容是否符合长度限制'
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Get('architecture')
|
||||
@ApiOperation({
|
||||
summary: 'WebSocket 架构信息',
|
||||
description: '展示WebSocket服务的技术架构和实现细节'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'WebSocket架构信息',
|
||||
})
|
||||
getArchitecture() {
|
||||
return {
|
||||
overview: {
|
||||
title: 'WebSocket 架构概览',
|
||||
description: '基于原生WebSocket的实时通信架构',
|
||||
version: '2.1.0',
|
||||
migrationFrom: 'Socket.IO',
|
||||
migrationDate: '2026-01-09'
|
||||
},
|
||||
technicalStack: {
|
||||
server: {
|
||||
framework: 'NestJS',
|
||||
websocketLibrary: 'ws (原生WebSocket)',
|
||||
adapter: '@nestjs/platform-ws',
|
||||
port: 3001,
|
||||
path: '/game'
|
||||
},
|
||||
proxy: {
|
||||
server: 'Nginx',
|
||||
sslTermination: true,
|
||||
loadBalancing: 'Single Instance',
|
||||
pathRouting: '/game -> localhost:3001'
|
||||
},
|
||||
authentication: {
|
||||
method: 'JWT Bearer Token',
|
||||
validation: 'Real-time on connection',
|
||||
sessionManagement: 'In-memory with Redis backup'
|
||||
}
|
||||
},
|
||||
features: {
|
||||
connectionManagement: {
|
||||
maxConnections: 1000,
|
||||
connectionPooling: true,
|
||||
automaticReconnection: 'Client-side',
|
||||
heartbeat: '30s interval'
|
||||
},
|
||||
messaging: {
|
||||
messageTypes: ['login', 'chat', 'position'],
|
||||
messageRouting: 'Room-based (by map)',
|
||||
messageFiltering: 'Content and rate limiting',
|
||||
messageHistory: 'Not stored (real-time only)'
|
||||
},
|
||||
roomManagement: {
|
||||
strategy: 'Map-based rooms',
|
||||
autoJoin: 'On position update',
|
||||
autoLeave: 'On disconnect or map change',
|
||||
broadcasting: 'Room-scoped and global'
|
||||
}
|
||||
},
|
||||
performance: {
|
||||
latency: '< 50ms (local network)',
|
||||
throughput: '1000+ messages/second',
|
||||
memoryUsage: '~1MB per 100 connections',
|
||||
cpuUsage: 'Low (event-driven)'
|
||||
},
|
||||
monitoring: {
|
||||
metrics: [
|
||||
'Active connections count',
|
||||
'Messages per second',
|
||||
'Authentication success rate',
|
||||
'Error rate by type'
|
||||
],
|
||||
logging: [
|
||||
'Connection events',
|
||||
'Authentication attempts',
|
||||
'Message routing',
|
||||
'Error conditions'
|
||||
],
|
||||
healthCheck: '/chat/status endpoint'
|
||||
},
|
||||
security: {
|
||||
authentication: 'JWT Token validation',
|
||||
authorization: 'Role-based access control',
|
||||
rateLimit: 'Per-user message rate limiting',
|
||||
contentFilter: 'Sensitive word filtering',
|
||||
inputValidation: 'Message format validation'
|
||||
},
|
||||
deployment: {
|
||||
environment: 'Production ready',
|
||||
scaling: 'Horizontal scaling supported',
|
||||
backup: 'Session state in Redis',
|
||||
monitoring: 'Integrated with application monitoring'
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
451
src/business/zulip/websocket_test.controller.ts
Normal file
451
src/business/zulip/websocket_test.controller.ts
Normal file
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* WebSocket 测试页面控制器
|
||||
*
|
||||
* 提供一个简单的WebSocket测试界面,可以直接在浏览器中测试WebSocket连接
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-09
|
||||
*/
|
||||
|
||||
import { Controller, Get, Res } from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
|
||||
@ApiTags('websocket')
|
||||
@Controller('websocket-test')
|
||||
export class WebSocketTestController {
|
||||
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: 'WebSocket 测试页面',
|
||||
description: '提供一个简单的WebSocket测试界面,可以直接在浏览器中测试连接和消息发送'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: 'WebSocket测试页面HTML',
|
||||
content: {
|
||||
'text/html': {
|
||||
schema: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
getTestPage(@Res() res: Response) {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>WebSocket 测试工具 - Pixel Game Server</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.status.disconnected { background-color: #ffebee; color: #c62828; }
|
||||
.status.connected { background-color: #e8f5e8; color: #2e7d32; }
|
||||
.status.connecting { background-color: #fff3e0; color: #ef6c00; }
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
input, textarea, select, button {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
button {
|
||||
background-color: #1976d2;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
}
|
||||
button:hover { background-color: #1565c0; }
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.message-log {
|
||||
height: 300px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
padding: 10px;
|
||||
background-color: #fafafa;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.message-item {
|
||||
margin-bottom: 8px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.message-sent { background-color: #e3f2fd; }
|
||||
.message-received { background-color: #f3e5f5; }
|
||||
.message-system { background-color: #fff3e0; }
|
||||
.message-error { background-color: #ffebee; }
|
||||
|
||||
.quick-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.quick-action {
|
||||
padding: 8px 12px;
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
}
|
||||
.quick-action:hover { background-color: #eeeeee; }
|
||||
|
||||
.info-panel {
|
||||
background-color: #e8f4fd;
|
||||
border: 1px solid #bbdefb;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info-panel h3 {
|
||||
margin-top: 0;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.two-column {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🎮 Pixel Game Server - WebSocket 测试工具</h1>
|
||||
|
||||
<div class="info-panel">
|
||||
<h3>📋 使用说明</h3>
|
||||
<p><strong>1. 获取JWT Token:</strong> 先通过 <code>/auth/login</code> 接口获取有效的JWT Token</p>
|
||||
<p><strong>2. 建立连接:</strong> 点击"连接"按钮建立WebSocket连接</p>
|
||||
<p><strong>3. 用户登录:</strong> 输入JWT Token并点击"登录"进行认证</p>
|
||||
<p><strong>4. 发送消息:</strong> 认证成功后可以发送聊天消息和位置更新</p>
|
||||
<p><strong>WebSocket地址:</strong> <code>wss://whaletownend.xinghangee.icu/game</code></p>
|
||||
</div>
|
||||
|
||||
<div class="two-column">
|
||||
<div class="container">
|
||||
<h2>🔌 连接控制</h2>
|
||||
<div id="connectionStatus" class="status disconnected">未连接</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="wsUrl">WebSocket 地址:</label>
|
||||
<input type="text" id="wsUrl" value="wss://whaletownend.xinghangee.icu/game" />
|
||||
</div>
|
||||
|
||||
<button id="connectBtn" onclick="toggleConnection()">连接</button>
|
||||
|
||||
<h3>🔐 用户认证</h3>
|
||||
<div class="form-group">
|
||||
<label for="jwtToken">JWT Token:</label>
|
||||
<textarea id="jwtToken" rows="3" placeholder="请输入从 /auth/login 获取的JWT Token"></textarea>
|
||||
</div>
|
||||
<button id="loginBtn" onclick="login()" disabled>登录</button>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>💬 消息发送</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="messageType">消息类型:</label>
|
||||
<select id="messageType">
|
||||
<option value="chat">聊天消息</option>
|
||||
<option value="position">位置更新</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="chatFields">
|
||||
<div class="form-group">
|
||||
<label for="chatContent">消息内容:</label>
|
||||
<input type="text" id="chatContent" placeholder="输入聊天消息" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="chatScope">消息范围:</label>
|
||||
<select id="chatScope">
|
||||
<option value="local">本地 (当前地图)</option>
|
||||
<option value="global">全局 (所有玩家)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="positionFields" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label for="posX">X坐标:</label>
|
||||
<input type="number" id="posX" value="150" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="posY">Y坐标:</label>
|
||||
<input type="number" id="posY" value="400" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="mapId">地图ID:</label>
|
||||
<select id="mapId">
|
||||
<option value="whale_port">Whale Port (鲸鱼港)</option>
|
||||
<option value="pumpkin_valley">Pumpkin Valley (南瓜谷)</option>
|
||||
<option value="novice_village">Novice Village (新手村)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="sendBtn" onclick="sendMessage()" disabled>发送消息</button>
|
||||
|
||||
<div class="quick-actions">
|
||||
<div class="quick-action" onclick="sendQuickMessage('Hello!')">快速发送: Hello!</div>
|
||||
<div class="quick-action" onclick="sendQuickMessage('大家好!')">快速发送: 大家好!</div>
|
||||
<div class="quick-action" onclick="sendQuickPosition()">发送位置更新</div>
|
||||
<div class="quick-action" onclick="clearLog()">清空日志</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>📋 消息日志</h2>
|
||||
<div id="messageLog" class="message-log"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let ws = null;
|
||||
let isLoggedIn = false;
|
||||
|
||||
// 切换消息类型时显示对应字段
|
||||
document.getElementById('messageType').addEventListener('change', function() {
|
||||
const chatFields = document.getElementById('chatFields');
|
||||
const positionFields = document.getElementById('positionFields');
|
||||
|
||||
if (this.value === 'chat') {
|
||||
chatFields.style.display = 'block';
|
||||
positionFields.style.display = 'none';
|
||||
} else {
|
||||
chatFields.style.display = 'none';
|
||||
positionFields.style.display = 'block';
|
||||
}
|
||||
});
|
||||
|
||||
function updateStatus(status, message) {
|
||||
const statusEl = document.getElementById('connectionStatus');
|
||||
statusEl.className = 'status ' + status;
|
||||
statusEl.textContent = message;
|
||||
}
|
||||
|
||||
function addMessage(type, content, timestamp = new Date()) {
|
||||
const log = document.getElementById('messageLog');
|
||||
const messageEl = document.createElement('div');
|
||||
messageEl.className = 'message-item message-' + type;
|
||||
|
||||
const timeStr = timestamp.toLocaleTimeString();
|
||||
messageEl.innerHTML = '<strong>[' + timeStr + ']</strong> ' + content;
|
||||
|
||||
log.appendChild(messageEl);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
function toggleConnection() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
} else {
|
||||
connect();
|
||||
}
|
||||
}
|
||||
|
||||
function connect() {
|
||||
const url = document.getElementById('wsUrl').value;
|
||||
updateStatus('connecting', '连接中...');
|
||||
|
||||
try {
|
||||
ws = new WebSocket(url);
|
||||
|
||||
ws.onopen = function() {
|
||||
updateStatus('connected', '已连接');
|
||||
document.getElementById('connectBtn').textContent = '断开';
|
||||
document.getElementById('loginBtn').disabled = false;
|
||||
addMessage('system', '✅ WebSocket连接成功');
|
||||
};
|
||||
|
||||
ws.onmessage = function(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
addMessage('received', '📥 ' + JSON.stringify(data, null, 2));
|
||||
|
||||
// 处理特定消息类型
|
||||
if (data.t === 'login_success') {
|
||||
isLoggedIn = true;
|
||||
document.getElementById('sendBtn').disabled = false;
|
||||
addMessage('system', '✅ 登录成功!用户: ' + data.username);
|
||||
} else if (data.t === 'login_error') {
|
||||
addMessage('error', '❌ 登录失败: ' + data.message);
|
||||
} else if (data.t === 'chat_render') {
|
||||
addMessage('system', '💬 收到消息: ' + data.from + ' 说: ' + data.txt);
|
||||
}
|
||||
} catch (e) {
|
||||
addMessage('received', '📥 ' + event.data);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
updateStatus('disconnected', '未连接');
|
||||
document.getElementById('connectBtn').textContent = '连接';
|
||||
document.getElementById('loginBtn').disabled = true;
|
||||
document.getElementById('sendBtn').disabled = true;
|
||||
isLoggedIn = false;
|
||||
addMessage('system', '🔌 WebSocket连接已关闭');
|
||||
};
|
||||
|
||||
ws.onerror = function(error) {
|
||||
addMessage('error', '❌ 连接错误: ' + error);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
updateStatus('disconnected', '连接失败');
|
||||
addMessage('error', '❌ 连接失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function login() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
addMessage('error', '❌ 请先建立WebSocket连接');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = document.getElementById('jwtToken').value.trim();
|
||||
if (!token) {
|
||||
addMessage('error', '❌ 请输入JWT Token');
|
||||
return;
|
||||
}
|
||||
|
||||
const message = {
|
||||
type: 'login',
|
||||
token: token
|
||||
};
|
||||
|
||||
ws.send(JSON.stringify(message));
|
||||
addMessage('sent', '📤 ' + JSON.stringify(message, null, 2));
|
||||
}
|
||||
|
||||
function sendMessage() {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
addMessage('error', '❌ 请先建立WebSocket连接');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
addMessage('error', '❌ 请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
const messageType = document.getElementById('messageType').value;
|
||||
let message;
|
||||
|
||||
if (messageType === 'chat') {
|
||||
const content = document.getElementById('chatContent').value.trim();
|
||||
if (!content) {
|
||||
addMessage('error', '❌ 请输入消息内容');
|
||||
return;
|
||||
}
|
||||
|
||||
message = {
|
||||
t: 'chat',
|
||||
content: content,
|
||||
scope: document.getElementById('chatScope').value
|
||||
};
|
||||
} else {
|
||||
message = {
|
||||
t: 'position',
|
||||
x: parseInt(document.getElementById('posX').value),
|
||||
y: parseInt(document.getElementById('posY').value),
|
||||
mapId: document.getElementById('mapId').value
|
||||
};
|
||||
}
|
||||
|
||||
ws.send(JSON.stringify(message));
|
||||
addMessage('sent', '📤 ' + JSON.stringify(message, null, 2));
|
||||
|
||||
// 清空聊天输入框
|
||||
if (messageType === 'chat') {
|
||||
document.getElementById('chatContent').value = '';
|
||||
}
|
||||
}
|
||||
|
||||
function sendQuickMessage(content) {
|
||||
if (!isLoggedIn) {
|
||||
addMessage('error', '❌ 请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('messageType').value = 'chat';
|
||||
document.getElementById('chatContent').value = content;
|
||||
document.getElementById('messageType').dispatchEvent(new Event('change'));
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
function sendQuickPosition() {
|
||||
if (!isLoggedIn) {
|
||||
addMessage('error', '❌ 请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('messageType').value = 'position';
|
||||
document.getElementById('messageType').dispatchEvent(new Event('change'));
|
||||
sendMessage();
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('messageLog').innerHTML = '';
|
||||
}
|
||||
|
||||
// 页面加载完成后的初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
addMessage('system', '🎮 WebSocket测试工具已就绪');
|
||||
addMessage('system', '💡 提示: 请先通过 /auth/login 接口获取JWT Token');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||
res.send(html);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
*
|
||||
* 业务服务:
|
||||
* - ZulipService: 主协调服务,处理登录、消息发送等核心业务流程
|
||||
* - ZulipWebSocketGateway: WebSocket统一网关,处理客户端连接
|
||||
* - CleanWebSocketGateway: WebSocket统一网关,处理客户端连接
|
||||
* - SessionManagerService: 会话状态管理和业务逻辑
|
||||
* - MessageFilterService: 消息过滤和业务规则控制
|
||||
*
|
||||
@@ -43,7 +43,6 @@
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ZulipWebSocketGateway } from './zulip_websocket.gateway';
|
||||
import { CleanWebSocketGateway } from './clean_websocket.gateway';
|
||||
import { ZulipService } from './zulip.service';
|
||||
import { SessionManagerService } from './services/session_manager.service';
|
||||
@@ -52,6 +51,8 @@ import { ZulipEventProcessorService } from './services/zulip_event_processor.ser
|
||||
import { SessionCleanupService } from './services/session_cleanup.service';
|
||||
import { ChatController } from './chat.controller';
|
||||
import { WebSocketDocsController } from './websocket_docs.controller';
|
||||
import { WebSocketOpenApiController } from './websocket_openapi.controller';
|
||||
import { WebSocketTestController } from './websocket_test.controller';
|
||||
import { ZulipAccountsController } from './zulip_accounts.controller';
|
||||
import { ZulipCoreModule } from '../../core/zulip_core/zulip_core.module';
|
||||
import { ZulipAccountsModule } from '../../core/db/zulip_accounts/zulip_accounts.module';
|
||||
@@ -94,6 +95,10 @@ import { AuthModule } from '../auth/auth.module';
|
||||
ChatController,
|
||||
// WebSocket API文档控制器
|
||||
WebSocketDocsController,
|
||||
// WebSocket OpenAPI规范控制器
|
||||
WebSocketOpenApiController,
|
||||
// WebSocket测试页面控制器
|
||||
WebSocketTestController,
|
||||
// Zulip账号关联管理控制器
|
||||
ZulipAccountsController,
|
||||
],
|
||||
|
||||
42
src/main.ts
42
src/main.ts
@@ -89,6 +89,12 @@ async function bootstrap() {
|
||||
- 系统状态监控
|
||||
- Zulip 集成状态
|
||||
|
||||
### 🔌 WebSocket 接口 (websocket)
|
||||
- 实时消息传输
|
||||
- 位置同步
|
||||
- 地图房间管理
|
||||
- 连接状态监控
|
||||
|
||||
### 👑 管理员后台 (admin)
|
||||
- 用户管理
|
||||
- 系统监控
|
||||
@@ -98,12 +104,16 @@ async function bootstrap() {
|
||||
|
||||
游戏聊天功能主要通过 WebSocket 实现:
|
||||
|
||||
**连接地址**: \`ws://localhost:3000/game\`
|
||||
**连接地址**: \`wss://whaletownend.xinghangee.icu/game\` (原生WebSocket)
|
||||
|
||||
**重要变更**: 已从Socket.IO迁移到原生WebSocket,提升性能和稳定性
|
||||
|
||||
**连接路径**: \`/game\` - 统一的WebSocket入口
|
||||
|
||||
**支持的事件**:
|
||||
- \`login\`: 用户登录(需要 JWT Token)
|
||||
- \`chat\`: 发送聊天消息
|
||||
- \`position_update\`: 位置更新
|
||||
- \`position\`: 位置更新
|
||||
|
||||
**JWT Token 要求**:
|
||||
- issuer: \`whale-town\`
|
||||
@@ -119,10 +129,30 @@ async function bootstrap() {
|
||||
- Whale Port (鲸鱼港)
|
||||
- Pumpkin Valley (南瓜谷)
|
||||
- Novice Village (新手村)
|
||||
|
||||
## 最近更新 (v2.1.0)
|
||||
|
||||
### 🚀 WebSocket 架构升级
|
||||
- ✅ 移除Socket.IO依赖,使用原生WebSocket
|
||||
- ✅ 实现地图房间分组管理
|
||||
- ✅ 支持本地和全局消息广播
|
||||
- ✅ 新增实时连接监控
|
||||
|
||||
### 📚 文档完善
|
||||
- ✅ 新增WebSocket专用API文档
|
||||
- ✅ 提供交互式消息格式展示
|
||||
- ✅ 包含测试工具和示例代码
|
||||
- ✅ 完整的开发者指南
|
||||
|
||||
### 🔧 性能优化
|
||||
- ✅ 更高效的消息路由机制
|
||||
- ✅ 优化连接池管理
|
||||
- ✅ 增强错误处理和日志记录
|
||||
`)
|
||||
.setVersion('2.0.0')
|
||||
.setVersion('2.1.0')
|
||||
.addTag('auth', '🔐 用户认证相关接口')
|
||||
.addTag('chat', '💬 聊天系统相关接口')
|
||||
.addTag('websocket', '🔌 WebSocket接口文档和测试')
|
||||
.addTag('admin', '👑 管理员后台相关接口')
|
||||
.addBearerAuth(
|
||||
{
|
||||
@@ -135,8 +165,10 @@ async function bootstrap() {
|
||||
},
|
||||
'JWT-auth',
|
||||
)
|
||||
.addServer('http://localhost:3000', '开发环境')
|
||||
.addServer('https://whaletownend.xinghangee.icu', '生产环境')
|
||||
.addServer('http://localhost:3000', '开发环境 - REST API')
|
||||
.addServer('https://whaletownend.xinghangee.icu', '生产环境 - REST API')
|
||||
.addServer('wss://whaletownend.xinghangee.icu/game', '生产环境 - WebSocket')
|
||||
.addServer('ws://localhost:3001/game', '开发环境 - WebSocket')
|
||||
.build();
|
||||
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
|
||||
Reference in New Issue
Block a user