视频演示地址:

一、概念

C#-网络通讯框架(三)-SignalR(SignalRCore版)_Net7_服务器

1、什么是 SignalR?

  ASP.NET Core SignalR 是一个开放源代码库,可用于简化向应用添加实时 Web 功能。 实时 Web 功能使服务器端代码能够将内容推送到客户端。

2、适合 SignalR 的场景
  • 需要从服务器进行高频率更新的应用。 示例包括游戏、社交网络、投票、拍卖、地图和 GPS 应用。
  • 仪表板和监视应用。 示例包括公司仪表板、即时销售更新或旅行警报。
  • 协作应用。 协作应用的示例包括白板应用和团队会议软件。
  • 需要通知的应用。 社交网络、电子邮件、聊天、游戏、旅行警报和很多其他应用都需使用通知。
3、SignalR实时通信使用的传输技术(以下为使用的先后选择顺序,自动选择服务器和客户端能力范围内的最佳传输方法)
  • WebSockets
  • Server-Sent Events
  • 长轮询
4、SignalR支持的功能
  • 自动处理连接管理。
  • 同时向所有连接的客户端发送消息。 例如聊天室。
  • 向特定客户端或客户端组发送消息。
  • 对其进行缩放,以处理不断增加的流量。
  •  SignalR中心协议(SignalR 使用 Hubs(中心协议) 在客户端和服务器之间进行通信;内置中心协议:基于JSON的文本协议 和 基于 MessagePack 的二进制协议(JSON的代替方案,消息更小), 旧版浏览器必须支持 XHR级别2 才能提供 MessagePack 协议支持。)
5、SignalR服务器与客户端标准

  服务器标准:支持 ASP.NET Core 支持的任何服务器平台;如果服务器运行 IIS中,则 WebSockets 传输需要 Windows Server 2012 或更高版本上的 IIS 8.0 或更高版本。

  Web客户端标准:SignalR 面向 ES6。 对于不支持 ES6 的浏览器,请将库转译为 ES5。 有关详细信息,请参阅 使用ES6入门 – 使用 Traceur和Babel 将 ES6转为ES5

  .Net客户端标准:.NET 客户端可在 ASP.NET Core 支持的任何平台上运行(Xamarin.Android版本>8.4.0.1;Xamarin.iOS版本>11.14.0.4)。 

  Java 客户端标准:支持 Java 8 及更高版本。

二、知识点

1、创建SignalR Server

  Microsoft.AspNetCore.App已集成了Microsoft.AspNetCore.SignalR包;不需要独立安装Microsoft.AspNet.SignalR或Microsoft.AspNetCore.SignalR包

(1)添加SignalR服务
var builder = WebApplication.CreateBuilder(args);

// builder.Services.AddRazorPages();  // Razor应用时,AddSignalR在AddRazorPages后
builder.Services.AddSignalR();  // 1、添加SignalR服务
  • 1.
  • 2.
  • 3.
  • 4.
(2)启用SignalR服务
app.MapRazorPages();
app.MapHub<ChatHub>("/Chat");  // 2、启用SignalR服务

app.Run();
  • 1.
  • 2.
  • 3.
  • 4.
(3)创建并使用自定义Hub-ChatHub
  • 自定义方法名:使用[HubMethodName("方法名")]特性标识方法;
  • 可以使用DI中的服务作为参数;
  • 可以使用 OnConnectedAsync 和  OnDisconnectedAsync 虚拟方法来管理和跟踪连接;
  • 中心方法中引发的异常将发送到调用该方法的客户端;如下图的SendMessage被客户端调用,发生的异常会反馈给客户端。 
using Microsoft.AspNetCore.SignalR;

namespace SignalRChat_Js.Hubs
{
    /// <summary>
    /// 聊天Hub(管理聊天用胡的连接、组和消息)
    /// 继承Hub(管理连接、组和消息)
    /// SignalR 代码是异步模式,可提供最大的可伸缩性
    /// </summary>
    public class ChatHub : Hub
    {
        /// <summary>
        /// 发送信息
        /// </summary>
        /// <param name="user">用户</param>
        /// <param name="message">信息</param>
        /// <returns></returns>
        //[HubMethodName("SendMessageToUser")]  // 自定义方法名使用:[HubMethodName("方法名")]
        public async Task SendMessage(string user, string message,IDatabaseService dbService)  // 可以使用DI中的服务作为参数;services.AddSingleton<IDatabaseService, DatabaseServiceImpl>();
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);  // 调用ReceiveMessage方法
        }
        
        // 连接成功事件示例-将其ConnectionId添加到组SignalR Users
        public override async Task OnConnectedAsync()
        {
            await Groups.AddToGroupAsync(Context.ConnectionId, "SignalR Users");
            await base.OnConnectedAsync();
        }
        
        // 连接断开事件示例-记录异常
        public override async Task OnDisconnectedAsync(Exception? exception)
        {
            // Log(exception.Message);  // 记录异常
        
            await base.OnDisconnectedAsync(exception);
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
(4)服务器中(ChatHub之外)使用ChatHub(通过IHubContext调用)
  • 可以将IHubContext实例注入控制器、中间件或其他 DI 服务。
  • 当从Hub类外部调用客户端方法时,没有与该调用关联的调用方。 因此,无法访问ConnectionIdCallerOthers属性。
  • 注入强类型 HubContext时(见‘(5)强类型改造’),请确保中心继承自 Hub<T>。 使用 IHubContext<THub, T> 接口而不是 IHubContext<THub> 进行注入。
  • 在泛型代码中使用 IHubContext时,注入的 IHubContext<THub> 实例可以强制转换为 IHubContext,而无需指定泛型 Hub 类型。
// Controller控制器中

    /// <summary>
    /// 公告通知
    /// </summary>
    public class SysNoticeController : Controller
    {
        private readonly IHubContext<MessageHub> _hubContext;

        public SysNoticeController(IHubContext<MessageHub> hubContext)
        {
            _hubContext = hubContext;
        }
        
        /// <summary>
        /// 发送通知
        /// </summary>
        public void SendNotice(msg)
        {
            _hubContext.Clients.All.SendAsync("ReceiveMessage", "系统通知", message);  // 调用ReceiveMessage方法
        }
   }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
// 中间件管道中

app.Use(async (context, next) =>
{
    var hubContext = context.RequestServices
                            .GetRequiredService<IHubContext<ChatHub>>();
    //...
    
    if (next != null)
    {
        await next.Invoke();
    }
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
// Controller控制器-强类型

    public class ChatController : Controller
    {
        public IHubContext<ChatHub, IChatClient> _strongChatHubContext { get; }

    public ChatController(IHubContext<ChatHub, IChatClient> chatHubContext)
    {
        _strongChatHubContext = chatHubContext;
    }

    public async Task SendMessage(string user, string message)
    {
        await _strongChatHubContext.Clients.All.ReceiveMessage(user, message);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
/*
* 在泛型代码中使用 IHubContext
* 注入的 IHubContext<THub> 实例可以强制转换为 IHubContext,而无需指定泛型 Hub 类型。
*/

class MyHub : Hub
{ }

class MyOtherHub : Hub
{ }

app.Use(async (context, next) =>
{
    var myHubContext = context.RequestServices
                            .GetRequiredService<IHubContext<MyHub>>();
    var myOtherHubContext = context.RequestServices
                            .GetRequiredService<IHubContext<MyOtherHub>>();
    await CommonHubContextMethod((IHubContext)myHubContext);
    await CommonHubContextMethod((IHubContext)myOtherHubContext);

    await next.Invoke();
}

async Task CommonHubContextMethod(IHubContext context)
{
    await context.Clients.All.SendAsync("clientMethod", new Args());
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
2、创建SignalR Client
  • 客户端断线重连后,会被赋予一个新的ConnectionId,可通过Reconnected 事件获取。(注:如果 HubConnection 已配置为跳过协商,则Reconnected 事件处理程序的 connectionId 参数会为 null)
  • 若使用重连方法,推荐异步进行。
(1).Net版

  见“三-5”示例

(2)Javascript版

  见“三-1”示例

3、Hub知识点
(1) Hub. Context(用户标识:Context.UserIdentifier)

  类  Hub 包含一个  Context 属性,该属性包含以下属性以及有关连接的信息:

属性

说明

 ConnectionId

获取连接的唯一 ID(由 SignalR 分配)。 每个连接都有一个连接 ID。

 UserIdentifier

获取 用户标识符。 默认情况下,SignalR 使用与连接关联的  ClaimsPrincipal 中的  ClaimTypes.NameIdentifier 作为用户标识符。用户标识符区分大小写。

 User

获取与当前用户关联的  ClaimsPrincipal

 Items

获取可用于在此连接范围内共享数据的键/值集合。 数据可以存储在此集合中,会在不同的中心方法调用间为连接持久保存。

 Features

获取连接上可用的功能的集合。 目前,在大多数情况下不需要此集合,因此未对其进行详细记录。

 ConnectionAborted

获取一个  CancellationToken,它会在连接中止时发出通知。

   Hub.Context 还包含以下方法:

方法

说明

 GetHttpContext

 HttpContext返回连接的 ;如果连接未与 HTTP 请求关联,null则返回 。 对于 HTTP 连接,请使用此方法获取 HTTP 标头和查询字符串等信息。

 Abort

中止连接。

(2) Hub.Clients 
  • 当从Hub类外部调用客户端方法时,没有与该调用关联的调用方。 因此,无法访问ConnectionIdCallerOthers属性。
  • 用户组
  • 对组进行操作可以使用Client. Group()或Groups.AddToGroupAsync(Context.ConnectionId, groupName)、Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
  • 重新连接时不会保留组成员身份。 重新建立连接后,需要重新加入组。
  • 无法计算组的成员数,因为如果将应用程序扩展到多台服务器,则无法获取此信息。需要自己实现。
  • 组不是一项安全功能。 身份验证声明具有组不具备的功能,例如到期和撤销。 如果撤销用户对组的访问权限,应用必须从组中显式删除该用户。
  • 组名称区分大小写。

  类  Hub 包含一个  Clients 属性,该属性包含以下属性,用于服务器和客户端之间的通信:

属性

说明

 All

对所有连接的客户端调用方法

 Caller

对调用了中心方法的客户端调用方法

 Others

对所有连接的客户端调用方法(调用了方法的客户端除外)

   Hub.Clients 还包含以下方法:

方法

说明

 AllExcept

对所有连接的客户端调用方法(指定连接除外)

 Client

对连接的一个特定客户端调用方法

 Clients

对连接的多个特定客户端调用方法

 Group

对指定组中的所有连接调用方法

 GroupExcept

对指定组中的所有连接调用方法(指定连接除外)

 Groups

对多个连接组调用方法

 OthersInGroup

对一个连接组调用方法(不包括调用了中心方法的客户端)

 User

对与一个特定用户关联的所有连接调用方法

 Users

对与多个指定用户关联的所有连接调用方法

(3)向客户端发送消息(SendAsync)
public async Task SendMessage(string user, string message)
    => await Clients.All.SendAsync("ReceiveMessage", user, message);     // 使用 Clients.All 将消息发送到所有连接的客户端

public async Task SendMessageToCaller(string user, string message)
    => await Clients.Caller.SendAsync("ReceiveMessage", user, message);  // 使用 Clients.Caller 将消息发送回调用方

public async Task SendMessageToGroup(string userGroup,string user, string message)
    => await Clients.Group(userGroup).SendAsync("ReceiveMessage", user, message);  // 将消息发送给 SignalR Users 组中的所有客户端

public async Task SendMessageToGroup(string receiveUser,string user, string message)
    => await Clients.User(receiveUser).SendAsync("ReceiveMessage", user, message);  // 将消息发送给 user用户
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
(4)从客户端请求结果(示例为GetMessage方法)
  ① 服务器端

  这要求服务器使用 ISingleClientProxy.InvokeAsync,并且客户端从其 .On 处理程序返回结果。示例如下:

  方式一:接口中使用(推荐)

// 接口中调用
async void SomeMethod(IHubContext<MyHub> context)
{
    string result = await context.Clients.Client(connectionID).InvokeAsync<string>("GetMessage");  // 通过InvokeAsync调用。
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

   方式二:写在自定义的Hub中

public class ChatHub : Hub
{
    public async Task<string> WaitForMessage(string connectionId)
    {
        var message = await Clients.Client(connectionId).InvokeAsync<string>("GetMessage");
        return message;
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  ② 客户端
// C#版

hubConnection.On("GetMessage", async () =>
{
    Console.WriteLine("Enter message:");
    var message = await Console.In.ReadLineAsync();
    return message;
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
// javascript版

hubConnection.on("GetMessage", async () => {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("message");
        }, 100);
    });
    return promise;
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
// Java版

hubConnection.onWithResult("GetMessage", () -> {
    return Single.just("message");
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
(5)对SendAsync中“调用的方法名”进行强类型改造(示例为ReceiveMessage方法)

   使用 SendAsync 的缺点是它依赖于字符串来指定要调用的客户端方法。 如果客户端中的方法名称拼写错误或缺失,则这会使代码可能出现运行时错误。我们可以使用  Hub<T>强类型 Hub类来约束自定义的Hub类(使用强类型 Hub<T> 会禁止使用 SendAsync,只提供对 接口中定义的方法的访问),如下:

/// <summary>
/// 聊天Hub(管理聊天用胡的连接、组和消息)
/// 继承Hub(管理连接、组和消息)
/// SignalR 代码是异步模式,可提供最大的可伸缩性
/// </summary>
public class ChatHub : Hub<IChatClient>  // 使用强类型 Hub<T> 会禁止使用 SendAsync,只提供对 接口中定义的方法的访问。
{
    //旧的方法
    //public async Task SendMessage(string user, string message)
    //    => await Clients.All.SendAsync("ReceiveMessage", user, message);  // 调用ReceiveMessage方法
        
    /// <summary>
    /// 发送信息
    /// </summary>
    /// <param name="user">用户</param>
    /// <param name="message">信息</param>
    /// <returns></returns>
    public async Task SendMessage(string user, string message)
        => await Clients.All.ReceiveMessage(user, message);

    /// <summary>
    /// 发送信息-将消息发送回调用方
    /// </summary>
    /// <param name="user">用户</param>
    /// <param name="message">信息</param>
    /// <returns></returns>
    public async Task SendMessageToCaller(string user, string message)
        => await Clients.Caller.ReceiveMessage(user, message);

    /// <summary>
    /// 发送信息-将消息发送给 SignalR Users 组中的所有客户端
    /// </summary>
    /// <param name="user">用户组</param>
    /// <param name="user">用户</param>
    /// <param name="message">信息</param>
    /// <returns></returns>
    public async Task SendMessageToGroup(string userGroup,string user, string message)
        => await Clients.Group(userGroup).ReceiveMessage(user, message);
}

public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
(6)自定义传输对象(方法参数;增强历史版本的兼容性)
  ① 修改前(服务器自定义的方法)

  以如下所示的服务器端 API 为例:

public async Task<string> GetTotalLength(string param1)
{
    return param1.Length;
}
  • 1.
  • 2.
  • 3.
  • 4.

  JavaScript 客户端使用 invoke 调用此方法,如下所示:

connection.invoke("GetTotalLength", "value1");
  • 1.

  如果稍后将第二个参数添加到服务器方法,旧客户端不会提供此参数值。 例如:

public async Task<string> GetTotalLength(string param1, string param2)
{
    return param1.Length + param2.Length;
}
  • 1.
  • 2.
  • 3.
  • 4.


Microsoft.AspNetCore.SignalR.HubException: Failed to invoke 'GetTotalLength' due to an error on the server.
  • 1.

  在服务器上,你将看到如下日志消息:

System.IO.InvalidDataException: Invocation provides 1 argument(s) but target expects 2.
  • 1.

  旧客户端只发送了一个参数,但新的服务器 API 需要两个参数。 使用自定义对象作为参数可提供更大的灵活性。 让我们重新设计原始 API 以使用自定义对象。

  ② 修改后(服务器自定义的方法)

  单参

public class TotalLengthRequest
{
    public string Param1 { get; set; }
}

public async Task GetTotalLength(TotalLengthRequest req)
{
    return req.Param1.Length;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

  现在,客户端使用对象来调用方法:

connection.invoke("GetTotalLength", { param1: "value1" });
  • 1.

  添加参数

public class TotalLengthRequest
{
    public string Param1 { get; set; }
    public string Param2 { get; set; }
}

public async Task GetTotalLength(TotalLengthRequest req)
{
    var length = req.Param1.Length;
    if (req.Param2 != null)
    {
        length += req.Param2.Length;
    }
    return length;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

  当旧客户端发送单个参数时,额外的 Param2 属性将保留为 null。 你可以通过检查 Param2 是否为 null 来检测旧客户端发送的消息并应用默认值。 新客户端可以发送这两个参数。

connection.invoke("GetTotalLength", { param1: "value1", param2: "value2" });
  • 1.
  ③ 补充-客户端上自定义的方法(new {内容})

  从服务器端发送自定义对象:

public async Task Broadcast(string message)
{
    await Clients.All.SendAsync("ReceiveMessage", new
    {
        Message = message
    });
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

  在客户端,访问 Message 属性而不是使用参数:

connection.on("ReceiveMessage", (req) => {
    let msg=req.message;
});
  • 1.
  • 2.
  • 3.

  如果稍后决定将消息的发送方添加到有效负载中,请向对象添加一个属性:

public async Task Broadcast(string message)
{
    await Clients.All.SendAsync("ReceiveMessage", new
    {
        Sender = Context.User.Identity.Name,
        Message = message
    });
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

  旧客户端不需要 Sender 值,因此会忽略它。 新客户端可以通过更新为读取新属性来接受它:

connection.on("ReceiveMessage", (req) => {
    let message = req.message;
    if (req.sender) {
        message = req.sender + ": " + message;
    }
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

  在这种情况下,新客户端也可以容忍不提供 Sender 值的旧服务器。 由于旧服务器不提供 Sender 值,因此客户端在访问它之前会检查它是否存在。

(7) 中心筛选器
  • 在 ASP.NET Core 5.0 或更高版本中可用。
  • 允许在客户端调用中心方法之前和之后运行逻辑。
  • 中心筛选器可以全局应用或按中心类型应用。
  • 筛选器的添加顺序就是其运行顺序。 全局中心筛选器在本地中心筛选器之前运行。
  ① 配置中心筛选器
builder.Services.AddSignalR(options =>
    {
        // 全局筛选器将首先运行
        options.AddFilter<CustomFilter>();
    }).AddHubOptions<ChatHub>(options =>
    {
        // 本地筛选器在全局筛选器后运行
        options.AddFilter<LocCustomFilter>();
    }).AddHubOptions<ChatHub2>(options =>
    {
        // 本地筛选器按照顺序运行
        options.AddFilter<LocCustomFilter2>();
    });
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

  中心筛选器有三种添加方式:

// 1、按具体类型添加筛选器:
hubOptions.AddFilter<TFilter>();        // 将通过依赖项注入 (DI) 或激活的类型来解析。

// 2、按运行时类型添加筛选器:
hubOptions.AddFilter(typeof(TFilter));  // 将通过依赖项注入 (DI) 或激活的类型来解析。

// 3、按实例添加筛选器:
hubOptions.AddFilter(new MyFilter());   // 将像单一实例一样使用此实例;所有中心方法调用都将使用相同的实例。
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  ② 创建中心筛选器
  • 通过声明从 IHubFilter 继承的类来创建筛选器,并添加 InvokeMethodAsync 方法。
  • 还可以选择实现 OnConnectedAsync 和 OnDisconnectedAsync,以分别包装 OnConnectedAsync 和 OnDisconnectedAsync 中心方法。
  • 使用next 方法调用下一个筛选器。
  • 若要跳过筛选器中的中心方法调用,请引发 HubException 类型的异常,而不是调用 next。 如果客户端需要结果,则会收到错误。
// 继承IHubFilter
public class CustomFilter : IHubFilter
{
    // 添加InvokeMethodAsync方法
    public async ValueTask<object> InvokeMethodAsync(HubInvocationContext invocationContext, Func<HubInvocationContext, ValueTask<object>> next)
    {
        Console.WriteLine($"Calling hub method '{invocationContext.HubMethodName}'");
        try
        {
            return await next(invocationContext);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Exception calling '{invocationContext.HubMethodName}': {ex}");
            throw;
        }
    }

    // 实现连接成功事件
    public Task OnConnectedAsync(HubLifetimeContext context, Func<HubLifetimeContext, Task> next)
    {
        return next(context);
    }

    // 实现断开连接事件
    public Task OnDisconnectedAsync(HubLifetimeContext context, Exception exception, Func<HubLifetimeContext, Exception, Task> next)
    {
        return next(context, exception);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  ③ 使用中心筛选器的示例
  • 编写筛选器逻辑时,请尝试通过在中心方法上使用属性而不是检查中心方法名称,使其成为泛型逻辑。

  以某个筛选器为例,该筛选器将检查中心方法参数中是否有被禁短语,并将找到的任何短语替换为 ***。 对于此示例,假设定义了LanguageFilterAttribute类。 该类有一个名为FilterArgument的属性,可以在使用该属性时对其进行设置。

  a. 将该属性放在需要清理字符串参数的中心方法上:

public class ChatHub
{
    [LanguageFilter(filterArgument = 0)]
    public async Task SendMessage(string message, string username)
    {
        await Clients.All.SendAsync("SendMessage", $"{username} says: {message}");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

  b. 定义一个中心筛选器,以检查该属性并将中心方法参数中的被禁短语替换为 ***

public class LanguageFilter : IHubFilter
{
    // populated from a file or inline
    private List<string> bannedPhrases = new List<string> { "async void", ".Result" };

    public async ValueTask<object> InvokeMethodAsync(HubInvocationContext invocationContext, 
        Func<HubInvocationContext, ValueTask<object>> next)
    {
        var languageFilter = (LanguageFilterAttribute)Attribute.GetCustomAttribute(
            invocationContext.HubMethod, typeof(LanguageFilterAttribute));
        if (languageFilter != null &&
            invocationContext.HubMethodArguments.Count > languageFilter.FilterArgument &&
            invocationContext.HubMethodArguments[languageFilter.FilterArgument] is string str)
        {
            foreach (var bannedPhrase in bannedPhrases)
            {
                str = str.Replace(bannedPhrase, "***");
            }

            var arguments = invocationContext.HubMethodArguments.ToArray();
            arguments[languageFilter.FilterArgument] = str;
            invocationContext = new HubInvocationContext(invocationContext.Context,
                invocationContext.ServiceProvider,
                invocationContext.Hub,
                invocationContext.HubMethod,
                arguments);
        }

        return await next(invocationContext);
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

  c. 在Startup.ConfigureServices方法中注册中心筛选器。 为了避免每次调用都重新初始化被禁短语列表,中心筛选器将注册为单一实例:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSignalR(hubOptions =>
    {
        hubOptions.AddFilter<LanguageFilter>();
    });

    services.AddSingleton<LanguageFilter>();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  ④ HubInvocationContext 对象

HubInvocationContext 包含当前中心方法调用的信息。

属性

说明

类型

Context

HubCallerContext 包含有关连接的信息。

HubCallerContext

Hub

用于此中心方法调用的中心实例。

Hub

HubMethodName

正在调用的中心方法的名称。

string

HubMethodArguments

传递给中心方法的参数列表。

IReadOnlyList<string>

ServiceProvider

用于此中心方法调用的已限定范围的服务提供程序。

IServiceProvider

HubMethod

中心方法信息。

MethodInfo

  ⑤ HubLifetimeContext 对象

HubLifetimeContext 包含 OnConnectedAsync 和 OnDisconnectedAsync 中心方法的信息。

属性

说明

类型

Context

HubCallerContext 包含有关连接的信息。

HubCallerContext

Hub

用于此中心方法调用的中心实例。

Hub

ServiceProvider

用于此中心方法调用的已限定范围的服务提供程序。

IServiceProvider

三、示例(聊天室):

  官方示例地址: SignalR-samples(Android、WinFrom、Xamarin、UWP、Js等)

1、JavaScript使用SignalR教程

  教程见: ASP.NET Core SignalR 入门(Microsoft.AspNetCore.App已集成的包;不需要独立安装Microsoft.AspNet.SignalR或Microsoft.AspNetCore.SignalR包)

  代码地址: csharp_networkprotocol_signalr/SignalRChat_Js

2、TypeScript使用SignalR教程

  教程见: 使用 TypeScript 和 Webpack 开始使用 ASP.NET Core SignalR

3、Blazor使用SignalR教程

  教程见: 结合使用 ASP.NET Core SignalR 和 Blazor

4、WebAPI+Vue3使用SignalR教程

  代码地址: csharp_networkprotocol_signalr/SignalRServer_WebAPI与 csharp_networkprotocol_signalr\signalrvue。

(1)后台代码(WebAPI)

  Microsoft.AspNetCore.App已集成的包;不需要独立安装Microsoft.AspNet.SignalR或Microsoft.AspNetCore.SignalR包

  ① 启用SignalR(Program.cs)

using Microsoft.AspNetCore.SignalR;

...
 // 1、添加Cors跨域
 builder.Services.AddCors(options =>  // 配置Cors跨域
 {
     options.AddPolicy(name: "MyPolicy",
         policy =>
         {
             policy       
                 .WithOrigins("http://localhost:8080")    //.WithOrigins("http://localhost:8080/", "https://localhost:8080/", "http://localhost:8080", "http://localhost:5000/chatHub")
                                                          //.AllowAnyOrigin()  // 允许所有域
                 .AllowAnyHeader()  // 约束HTTP请求头-允许所有请求头;.WithHeaders("Content-Language","Content-Type");
                 .AllowCredentials()//允许cookie
                 .AllowAnyMethod();  // 约束HTTP方法-允许所有方法;.WithMethods("PUT", "DELETE", "GET","POST")
         });
  });

//2、添加SignalR服务,默认为json传输
builder.Services.AddSignalR();  // 
//builder.Services.AddSignalR(options =>
//{
//    //客户端发保持连接请求到服务端最长间隔,默认30秒,改成4分钟,网页需跟着设置connection.keepAliveIntervalInMilliseconds = 12e4;即2分钟
//    //options.ClientTimeoutInterval = TimeSpan.FromMinutes(4);
//    //服务端发保持连接请求到客户端间隔,默认15秒,改成2分钟,网页需跟着设置connection.serverTimeoutInMilliseconds = 24e4;即4分钟
//    //options.KeepAliveInterval = TimeSpan.FromMinutes(2);
//});

app.UseCors("MyPolicy");          // 3、启用Cors跨域
app.MapHub<ChatHub>("/chatHub");  // 4、启用SignalR服务
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.

  ② ChatHub.cs

using Microsoft.AspNetCore.SignalR;

namespace SignalRChat_Js.Hubs
{
    /// <summary>
    /// 聊天Hub(管理聊天用胡的连接、组和消息)
    /// 继承Hub(管理连接、组和消息)
    /// SignalR 代码是异步模式,可提供最大的可伸缩性
    /// </summary>
    public class ChatHub : Hub
    {
        /// <summary>
        /// 发送信息
        /// </summary>
        /// <param name="user">用户</param>
        /// <param name="message">信息</param>
        /// <returns></returns>
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.

  ③ 接口中使用

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.SignalR;
using SignalRChat_Js.Hubs;

namespace SignalRServer_WebAPI.Controllers
{
    /// <summary>
    /// 公告通知
    /// </summary>
    public class SysNoticeController : Controller
    {
        private readonly IHubContext<ChatHub> _hubContext;

        public SysNoticeController(IHubContext<ChatHub> hubContext)
        {
            _hubContext = hubContext;
        }

        /// <summary>
        /// 发送通知
        /// </summary>
        public void SendNotice(string message)
        {
            _hubContext.Clients.All.SendAsync("ReceiveMessage", "系统通知", message);  // 调用ReceiveMessage方法
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
(2)前台代码(Vue)

  ① 引用signalr库

npm install @microsoft/signalr
  • 1.

  ② Chat/Index.vue

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <!--
    <p>
      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
    </p>
    <h3>Installed CLI Plugins</h3>
    <ul>
      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
    </ul>
    -->

    <div class="container">
      <div class="row p-1">
        <div class="col-1">用户名</div>
        <div class="col-5"><input type="text" id="userInput" /></div>
      </div>
      <div class="row p-1">
        <div class="col-1">消息内容</div>
        <div class="col-5">
          <input type="text" class="w-100" id="messageInput" />
        </div>
      </div>
      <div class="row p-1">
        <div class="col-6 text-end">
          <input
            type="button"
            id="sendButton"
            value="发送"
            @click="sendMessageClicked"
          />
        </div>
      </div>
      <div class="row p-1">
        <div class="col-6">
          <hr />
        </div>
      </div>
      <div class="row p-1">
        <div class="col-6">
          <ul id="messagesList"></ul>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
// npm install @microsoft/signalr
//import { signalR } from "@/js/signalr/dist/browser/signalr.js";
import * as signalR from "@microsoft/signalr";

let connection;

export default {
  name: "HelloWorld",
  props: {
    msg: String,
  },

  data() {
    return {
      // connection: undefined,
    };
  },
  created() {},
  mounted() {
    this.OnLoad();
  },
  methods: {
    OnLoad() {
      // 1、创建并启动连接,连接地址为/chatHub
      connection = new signalR.HubConnectionBuilder()
        .withAutomaticReconnect() // 自动重连
        .withUrl("http://localhost:5000/chatHub")
        .build();

      // 2、分类为ReceiveMessage
      connection.on("ReceiveMessage", function (user, message) {
        var li = document.createElement("li");
        document.getElementById("messagesList").appendChild(li);

        li.textContent = `【${user}】 说: ${message}`;
      });


      // 重连失败后的关闭事件
      connection.onclose((error) => {
        // 展示断开的信息
        var li = document.createElement("li");
        document.getElementById("messagesList").appendChild(li);
        li.textContent = `连接断开,重连中!错误信息:` + error.message;

        connection.start().catch(function (err) {
          console.log(err.toString());
       });
      });

      // 3、连接成功后,发送 按钮可用
      connection
        .start()
        .then(function () {
          document.getElementById("sendButton").disabled = false;
        })
        .catch(function (err) {
          console.log(err.toString());
        });
    },

    // 发送事件
    sendMessageClicked() {
      var user = document.getElementById("userInput").value;
      var message = document.getElementById("messageInput").value;
      connection
        .invoke("SendMessage", "SignalRVue_"+user, message)
        .catch(function (err) {
          return console.log("错误信息"+err.toString());
        });
      //this.$message.success("消息发送成功");
      console.log("消息发送成功");
    },
  },
};
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.
  • 72.
  • 73.
  • 74.
  • 75.
  • 76.
  • 77.
  • 78.
  • 79.
  • 80.
  • 81.
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
  • 91.
  • 92.
  • 93.
  • 94.
  • 95.
  • 96.
  • 97.
  • 98.
  • 99.
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
  • 110.
  • 111.
  • 112.
  • 113.
  • 114.
  • 115.
  • 116.
  • 117.
  • 118.
  • 119.
  • 120.
  • 121.
  • 122.
  • 123.
  • 124.
  • 125.
  • 126.
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
  • 138.
  • 139.
  • 140.
  • 141.
  • 142.
  • 143.
  • 144.
5、.Net客户端使用教程SignalR-WinForm/MAUI

  代码地址:

(1)服务端:

  见4-(1)-WebAPI代码。

(2)客户端(WinForm版;MAUI版略,直接看代码):

  ① 安装Negut包:

Install-Package Microsoft.AspNetCore.SignalR.Client
  • 1.

  ② 连接/断开、接收功能。

/// <summary>
        /// 连接/断开
        /// </summary>
        private async void btnOpen_Click(object sender, EventArgs e)
        {
            try
            {
                if (!IsConnection)  // 打开连接
                {
                    adress = txtAdress.Text;

                    _connection = new HubConnectionBuilder()
                        .WithUrl(adress)  // "http://localhost:5000/chatHub"
                        .WithAutomaticReconnect()  // 自动重连(默认不会自动重连);在没有任何参数的情况下,WithAutomaticReconnect() 将客户端配置为在每次尝试重新连接之前分别等待 0、2、10 和 30 秒,在四次尝试失败后停止。
                        .Build();

                    _connection.Reconnecting += ReconnectingEvent;  // 重连事件
                    _connection.Reconnected += ReconnectedEvent;  // 重连成功事件
                    _connection.Closed += ClosedEvent;  // 重连失败后的断开事件

                    _connection.On<string, string>("ReceiveMessage", (s1, s2) => OnSend(s1, s2));  // 注册监听事件
                    await _connection.StartAsync();

                    IsConnection = true;
                    UpdateState(IsConnection);
                    Log("连接成功!地址:" + adress);
                    txtMsg.Focus();
                }
                else  // 关闭连接
                {
                    IsConnection = false;
                    await _connection.StopAsync();
                    UpdateState(false);
                }
            }
            catch (Exception ex)
            {
                IsConnection = false;
                UpdateState(IsConnection);
                Log(ex.ToString());
            }
        }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.

  ④ 发送功能

/// <summary>
        /// 发送
        /// </summary>
        private async void btnSend_Click(object sender, EventArgs e)
        {
            try
            {
                string userStr = txtUser.Text;
                string msgStr = txtMsg.Text;

                await _connection.InvokeAsync("SendMessage", "WinFormsApp_"+ userStr, msgStr);  // 调用Server的SendMessage方法
            }
            catch (Exception ex)
            {
                Log(ex.ToString());
            }
        }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

  ⑤ 事件

/// <summary>
        /// 接收事件
        /// </summary>
        /// <param name="user">用户</param>
        /// <param name="message">信息</param>
        private void OnSend(string user, string message)
        {
            Log($"【{user}】说:{message}");
        }

        /// <summary>
        /// 自动重连事件
        /// </summary>
        private Task ReconnectingEvent(Exception? arg)
        {
            // 可记录本次异常

            return Task.CompletedTask;
        }

        /// <summary>
        /// 重连成功事件
        /// </summary>
        private Task ReconnectedEvent(string? arg)
        {
            // 会生成新的ConnectionId;如果 HubConnection 已配置为跳过协商,则Reconnected 事件处理程序的 connectionId 参数会为 null。
            string newConnectionId = arg;

            return Task.CompletedTask;
        }

        /// <summary>
        /// 重连失败后的断开事件
        /// </summary>
        private async Task ClosedEvent(Exception? arg)
        {
            // 展示断开的信息
            Log("断开成功!地址:" + adress + arg!=null?";错误信息:"+ arg?.Message: "");

            // 可判断是人为断开还是异常断开
            if(IsConnection)
            {
                // 重连逻辑
                await Task.Delay(new Random().Next(0, 5) * 1000);  // 加间隔时间
                await _connection.StartAsync();
            }
        }
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
6、Java版

  教程见: ASP.NET Core SignalR Java 客户端

四、分布式环境中使用SignalR

1、方式一:使用Azure SignalR 服务

  见: 将 ASP.NET Core SignalR 应用发布到 Azure 应用服务

2、方式二:使用Redis集群中发布/订阅功能

  见: 设置 Redis 集群以实现 ASP.NET Core SignalR 横向扩展

五、搭建SignalR流式服务器(流式传输)

  见: 在 ASP.NET Core SignalR 中使用流式传输

六、SignalR中使用MessagePack(更小的传输载体)

  见: 在 ASP.NET Core SignalR 中使用 MessagePack 中心协议

七、常见问题:

1、报错“An attempt was made to access a socket in a way forbidden by its access permissions...”

  SignalR 连接是持久的,会占用服务器的 TCP 连接数量。当TCP 连接数量用完时,你会看到这个错误(随机套接字错误和连接重置错误)。

  处理对策如下:① 修改服务器的TCP连接数量限制;②  将SignalR与其他 Web 应用部署在不同的服务器上。

 

作者:꧁执笔小白꧂