天天看點

Asp.net Core中使用SignalR出現ObjectDisposedException的解決問題描述解決辦法問題原因

SignalR出現ObjectDisposedException的解決

  • 問題描述
    • 功能要求
    • 異常位置
  • 解決辦法
  • 問題原因

問題描述

功能要求

業務上要實作伺服器端定時向用戶端推送某些最新消息,方案的思路是:當用戶端連接配接上來時記錄下它的ConnectionId,使用Timer建立定時任務,定時向該ConnectionId的用戶端發送消息。

異常位置

在定時任務中使用Clients.Client(connId) 或其他方法擷取用戶端連接配接時冒出ObjectDisposedException。

Asp.net Core中使用SignalR出現ObjectDisposedException的解決問題描述解決辦法問題原因

異常位置代碼:

public async void LoopTask(object state)
{
	// state是ConnectionId
    string connId = state.ToString();

    // 任何方式擷取用戶端連接配接都會冒ObjectDisposedException
    var client = Clients.Client(connId);

    string arg = "this is an argument";
    await client.SendAsync("Test", arg);
}
           

有問題的代碼總覽:

public class TestHub : Hub
    {
        // 定時任務字典。Dictionary不是線程安全的,實際應用中應該加鎖或者使用ConcurrentDictionary
        private static Dictionary<string, Timer> Tasks { get; set; }

        static TestHub()
        {
            Tasks = new Dictionary<string, Timer>();
        }

        public override Task OnDisconnectedAsync(Exception exception)
        {
            string connId = Context.ConnectionId;
            if(Tasks.TryGetValue(connId,out Timer timer))
            {
                timer.Dispose();
            }
            return base.OnDisconnectedAsync(exception);
        }

        public override Task OnConnectedAsync()
        {
            string connId = Context.ConnectionId;
            // 0毫秒後開始每5秒執行一次
            Timer timer = new Timer(LoopTask, connId, 0, 5000);
            Tasks.Add(connId, timer);
            return base.OnConnectedAsync();
        }

        public async void LoopTask(object state)
        {
            string connId = state.ToString();
            // 任何方式擷取用戶端連接配接都會冒ObjectDisposedException
            var client = Clients.Client(connId);
            string arg = "this is an argument";
            await client.SendAsync("Test", arg);
        }
    }
           

解決辦法

因為Hub是臨時性的,請求完成後會dispose掉,是以所有不在請求内的操作都不能直接使用Hub裡面的對象(Clients, Groups等)擷取用戶端連接配接。

Asp.net Core中使用SignalR出現ObjectDisposedException的解決問題描述解決辦法問題原因

這裡要單獨開一個用戶端連接配接的類,利用Asp.net core自帶依賴注入将IHubContext注入到類中,在類中用IHubContext進行操作。

這裡要注意的是,Asp.net 和Asp.net core是不一樣的,Asp.net中可以用GlobalHost中擷取到IHubContext,而Asp.net core中就不能這樣擷取。

Asp.net Core中使用SignalR出現ObjectDisposedException的解決問題描述解決辦法問題原因

修改後代碼總覽:

public class ClientConnection : IDisposable
    {
    	// 連接配接辨別,也可以是userId等,反正能定位到用戶端就行
        public string ConnectionId { get; set; }
		
		// HubContext,用于擷取用戶端連接配接
        private IHubContext<TestHub> Context { get; set; }

		// 定時任務
        private Timer Timer { get; set; }

        public ClientConnection(IHubContext<TestHub> hubContext, string connId)
        {
            this.ConnectionId = connId;
            this.Context = hubContext;

            // 0毫秒後開始每5秒執行一次
            Timer = new Timer(LoopTask, null, 0, 5000);
        }

        public async void LoopTask(object state)
        {
            var client = Context.Clients.Client(this.ConnectionId);
            string arg = "this is an argument";

            await client.SendAsync("Test", arg);
        }

        public void Dispose()
        {
            this.Timer.Dispose();
        }
    }

    public class TestHub : Hub
    {
        // 用于擷取依賴注入對象
        private IServiceProvider Service { get; set; }

        // 這裡其實不應該把連接配接儲存在Hub中,demo的話随便了(記得加鎖)
        private static List<ClientConnection> Connections;

        static TestHub()
        {
            Connections = new List<ClientConnection>();
        }

        public TestHub(IServiceProvider service)
        {
            Service = service;
        }

        public override Task OnDisconnectedAsync(Exception exception)
        {
            string connId = Context.ConnectionId;

            var conn = Connections.Find(s => s.ConnectionId == connId);
            if(conn != null)
            {
                Connections.Remove(conn);
                conn.Dispose();
            }

            return base.OnDisconnectedAsync(exception);
        }

        public override Task OnConnectedAsync()
        {
            string connId = Context.ConnectionId;
			// 這裡不用在StartUp裡面AddScoped也可以擷取到
            IHubContext<TestHub> context = Service.GetService(typeof(IHubContext<TestHub>)) as IHubContext<TestHub>;
            ClientConnection conn = new ClientConnection(context, connId);
            Connections.Add(conn);

            return base.OnConnectedAsync();
        }
    }
           

問題原因

問題的原因就在于Hub執行個體的暫時性,跟Controller一樣,一個請求生成一個執行個體,似乎也合情合理。但是,雖然Hub是臨時性的但是它管理的連接配接是持久的(WebSocket長連接配接),這就很有迷惑性,容易讓人以為Hub也是持久存在的。

Asp.net Core中使用SignalR出現ObjectDisposedException的解決問題描述解決辦法問題原因

最後,建議入坑的過程中還是要多看文檔,其實微軟的文檔确實做得不錯,雖然很多都是機器翻譯,而且詞彙都比較晦澀,但重點都會有特别說明,這點可以給個贊。

參考

Asp.net Core中SignalR擷取IHubContext的文檔

繼續閱讀