概述

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

  适合SignalR的候选项:

  • 需要从服务器进行高频率更新的应用。 示例包括游戏、社交网络、投票、拍卖、地图和 GPS 应用。
  • 仪表板和监视应用。 示例包括公司仪表板、即时销售更新或旅行警报。
  • 协作应用。 协作应用的示例包括白板应用和团队会议软件。
  • 需要通知的应用。 社交网络、电子邮件、聊天、游戏、旅行警报和很多其他应用都需使用通知。

  SignalR 提供用于创建服务器到客户端远程过程调用 (RPC) 的 API。 RPC 从服务器端 .NET Core 代码调用客户端上的函数。 提供多个受支持的平台,其中每个平台都有各自的客户端 SDK。 因此,RPC 调用所调用的编程语言有所不同。

  以下是 ASP.NET Core SignalR 的一些功能:

  • 自动处理连接管理。
  • 同时向所有连接的客户端发送消息。 例如聊天室。
  • 向特定客户端或客户端组发送消息。
  • 对其进行缩放,以处理不断增加的流量。
  • SignalR 中心协议

传输

  SignalR 支持以下用于处理实时通信的技术(按正常回退的顺序):

  • WebSockets
  • Server-Sent Events
  • Long Pooling

  SignalR 自动选择服务器和客户端能力范围内的最佳传输方法。

中心

  SignalR 使用中心在客户端和服务器之间进行通信。
  Hub 是一种高级管道,允许客户端和服务器相互调用方法。 SignalR 自动处理跨计算机边界的调度,并允许客户端调用服务器上的方法,反之亦然。 可以将强类型参数传递给方法,从而支持模型绑定。 SignalR 提供两种内置中心协议:基于 JSON 的文本协议和基于 MessagePack 的二进制协议。 与 JSON 相比,MessagePack 通常会创建更小的消息。 旧版浏览器必须支持 XHR 级别 2 才能提供 MessagePack 协议支持。

  中心通过发送包含客户端方法的名称和参数的消息来调用客户端代码。 作为方法参数发送的对象使用配置的协议进行反序列化。 客户端尝试将名称与客户端代码中的方法匹配。 当客户端找到匹配项时,它会调用该方法并将反序列化的参数数据传递给它。

实时Web应用

创建Web应用项目

  如下,创建一个基于Razor的Web项目

添加SignalR客户端

  ASP.NET Core 共享框架中包含 SignalR 服务器库。 JavaScript 客户端库不会自动包含在项目中。 对于此教程,使用库管理器 (LibMan) 从 unpkg 获取客户端库。 unpkg 是一个快速的全局内容分发网络,适用于 npm 上的所有内容。

  1. 在“解决方案资源管理器”>中,右键单击项目,然后选择“添加”“客户端库”。
  2. 在“添加客户端库”对话框中:
    • 为“提供程序”选择“unpkg
    • 对于“库”,请输入 @microsoft/signalr@latest
    • 选择“选择特定文件”,展开“dist/browser”文件夹,然后选择 signalr.jssignalr.min.js
    • 将“目标位置”设置为 wwwroot/js/signalr/。
    • 选择“安装” 。

LibMan 创建 wwwroot/js/signalr 文件夹并将所选文件复制到该文件夹。

  1. 在集成终端中,在卸载任何早期版本(如果存在)后,运行以下命令,安装 LibMan。
1
2
dotnet tool uninstall -g Microsoft.Web.LibraryManager.Cli
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
  1. 导航到项目文件夹(包含 SignalRChat.csproj 文件的文件夹)。
  2. 使用 LibMan 运行以下命令,以获取 SignalR 客户端库。 可能需要几秒钟才能显示输出。
1
libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js
- 使用 unpkg 提供程序。
- 将文件复制到 `wwwroot/js/signalr` 目标。
- 仅复制指定的文件。

创建SignalR中心

  中心是一个类,用作处理客户端 - 服务器通信的高级管道。

  1. 在 SignalRChat 项目文件夹中,创建 Hubs 文件夹。
  2. 在 Hubs 文件夹中,使用以下代码创建 ChatHub 类:
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
using Microsoft.AspNetCore.SignalR;

namespace SignalRChat.Hubs;

public class ChatHub : Hub
{
public async Task Login(string user)
{
// AllExcept:除了指定ConnectionId的其他用户
await Clients.AllExcept(Context.ConnectionId)
.SendAsync("Online", $"{user} 进入了群聊!");
}

public async Task Logout(string user)
{
// AllExcept:除了指定ConnectionId的其他用户
await Clients.AllExcept(Context.ConnectionId)
.SendAsync("Online", $"{user} 退出了群聊!");
}

public async Task SendMessage(string user, string message)
{
// All:所有用户
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}

  ChatHub 类继承自 SignalR HubHub管理连接、组和消息

配置SignalR

  必须将 SignalR服务器配置为将 SignalR 请求传递给 SignalR。 将以下突出显示的代码添加到 Program.cs 文件。

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
using SignalRChat.Hubs;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();
// 添加Signal服务
builder.Services.AddSignalR();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();
// 配置ChatHub
app.MapHub<ChatHub>("/chatHub");

app.Run();

添加SignalR客户端代码

  使用以下代码替换Pages/Index.cshtml中的内容:

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
@page
<div class="container">
<div class="row p-1">
<div class="col-1">User</div>
<div class="col-5"><input type="text" id="userInput" /></div>
</div>
<div class="row p-1">
<div class="col-1">Message</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="Send Message" />
</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>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>
  • 创建文本框和提交按钮
  • 使用id="messagesList"创建一个列表,用于显示从SignalR中心接收的消息
  • 包含对SignalR的脚本应用,并在下一步中创建chat.js应用代码

  在wwwroot/js目录下,使用以下代码创建chat.js文件:

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
"use strict"

const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.build();

// 禁用发送按钮
document.querySelector("#sendButton").disabled = true

// 监听接收消息
connection.on("ReceiveMessage",function (user,message){
const li = document.createElement("li")
document.querySelector("#messagesList").appendChild(li)
li.textContent = `${user} says ${message}`
})

// 启动signalr连接
connection.start().then(function (){
document.querySelector("#sendButton").disabled = false
}).catch(function (err){
return console.log(err.toString())
})

// 监听消息发送
document.querySelector("#sendButton").addEventListener("click",function (event){
const user = document.querySelector("#userInput").value
const message = document.querySelector("#messagesList").value
connection.invoke("SendMessage",user,message).catch(function (err){
return console.log(err.toString())
})
event.preventDefault()
})

  • 创建并启动连接。
  • 向“提交”按钮添加一个用于向中心发送消息的处理程序。
  • 向连接对象添加一个用于从中心接收消息并将其添加到列表的处理程序。

on用于监听SignalR的函数回调,invoke用于调用SignalR中心的函数

运行应用

  从地址栏复制 URL,打开另一个浏览器实例或选项卡,并在地址栏中粘贴该 URL。选择任一浏览器,输入名称和消息,然后选择“发送消息”按钮。两个页面上立即显示名称和消息。

如果应用不起作用,请打开浏览器开发人员工具 (F12) 并转到控制台。 查找与 HTML 和 JavaScript 代码相关的可能错误。 例如,如果 signalr.js 被放在一个与指示不同的文件夹中,对该文件的引用将不起作用,导致控制台中出现 404 错误。

如果 Chrome 中发生了 ERR_SPDY_INADEQUATE_TRANSPORT_SECURITY 错误,请运行以下命令以更新开发证书:

1
2
dotnet dev-certs https --clean
dotnet dev-certs https --trust

.NET客户端

创建WPF应用程序

  如下,创建一个WPF桌面应用:

安装SignalR.NET客户端包

1
Install-Package Microsoft.AspNetCore.SignalR.Client
1
dotnet add package Microsoft.AspNetCore.SignalR.Client

连接到中心

  若要建立连接,请创建 HubConnectionBuilder 并调用 Build在建立连接期间,可以配置中心URL、协议、传输类型、日志级别、标头和其他选项。 可通过将任何 HubConnectionBuilder 方法插入 Build 中来配置任何必需选项。 使用 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
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
<Window
Height="450"
Width="800"
mc:Ignorable="d"
x:Class="SignalRChatDesktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SignalRChatDesktop"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="80" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock
FontSize="16"
Margin="10,0"
Text="XXX水友群"
VerticalAlignment="Center" />
<Button
Click="BtnIn_OnClick"
Content="进入"
Height="30"
Width="100"
x:Name="btnIn" />
<Button
Click="BtnOut_OnClick"
Content="退出"
Height="30"
Margin="10,0"
Width="100"
x:Name="btnOut" />
</StackPanel>

<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>

<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="3*" />
<RowDefinition Height="*" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>

<TextBox
FontSize="15"
Margin="5"
x:Name="txtMsg" />

<TextBox
FontSize="15"
Grid.Row="1"
Margin="5"
Name="txtSend" />

<Button
Background="Green"
BorderThickness="0"
Click="BtnSend_OnClick"
Content="发送"
Foreground="White"
Grid.Row="2"
Margin="5"
Name="btnSend" />
</Grid>

<TextBox
Grid.Column="1"
Margin="5"
Name="txtInfo" />

</Grid>
</Grid>
</Window>
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
using System.Windows;
using Microsoft.AspNetCore.SignalR.Client;

namespace SignalRChatDesktop;

public partial class MainWindow : Window
{
private HubConnection _connection;

public MainWindow()
{
InitializeComponent();

_connection = new HubConnectionBuilder()
.WithUrl("http://localhost:5098/ChatHub") // 配置SignalrR中心地址
.WithAutomaticReconnect() // 配置自动重新连接
.Build();

_connection.On<string>("Online",
(message) => { this.Dispatcher.Invoke(() => { txtInfo.Text += message + Environment.NewLine; }); });
_connection.On<string, string>("ReceiveMessage",
(user, message) =>
{
this.Dispatcher.Invoke(() => { txtMsg.Text += $"{user} says {message}" + Environment.NewLine; });
});


_connection.StartAsync();
}

private void BtnIn_OnClick(object sender, RoutedEventArgs e)
{
var user = $"水友{Random.Shared.Next(1000, 10000)}";
this.Title = user;

_connection.InvokeAsync("Login", user);
}

private void BtnOut_OnClick(object sender, RoutedEventArgs e)
{
_connection.InvokeAsync("Logout", this.Title);
_connection.StopAsync();
this.Close();
}

private void BtnSend_OnClick(object sender, RoutedEventArgs e)
{
var msg = this.txtSend.Text;
if (string.IsNullOrWhiteSpace(msg))
return;

_connection.InvokeAsync("SendMessage", this.Title, msg);
}
}
水友群聊-效果图

Hub

Clients

Client 描述
All 广播的方式,发送消息给所有连接到该Hub的客户端
Caller 发送消息给调用当前Hub方法的客户端(即发送消息的客户端本身)
Client 发送消息给指定connectionId的客户端。connectionId是每个连接的唯一标识
Clients 发送消息给指定多个connectionId的客户端集合
AllExcept 发送消息给除指定connectionId之外的所有客户端
Group 发送消息给指定组内的所有客户端。组是可以动态加入或离开的
Groups 发送消息给指定的多个组内的客户端
Others 发送消息给除调用当前Hub方法的客户端以外的所有连接的客户端
OthersInGroup 发送消息给某个组中除当前调用方法的客户端外的所有客户端
User 发送消息给指定userId的所有客户端。每个用户可以有多个连接
Users 发送消息给指定多个userId的客户端

发布事件

1
2
3
4
5
6
7
8
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.build();

connection.invoke("SendMessage",user,message).catch(function (err){
return console.log(err.toString())
})

1
2
3
4
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}

订阅事件

1
2
3
4
5
6
7
8
9
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.build();

connection.on("ReceiveMessage",function (user,message){
const li = document.createElement("li")
document.querySelector("#messagesList").appendChild(li)
li.textContent = `${user} says ${message}`
})
1
2
3
4
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}

IHubContext

  IHubContext 是 SignalR 中的一个接口,主要用于在 不依赖 Hub 类实例 的情况下与连接的客户端进行通信。这在你需要从 Hub 之外 的地方发送消息给客户端时非常有用,比如在控制器、后台服务、或其他应用程序组件中。

  以下情况非使用IHubContext

  • 后台任务 :如果你有一些后台服务或定时任务,需要向客户端发送实时消息,你可以使用 IHubContext。
  • 控制器或其他服务类 :在 ASP.NET Core 中的控制器或其他服务类中,使用 IHubContext 可以与客户端通信。
  • 跨服务通信 :例如,接收第三方 API 请求后,将结果发送给连接的客户端。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class NotificationController : Controller
{
private readonly IHubContext<ChatHub> _hubContext;

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

[HttpPost]
public async Task<IActionResult> Notify(string message)
{
// 使用 IHubContext 发送消息给所有客户端
await _hubContext.Clients.All.SendAsync("NotificationMessage", message);
return Ok();
}
}

水平扩展

  如果你需要支持更多的客户端,可以通过水平扩展(如使用 RedisAzure SignalR Service)来增加服务器的容量。这样,你可以将负载分配到多台服务器上:

  • Redis :使用 Redis 发布/订阅系统,可以在多台服务器上同步客户端的连接。
  • Azure SignalR Service :这是一个完全托管的服务,提供了自动扩展能力,可以处理大量的并发连接。

Redis

  使用 Redis 扩展 SignalR 可以实现跨多个服务器的消息传递和连接管理。这允许你在负载均衡的情况下支持更多的并发连接。以下是如何实现 Redis 扩展 SignalR 的步骤:

  1. 安装Redis库
1
Install-Package Microsoft.AspNetCore.SignalR.StackExchangeRedis
  1. IRedisDirectConnection服务
1
2
3
4
public interface IRedisDirectConnection
{
List<RedisKey> SearchKeys(string pattern);
}
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
public class RedisDirectConnection : IRedisDirectConnection
{
private ConnectionMultiplexer _redisConnectionMultiplexer;
private readonly ConfigurationManager _configurationRoot;


public RedisDirectConnection(ConfigurationManager configurationRoot)
{
_configurationRoot = configurationRoot;
}

public ConnectionMultiplexer GetRedisConnection()
{
var isConnected = false;
if (_redisConnectionMultiplexer != null)
isConnected = _redisConnectionMultiplexer.IsConnected;
if (!isConnected)
{
var redisConnectionString = _configurationRoot.GetConnectionString("RedisConnection");
ConfigurationOptions options = ConfigurationOptions.Parse(redisConnectionString);
_redisConnectionMultiplexer = ConnectionMultiplexer.Connect(options);
}
return _redisConnectionMultiplexer;
}

public List<RedisKey> SearchKeys(string pattern)
{
EndPoint endPoint = GetRedisConnection().GetEndPoints().First();
return GetRedisConnection().GetServer(endPoint).Keys(pattern: "master"+pattern).ToList();
}
}
  1. Hub类
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
public class ClientConnected
{
public string Id { get; set; }
public string Email { get; set; }
public string ConnectionId { get; set; }
}

public class ChatHub : Hub
{
private readonly UserManager<IdentityUser> _userManager;
private readonly IDistributedCache _distributedCache;
private readonly IRedisDirectConnection _redisDirectConnection;

public ChatHub(UserManager<IdentityUser> userManager, IDistributedCache distributedCache, IRedisDirectConnection redisDirectConnection)
{
_userManager = userManager;
_distributedCache = distributedCache;
_redisDirectConnection = redisDirectConnection;
}


public override async Task OnConnectedAsync()
{
var hostName = Dns.GetHostName();
await Clients.Caller.SendAsync("LogHostName", hostName);
var email = Context.User?.Identity?.Name;
if (string.IsNullOrEmpty(email))
return;
var redisKeys = _redisDirectConnection.SearchKeys("SignalR:User:*");

var listClients = new List<ClientConnected>();
foreach (var keyStr in redisKeys.Select(key => key.ToString().Replace("master", string.Empty)))
{
var json = await _distributedCache.GetStringAsync(keyStr);
if (string.IsNullOrEmpty(json)) continue;

var clientConnected =
JsonConvert.DeserializeObject<ClientConnected>(json);

listClients.Add(clientConnected);
}

foreach (var clientConnected in listClients.Where(a => a.Email != email).ToList())
{
await Clients.Caller.SendAsync("OnUserConnect", clientConnected);
}

var user = await _userManager.Users.FirstOrDefaultAsync(a => a.Email == email);

var client = new ClientConnected()
{
Id = user.Id,
Email = user.Email,
ConnectionId = Context.ConnectionId
};
listClients.Add(client);
await _distributedCache.SetStringAsync($"SignalR:User:user-{client.Id}", JsonConvert.SerializeObject(client));
//_clientsConnected.Add(client);

//cache to individual not the full tself
await Groups.AddToGroupAsync(Context.ConnectionId, $"user-{client.Id}"); //Mandar para os outros connectionid da plataforma nao

await Clients.AllExcept(listClients.Where(a => a.Email == email).Select(a => a.ConnectionId)).SendAsync("OnUserConnect", client);

await base.OnConnectedAsync();
}

public override async Task OnDisconnectedAsync(Exception? exception)
{
var email = Context.User?.Identity?.Name;
if (!string.IsNullOrEmpty(email))
{
var user = await _userManager.Users.FirstOrDefaultAsync(a => a.Email == email);
var cacheKey = $"SignalR:User:user-{user.Id}";

var client = await _distributedCache.GetStringAsync(cacheKey);
if (client != null)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user-{user.Id}");
await Clients.All.SendAsync("OnUserDisconnect", JsonConvert.DeserializeObject<ClientConnected>(client));
}

await _distributedCache.RemoveAsync(cacheKey);
}

//In this case ALL is ok.. but in a case of a full page, subscription would be recommended
await base.OnDisconnectedAsync(exception);
}

public async Task SendMessage(string toUser, string message)
{
/* await Clients.All.SendAsync("OnReceiveMessage", toUser, message);


await Clients.Caller.SendAsync("OnReceiveMessage", toUser, message);*/

var email = Context.User?.Identity?.Name;
var user = await _userManager.Users.FirstOrDefaultAsync(a => a.Email == email);
var fromName = user.Email;

await Clients.Group($"user-{toUser}").SendAsync("OnReceiveMessage", user.Id, fromName, message);
}
}
  1. 配置Redis连接
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var redisConnectionString = builder.Configuration.GetConnectionString("RedisConnection");

builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConnectionString;
options.InstanceName = "master";
});

builder.Services.AddSignalR().AddStackExchangeRedis(redisConnectionString);
builder.Services.AddSingleton<IRedisDirectConnection, RedisDirectConnection>();

...

app.MapHub<ChatHub>("/chathub");