.Net Core SignalR - 連接超時 - 心跳定時器 - 連接狀態更改處理
只是要預先明確,這個問題是關於 .Net Core SignalR,而不是以前的版本。
新的 SignalR 與 IIS 後面的 WebSockets 存在問題(我無法讓它們在 Chrome/Win7/IIS express 上工作)。因此,我使用的是伺服器發送事件 (SSE)。但是,問題是大約 2 分鐘後超時,連接狀態從 2 變為 3。自動重新連接已被刪除(顯然在以前的版本中它並不能很好地工作)。
我現在想實現一個心跳計時器來阻止客戶端超時,每 30 秒一次的滴答聲就可以完成這項工作。
11 月 10 日更新
我現在已經設法實現伺服器端 Heartbeat,基本上取自 Ricardo Peres 的https://weblogs.asp.net/ricardoperes/signalr-in-asp-net-core
- 在 startup.cs 中,添加到
public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)app.UseSignalR(routes => { routes.MapHub<TheHubClass>("signalr"); }); TimerCallback SignalRHeartBeat = async (x) => { await serviceProvider.GetService<IHubContext<TheHubClass>>().Clients.All.InvokeAsync("Heartbeat", DateTime.Now); }; var timer = new Timer(SignalRHeartBeat).Change(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(30));
- 集線器類
對於 HubClass,我添加了
public async Task HeartBeat(DateTime now) => await Clients.All.InvokeAsync("Heartbeat", now);顯然,計時器、正在發送的數據(我只是發送一個 DateTime)和客戶端方法名稱都可以不同。
更新.Net Core 2.1+
請參閱下面的評論;不應再使用計時器回調。我現在已經實現了一個 IHostedService(或者更確切地說是抽象的 BackgroundService)來做到這一點:
public class HeartBeat : BackgroundService { private readonly IHubContext<SignalRHub> _hubContext; public HeartBeat(IHubContext<SignalRHub> hubContext) { _hubContext = hubContext; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { await _hubContext.Clients.All.SendAsync("Heartbeat", DateTime.Now, stoppingToken); await Task.Delay(30000, stoppingToken); } } }在您的啟動課程中,將其連接到之後
services.AddSignalR();:services.AddHostedService<HeartBeat>();
- 客戶
var connection = new signalR.HubConnection("/signalr", { transport: signalR.TransportType.ServerSentEvents }); connection.on("Heartbeat", serverTime => { console.log(serverTime); });初始問題的剩餘部分
剩下的是如何正確地重新連接客戶端,例如在 IO 暫停之後(瀏覽器的電腦進入睡眠狀態、失去連接、更改 Wifi 或其他)
我已經實現了一個正常工作的客戶端心跳,至少在連接中斷之前:
- 集線器類:
public async Task HeartBeatTock() => await Task.CompletedTask;- 客戶:
var heartBeatTockTimer; 函式 sendHeartBeatTock() { connection.invoke(“HeartBeatTock”); } connection.start().then(args => { heartBeatTockTimer = setInterval(sendHeartBeatTock, 10000); });
例如,在瀏覽器掛起 IO 之後,invoke 方法會拋出一個異常——它不能被簡單的 try/catch 擷取,因為它是非同步的。我試圖為我的 HeartBeatTock 做的事情類似於(虛擬碼):
function sendHeartBeatTock try connection.invoke("HeartbeatTock) catch exception try connection.stop() catch exception (and ignore it) finally connection = new HubConnection().start() repeat try connection.invoke("HeartbeatTock") catch exception log("restart did not work") clearInterval(heartBeatTockTimer) informUserToRefreshBrowser()現在,由於幾個原因,這不起作用。由於非同步執行,invoke 在程式碼塊執行後拋出異常。它看起來好像公開了一個 .catch() 方法,但我不確定如何在那裡正確地實現我的想法。另一個原因是,開始一個新的連接需要我重新實現所有的伺服器呼叫,比如“connection.on(”send"…)——這看起來很傻。
任何有關如何正確實現重新連接客戶端的提示將不勝感激。
我現在有一個可行的解決方案(到目前為止在 Chrome 和 FF 中測試過)。為了激勵您想出更好的東西,或者在自己想出類似的東西時為您節省一點時間,我在這裡發布我的解決方案:
上述問題中描述了 Heartbeat-“Tick”消息(伺服器定期 ping 客戶端)。客戶端(“Tock”部分)現在有:
- 註冊連接的函式,以便可以重複回調方法(connection.on());否則在重新啟動“新 HubConnection”後它們會失去
- 註冊 TockTimer 的函式
- 和一個實際發送 Tock ping 的函式
tock 方法在發送時擷取錯誤,並嘗試啟動新連接。由於計時器一直在執行,我正在註冊一個新連接,然後只需坐下來等待下一次呼叫。將客戶端放在一起:
// keeps the connection object var connection = null; // stores the ID from SetInterval var heartBeatTockTimer = 0; // how often should I "tock" the server var heartBeatTockTimerSeconds = 10; // how often should I retry after connection loss? var maxRetryAttempt = 5; // the retry should wait less long then the TockTimer, or calls may overlap var retryWaitSeconds = heartBeatTockTimerSeconds / 2; // how many retry attempts did we have? var currentRetryAttempt = 0; // helper function to wait a few seconds $.wait = function(miliseconds) { var defer = $.Deferred(); setTimeout(function() { defer.resolve(); }, miliseconds); return defer; }; // first routine start of the connection registerSignalRConnection(); function registerSignalRConnection() { ++currentRetryAttempt; if (currentRetryAttempt > maxRetryAttempt) { console.log("Clearing registerHeartBeatTockTimer"); clearInterval(heartBeatTockTimer); heartBeatTockTimer = 0; throw "Retry attempts exceeded."; } if (connection !== null) { console.log("registerSignalRConnection was not null", connection); connection.stop().catch(err => console.log(err)); } console.log("Creating new connection"); connection = new signalR.HubConnection("/signalr", { transport: signalR.TransportType.ServerSentEvents }); connection.on("Heartbeat", serverTime => { console.log(serverTime); }); connection.start().then(() => { console.log("Connection started, starting timer."); registerHeartBeatTockTimer(); }).catch(exception => { console.log("Error connecting", exception, connection); }); } function registerHeartBeatTockTimer() { // make sure we're registered only once if (heartBeatTockTimer !== 0) return; console.log("Registering registerHeartBeatTockTimer"); if (connection !== null) heartBeatTockTimer = setInterval(sendHeartBeatTock, heartBeatTockTimerSeconds * 1000); else console.log("Connection didn't allow registry"); } function sendHeartBeatTock() { console.log("Standard attempt HeartBeatTock"); connection.invoke("HeartBeatTock").then(() => { console.log("HeartbeatTock worked.") }) .catch(err => { console.log("HeartbeatTock Standard Error", err); $.wait(retryWaitSeconds * 1000).then(function() { console.log("executing attempt #" + currentRetryAttempt.toString()); registerSignalRConnection(); }); console.log("Current retry attempt: ", currentRetryAttempt); }); }
這是在 IIS 後面執行 SignalR Core 時出現的問題。IIS 將在 2 分鐘後關閉空閒連接。長期計劃是添加保持活動消息,作為副作用,這將阻止 IIS 關閉連接。要暫時解決此問題,您可以:
- 定期向客戶端發送消息
- 按照此處所述更改 IIS 中的空閒超時設置
- 如果連接關閉,則在客戶端重新啟動連接
- 使用不同的傳輸方式(例如長輪詢,因為您不能在 IIS 後面的 Win7/Win2008 R2 上使用 webSockets)