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

Unity+SignalR场景下HttpRequestException导致HoloLens帧率骤降问题

问题:HoloLens客户端SignalR断开重连时帧率骤降

问题场景

使用ASP.NET Core Web API服务器 + Unity/HoloLens客户端,正常连接运行无异常。测试断开重连流程:启动服务器→客户端连接→关闭服务器。断开后WithAutomaticReconnect定义的逻辑触发,随后连接关闭;在Closed方法中调用自定义Action触发Restart操作,此时StartAsync抛出HttpRequestException(符合预期,因服务器离线),但HoloLens应用帧率从60FPS骤降至10FPS,持续约20秒后恢复。

错误信息

Default: ⨯[SignalRClient]: System.Net.Http.HttpRequestException: An error occurred while sending the request
System.Net.WebException: Error: ConnectFailure (No connection could be established because the target computer refused to connect.

UnityEngine.DebugLogHandler:LogFormat (UnityEngine.LogType,UnityEngine.Object,string,object[])
MyLogHandler:LogFormat (UnityEngine.LogType,UnityEngine.Object,string,object[]) (at Assets/Scripts/Logging/MyLogHandler.cs:29)
UnityEngine.Logger:LogError (string,object)
MyApp.Logging.MyLogger:DoLog (MyApp.Logging.DebugMode,System.Action`2<string, object>,string,object,object[]) (at Assets/Scripts/Logging/MyLogger.cs:66)
MyApp.Logging.MyLogger:LogError (MyApp.Logging.DebugMode,object,object[]) (at Assets/Scripts/Logging/MyLogger.cs:33)
SignalRClient/<StartConnection>d__4:MoveNext () (at Assets/Scripts/Clients/SignalRClient.cs:78)
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.SignalR.Client.HubConnection/<StartAsync>d__48:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.SignalR.Client.HubConnection/<StartAsyncInner>d__49:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.SignalR.Client.HubConnection/<StartAsyncCore>d__58:MoveNext ()
System.Runtime.CompilerServices.AsyncValueTaskMethodBuilder`1<Microsoft.AspNetCore.Connections.ConnectionContext>:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnectionFactory/d__3:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/ d__40:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/d__41:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/d__44:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<Microsoft.AspNetCore.Http.Connections.NegotiationResponse>:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/<GetNegotiationResponseAsync>d__52:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<Microsoft.AspNetCore.Http.Connections.NegotiationResponse>:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.HttpConnection/d__45:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Net.Http.HttpResponseMessage>:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.Internal.LoggingHttpMessageHandler/<SendAsync>d__2:MoveNext ()
System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<System.Net.Http.HttpResponseMessage>:SetException (System.Exception)
Microsoft.AspNetCore.Http.Connections.Client.Internal.AccessTokenHttpMessageHandler/d__3:MoveNext ()
System.Threading.Tasks.TaskFactory`1<System.Net.WebResponse>:FromAsyncCoreLogic (System.IAsyncR

代码实现

public class SignalRClient
{
    private Action _connectionClosed;

    public SignalRClient()
    {
        _connectionClosed += Restart;

        _hubConnection = new HubConnectionBuilder()
            .WithUrl(ConfigModel.Config.MyUri + "/hmdHub")
            .WithAutomaticReconnect(new[]{ TimeSpan.FromSeconds(5) })
            .Build();

        _hubConnection.Reconnecting += Reconnecting;
        _hubConnection.Reconnected += Reconnected;
        _hubConnection.Closed += Closed;
    }

    public async void Start()
    {
        ConfigureConnection();
        _ = await StartConnection();
    }

    public async void Restart()
        => _ = await StartConnection();

    public async Task Dispose()
    {
        _connectionClosed -= Restart;

        _hubConnection.Reconnecting -= Reconnecting;
        _hubConnection.Reconnected -= Reconnected;
        _hubConnection.Closed -= Closed;

        await _hubConnection.StopAsync();
        await _hubConnection.DisposeAsync();
    }

    private void ConfigureConnection()
    {
        _hubConnection.On<Alarm>("CreateAlarm", (alarmObject) =>
        {
            MyLogger.Log(DebugMode.Default, "SignalRClient>CreateAlarm", $"Got new alarm with id: {alarmObject.Id} with message: {alarmObject.Message}");
        });
    }

    private async Task<bool> StartConnection()
    {
        _tokenSource = new CancellationTokenSource();

        while (true)
        {
            try
            {
                await _hubConnection.StartAsync(_tokenSource.Token);
                MyLogger.LogSuccess(DebugMode.Default, this, "Connected to SignalR-Hub");
                return true;
            }
            catch when (_tokenSource.IsCancellationRequested)
            {
                return false;
            }
            catch(Exception ex)
            {
                MyLogger.LogError(DebugMode.Default, this, ex.ToString());
                await Task.Delay(5000);
            }
        }
    }

    private Task Reconnecting(Exception arg)
    {
        MyLogger.Log(DebugMode.Default, this, "Attempting to reconnect...");
        return Task.CompletedTask;
    }

    private Task Reconnected(string arg)
    {
        MyLogger.LogSuccess(DebugMode.Default, this, "Connection restored.");
        return Task.CompletedTask;
    }

    private Task Closed(Exception arg)
    {
        MyLogger.LogWarning(DebugMode.Default, this, "Connection closed.");
        _connectionClosed?.Invoke();
        return Task.CompletedTask;
    }

    private HubConnection _hubConnection;
    private CancellationTokenSource _tokenSource;
}

原因分析

  1. 双重重连逻辑冲突:同时启用了SignalR内置的WithAutomaticReconnect和手动触发的Restart重连循环,两个独立的重连任务同时发起网络请求,占用HoloLens有限的CPU和网络资源,引发帧率下降。
  2. async void滥用:Start和Restart使用async void,导致异步任务脱离Unity上下文管理,异常无法被正确捕获,还可能造成主线程调度混乱。
  3. 日志输出过载:每次连接失败都会打印完整的异常堆栈,高频日志输出占用大量CPU资源,进一步拖慢帧率。

解决方案

1. 移除重复重连逻辑

保留SignalR内置的WithAutomaticReconnect即可,它会自动处理断开后的重连流程,无需手动触发:

public SignalRClient()
{
    // 移除手动重连绑定
    // _connectionClosed += Restart;

    _hubConnection = new HubConnectionBuilder()
        .WithUrl(ConfigModel.Config.MyUri + "/hmdHub")
        .WithAutomaticReconnect(new[]{ TimeSpan.FromSeconds(5) })
        .Build();

    _hubConnection.Reconnecting += Reconnecting;
    _hubConnection.Reconnected += Reconnected;
    _hubConnection.Closed += Closed;
}

private Task Closed(Exception arg)
{
    MyLogger.LogWarning(DebugMode.Default, this, "Connection closed.");
    // 移除手动重连触发
    // _connectionClosed?.Invoke();
    return Task.CompletedTask;
}

2. 替换async void为async Task

Unity中异步方法应使用async Task,避免上下文丢失和异常无法处理:

public async Task Start()
{
    ConfigureConnection();
    await StartConnection();
}

// 若需手动重启,同步修改为async Task
public async Task Restart()
    => await StartConnection();

调用时在Unity异步上下文执行:

// 在MonoBehaviour脚本中
private async void Start()
{
    await _signalRClient.Start();
}

3. 优化日志输出

简化错误日志,避免每次失败都打印完整堆栈:

catch(Exception ex)
{
    MyLogger.LogError(DebugMode.Default, this, $"连接失败,5秒后重试:{ex.Message}");
    await Task.Delay(5000);
}

4. 确保重连循环唯一性

若坚持使用手动重连,必须移除SignalR内置重连,避免双重任务:

_hubConnection = new HubConnectionBuilder()
    .WithUrl(ConfigModel.Config.MyUri + "/hmdHub")
    // 移除内置重连
    // .WithAutomaticReconnect(new[]{ TimeSpan.FromSeconds(5) })
    .Build();

内容的提问来源于stack exchange,提问作者Perazim

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 22:45:08