ASP.NET Core SignalR ,九:SignalR .NET 客户端

此为系列文章,对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);                
}

额外资源