此為系列文章,對MSDN ASP.NET Core SignalR 的官方文檔進行系統學習與翻譯。其中或許會添加本人對 ASP.NET Core 的淺顯理解
ASP.NET Core SignalR .NET 類庫允許你從.NET app 和 SignalR hubs 進行通信。
本章的示例代碼是一個WPF 應用程序,其使用了SignalR .NET客戶端類庫。
安裝SignalR .NET 客戶端包
.NET 客戶端需要Microsoft.AspNetCore.SignalR.Client 包來連接到 SignalR hubs。在Visual studio 中,為了安裝SignalR 客戶端類庫,在PMC窗口中運行如下命令:
Install-Package Microsoft.AspNetCore.SignalR.Client
如果通過.NET Core 腳手架,可以通過如下命令來安裝:
dotnet add package Microsoft.AspNetCore.SignalR.Client
連接到一個 中心
為了建立連接,我們可以創建一個 HubConnectionBuilder 並調用 Build。中心 的 URL,協議,傳送類型,日志級別,頭信息,以及其他選項都可以在構建連接的時候被配置。通過插入任意的HubConnectionBuilder 方法到Build來配置所需的選項。然后使用 StartAsync 來啟動這個連接。
using System; using System.Threading.Tasks; using System.Windows; using Microsoft.AspNetCore.SignalR.Client; namespace SignalRChatClient { public partial class MainWindow : Window { HubConnection connection; public MainWindow() { InitializeComponent(); connection = new HubConnectionBuilder() .WithUrl("http://localhost:53353/ChatHub") .Build(); connection.Closed += async (error) => { await Task.Delay(new Random().Next(0,5) * 1000); await connection.StartAsync(); }; } private async void connectButton_Click(object sender, RoutedEventArgs e) { connection.On<string, string>("ReceiveMessage", (user, message) => { this.Dispatcher.Invoke(() => { var newMessage = $"{user}: {message}"; messagesList.Items.Add(newMessage); }); }); try { await connection.StartAsync(); messagesList.Items.Add("Connection started"); connectButton.IsEnabled = false; sendButton.IsEnabled = true; } catch (Exception ex) { messagesList.Items.Add(ex.Message); } } private async void sendButton_Click(object sender, RoutedEventArgs e) { try { await connection.InvokeAsync("SendMessage", userTextBox.Text, messageTextBox.Text); } catch (Exception ex) { messagesList.Items.Add(ex.Message); } } } }
處理丟失的連接
自動重連
使用 HubConnectionBuilder 上的WithAutomaticReconnect
方法,HubConnection 可以被配置為自動重連。默認情況下它不會自動重連。
HubConnection connection= new HubConnectionBuilder() .WithUrl(new Uri("http://127.0.0.1:5000/chatHub")) .WithAutomaticReconnect() .Build();
沒有任何參數,WithAutomaticReconnect 配置客戶端依次等待 0,2,10,30秒來嘗試進行連接,如果4次連接嘗試均失敗,那么便會停止連接。
在開始任何重連嘗試之前,HubConnection 會過渡到HubConnectionState.Reconnecting狀態並觸發Reconnecting 事件。這便提供了一個時機,我們可以在這個事件中警告用戶連接已經丟失,並禁用掉UI元素。非交互性app可以將消息加入隊列或者丟棄消息。
connection.Reconnecting += error => { Debug.Assert(connection.State == HubConnectionState.Reconnecting); // Notify users the connection was lost and the client is reconnecting. // Start queuing or dropping messages. return Task.CompletedTask; };
如果在客戶端的前4次嘗試之內便成功連接的話,HubConnection
會過渡回 Connected 狀態並觸發
Reconnected事件。這也提供了一個時機來通知用戶連接已經被重新建立,並入隊任何隊列消息。
因為連接看起來對於服務端來說是完全新的,因而一個新的
ConnectionId會被提供給
Reconnected事件處理器。
注意:如果
HubConnection被配置為skip negotiation,那么
Reconnected事件處理器的connectionId參數將會為null。
connection.Reconnected += connectionId => { Debug.Assert(connection.State == HubConnectionState.Connected); // Notify users the connection was reestablished. // Start dequeuing messages queued while reconnecting if any. return Task.CompletedTask; };
WithAutomaticReconnect() 方法不會配置HubConnection重試最初的啟動失敗,因此,啟動失敗需要進行手動處理。
public static async Task<bool> ConnectWithRetryAsync(HubConnection connection, CancellationToken token) { // Keep trying to until we can start or the token is canceled. while (true) { try { await connection.StartAsync(token); Debug.Assert(connection.State == HubConnectionState.Connected); return true; } catch when (token.IsCancellationRequested) { return false; } catch { // Failed to connect, trying again in 5000 ms. Debug.Assert(connection.State == HubConnectionState.Disconnected); await Task.Delay(5000); } } }
如果客戶端在其前4次嘗試之內(自動重連)沒有成功重連,那么HubConnection 會過渡到 Disconnected 狀態並觸發Closed 狀態。這提供了一個機會我們可以嘗試手動重啟連接或者通知用戶連接已經永久丟失。
connection.Closed += error => { Debug.Assert(connection.State == HubConnectionState.Disconnected); // Notify users the connection has been closed or manually try to restart the connection. return Task.CompletedTask; };
在斷開或者更改重連定時之前,為了配置自定義的重連嘗試次數,WithAutomaticReconnect方法接收一個數字數組作為參數,其以毫秒數表示了在各個重連嘗試之前需要等待的延遲。
HubConnection connection= new HubConnectionBuilder() .WithUrl(new Uri("http://127.0.0.1:5000/chatHub")) .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.Zero, TimeSpan.FromSeconds(10) }) .Build(); // .WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30) }) yields the default behavior.
之前的代碼配置HubConnection
在連接丟失之后立即嘗試重連。這和默認的配置是一樣的。
如果第一次重連嘗試失敗了,第二次重連嘗試在等待2秒之后也會立即開始,就如通它在默認配置中的一樣。
如果第二次重連嘗試失敗了,第三次重連嘗試會在10秒內開始,其同樣和默認配置保持一致。
在第三次重連嘗試失敗之后,便會停止連接,自定義行為便相異於默認行為。在gxeng默認配置中,在30秒之后還會有另一次重連嘗試。
如果你想對定時以及自動重連的次數具有更多的控制,WithAutomaticReconnect 也可以接收一個實現了IRetryPolicy接口的對象作為參數,其具有一個單獨的名為NextRetryDelay的方法。
NextRetryDelay 具有一個單獨的類型為RetryContext 的參數。RetryContext 具有三個屬性,PreviousRetryCount,ElapsedTime
,RetryReason,它們分別是 long,TimeSpan,
Exception類型。在第一次重連嘗試之前,PreviousRetryCount和ElapsedTime都會是 0,而RetryReason會是導致連接丟失的異常。在每一次失敗的重試嘗試之后,PreviousRetryCount 會自增加1,ElapsedTime會被更新以反映到現在為止花費在重連上的時間。
RetryReason將會是導致上一次重連嘗試失敗的原因。
NextRetryDelay
要么返回一個表示在下一次重連嘗試之前需要等待的時間的TimeSpan,要么返回null,返回null時,表示HubConnection 應該停止重連。
public class RandomRetryPolicy : IRetryPolicy { private readonly Random _random = new Random(); public TimeSpan? NextRetryDelay(RetryContext retryContext) { // If we've been reconnecting for less than 60 seconds so far, // wait between 0 and 10 seconds before the next reconnect attempt. if (retryContext.ElapsedTime < TimeSpan.FromSeconds(60)) { return TimeSpan.FromSeconds(_random.NextDouble() * 10); } else { // If we've been reconnecting for more than 60 seconds so far, stop reconnecting. return null; } } }
HubConnection connection = new HubConnectionBuilder() .WithUrl(new Uri("http://127.0.0.1:5000/chatHub")) .WithAutomaticReconnect(new RandomRetryPolicy()) .Build();
除此之外,你可以寫代碼來手動連接你的客戶端,如同下一章節所演示的那樣。
手動重連
使用Closed事件來響應丟失的連接。比如,或許你想自動重連。
Closed
事件需要一個返回Task的委托,其允許不使用 async void 的 異步代碼。為了在一個異步運行的Closed 事件處理器中滿足委托簽名,可以返回Task.CompletedTask。
connection.Closed += (error) => { // Do your close logic. return Task.CompletedTask; };
支持異步的主要原因是這樣你可以重啟連接。啟動連接是一個異步動作。
在重啟連接的 Closed 處理程序中,考慮等待一些隨機延遲以防止服務器過載,如下示例所示:
connection.Closed += async (error) => { await Task.Delay(new Random().Next(0,5) * 1000); await connection.StartAsync(); };
從客戶端調用 中心 方法
InvokeAsync 可以調用 中心 的方法。將 中心 方法的名稱和方法中定義的參數傳遞給 InvokeAsync。SignalR是異步的,因此,當調用時,使用async 和 await關鍵字。
await connection.InvokeAsync("SendMessage", userTextBox.Text, messageTextBox.Text);
InvokeAsync
方法返回一個Task,其將在服務端方法返回的時候完成。如果有任何返回值的話,都應該作為Task 的結果返回。服務端方法返回的任何異常都會產生一個有錯誤的Task。使用await
標記來等待服務端方法完成,使用 try...catch標簽來處理異常。
SendAsync
方法返回一個Task,其會在消息被送到服務端時完成。因為這個方法不會等待服務端方法完成,所以其沒有提供返回值。當發送消息時客戶端拋出的任何異常均會產生一個錯誤的Task。同樣,使用try...catch 來處理錯誤。
注意:如果你正在以無服務模式 使用Azure SignalR 服務,那么你不能從客戶端調用 hubs 方法。更多信息,請參考SignalR Service documentation。
從 中心 調用客戶端方法
在構建連接之后,啟動連接之前,使用 connection.On
定義 中心 調用的方法。
connection.On<string, string>("ReceiveMessage", (user, message) => { this.Dispatcher.Invoke(() => { var newMessage = $"{user}: {message}"; messagesList.Items.Add(newMessage); }); });
當服務端代碼使用 SendAsync 方法時,如上在connection.On
中的代碼會被運行。
public async Task SendMessage(string user, string message) { await Clients.All.SendAsync("ReceiveMessage", user,message); }
錯誤處理及日志
使用try-catch語句來處理錯誤。在 一個錯誤產生后,檢查異常對象以決定采取合適的動作。
try { await connection.InvokeAsync("SendMessage", userTextBox.Text, messageTextBox.Text); } catch (Exception ex) { messagesList.Items.Add(ex.Message); }
額外資源