ASP.NET+React扑克游戏SignalR偶发事件接收失效排查求助
问题概述
基于ASP.NET + React开发的扑克游戏,使用SignalR实现实时通信,多数场景运行正常,但偶尔会出现客户端无法接收服务器事件、与其他玩家不同步的情况,刷新页面后恢复正常,暂无法定位触发条件。
核心问题分析与修复方案
1. 数据库中SignalR连接ID未更新(最关键原因)
问题点:GameClass的OnConnectedAsync方法中,更新玩家SignalRconnectionId的代码被完全注释。当客户端自动重连后,服务器会生成新的ConnectionId,但数据库中玩家的连接ID仍是旧值,导致服务器用旧ID发送事件,客户端自然收不到。
修复代码:恢复并优化OnConnectedAsync逻辑,确保每次连接(包括重连)时更新数据库中的连接ID:
public override Task OnConnectedAsync() { HttpContext httpContext = Context.GetHttpContext(); // 直接从Query获取UserIdentity,避免拆分字符串的不可靠性 var playerUniqueId = httpContext.Request.Query["UserIdentity"].ToString(); if (!string.IsNullOrEmpty(playerUniqueId)) { var player = _dbCardGameContext.Player.FirstOrDefault(x => x.PlayerUniqueId == playerUniqueId); if (player != null) { player.LastActionTime = DateTime.Now; player.Modified = DateTime.Now; player.SignalRconnectionId = Context.ConnectionId; player.IsConnected = true; _dbCardGameContext.SaveChanges(); } else { _dbCardGameContext.Player.Add(new Player() { Created = DateTime.Now, IsConnected = true, LastActionTime = DateTime.Now, PlayerUniqueId = playerUniqueId, SignalRconnectionId = Context.ConnectionId }); _dbCardGameContext.SaveChanges(); } } return base.OnConnectedAsync(); }
2. 自动重连后事件未重新绑定
问题点:虽然启用了withAutomaticReconnect(),但重连成功后,原有的事件绑定可能失效;当前绑定逻辑的useEffect依赖connection.connectionId,但未先解绑旧事件,可能导致重复绑定或绑定失效。
修复代码:优化客户端事件绑定逻辑,先解绑再绑定,确保重连后事件正常生效:
useEffect(() => { if (!connection) return; // 先解绑所有同名事件,避免重复绑定 connection.off("OtherPlayerDisconnected"); connection.off("ReceiveHashV1"); connection.off("ReceiveEndHandSummary"); connection.off("ReceiveEndGameSummary"); connection.off("ReceiveCancelHandNotification"); connection.off("RemovedNotification"); connection.off("ReceiveMessage"); if (connection.state === HubConnectionState.Connected) { connection.on("OtherPlayerDisconnected", (UserId, UserName) => { OtherPlayerDisconnected(UserId, UserName); }); connection.on("ReceiveHashV1", () => { SendRequest({ url: "GameV2/_GetGameHash", method: "post", data: { GameCode }, }).then((result) => { LogRocket.log("Received GameHash on Game: " + GameCode, { GameHash: result.data, }); setGameHash(result.data); }); }); // 其他事件绑定... } return () => { // 组件卸载时强制解绑所有事件 connection.off("OtherPlayerDisconnected"); connection.off("ReceiveHashV1"); connection.off("ReceiveEndHandSummary"); connection.off("ReceiveEndGameSummary"); connection.off("ReceiveCancelHandNotification"); connection.off("RemovedNotification"); connection.off("ReceiveMessage"); }; }, [connection, connection.connectionId, GameCode]);
3. 服务器端消息发送未处理异常与异步等待
问题点:部分发送事件的方法未做空值判断和异常捕获,比如SendRemoveNotification中若player为null会直接报错;OnDisconnectedAsync中循环发送消息时未使用await,导致消息未发送完成方法就结束。
修复代码:添加异常处理与异步等待:
// 修复SendRemoveNotification方法 public async Task SendRemoveNotification(string GameCode, string UserId) { var player = await _dbCardGameContext.Player .Where(x => x.GameCode == GameCode && x.PlayerUniqueId == UserId) .FirstOrDefaultAsync(); if (player != null && !string.IsNullOrEmpty(player.SignalRconnectionId)) { try { await Clients.Client(player.SignalRconnectionId).SendAsync("RemovedNotification"); } catch (Exception ex) { // 记录日志,便于排查发送失败原因 // _logger.LogError(ex, "发送RemovedNotification失败,连接ID:{ConnectionId}", player.SignalRconnectionId); } } } // 修复OnDisconnectedAsync方法 public override async Task OnDisconnectedAsync(Exception exception) { Console.Out.WriteLine(Context.ConnectionId); try { var disconnectedPlayer = _dbCardGameContext.Player.FirstOrDefault(x => x.SignalRconnectionId == Context.ConnectionId); if (disconnectedPlayer != null && !string.IsNullOrEmpty(disconnectedPlayer.GameCode)) { disconnectedPlayer.IsConnected = false; disconnectedPlayer.IsActive = false; disconnectedPlayer.IsCurrent = false; disconnectedPlayer.IsDealer = false; disconnectedPlayer.IsFolded = false; _dbCardGameContext.SaveChanges(); var relatedPlayers = _dbCardGameContext.Player .Where(x => x.GameCode == disconnectedPlayer.GameCode && x.SignalRconnectionId != Context.ConnectionId) .ToList(); foreach (var player in relatedPlayers) { if (!string.IsNullOrEmpty(player.SignalRconnectionId)) { try { await Clients.Client(player.SignalRconnectionId).SendAsync("OtherPlayerDisconnected", disconnectedPlayer.PlayerUniqueId, disconnectedPlayer.UserName); } catch (Exception ex) { // 记录日志 } } } } } catch (Exception ex) { // 记录日志 } await base.OnDisconnectedAsync(exception); }
4. 客户端连接管理混乱
问题点:user变化时会创建新连接,但旧连接未正确关闭,可能导致资源泄漏或事件绑定冲突。
修复代码:创建新连接前关闭旧连接,添加连接状态日志便于排查:
// 修复连接初始化的useEffect useEffect(() => { let isMounted = true; const startConnection = async () => { // 关闭旧连接 if (connection) { try { await connection.stop(); } catch (e) { LogRocket.log("关闭旧连接失败", e); } } const newConnection = await MainV2.startConnectionWithUserIdentity(user.Id); LogRocket.log("Connected to SignalR", newConnection.connectionId); if (isMounted) { setConnection(newConnection); } }; startConnection(); return () => { isMounted = false; if (connection) { connection.stop().catch(e => LogRocket.log("卸载时关闭连接失败", e)); } }; }, [user]); // 优化startConnectionWithUserIdentity函数 const startConnectionWithUserIdentity = async (UniqueId) => { const connection = new HubConnectionBuilder() .withUrl(GetFullURL(`GameClass?UserIdentity=${UniqueId}`)) .withAutomaticReconnect() .build(); // 添加连接状态监听,便于排查问题 connection.onclose((error) => { LogRocket.log("SignalR连接关闭", error); }); connection.onreconnected((connectionId) => { LogRocket.log("SignalR重新连接成功", connectionId); }); connection.onreconnecting((error) => { LogRocket.log("SignalR正在重新连接", error); }); if (connection.state !== HubConnectionState.Connected) { await connection.start(); } return connection; };
内容的提问来源于stack exchange,提问作者Demon Wizard

