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

基于SignalR Hub的集中式通知服务单例化问题及作用域服务重复连接解决方案求助

基于SignalR Hub的集中式通知服务单例化问题及作用域服务重复连接解决方案求助

问题拆解与分析

首先帮你理清楚核心矛盾:你想要一个全局唯一的通知服务实例来管理SignalR连接,避免重复连接和状态不同步,但因为依赖了Scoped的NavigationManager,直接注册单例会报错;而现在用Scoped注册又导致多个实例、连接未统一启动的问题。另外,我注意到你的现有代码里还有几个容易踩坑的细节:

  • 每次调用StartAsync都会重复注册SystemStateChanged事件处理程序,导致同一个状态变更被触发多次
  • _started标记不管连接是否成功都设为true,后续连不上也没法重试
  • 没有处理SignalR重连后的状态同步,可能导致客户端状态滞后
  • 直接用Action事件容易造成Blazor组件的内存泄漏

针对性解决方案

根据你使用的Blazor环境(WebAssembly/Server),给你两套具体的实现方案:


方案一:Blazor WebAssembly(单例服务,推荐)

在WASM里,每个浏览器标签页是独立的应用实例,NavigationManager的行为和单例一致,所以完全可以把服务注册成单例:

1. 重构SystemStateClientService
public class SystemStateClientService : IAsyncDisposable
{
    private readonly HubConnection _hubConnection;
    private bool _isConnectedSuccessfully = false;
    private CurrentSystemState? _currentState = null;
    // 用WeakEventManager避免内存泄漏
    private readonly WeakEventManager<CurrentSystemState> _stateChangedManager = new();

    public event Action<CurrentSystemState>? OnStateChanged
    {
        add => _stateChangedManager.AddEventHandler(value);
        remove => _stateChangedManager.RemoveEventHandler(value);
    }

    public SystemStateClientService(NavigationManager navigation)
    {
        // 只在构造函数里初始化一次HubConnection和事件订阅
        _hubConnection = new HubConnectionBuilder()
            .WithUrl(navigation.ToAbsoluteUri("/hubs/notification"))
            .WithAutomaticReconnect()
            .Build();

        // 注册一次状态变更事件,避免重复订阅
        _hubConnection.On<string>("SystemStateChanged", HandleStateUpdate);
        
        // 重连后自动同步最新状态
        _hubConnection.Reconnected += async _ =>
        {
            Console.WriteLine("🔄 SignalR连接已重连,同步最新状态...");
            await GetCurrentStateAsync();
        };

        // 监控连接状态变化,方便调试
        _hubConnection.StateChanged += state =>
        {
            Console.WriteLine($"🔌 SignalR状态变更: {state.OldState} → {state.NewState}");
        };
    }

    private void HandleStateUpdate(string newStateStr)
    {
        if (Enum.TryParse(newStateStr, out CurrentSystemState newState))
        {
            _currentState = newState;
            _stateChangedManager.HandleEvent(this, newState, nameof(OnStateChanged));
        }
    }

    public async Task StartAsync()
    {
        if (_isConnectedSuccessfully && _hubConnection.State == HubConnectionState.Connected)
            return;

        try
        {
            if (_hubConnection.State == HubConnectionState.Disconnected)
            {
                await _hubConnection.StartAsync();
                Console.WriteLine("✅ SignalR连接已启动");
                _isConnectedSuccessfully = true;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"❌ SignalR连接启动失败: {ex.Message}");
            _isConnectedSuccessfully = false;
        }
    }

    public async Task<CurrentSystemState?> GetCurrentStateAsync()
    {
        // 惰性启动连接:如果还没连,先尝试启动
        if (!_isConnectedSuccessfully)
        {
            await StartAsync();
        }

        // 本地有缓存直接返回
        if (_currentState != null)
            return _currentState;

        try
        {
            var stateStr = await _hubConnection.InvokeAsync<string>("GetCurrentSystemState");
            if (Enum.TryParse(stateStr, out CurrentSystemState parsedState))
            {
                _currentState = parsedState;
                _stateChangedManager.HandleEvent(this, parsedState, nameof(OnStateChanged));
                return parsedState;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"❌ 获取当前状态失败: {ex.Message}");
        }
        return null;
    }

    public async ValueTask DisposeAsync()
    {
        await _hubConnection.DisposeAsync();
    }
}
2. 注册单例服务

修改program.cs:

builder.Services.AddSingleton<SystemStateClientService>();
3. 全局启动连接

在MainLayout.razor里启动一次即可,所有组件共享同一个实例:

@inherits LayoutComponentBase
@inject SystemStateClientService SystemStateClient

<!-- 你的布局代码 -->

@code {
    protected override async Task OnInitializedAsync()
    {
        await SystemStateClient.StartAsync();
        await base.OnInitializedAsync();
    }
}

方案二:Blazor Server(Scoped服务,符合会话隔离)

Blazor Server里每个用户会话对应一个Scoped容器,所以服务得注册成Scoped,确保每个用户有独立的SignalR连接:

1. 服务实现(同方案一的代码,无需调整)
2. 注册Scoped服务
builder.Services.AddScoped<SystemStateClientService>();
3. 全局初始化连接

在App.razor里启动连接,确保用户会话一开始就建立好连接:

<Router AppAssembly="@typeof(App).Assembly">
    <!-- 你的路由代码 -->
</Router>

@inject SystemStateClientService SystemStateClient

@code {
    protected override async Task OnInitializedAsync()
    {
        await SystemStateClient.StartAsync();
        await base.OnInitializedAsync();
    }
}

组件端使用注意事项

如果你的组件订阅了OnStateChanged事件,记得实现IDisposable来取消订阅(用WeakEventManager的话可以跳过,但手动取消更稳妥):

@implements IDisposable
@inject SystemStateClientService SystemStateClient

<p>当前系统状态: @CurrentState</p>

@code {
    private CurrentSystemState? CurrentState;

    protected override void OnInitialized()
    {
        SystemStateClient.OnStateChanged += UpdateState;
        base.OnInitialized();
    }

    private void UpdateState(CurrentSystemState state)
    {
        CurrentState = state;
        StateHasChanged();
    }

    public void Dispose()
    {
        SystemStateClient.OnStateChanged -= UpdateState;
    }
}

这样调整后,你就能拥有一个全局统一的通知服务,SignalR连接仅建立一次,状态同步及时,还能避免内存泄漏和重复连接的问题。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 10:35:31