页面跳转时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调用。
前端修复步骤
- 在Connector类中添加断开连接、管理监听的方法
- 在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
相关产品推荐
相关产品推荐

