SignalR WebSocket 协议文档

所有 Hub 支持 WebSocket 传输。LoginHub 和 CustomerHub 客服端需 JWT Token(access_token 参数)。本轮不调整现有时间字段的单位和解析方式。

LoginHub — /loginhub
WS + JWT,用于大厅资产同步、全服广播、邮件通知

📤 客户端 → 服务器

方法参数说明
C→S Bindstring userId必须:连接后立即调用,将连接与 userId 绑定。服务端加入 userId 命名的 SignalR 组,后续定向推送通过此组发送。
C→S DismissPendingRoomNEWstring userId用户拒绝返回断线房间。登录后若收到 PendingRoomNotice,用户选择"离开"时调用此方法,服务端清理旧房间残留。

📥 服务器 → 客户端

事件数据发送时机
S→C Broadcast
{
  UserName: string,   // 昵称
  AvatarUrl: string,  // 头像URL
  Level: int,         // 等级
  VipLevel: int,      // VIP等级
  Message: string,    // 消息内容
  Time: long          // Unix时间戳(秒)
}
大喇叭全服广播
S→C KillAnnouncement
{
  Text: string,          // 公告文案: "恭喜玩家momo在风雷之渊高级场捕获 金龙*2000炮倍 100000000大奖"
  UserName: string,      // 玩家名: "momo"
  GameTypeName: string,  // 游戏类型: "风雷之渊"
  RoomName: string,      // 房间名: "高级场"
  FishName: string,      // 鱼名: "金龙"
  Multiple: int,         // 倍数: 2000
  Gold: long,            // 获得金币: 100000000
  Time: long             // Unix时间戳(秒)
}
全局击杀公告(特殊Boss击杀 / 机器人模拟击杀),走 LoginHub
S→C AssetUpdated
{
  CurrencyType: int,  // 货币类型: 1=金币 2=保险箱 3=钻石
  ChangeType: int,    // 变动类型: 1=增加 2=减少
  Amount: long,       // 变动数量
  NewGold: long,      // 最新金币
  NewDiamond: int,    // 最新钻石
  Source: string,     // 来源: "游戏金币结算"、"救济金"、"签到" 等
  Remark: string      // 备注(房间名等)
}
资产变更:退出房间结算 / 后台充值扣币 / 救济金 / 签到 / 捕鱼返利
S→C LevelChangedNEW
{
  NewLevel: int,      // 最新等级
  NewExp: long,       // 最新经验
  NewGold: long,      // 最新金币
  NewDiamond: int,    // 最新钻石
  Source: string,     // 来源
  Remark: string      // 备注
}
后台修改玩家等级
S→C BackpackUpdated
{
  Action: string,     // "add" | "update" | "delete"
  PropNumber: string, // 道具编号
  Count: int          // 数量
}
道具背包变更(后台操作)
S→C NewEmail
{
  Title: string,   // 邮件标题
  EmailId: string  // 邮件ID
}
收到新邮件通知
S→C ChangeConfigNEW
changeType: int

1 = 活动配置更新
2 = 游戏配置更新
后台活动配置或游戏配置保存成功并发布 CAP 后,由 UserHost 通过 LoginHub 向所有在线玩家广播。前端收到后应按 changeType 重新请求对应配置。监听示例:connection.on("ChangeConfig", changeType => { ... })
S→C AccountKickedNEW
{
  userId: string   // 被顶的玩家ID
}
账号被顶通知。该账号在其他设备登录时,旧连接收到此事件。前端应提示"账号已在其他地方登录"并清空房间 UI、断开游戏连接。
S→C KickToMainPageNEW
{
  userId: string   // 被踢的玩家ID
}
强制退出到主页面。当前账号的连接已失效(被顶号/用户不匹配),前端收到后应立即清除所有游戏 UI 并导航到主页面/登录页。
S→C PendingRoomNoticeNEW
{
  roomId: string,    // 房间ID
  gameType: string,  // 游戏类型: "风雷之渊"
  roomName: string   // 房间名: "高级场"
}
登录后检测到有未完成的断线游戏。金币已结算至当前余额。前端应弹窗提示用户选择"返回房间"(调 Reconnect)或"离开"(调 DismissPendingRoom),60秒无操作自动离开。
FishGameHub — /fishinghub
WS,捕鱼游戏核心 Hub。连接后先调 Reconnect 绑定用户。

🤖 机器人说明:捕鱼房间中的机器人(IsBot=true)由服务端完全模拟。客户端无需发送机器人指令,机器人在协议层面与真人一致——同样触发 RoomFireNotice、RoomKillNotice 等事件。

📤 客户端 → 服务器

方法参数返回说明
C→S Reconnectstring userId无断线重连必须先调用。服务端返回 RoomInfo(含 activeFishes)并恢复房间状态
C→S JoinRoomstring roomId, string userId, int posituin无加入房间。posituin=-1 自动分配座位
C→S FireCannonstring roomId, string userId, long gold, int count, long timestamp, string sign, object? extra
{ bullets: [{ bulletId: string, gold: long }], gold: long, timestamp: long }
发炮。每颗返回子弹携带本次发射炮倍;外层 gold 为玩家剩余金币。sign=MD5签名
C→S KillFishstring roomId, string userId, string fishNumber, string fihsId, long gold, long timespan, string bulletId, long timestamp, string sign
{ gold: long, timestamp: long }
击杀鱼
C→S LeaveRoomstring roomId, string userId无退出房间(金币结算后自动 Context.Abort 断开)
C→S UsePropstring roomId, string userId, string propNumber, int diamond=0int 剩余数量使用道具(钻石购买时传 diamond)。返回背包剩余数量,-1 表示失败。冰冻道具(40003)触发 FishFrozen 事件
C→S ChangeGunMultiplestring roomId, string userId, int multiple无更改炮倍
C→S GetRoomInfostring roomId无结果通过 GetRoomInfoResult 返回
C→S GetPlayerInfostring roomId, string userId无获取玩家信息
C→S SyncGameDatastring roomId, object data无同步游戏数据
C→S SetPlayerAfkNEWstring roomId, string userId无玩家挂机,触发 PlayerAfkNotice
C→S CancelPlayerAfkNEWstring roomId, string userId无取消挂机,触发 PlayerCancelAfkNotice,返回 RoomInfo(含 activeFishes)同步当前鱼状态

📥 服务器 → 客户端

事件数据目标说明
S→C BindSuccessstring调用者重连绑定成功
S→C Exception
{
  code: int,      // 错误码(0=未知错误)
  message: string  // 错误描述
}

错误码说明:
1001=房间已满
1002=金币不足
1003=房间不存在
1004=玩家不在房间
1005=签名错误
1006=请求已过期
1007=鱼配置不存在
1008=子弹参数无效
1009=金币参数超出范围
1011=用户不存在
1012=道具配置不存在
1013=道具数量不足
1014=钻石不足
1015=炮倍参数无效
调用者错误通知。前端根据 code 处理业务逻辑
S→C RoomInfo
RedisGameRoom 扩展字段,包含首次及当前活跃鱼:
{
  RoomId, RoomName, GameType, RoomLevel, RoomPartition,
  RoomNumber, MaxPlayerCount, CurrentOnlinePlayerCount,
  RobotCount, Players[], CreateUserId, SystemFactor,
  activeFishes: [{            // 首次进房/后续进房/重连/取消挂机时包含
    fishId: number,
    fishIds: number[],
    fishNumber: string,
    routeId: number,
    fishArrayNumber: string,
    createdAt: number,         // 毫秒
    expireAt: number,          // 毫秒
    remainingSeconds: int
  }]
}
调用者加入房间/断线重连/取消挂机返回房间信息。先用 activeFishes 初始化鱼群;服务端约 300ms 后还会向当前玩家补发同一批 OnFishSpawn(init),前端须按 fishId 去重。
S→C SyncPlayerGoldlong调用者金币同步(金币≤0时触发)
S→C SendPlayerInfoResultlong?调用者玩家Gold值
S→C GetRoomInfoResultRedisGameRoom?调用者房间信息查询结果
S→C RoomKillNotice
{
  fishNumber: string, userId: string, gold: long,
  fishId: string, timespan: long, killtime: long,
  msg: string,         // "时间:X秒"
  beishu: double,
  skillsGold: long,    // 炮倍金币
  fishScore: int,
  param: object?,      // Boss爆奖结果
  drop: object?        // Boss掉落 { propNumber, propName, count }
}
房间组击杀通知
S→C RoomFireNotice{ userId, bulletIds[], gold, count, extra, isBot }房间组发炮通知
S→C RoomJoinNotice
{
  userId: string,           // 玩家ID
  userName: string,         // 玩家名字
  userNumber: string,       // 玩家编号
  avatar: string,           // 玩家头像
  isRedName: bool,          // 是否红名
  level: int,               // 玩家等级
  exp: long,                // 经验值
  maxExp: long,             // 当前等级最大经验值
  vipLevel: int,            // VIP等级
  gold: long,               // 金币
  diamond: int,             // 钻石
  cannonId: string,         // 炮台
  wingsId: string,          // 翅膀
  warGodId: string,         // 战神
  cannonMultiple: int,      // 炮倍
  position: int,            // 位置
  beginCoin: long,          // 进场金币
  maxGold: long,            // 最高金币
  beginTime: long,          // 进场时间
  state: int,               // 状态 0正常 1断线中
  playerFactor: object,     // 玩家因子
  totalRechargeGold: long,  // 累计充值
  safeBoxGold: long,        // 保险箱金币
  isBot: bool,              // 是否机器人
  isAfk: bool               // 是否挂机
}
房间组玩家加入
S→C RoomLeaveNoticestring userId房间组玩家离开
S→C BotLeaveRoomNoticeNEW
{ userId: string }
房间组机器人退出房间
S→C HostDisconnectNEW无房间组房主断线,其他玩家可接受机器人控制权
S→C RoomDisconnectNoticestring userId房间组玩家断线
S→C ChangeGunMultipleNotice{ userId, multiple }房间组炮倍变更
S→C ChangeControllerUserstring userId房间组房主控制权移交
S→C UsePropNotice{ userId, propNumber }房间组使用道具
S→C SummonCardFishNoticeNEW
{
  roomId: string,       // 房间ID
  userId: string,       // 使用召唤卡的玩家ID
  userNumber: string,   // 玩家编号
  userName: string,     // 玩家昵称
  fishType: int,        // 2=Boss鱼,3=黄金鱼
  fishTypeName: string, // "Boss鱼"或"黄金鱼"
  fishNumber: string,   // 召唤出的鱼配置编号
  fishId: number        // 本次生成的鱼实例ID
}
房间组召唤卡成功生成鱼后广播。鱼的路径和生命周期仍由 OnFishSpawn 处理;本事件用于显示“哪个玩家召唤了什么鱼”等提示。
S→C PlayerAfkNoticeNEW{ userId: string }房间组玩家挂机通知
S→C PlayerCancelAfkNoticeNEW{ userId: string }房间组取消挂机通知
S→C TorpedoResultNoticeNEW
{ userId: string, propNumber: string, gold: long, color: string }
房间组鱼雷使用结果通知
S→C OnPlayerInfoNEW
{ userId, userName, gold, diamond, viplvl, cannonMultiple, cannonId, wingsId, position }
调用者玩家信息同步
S→C SyncGameDataNEWobject data房间组游戏数据同步(服务端主动推送)
S→C FishFrozenNEW无房间组冰冻道具(40003)生效,鱼存活时间延长 15 秒(客户端写死 15s 倒计时)
S→C OnSeatChange
// type=join 时:
{ type: "join", playerId: string, position: int, playerName: string,
  nickname: string, gold: long, diamond: int, vipLevel: int,
  turretId: string, turretRate: int, turretLv: int, headId: string,
  roomRate: int, bulletPower: int }
// type=leave 时:
{ type: "leave", userId: string, playerId: string,
  position: int, showWaiting: bool? }
房间组座位变更(加入/离开)
S→C OnFishSpawnNEW
{
  type: "init" | "spawn" | "sync", // init=进房补发,spawn=定时出鱼,sync=主动同步
  timestamp: number,        // 服务器当前时间戳(毫秒)
  fish: [{
    fishId: number,         // 普通鱼ID;鱼阵时为首条普通鱼ID(兼容旧客户端)
    fishNumber: string,     // 普通鱼=鱼编号;鱼阵=""
    routeId: number,        // 普通鱼=路径编号;鱼阵=固定阵型编号(如71001)
    fishArrayNumber: string,// 鱼阵编号(非鱼阵时为"")
    fishIds: number[],      // 所有出鱼必传;普通鱼=[fishId],鱼阵=阵内全部鱼ID
    createdAt: number,      // 鱼创建时间戳(毫秒),前端用它算路径进度
    expireAt: number,       // 鱼过期时间戳(毫秒)
    remainingSeconds: int   // 鱼剩余存活秒数
  }]
}
房间组(init/spawn)/调用者(sync)fishIds 在首次出鱼、定时出鱼、入房及重连同步中都必须提供。普通鱼为单元素集合 [fishId];鱼阵按鱼阵配置的生成顺序使用 ID 集合。鱼阵的 fishNumber 为空,鱼信息由 fishArrayNumber 对应的本地配置确定。击中时把对应 ID 作为 KillFish 的 fihsId 原样传回。
S→C KillNotice
{
  FishId: string,     // 鱼编号
  userName: string,   // 玩家名
  gold: long,         // 金币
  // 机器人模拟时额外字段:
  fishName: string?,  // 鱼名
  multiplier: int?    // 倍数
}
全部全局击杀公告(真人击杀/机器人模拟)
FishRoomHub — /fishingroomhub
WS,房间列表

📤 客户端 → 服务器

方法参数说明
C→S GetRoomListstring userId, int gameType, int roomLevel, int roomPartition获取房间列表。结果通过 GetRoomListResult 事件返回
C→S GetRoomInfostring roomId获取房间详情

📥 服务器 → 客户端

事件数据说明
S→C GetRoomListResultList<GameRoomReponse>
GameRoomReponse: { RoomId, MaxPlayerCount, CurrentOnlinePlayerCount, RobotCount, RoomNumber }
房间列表(仅统计真人)
S→C RoomChangeRedisGameRoom房间变化通知(人数、状态变更)
S→C GetRoomInfoResultRedisGameRoom?房间详情
ChessGameHub — /chessgamehub
WS,中国象棋

🏠 房间列表说明:大厅只返回后台预配置的固定数字房间号。服务端不会再创建随机 GUID 展示房间,并会过滤、清理“无房主、无玩家”的旧异常房间。前端收到 GetRoomList、CreateRoomNotice、JoinRoomNotice 或 ExitRoomNotice 后,应按 roomId 更新同一条房间数据,不要追加重复房间。

📤 客户端 → 服务器

方法参数说明
C→S CreateRoomCreateRoomParam { roomId, userId, gameType(1=我方黑 2=我方红), password, gold }创建房间
C→S JoinGamestring gameId, string pwd, string userId加入游戏
C→S GameReadystring gameId, string userId准备
C→S EndGamestring gameId, string winUserId直接结束(对方获胜)
C→S RequestEndGamestring gameId, string userId, string winUserId请求结束(需双方确认)
C→S RestartGamestring gameId, string userId对局结束后重新开始
C→S GoldConfigstring gameId, long gold, string userId房主设置金币
C→S ExitRoomstring gameId, string userId退出(房主退出清空房间,非房主保留房间)
C→S Heartbeatstring gameId心跳,更新活跃时间
C→S CheckTimeoutstring gameId检查超时(60秒断线判负)
C→S GetRoomListint partition=0房间列表;0=全部分区,正数=指定分区
C→S GetRoomStatestring gameId获取当前房间完整状态,用于返回场景或重连后主动校准
C→S KickPlayerstring gameId, string targetUserId房主在开局前踢出指定玩家
C→S SyncGameDatastring gameId, string data同步棋子移动
C→S GameOperationstring gameId, int type, string data
type: 1=求和 2=认输(自动结束) 3=悔棋
游戏操作

📥 服务器 → 客户端

事件数据目标
S→C GameErrorstring调用者
S→C BeginGameChessGameRoom调用者/房间组
S→C JoinRoomChessGameRoom调用者
S→C JoinRoomNoticeChessGameRoom象棋大厅组
S→C RoomStateNoticeChessGameRoom房间组/调用者,房间完整状态
S→C GoldConfigRequired{ targetUserId, room }房间组,两名真人到齐后提示指定红方设置金币
S→C Reconnect{ userId, userName }房间组
S→C Disconnect{ userId, disconnectTime, timeoutSeconds }房间组
S→C ReadyNoticestring userId房间组
S→C AllBeginTimeChessGameRoom房间组
S→C GameEnd{ winUserId, room }房间组
S→C EndGameRequestSentstring调用者
S→C OpponentRequestEndstring userId房间组
S→C RestartGameSuccessChessGameRoom房间组
S→C RestartRoomNoticeChessGameRoom全部
S→C ExitRoomNoticeChessGameRoom象棋大厅组
S→C ExitRoomSuccessstring userId房间组
S→C KickedFromRoom{ gameId, userId }被踢玩家
S→C CreateRoomNoticeChessGameRoom象棋大厅组
S→C GoldConfigSuccessChessGameRoom房间组
S→C GoldConfigRoomNoticeChessGameRoom全部
S→C GetRoomListList<ChessGameRoom>调用者;仅固定房间,按 roomId 覆盖本地列表
S→C SyncGameDatastring全部
S→C GameOperation{ type, data }全部
CustomerHub — /customerHub
WS,客服即时通讯。客服端需 JWT,玩家端无需 Token。

📤 客户端 → 服务器

方法参数说明
C→S JoinConversationstring conversationId加入会话房间组
C→S LeaveConversationstring conversationId离开会话房间组
C→S SendMessagestring conversationId, int senderType, string senderId, string senderName, string? senderAvatar, int contentType, string content, string guId=""
senderType: 1=用户 2=客服
contentType: 1=文字 2=图片
conversationId 为空时自动创建新会话
发送消息
C→S StartConversationstring userId, string userName主动发起新会话

📥 服务器 → 客户端

事件数据目标
S→C OnMessageReceived
{
  GuId: string,
  ConversationId: string,
  SenderType: int,
  SenderId: string,
  SenderName: string,
  SenderAvatar: string?,
  ContentType: int,
  Content: string,
  CreateTime: DateTime
}
会话组
S→C OnConversationStartedConversationEntity调用者
S→C OnNewConversation{ conversationId, customerId?, customerName?, userId?, userName?, status, createTime }客服组
S→C Exception
{
  code: int,
  message: string
}
调用者错误通知,code 同上表

注意:客服连接时携带 JWT,服务端自动从 uid Claim 加入通知组,无需手动 JoinCustomerQueue。

🔗 Hub 路由总览

Hub路由认证
LoginHub/loginhubJWT
FishGameHub/fishinghub方法传参 userId
FishRoomHub/fishingroomhub方法传参 userId
ChessGameHub/chessgamehub方法传参 userId
CustomerHub/customerHub客服需JWT,玩家无需

📦 通用数据结构

名称字段
RedisGameRoom
{
  RoomId, RoomName, GameType, RoomLevel, RoomPartition,
  RoomNumber, MaxPlayerCount, CurrentOnlinePlayerCount,
  RobotCount, Players[], CreateUserId,
  SystemFactor: double     // 当前系统因子(根据当日盈利动态调整)
}
RedisGamePlayer
{
  UserId, UserName, UserNumber, Avatar, IsRedName,
  Level, VipLevel, Gold, Diamond, Position,
  CannonId, WingsId, WarGodId, CannonMultiple,
  BeginCoin, MaxGold, BeginTime, State(0=正常 1=断线),
  CId, PlayerFactor, TotalRechargeGold, SafeBoxGold,
  IsBot, IsAfk
}
ChessGameRoom
{
  RoomId, state(1=等待 2=游戏中 3=已结束), users[],
  gameType(1=我方黑 2=我方红), gold, password,
  createUserId, robotCount, isGameEnded,
  timeoutSeconds(默认60), endGameRequests, pendingWinUserId
}
ConversationEntity
{
  UserId?, UserName?, CustomerId?, CustomerName?,
  Status(0=等待 1=进行中 2=已结束), CreateTime,
  EndTime?, LastReadTime?
}

📤 发送基础设施(IHubSend<T>)

方法说明
SendUserCId(cid, method, obj)发送给指定连接
SendUserUserId(userId, method, obj)通过 userId 查找 CId 后发送
SendGroup(groupId, method, obj)发送给 SignalR 群组
SendAll(method, obj)广播给所有客户端
GroupExcept(cid, groupId, method, obj)发送给群组排除指定连接

流程说明

1. 登录 / Bind 流程 (LoginHub)

  1. 客户端连接 /loginhub (JWT)
  2. 调用 Bind(userId),加入 userId 组
  3. Bind 读取 Redis LoginCId:
    • 若存在旧 LoginCId 且不等于当前连接 -> 顶号:
      • 发 AccountKicked 给旧 LoginHub 连接
      • 查旧 LoginHub 关联的游戏 Hub CId,发 KickToMainPage
    • 更新 Redis LoginCId = 当前连接 CId
  4. 检测断线房间 (PendingRoomId):
    • 有 -> 发 PendingRoomNotice(弹窗:返回房间 / 离开)
    • 无 -> 进入大厅

2. 顶号流程

  • 设备 A 登录:Bind -> LoginCId = cid_A
  • 设备 B 登录:Bind -> LoginCId = cid_A != cid_B -> 发 AccountKicked 到 cid_A -> 更新 LoginCId = cid_B -> A 被顶
  • 设备 C 登录:Bind -> LoginCId = cid_B != cid_C -> 发 AccountKicked 到 cid_B -> 更新 LoginCId = cid_C -> B 被顶
  • 关键:仅 LoginHub.Bind 负责发 AccountKicked,FishGameHub 不再发送

3. JoinRoom 流程 (FishGameHub)

  1. 客户端调 JoinRoom(roomId, userId, posituin)
  2. 检查玩家是否已在房间 -> 是则返回
  3. 获取/创建房间 roomInfo
  4. 判断是否首个真人进房:
    • 是 -> RegisterRoom(roomId, roomLevel) 注册出鱼服务
    • 否 -> GetActiveFishes(roomId) 获取当前活跃鱼列表
  5. 发 RoomInfo(cid, room, activeFishes) 给调用者
  6. 发 OnSeatChange(join) 通知房间组

4. Reconnect 流程 (FishGameHub)

  1. 客户端连接 FishGameHub 后立即调 Reconnect(userId)
  2. 查 Redis 有无断线房间 (PendingRoomId):
    • 有 -> 恢复玩家状态 State=0,走 JoinRoom 逻辑(含鱼同步)
    • 无 -> 无操作,等待后续指令
  3. 返回结果:RoomInfo(首个真人注册后的首次鱼群放入 activeFishes,约 300ms 后向当前玩家补发 OnFishSpawn(type=init))

5. CancelPlayerAfk 流程

  1. 客户端调 CancelPlayerAfk(roomId, userId)
  2. 清除玩家 IsAfk 标记
  3. GetActiveFishes(roomId) 获取当前活跃鱼
  4. 发 RoomInfo(cid, room, activeFishes) 给调用者(同步鱼状态)
  5. 发 PlayerCancelAfkNotice 通知房间组

6. LeaveRoom / 退出流程

  1. 客户端调 LeaveRoom(roomId, userId)
  2. 金币结算,从房间移除玩家
  3. 发 RoomLeaveNotice 通知房间组
  4. 检查剩余真人:
    • 无真人 -> UnregisterRoom(roomId) 注销出鱼 + 清理房间
    • 有真人 -> 房主转移(若原房主退出),发 ChangeControllerUser

7. 房主断线 / 所有权转移

  1. 房主断线触发 HandleDisconnect
  2. 查找下一个非挂机真人作为新房主:
    • 找到 -> 转移所有权,发 ChangeControllerUser(新房主 userId) 通知房间组
    • 未找到 -> 发 HostDisconnect(前端禁用发炮/出鱼,显示等待提示)

8. 出鱼服务 (FishSpawnService)

  • 首个真人进房 -> RegisterRoom -> 启动定时器持续出鱼
  • 定时出鱼 -> OnFishSpawn(type=spawn) 广播房间组
  • 后续真人进房/重连/取消挂机 -> GetActiveFishes -> 嵌入 RoomInfo.activeFishes 返回,并约 300ms 后补发 OnFishSpawn(type=init)
  • 最后一个真人退出/断线 -> UnregisterRoom -> 停止定时器 + 清除活跃鱼