WebSocket API 文档
项目:对面是AI吗
协议:WebSocket
数据格式:JSON(文本帧)
基础URL:wss://game.xfcode.top
心跳:客户端发送{"type":"ping"},服务端回复{"type":"pong"},10s 间隔,65s 超时断开
更新日期:2026-08-10
通用约定
消息格式
所有消息均为 JSON 文本帧,以 type 字段区分消息类型:
{"type": "消息类型", ...其他字段}通用消息类型
所有游戏模式均支持以下通用消息:
| type | 方向 | 说明 |
|---|---|---|
ping | C→S | 心跳请求 |
pong | S→C | 心跳响应 |
get_stickers | C→S | 获取表情包差异更新 |
stickers_list | S→C | 表情列表(有变更时) |
stickers_unchanged | S→C | 表情无变化 |
error | S→C | 通用错误 {"type":"error","message":"..."} |
认证机制
所有 WebSocket 端点共用同一套身份认证体系,由 BaseGameHandler 统一实现。
连接阶段(onOpen)
WebSocket 连接建立时不进行身份验证,仅做以下检查:
- IP 提取 — 优先级:
CF-Connecting-IP>X-Forwarded-For>X-Real-IP>remote_addr - IP 去重(可选,默认开启)— 同一 IP 已有活跃连接时关闭旧连接
- IP 级封禁检查 — 调用
BanRepository::isBanned(),被封则推送{"type":"error","message":"您已被管理员封禁"}并关闭连接 - 全服公告推送 — 如有生效中的 Redis 广播,推送给新连接
真正的身份验证发生在客户端发送第一条业务消息时(如 join、lobby_join、gomoku_join、WhoisAI_match)。
业务消息认证(三级验证)
所有需要身份的消息均携带以下字段(三选一即可):
| 字段 | 说明 | 优先级 |
|---|---|---|
player_token | 已有的 JWT Token(老玩家) | 最高 |
password | 密码(新玩家注册/换设备恢复) | 中 |
仅 nickname | 昵称(兼容旧数据,需同设备 IP+FP 匹配) | 最低 |
Level 1:Token 验证(老玩家优先)
Client → Server: {type: "join", nickname: "小明", player_token: "eyJ..."}- Token 格式:
base64(json_payload).hmac_sha256_sig - Payload 结构:
{player_id, exp(365天), iat, jti} - HMAC 密钥:玩家在数据库中的
password_hash(bcrypt) - 验证内容:签名正确性 + 过期时间 + player_id 有效性
Token 可用跨模式复用 — 同一 player_id 可在 /ws、/ws/lobby、/ws/gomoku、/ws/WhoisAI 之间通用。
Level 2:密码验证(新玩家/换设备)
Client → Server: {type: "join", nickname: "小明", password: "mypassword"}- 按昵称查数据库,
password_verify($password, $record['password_hash']) - 密码正确 → 生成新 Token 并返回
- 密码错误 → 返回
{"type":"error","message":"密码不正确"},不中断连接,可重试 - 昵称不存在 → 视为新玩家注册,自动创建身份
Level 3:仅昵称(兼容旧迁移数据)
Client → Server: {type: "join", nickname: "小明"}- 按昵称查数据库
- 存在且同设备(IP + Fingerprint 匹配)→ 允许复用旧账号,生成新 Token
- 存在但设备不同 → 返回
{"type":"error","message":"该昵称已被占用,请换一个或输入密码"} - 不存在 → 视为新玩家,自动创建
新玩家注册
当昵称在数据库中不存在时,自动触发注册流程:
- 检查同 IP 账号数 ≤ 3(超出则拒绝)
- 调用
PlayerStatsRepository::createPlayer(nickname, ip, fp, password)创建记录 - 密码使用传入的
password或随机生成bin2hex(random_bytes(8)) - BCrypt cost 10 加密存储
- 生成 JWT Token 返回给客户端
在线唯一性
同一 player_id 通过 Redis SETNX 锁定(120s TTL),防止多地同时登录:
- 登录时
claimOnlineLock()尝试获取锁 - 连接关闭时自动释放
- 获取锁失败 → 关闭已有连接,推送
{"type":"system","text":"该账号已在其他地方登录..."}
验证失败处理汇总
| 端点 | 消息类型 | 失败响应 type | 失败示例 | 连接 |
|---|---|---|---|---|
/ws | join | error | {"type":"error","message":"密码不正确"} | 不断开 |
/ws/lobby | lobby_join | lobby_error | {"type":"lobby_error","text":"密码不正确"} | 不断开 |
/ws/gomoku | gomoku_join | gomoku_error | {"type":"gomoku_error","data":"身份验证失败"} | 不断开 |
/ws/WhoisAI | WhoisAI_match | WhoisAI_error | {"type":"WhoisAI_error","text":"密码不正确"} | 不断开 |
| 全部 | onOpen | error | {"type":"error","message":"您已被管理员封禁"} | close |
| 全部 | online lock | system | {"type":"system","text":"该账号已在其他地方登录..."} | close |
Token 机制(内部)
生成: base64(json_encode({player_id, exp(365天), iat, jti(随机8字节)})) . "." . hmac_sha256(payload, password_hash)
验证: 解析 payload + sig → 查 DB 获取 password_hash → 重新计算 HMAC 对比
密钥: 每个玩家独立的 bcrypt password_hash
有效期: 365 天1. "对面是AI吗" 主游戏
WebSocket 路径:/ws
Handler:GameWebSocketHandler
1.1 通用消息
error
方向:S→C
{"type":"error","message":"ERROR_MESSAGE"}ping / pong
C→S:
{"type":"ping"}S→C:
{"type":"pong"}说明:心跳机制,每 10 秒一次。65 秒无心跳则断开连接。
1.2 匹配与对局流程
join(加入匹配)
方向:C→S
{
"type": "join",
"nickname": "小明",
"duration": 600,
"fingerprint": "可选的设备指纹",
"password": "密码(新玩家创建身份或恢复身份时)",
"player_token": "已有的 token(已登录时)",
"reconnect_session_id": "重连恢复时的 session_id"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
nickname | string | 是 | 1-16 字符 |
duration | int | 是 | 对局时长:300(5分钟)或 600(10分钟) |
fingerprint | string | 否 | 设备指纹 |
password | string | 条件 | 新玩家创建身份或恢复时 |
player_token | string | 条件 | 已有 token 时,与 password 二选一 |
reconnect_session_id | string | 否 | 断线重连恢复 |
说明:加入匹配队列。身份验证成功后进入等待。
matched(匹配成功)
方向:S→C
{
"type": "matched",
"opponent_name": "小红",
"duration": 600,
"session_id": "sess_abc123",
"player_id": "player_xxx",
"token": "eyJ..."
}| 字段 | 说明 |
|---|---|
opponent_name | 对手昵称(可能为 AI 别名) |
duration | 对局时长 |
session_id | 对局会话 ID |
player_id | 你的玩家 ID |
token | JWT Token(新玩家首次匹配时下发) |
说明:匹配成功后自动推送,开始聊天阶段。
message(发送聊天消息)
方向:C→S
{
"type": "message",
"text": "你好,请问你是 AI 吗?"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
text | string | 是 | 消息内容,最多 300 字符 |
触发条件:对局状态为 chatting(聊天阶段)。
对手收到:
{"type":"message","text":"你好,请问你是 AI 吗?","sender":"对方"}旁观者收到:
{"type":"spectate_message","text":"...","sender":"玩家1","side":"left"}sticker(发送表情)
方向:C→S
{"type":"sticker","id":"表情ID"}对手收到:
{"type":"sticker","id":"xxx","name":"大笑","url":"/stickers/xxx.png","sender":"对方"}judge(判定)
方向:C→S
{
"type": "judge",
"guess": "human",
"tag": "有逻辑"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
guess | string | 是 | "human"(认为是真人)或 "ai"(认为是 AI) |
tag | string | 否 | 标签,用于风格画像 |
触发条件:聊天阶段结束进入判定阶段。
对手判定进度通知(对手收到):
{
"type": "judge_notify",
"message": "对方已做出判断,你需要在 60 秒内完成判定,否则判负",
"seconds_remaining": 60
}判定结果(双方都判定完成后,双方收到):
{
"type": "judged",
"truth": "human",
"opponent_guess": "ai",
"opponent_tag": "机器人",
"session_id": "sess_abc123",
"opponent_name": "小红",
"player_id": "player_xxx"
}| 字段 | 说明 |
|---|---|
truth | 你的真实身份:"human" 或 "ai" |
opponent_guess | 对手对你的判断 |
opponent_tag | 对手给你的标签 |
leave(主动离开)
方向:C→S
{"type":"leave"}对手收到:
{"type":"system","text":"对方已离开"}然后:
{
"type":"timeout",
"reason":"opponent_left",
"opponent_truth":"ai",
"session_id":"sess_abc123",
"opponent_name":"小红"
}leave_result(确认离开结果页)
方向:C→S
{"type":"leave_result","session_id":"sess_abc123"}说明:查看对战结果后确认离开,不主动断连,仅标记离开意向用于跨进程同步清理。
save_history(保存聊天记录)
方向:C→S
{"type":"save_history","session_id":"sess_abc123"}响应:
{"type":"save_history_status","success":true,"message":"聊天记录已保存","id":123}leave_message(留言)
方向:C→S
{"type":"leave_message","text":"打得很精彩!"}响应:
{"type":"leave_message_status","success":true,"message":"留言已保存"}report(举报)
方向:C→S
{"type":"report","reason":"对方使用了不文明用语"}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
reason | string | 是 | 举报原因,最多 100 字符 |
响应:
{"type":"report_result","success":true,"message":"举报已提交,管理员将尽快处理"}update_nickname(更新昵称)
方向:C→S
{
"type": "update_nickname",
"nickname": "新昵称",
"fp": "设备指纹"
}响应:
{"type":"update_nickname_result","success":true}
// 或
{"type":"update_nickname_result","error":"昵称已被占用"}1.3 服务器主动推送
timeout(超时)
方向:S→C
{
"type": "timeout",
"reason": "chat_expired",
"session_id": "sess_abc123",
"opponent_name": "小红"
}| reason 值 | 说明 |
|---|---|
chat_expired | 聊天时间到,进入判定阶段 |
you_timeout | 你判定超时,判负 |
opponent_timeout | 对手判定超时,你判胜 |
both_timeout | 双方判定超时,平局 |
opponent_left | 对手离开了对局 |
no_mutual_chat | 60 秒内双方无互发消息,对局终止 |
system(系统消息,对局内)
方向:S→C
{"type":"system","text":"已进入判定阶段,请判断对方是真人还是 AI"}1.4 管理员消息
admin_ban(管理员封禁)
方向:C→S
{"type":"admin_ban","token":"管理员token","reason":"违规行为"}被禁者收到:
{"type":"banned","text":"你已被管理员封禁。原因:违规行为"}admin_verify(验证管理员)
方向:C→S
{"type":"admin_verify","token":"管理员token"}响应:
{"type":"admin_config","ws_url":"/admin/ws","super_admin":true}2. 大厅聊天室
WebSocket 路径:/ws/lobby
Handler:LobbyChatWebSocketHandler
2.1 连接建立
服务端推送(连接成功后):
{"type":"lobby_history","messages":[...]}
{"type":"lobby_online_count","players":[{"name":"小明","fd":1},...]}2.2 用户消息
lobby_join(加入聊天室)
方向:C→S
{
"type": "lobby_join",
"nickname": "小明",
"password": "密码",
"player_token": "已有 token"
}响应:
{"type":"lobby_joined","nickname":"小明","token":"eyJ..."}广播(在线人数变化时):
{"type":"lobby_online_count","players":[...]}广播(进入通知):
{"type":"lobby_system","text":"小明 进入了聊天室"}lobby_chat(发送聊天消息)
方向:C→S
{
"type": "lobby_chat",
"content": "大家好!",
"reply_to_id": 123,
"reply_to_name": "小红",
"reply_to_text": "原始消息内容"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
content | string | 是 | 消息内容 |
reply_to_id | int | 否 | 回复的消息 ID |
reply_to_name | string | 否 | 回复对象的昵称 |
reply_to_text | string | 否 | 回复消息的原文摘要 |
触发条件:已加入聊天室 + 未被禁言 + 通过频率限制。
广播:
{
"type": "lobby_chat",
"id": 456,
"sender_name": "小明",
"sender_id": "player_xxx",
"content": "大家好!",
"reply_to": {"id":123,"name":"小红","text":"原始消息内容"},
"mentions": ["小红"],
"time": "14:30",
"created_at": "2026-08-10 14:30:00"
}@提醒(被 @ 的用户收到):
{
"type": "lobby_mentioned",
"message_id": 456,
"sender_name": "小明",
"content": "大家好!"
}lobby_sticker(发送表情)
方向:C→S
{"type":"lobby_sticker","id":"表情ID"}广播:
{
"type": "sticker",
"id": "xxx",
"name": "大笑",
"url": "/stickers/xxx.png",
"sender": "小明",
"sender_id": "player_xxx"
}lobby_nudge(拍一拍)
方向:C→S
{"type":"lobby_nudge","target_fd":123,"target_nickname":"小红"}目标收到:
{"type":"lobby_nudged","sender_name":"小明"}广播:
{"type":"lobby_system","text":"小明 拍了拍 小红"}lobby_revoke(撤回消息)
方向:C→S
{"type":"lobby_revoke","message_id":456}限制:只能撤回自己 3 分钟内的消息。
广播:
{"type":"lobby_revoke","message_id":456,"sender_name":"小明"}lobby_report(举报消息)
方向:C→S
{"type":"lobby_report","message_id":456,"reason":"不文明用语"}响应:
{"type":"lobby_report_ok","message":"举报已提交,管理员将尽快处理"}2.3 点歌系统
lobby_song_search(搜索歌曲)
方向:C→S
{"type":"lobby_song_search","keyword":"周杰伦"}响应:
{"type":"lobby_song_search_result","songs":[{...}]}lobby_song_request(点歌)
方向:C→S
{"type":"lobby_song_request","song_id":"xxx","nickname":"小明"}响应:
{"type":"lobby_song_requested","song":{...}}lobby_song_vote / lobby_song_remove_vote(投票/取消投票)
{"type":"lobby_song_vote","song_id":"xxx"}{"type":"lobby_song_remove_vote","song_id":"xxx"}广播:
{"type":"lobby_vote_update","song_id":"xxx","votes":5}
{"type":"lobby_remove_vote_update","song_id":"xxx","remove_votes":3}lobby_song_list / lobby_song_current(歌单查询)
方向:C→S
{"type":"lobby_song_list"}
{"type":"lobby_song_current"}响应(歌单):
{"type":"lobby_song_list","playlist":[...],"pool":[...],"playing":{...}}响应(当前播放):
{"type":"lobby_song_current","song":{...}}
// 或等待中
{"type":"lobby_song_current","waiting":true}广播(列表更新):
{"type":"list_update","playlist":[...],"pool":[...],"playing":{...}}2.4 管理员消息
lobby_delete(管理员删消息)
{"type":"lobby_delete","message_id":456}广播:
{"type":"lobby_message_deleted","message_id":456}lobby_mute(管理员禁言)
{"type":"lobby_mute","target_fd":123,"minutes":10}目标收到:
{"type":"lobby_system","text":"你已被管理员禁言 10 分钟"}广播:
{"type":"lobby_system","text":"小明 已被管理员禁言 10 分钟"}lobby_ban(管理员封禁)
{"type":"lobby_ban","target_fd":123,"reason":"违规"}执行封禁并断开目标 WebSocket 连接。
lobby_admin_verify(管理员验证)
{"type":"lobby_admin_verify","token":"管理员token"}响应:
{
"type": "lobby_admin_verified",
"is_admin": true,
"username": "admin",
"role": "super_admin",
"super_admin": true
}3. "谁是AI"
WebSocket 路径:/ws/WhoisAI
Handler:WhoisAIWebSocketHandler
3.1 连接建立
S→C(连接成功后):
{"type":"WhoisAI_connected"}3.2 匹配与游戏流程
WhoisAI_match(加入匹配)
方向:C→S
{
"type": "WhoisAI_match",
"nickname": "小明",
"password": "密码",
"player_token": "已有 token",
"fp": "设备指纹"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
nickname | string | 是 | 1-12 字符 |
password | string | 条件 | 新玩家或恢复时 |
player_token | string | 条件 | 已有 token 时 |
fp | string | 否 | 设备指纹 |
响应(匹配成功,开房):
{
"type": "WhoisAI_matched",
"pool_count": 5,
"nickname": "小明",
"token": "eyJ..."
}广播(给匹配池其他人):
{"type":"WhoisAI_pool_count","pool_count":5}匹配规则:
- 满 6 人立即开局
- 满 4 人等待 3 秒后开局
WhoisAI_cancel_match(取消匹配)
方向:C→S
{"type":"WhoisAI_cancel_match"}响应:
{"type":"WhoisAI_match_cancelled"}WhoisAI_connect_ack(连接确认)
方向:C→S
{"type":"WhoisAI_connect_ack","room_id":"room_xxx"}说明:连接检查阶段,全部在线玩家确认后进入讨论。超时未确认的会被标记离线,不影响开局。
WhoisAI_connect_check(开局通知)
方向:S→C
{
"type": "WhoisAI_connect_check",
"room_id": "room_xxx",
"room_code": "ABCDE",
"identity": "human",
"seat": 1,
"players": [
{"seat":1,"name":"玩家1"},
{"seat":2,"name":"玩家2"},
{"seat":3,"name":"玩家3"},
{"seat":4,"name":"玩家4"},
{"seat":5,"name":"玩家5"},
{"seat":6,"name":"玩家6"}
],
"player_count": 6
}| 字段 | 说明 |
|---|---|
identity | 你的身份:"human" 或 "ai" |
seat | 你的座位号,1-6 |
players | 所有玩家列表(匿名,不透露身份) |
room_code | 房间码,用于旁观 |
WhoisAI_phase_discussion(讨论阶段开始)
方向:S→C
{
"type": "WhoisAI_phase_discussion",
"room_id": "room_xxx",
"round": 1,
"duration": 180,
"identity": "human",
"my_seat": 1,
"players": [
{"seat":1,"name":"玩家1","avatar":"..."}
]
}同时广播系统消息:
{"type":"WhoisAI_system","text":"第 1 轮讨论开始,你有 180 秒时间找出 AI"}WhoisAI_chat(讨论发言)
方向:C→S
{"type":"WhoisAI_chat","text":"我觉得 3 号玩家很可疑"}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
text | string | 是 | 消息内容,最多 300 字符 |
触发条件:玩家存活 + 房间状态为讨论中。
广播:
{
"type": "WhoisAI_message",
"sender_seat": 1,
"sender_name": "玩家1",
"text": "我觉得 3 号玩家很可疑",
"time": "14:30"
}WhoisAI_sticker(发送表情)
方向:C→S
{"type":"WhoisAI_sticker","id":"表情ID"}广播:
{
"type": "WhoisAI_sticker",
"id": "xxx",
"name": "困惑",
"url": "/stickers/xxx.png",
"sender_seat": 1,
"sender_name": "玩家1"
}WhoisAI_phase_voting(投票阶段开始)
方向:S→C
{
"type": "WhoisAI_phase_voting",
"room_id": "room_xxx",
"round": 1,
"duration": 30,
"candidates": [
{"seat":1,"name":"玩家1"},
{"seat":2,"name":"玩家2"},
{"seat":3,"name":"玩家3"}
]
}WhoisAI_vote(投票淘汰)
方向:C→S
{"type":"WhoisAI_vote","target_seat":3}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
target_seat | int | 是 | 要投出局的玩家座位号,不能投自己且目标必须存活 |
响应(投票确认):
{"type":"WhoisAI_vote_ok","target_seat":3}广播(全局进度):
{"type":"WhoisAI_vote_progress","voted_count":3,"alive_count":5}WhoisAI_vote_result(投票结果)
方向:S→C
有人被淘汰:
{
"type": "WhoisAI_vote_result",
"eliminated_seat": 3,
"eliminated_name": "玩家3",
"identity": "ai",
"text": "玩家3 被投票淘汰,真实身份是 AI!",
"players": [
{"seat":1,"name":"玩家1","alive":true}
]
}平票无人淘汰:
{"type":"WhoisAI_system","text":"投票结果为平票,本轮无人淘汰"}WhoisAI_game_over(游戏结束)
方向:S→C
{
"type": "WhoisAI_game_over",
"room_id": "room_xxx",
"winner": "human",
"text": "所有 AI 已被淘汰,人类胜利!",
"reason": "vote",
"players": [
{"seat":1,"name":"玩家1","identity":"human","alive":true}
],
"my_seat": 1,
"player_id": "player_xxx",
"messages": [...]
}| 字段 | 说明 |
|---|---|
winner | "human"(人类胜)或 "ai"(AI 胜) |
reason | 结束原因:"vote"(投票结束)、"disconnect"(断线) |
players | 所有玩家的完整身份信息 |
WhoisAI_report(举报)
方向:C→S
{
"type": "WhoisAI_report",
"target_name": "玩家3",
"message_text": "不当言论内容",
"reason": "违规"
}响应:
{"type":"WhoisAI_report_ok","message":"举报已提交,管理员将尽快处理"}3.3 断线处理
- 匹配中掉线 → 自动离开匹配池
- 连接确认阶段掉线 → 不影响开局(标记离线)
- 对局中掉线 → 自动淘汰,广播
WhoisAI_system - 仅剩人类/AI → 对应的另一方获胜
4. 五子棋
WebSocket 路径:/ws/gomoku
Handler:GomokuWebSocketHandler
4.1 连接建立
连接建立后仅初始化连接记录(IP、去重、封禁检查),不发送额外消息。
4.2 游戏消息
gomoku_join(加入五子棋)
方向:C→S
{
"type": "gomoku_join",
"fp": "设备指纹",
"nickname": "小明",
"password": "密码",
"player_token": "已有 token"
}响应:
{"type":"gomoku_joined","data":{"token":"eyJ..."}}gomoku_create_room(创建房间)
方向:C→S
{
"type": "gomoku_create_room",
"firstMove": 1,
"boardSize": 15
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
firstMove | int | 是 | 先手:1=黑先,2=让对手先 |
boardSize | int | 是 | 棋盘大小,5-30 |
触发条件:已通过 gomoku_join 验证身份。
响应:
{"type":"gomoku_room_created","data":{"roomId":"ABCDE","color":1}}| 字段 | 说明 |
|---|---|
roomId | 5位房间码,用于分享和加入 |
color | 你的棋子颜色:1=黑棋,2=白棋 |
gomoku_join_room(加入房间)
方向:C→S
{"type":"gomoku_join_room","roomId":"ABCDE"}触发条件:已通过 gomoku_join 验证身份。
场景一:作为对手加入,发送给双方
房主收到:
{
"type": "gomoku_game_start",
"data": {
"roomId": "ABCDE",
"settings": {"boardSize":15,"firstMove":1},
"currentTurn": 1,
"myColor": 1
}
}加入者收到:
{
"type": "gomoku_game_start",
"data": {
"roomId": "ABCDE",
"settings": {"boardSize":15,"firstMove":1},
"currentTurn": 1,
"myColor": 2
}
}场景二:作为旁观者加入
{
"type": "gomoku_spectate_start",
"data": {
"roomId": "ABCDE",
"settings": {"boardSize":15},
"board": [[0,0,...],[0,0,...]],
"currentTurn": 1
}
}gomoku_place_piece(落子)
方向:C→S
{"type":"gomoku_place_piece","r":7,"c":7}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
r | int | 是 | 行坐标(0-based) |
c | int | 是 | 列坐标(0-based) |
广播(双方+旁观者):
{
"type": "gomoku_piece_placed",
"data": {"r":7,"c":7,"color":1,"nextTurn":2}
}gomoku_game_over(游戏结束)
方向:S→C
{
"type": "gomoku_game_over",
"data": {
"winner": 1,
"reason": "win",
"winPath": [[3,3],[4,4],[5,5],[6,6],[7,7]]
}
}| 字段 | 说明 |
|---|---|
winner | 1=黑棋胜,2=白棋胜,0=平局 |
reason | "win"(五连)、"认输"、"超时"、"opponent_disconnected" |
winPath | 获胜的 5 个棋子坐标 |
gomoku_surrender(认输)/ gomoku_timeout(超时)
方向:C→S
{"type":"gomoku_surrender"}{"type":"gomoku_timeout"}广播:
{
"type": "gomoku_game_over",
"data": {"winner":2,"reason":"认输","winPath":[]}
}gomoku_request_rematch(请求重赛)
方向:C→S
{"type":"gomoku_request_rematch"}说明:双方都发送 gomoku_request_rematch 后自动重新开局。
重新开局:
- 双方角色互换,收到
gomoku_game_start - 旁观者收到
gomoku_spectate_start
gomoku_cancel_wait(取消等待)
方向:C→S
{"type":"gomoku_cancel_wait"}说明:房主取消等待,删除房间。
gomoku_chat_message(局内聊天)
方向:C→S
{
"type": "gomoku_chat_message",
"msg": "好棋!"
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
msg | string | 是 | 消息内容,最多 300 字符 |
广播(对手+旁观者,不含发送者):
{
"type": "gomoku_chat_message",
"data": {"msg":"好棋!","from":1,"time":"14:30"}
}| 字段 | 说明 |
|---|---|
from | 1=黑棋方,2=白棋方 |
gomoku_error(错误)
方向:S→C
{"type":"gomoku_error","data":"错误描述"}4.3 断线处理
对局中:
- 断线方自动判负
- 对手收到
gomoku_opponent_disconnected - 旁观者收到
gomoku_opponent_disconnected - 房间自动删除
- 双方战绩异步写入
对局已结束:仅清理连接绑定。
5. 管理后台 WebSocket
WebSocket 路径:/{adminPath}/ws(默认 /admin/ws)
管理后台 WebSocket 主要用于管理员操作(封禁管理、广播、用户管理、观战等),具体协议不在本文档范围内,详见 api-admin.md。
附录:WebSocket 路径与 Handler 对照表
| WebSocket 路径 | Handler | 前缀 | 说明 |
|---|---|---|---|
/ws | GameWebSocketHandler | 无 | "对面是AI吗" 主游戏 |
/ws/lobby | LobbyChatWebSocketHandler | lobby_ | 大厅聊天室 |
/ws/WhoisAI | WhoisAIWebSocketHandler | WhoisAI_ | "谁是AI" |
/ws/gomoku | GomokuWebSocketHandler | gomoku_ | 五子棋 |
/{adminPath}/ws | AdminWebSocketHandler | — | 管理后台 |