You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET+React扑克游戏SignalR偶发事件接收失效排查求助

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 12:44:59