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

页面跳转时SignalR Hub未断开,后台API仍持续调用问题求助

问题描述

我实现了一套SignalR相关功能:后端创建EODStatusHub,通过UserHandler维护连接ID集合;TimedEODService作为后台服务每5秒检查连接数,有连接时调用API推送数据。前端通过TypeScript的Connector单例创建SignalR连接,在React组件EODStatus中接收消息。目前用户停留在页面时功能正常,但离开页面时SignalR连接未触发断开,导致后台仍持续调用API,求解决方法。


后端SignalR Hub代码

public class EODStatusHub : Hub    
{
    public override Task OnConnectedAsync()
    {
        UserHandler.ConnectedIds.Add(Context.ConnectionId);
        return base.OnConnectedAsync();
    }

    public override Task OnDisconnectedAsync(Exception exception)
    {
        UserHandler.ConnectedIds.Remove(Context.ConnectionId);
        return base.OnDisconnectedAsync(exception);
    }
}

public static class UserHandler
{
    public static HashSet<string> ConnectedIds = new HashSet<string>();
}

后台服务代码

public class TimedEODService : BackgroundService
{
    private readonly ILogger<TimedEODService> _logger;
    private readonly IHubContext<EODStatusHub> _eodStatusHubContext;
    private readonly IEODService _eodService;

    public TimedEODService(ILogger<TimedEODService> logger, IHubContext<EODStatusHub> eodStatusHubContext, IEODService eodService)
    {
        _logger = logger;
        _eodStatusHubContext = eodStatusHubContext;
        _eodService = eodService;
    }

    protected override async Task ExecuteAsync(CancellationToken token)
    {
        await Task.Yield();

        while (!token.IsCancellationRequested)
        {
            await Task.Delay(5000, token);
            if (UserHandler.ConnectedIds.Count > 0)
            {
                StatusReport eodStatus = await _eodService.GetEODStatus();
                _eodStatusHubContext.Clients.All.SendAsync("eodReceived", eodStatus);
            }
        }
    }
}

前端TypeScript连接代码

import * as signalR from "@microsoft/signalr";
const URL = "https://localhost:7082/pochub"; //or whatever your backend port is
class Connector {
    private connection: signalR.HubConnection;
    public events: (onMessageReceived: ( message: any) => void) => void;
    static instance: Connector;
    constructor() {
        this.connection = new signalR.HubConnectionBuilder()
            .withUrl(URL)
            .withAutomaticReconnect()
            .build();
        this.connection.start().catch(err => document.write(err));
        this.events = (onMessageReceived) => {
            this.connection.on("eodReceived", ( message) => {
                onMessageReceived(message);
            });
        };
    }
    public newMessage = (messages: string) => {
        this.connection.send("newMessage", "foo", messages).then(x => console.log("sent"))
    }
    public static getInstance(): Connector {
        if (!Connector.instance)
            Connector.instance = new Connector();
        return Connector.instance;
    }
}
export default Connector.getInstance;

React页面组件代码

const EODStatus = () => {
     const { newMessage, events } = Connector();
     const [message, setMessage] = useState("initial value");   
     const initialRender = useRef<boolean>(false);    
            useEffect(() => {      
                events((message) => setMessage(message));
            }, []);
           return (
           <>   
    <span>message from signalR: <span style={{ color: "green" }}>{JSON.stringify(message)} 
       </span>
       </>
    }

问题原因与解决方案

核心问题:前端连接未在页面离开时主动断开

当前前端采用单例模式创建SignalR连接,组件卸载时不会自动终止连接;加上开启了withAutomaticReconnect,页面跳转后连接会持续尝试重连,导致后端OnDisconnectedAsync无法触发,静态集合中的连接ID无法被移除,后台服务持续执行API调用。

前端修复步骤

  1. 在Connector类中添加断开连接、管理监听的方法
  2. 在React组件的useEffect中返回清理函数,组件卸载时主动断开连接并移除监听

修改后的Connector代码

import * as signalR from "@microsoft/signalr";
const URL = "https://localhost:7082/pochub";
class Connector {
    private connection: signalR.HubConnection;
    private messageListeners: ((message: any) => void)[] = [];
    static instance: Connector;
    
    constructor() {
        this.connection = new signalR.HubConnectionBuilder()
            .withUrl(URL)
            .withAutomaticReconnect()
            .build();
        this.connection.start().catch(err => console.error(err));
        
        // 统一分发消息到所有监听者
        this.connection.on("eodReceived", (message) => {
            this.messageListeners.forEach(listener => listener(message));
        });
    }
    
    // 添加消息监听,返回移除监听的函数
    public onMessageReceived(listener: (message: any) => void) {
        this.messageListeners.push(listener);
        return () => {
            const index = this.messageListeners.indexOf(listener);
            if (index !== -1) {
                this.messageListeners.splice(index, 1);
            }
        };
    }
    
    // 主动断开连接并清空监听
    public async stopConnection() {
        if (this.connection.state === signalR.HubConnectionState.Connected) {
            await this.connection.stop();
            this.messageListeners = [];
        }
    }
    
    public newMessage = (messages: string) => {
        this.connection.send("newMessage", "foo", messages).then(x => console.log("sent"))
    }
    
    public static getInstance(): Connector {
        if (!Connector.instance)
            Connector.instance = new Connector();
        return Connector.instance;
    }
}
export default Connector.getInstance;

修改后的React组件代码

import { useState, useEffect } from 'react';
const EODStatus = () => {
    const connector = Connector();
    const [message, setMessage] = useState("initial value");   
    
    useEffect(() => {
        // 添加监听并获取移除函数
        const removeListener = connector.onMessageReceived((msg) => setMessage(msg));
        
        // 组件卸载时执行清理
        return async () => {
            removeListener();
            await connector.stopConnection();
        };
    }, [connector]);

    return (
        <>   
            <span>message from signalR: <span style={{ color: "green" }}>{JSON.stringify(message)}</span></span>
        </>
    );
}
export default EODStatus;

后端潜在问题修复:线程安全的连接集合

原代码中HashSet<string>不是线程安全集合,SignalR的连接/断开事件由多线程触发,可能导致连接ID统计错误。需替换为线程安全集合:

修改后的UserHandler代码

using System.Collections.Concurrent;

public static class UserHandler
{
    // 使用线程安全的ConcurrentHashSet(.NET Core 3.0+支持)
    public static ConcurrentHashSet<string> ConnectedIds = new ConcurrentHashSet<string>();
}

若项目不支持ConcurrentHashSet,可使用ConcurrentDictionary模拟:

using System.Collections.Concurrent;

public static class UserHandler
{
    public static ConcurrentDictionary<string, bool> ConnectedIds = new ConcurrentDictionary<string, bool>();
}

对应修改Hub中的代码

public override Task OnConnectedAsync()
{
    UserHandler.ConnectedIds.TryAdd(Context.ConnectionId, true);
    return base.OnConnectedAsync();
}

public override Task OnDisconnectedAsync(Exception exception)
{
    UserHandler.ConnectedIds.TryRemove(Context.ConnectionId, out _);
    return base.OnDisconnectedAsync(exception);
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 10:39:52