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

Blazor Server应用SignalR连接意外关闭问题排查与优化咨询

Blazor Server SignalR连接异常排查与稳定性优化

问题背景

开发基于Blazor Server的气象站应用,通过SignalR将树莓派3B(arm64架构)上的传感器实时数据推送到客户端。目前客户端无法接收数据,连接被服务器意外关闭,且Closed事件处理中出现ObjectDisposedException。

关键日志

info: Microsoft.AspNetCore.Http.Connections.Client.HttpConnection[6]
      HttpConnection Disposed.
dbug: Microsoft.AspNetCore.SignalR.Client.HubConnection[46]
      Canceling all outstanding invocations.
dbug: Microsoft.AspNetCore.SignalR.Client.HubConnection[21]
      HubConnection stopped.
dbug: Microsoft.AspNetCore.SignalR.Client.HubConnection[51]
      Invoking the Closed event handler.
fail: WeatherStationBlazor.Components.Pages.Weather[0]
      Connection closed:
fail: Microsoft.AspNetCore.SignalR.Client.HubConnection[27]
      An exception was thrown in the handler for the Closed event.
      System.ObjectDisposedException: Cannot access a disposed object.
      Object name: 'Microsoft.AspNetCore.SignalR.Client.HubConnection'.

相关代码

Program.cs

using Microsoft.AspNetCore.Http.Connections;
using WeatherStationBlazor.Components;
using WeatherStationBlazor.Data;

namespace WeatherStationBlazor
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            builder.Services.AddRazorComponents()
                .AddInteractiveServerComponents();
            builder.Services.AddHostedService<SensorDataBackgroundService>();
            builder.Services.AddSingleton<Bme280Service>();
            builder.Services.AddSignalR(hubOptions =>
            {
                hubOptions.EnableDetailedErrors = true;
                hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);
            });

            builder.WebHost.ConfigureKestrel(options =>
            {
                options.ListenAnyIP(5000);
            });

            var app = builder.Build();

            // Configure the HTTP request pipeline.
            if (!app.Environment.IsDevelopment())
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }

            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseAntiforgery();

            app.MapRazorComponents<App>()
                .AddInteractiveServerRenderMode();
            app.MapHub<SensorHub>("/sensorHub", options =>
            {
                options.Transports =
                    HttpTransportType.WebSockets |
                    HttpTransportType.LongPolling;
            });

            app.Run();
        }
    }
}

客户端Blazor组件(Weather.razor)

@page "/weather"
@using Microsoft.AspNetCore.SignalR.Client
@inject WeatherStationBlazor.Data.Bme280Service Bme280Service
@inject NavigationManager NavigationManager
@inject ILogger<Weather> Logger
@implements IAsyncDisposable

<h3>Real-Time Weather Data</h3>

@if (!dataLoaded)
{
    <p>Loading...</p>
}
else
{
    <p>Temperature: @temperature?.ToString("F2") °C</p>
    <p>Humidity: @humidity?.ToString("F2") %</p>
    <p>Pressure: @pressure?.ToString("F2") hPa</p>
}

@code {
    private double? temperature;
    private double? humidity;
    private double? pressure;
    private bool dataLoaded = false;

    private HubConnection? hubConnection;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            Logger.LogInformation("Initializing Hub connection...");
            hubConnection = new HubConnectionBuilder()
                .WithUrl(NavigationManager.ToAbsoluteUri("/sensorHub"))
                .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.Zero, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(5) })
                .ConfigureLogging(logging =>
                {
                    logging.AddConsole();
                    logging.SetMinimumLevel(LogLevel.Debug);
                })
                .Build();

            hubConnection.Closed += async (error) =>
            {
                Logger.LogError($"Connection closed: {error?.Message}");
                await Reconnect();
            };

            hubConnection.On<double, double, double>("ReceiveSensorData", async (temp, hum, pres) =>
            {
                Logger.LogInformation($"Received data: Temperature: {temp}, Humidity: {hum}, Pressure: {pres}");
                temperature = temp;
                humidity = hum;
                pressure = pres;
                await InvokeAsync(StateHasChanged);
            });

            await StartHubConnectionAsync();

            // Load initial data
            Logger.LogInformation("Loading initial sensor data...");
            var data = await Bme280Service.ReadSensorDataAsync();
            temperature = data.temperature;
            humidity = data.humidity;
            pressure = data.pressure;
            dataLoaded = true;
        }
        catch (Exception ex)
        {
            Logger.LogError($"Error initializing Hub connection: {ex.Message}");
            dataLoaded = false;
        }
    }

    private async Task StartHubConnectionAsync()
    {
        if (hubConnection != null && hubConnection.State == HubConnectionState.Disconnected)
        {
            try
            {
                await hubConnection.StartAsync();
                Logger.LogInformation("Hub connection started successfully.");
            }
            catch (Exception ex)
            {
                Logger.LogError($"Error starting Hub connection: {ex.Message}");
                await Task.Delay(5000); // Wait for 5 seconds before retrying
                await StartHubConnectionAsync();
            }
        }
    }

    private async Task Reconnect()
    {
        Logger.LogInformation("Attempting to reconnect...");
        while (hubConnection?.State == HubConnectionState.Disconnected)
        {
            try
            {
                await hubConnection.StartAsync();
                Logger.LogInformation("Reconnected to Hub.");
                return;
            }
            catch (Exception ex)
            {
                Logger.LogError($"Reconnection attempt failed: {ex.Message}");
                await Task.Delay(2000); // Wait for 2 seconds before retrying
            }
        }
    }

    public async ValueTask DisposeAsync()
    {
        if (hubConnection != null)
        {
            var connection = hubConnection;
            hubConnection = null;

            try
            {
                await connection.StopAsync();
                Logger.LogInformation("Hub connection stopped successfully.");
            }
            catch (Exception ex)
            {
                Logger.LogError($"Error stopping Hub connection: {ex.Message}");
            }
            finally
            {
                await connection.DisposeAsync();
                Logger.LogInformation("Hub connection disposed.");
            }
        }
    }
}

已尝试方案

  • 配置WithAutomaticReconnect()实现自动重连
  • 调整SignalR的KeepAliveInterval与ClientTimeoutInterval参数
  • 确保组件销毁时正确释放HubConnection

问题解答

1. 服务器意外关闭SignalR连接的可能原因

  • 资源限制:树莓派3B内存/CPU资源有限,当传感器数据推送频繁或后台服务占用过高时,Kestrel可能因资源不足主动断开连接
  • 超时配置不匹配:当前KeepAliveInterval设为1分钟,但客户端默认超时(30秒)早于服务器心跳,导致连接被判定为失效
  • WebSocket兼容性:arm64环境下Kestrel的WebSocket实现可能存在兼容性问题,降级为LongPolling可验证此问题
  • 异常未处理:SensorHub或后台推送服务中存在未捕获的异常,导致连接被服务器终止
  • 网络波动:树莓派所在网络不稳定,出现丢包或延迟导致连接中断

2. 确保客户端持续接收数据的修复措施

  • 修复Closed事件的ObjectDisposedException:在Reconnect方法中先检查连接是否已被释放,避免访问已销毁的对象:
    private async Task Reconnect()
    {
        Logger.LogInformation("Attempting to reconnect...");
        while (true)
        {
            if (hubConnection == null) break;
            if (hubConnection.State != HubConnectionState.Disconnected) break;
            
            try
            {
                await hubConnection.StartAsync();
                Logger.LogInformation("Reconnected to Hub.");
                return;
            }
            catch (Exception ex)
            {
                Logger.LogError($"Reconnection attempt failed: {ex.Message}");
                await Task.Delay(2000);
            }
        }
    }
    
  • 统一重连逻辑:移除自定义Reconnect方法,依赖WithAutomaticReconnect()的内置重连机制,同时监听Reconnected事件重新订阅:
    hubConnection.Reconnected += async (connectionId) =>
    {
        Logger.LogInformation($"Reconnected with connection ID: {connectionId}");
        hubConnection.On<double, double, double>("ReceiveSensorData", OnReceiveSensorData);
    };
    
    private async Task OnReceiveSensorData(double temp, double hum, double pres)
    {
        Logger.LogInformation($"Received data: Temperature: {temp}, Humidity: {hum}, Pressure: {pres}");
        temperature = temp;
        humidity = hum;
        pressure = pres;
        await InvokeAsync(StateHasChanged);
    }
    
  • 匹配超时配置:在客户端连接时设置与服务器一致的心跳和超时时间:
    hubConnection = new HubConnectionBuilder()
        .WithUrl(NavigationManager.ToAbsoluteUri("/sensorHub"), options =>
        {
            options.ClientTimeoutInterval = TimeSpan.FromMinutes(2);
            options.KeepAliveInterval = TimeSpan.FromMinutes(1);
        })
        .WithAutomaticReconnect()
        .Build();
    }
    

3. Blazor Server中提升SignalR连接稳定性的配置与模式

  • 复用HubConnection实例:通过单例服务注入全局HubConnection,避免每个组件创建独立连接,减少连接开销
  • 优化资源占用:在树莓派上限制Kestrel的并发连接数,避免资源耗尽:
    builder.WebHost.ConfigureKestrel(options =>
    {
        options.ListenAnyIP(5000);
        options.Limits.MaxConcurrentConnections = 10;
        options.Limits.MaxConcurrentUpgradedConnections = 10;
    });
    
  • 异常隔离:在后台推送服务中捕获数据推送异常,避免单条数据推送失败导致连接中断:
    try
    {
        await _hubContext.Clients.All.SendAsync("ReceiveSensorData", temp, hum, pres);
    }
    catch (Exception ex)
    {
        _logger.LogError($"Failed to send sensor data: {ex.Message}");
    }
    
  • 禁用不必要的中间件:若未配置SSL证书,关闭app.UseHttpsRedirection(),避免因重定向导致WebSocket连接失败
  • 实时状态监控:通过hubConnection.StateChanged事件监控连接状态,及时更新UI提示用户连接状态变化

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 14:09:53