
适合 SignalR 的应用场景:
- 需要从服务器进行高频率更新的应用。 示例包括游戏、社交网络、投票、拍卖、地图和 GPS 应用。
- 仪表板和监视应用。
- 协作应用。 协作应用的示例包括白板应用和团队会议软件。
- 需要通知的应用。 社交网络、电子邮件、聊天、游戏、旅行警报和很多其他应用都需使用通知。
客户端负责通过 HubConnection 对象建立到服务器终结点的连接。 Hub 连接在每个目标平台中表示:
-
.NET 客户端:Microsoft.AspNetCore.SignalR.Client.HubConnection
- JavaScript 客户端:@microsoft/signalr.HubConnection
- Java 客户端:com.microsoft.signalr.HubConnection
1、SignalR服务端
builder.Services.AddSignalR(;// 即时通讯
app.UseEndpoints(endpoints =>
{
// 注册集线器
endpoints.MapHub<OnlineUserHub>("/hubs/onlineUser";
};
定义集线器只需要继承 Hub 或 Hub<TStrongType> 泛型基类即可。
public class ChatHub : Hub { public async Task SendMessage(string user, string message => await Clients.All.SendAsync("ReceiveMessage", user, message; }
泛型强类型方法是使用 Hub<T>的强类型Hub类。在以下示例中 ChatHub ,客户端方法已提取到名为 的 IChatClient接口中:
public interface IChatClient { Task ReceiveMessage(string user, string message; }
此接口可用于将前面的 ChatHub 示例重构为强类型:
public class ChatHub : Hub<IChatClient> { public async Task SendMessage(string user, string message => await Clients.All.ReceiveMessage(user, message; public async Task SendMessageToCaller(string user, string message => await Clients.Caller.ReceiveMessage(user, message; public async Task SendMessageToGroup(string user, string message => await Clients.Group("SignalR Users".ReceiveMessage(user, message; }
这样Clients的对象都具备了接口定义的 ReceiveMessage方法调用,实际这个就是客户端的方法。
Hub<IChatClient> 可以对客户端方法进行编译时检查。 这可以防止使用字符串引起的问题,因为 Hub<T> 只能提供对 接口中定义的方法的访问权限。 使用强类型 Hub<T> 会禁止使用 SendAsync。
Hub服务端中心
public interface IClient { Task<string> GetMessage(; } public class ChatHub : Hub<IClient> { public async Task<string> WaitForMessage(string connectionId { string message = await Clients.Client(connectionId.GetMessage(; return message; } }
.NET 客户端
客户端在其 .On(... 处理程序中返回结果,如下所示:
hubConnection.On("GetMessage", async ( => { Console.WriteLine("Enter message:"; var message = await Console.In.ReadLineAsync(; return message; };
Typescript 客户端
hubConnection.on("GetMessage", async ( => {
let promise = new Promise((resolve, reject => {
setTimeout(( => {
resolve("message";
}, 100;
};
return promise;
};
Java 客户端
hubConnection.onWithResult("GetMessage", ( -> {
return Single.just("message";
};
在框架中整合SignalR的Hub的时候,我们定义一个接口IOnlineUserHub,以便强类型对客户端接口方法的调用,减少错误。
public class OnlineUserHub : Hub<IOnlineUserHub>
{
private readonly IOnlineUserService _onlineUserService;
private readonly IHubContext<OnlineUserHub, OnlineUserHub> _chatHubContext;
public OnlineUserHub(IOnlineUserService onlineUserService,
IHubContext<OnlineUserHub, IOnlineUserHub> onlineUserHubContext
{
_onlineUserService = onlineUserService;
_chatHubContext = onlineUserHubContext;
}
}
对象Hub<T>本身可以通过注入一个 IHubContext<OnlineUserHub, OnlineUserHub> 接口来获得对它的调用,如上面构造函数所示。该Hub一般还需要重写连接和断开的处理操作,如下代码所示。
/// <summary>
/// 连接后处理
/// </summary>
/// <returns></returns>
public override async Task OnConnectedAsync(
{
var httpContext = Context.GetHttpContext(;
var token = httpContext!.Request.Query["access_token"];
if (string.IsNullOrWhiteSpace(token return;
................
//向客户端提供在线用户信息
await _chatHubContext.Clients.Groups(groupName.OnlineUserList(new OnlineUserList
{
ConnectionId = user.ConnectionId,
RealName = user.RealName + $"({client.UA.Family}", //加上实际终端
Online = true,
UserList = userList.Items.ToList(
};
//更新在线用户缓存
await RedisHelper.SetAsync(CacheConst.KeyOnlineUser, userList.Items.ToList(;
}
上下文对象
类 Hub 包含一个 Context 属性,该属性包含以下属性以及有关连接的信息:
| 属性 | 说明 |
|---|---|
| ConnectionId | 获取连接的唯一 ID(由 SignalR 分配)。 每个连接都有一个连接 ID。 |
| UserIdentifier | 获取用户标识符。 默认情况下,SignalR 使用与连接关联的 ClaimsPrincipal 中的 ClaimTypes.NameIdentifier 作为用户标识符。 |
| User | 获取与当前用户关联的 ClaimsPrincipal。 |
| Items | 获取可用于在此连接范围内共享数据的键/值集合。 数据可以存储在此集合中,会在不同的中心方法调用间为连接持久保存。 |
| Features | 获取连接上可用的功能的集合。 目前,在大多数情况下不需要此集合,因此未对其进行详细记录。 |
| ConnectionAborted | 获取一个 CancellationToken,它会在连接中止时发出通知。 |
| 方法 | 说明 |
|---|---|
| GetHttpContext | 返回 HttpContext 连接的 ;如果连接未与 HTTP 请求关联, null 则返回 。 对于 HTTP 连接,请使用此方法获取 HTTP 标头和查询字符串等信息。 |
| Abort | 中止连接。 |
客户端对象
类 Hub 包含一个 Clients 属性,该属性包含以下用于服务器和客户端之间通信的属性:
| 属性 | 说明 |
|---|---|
| All | 对所有连接的客户端调用方法 |
| Caller | 对调用了中心方法的客户端调用方法 |
| Others | 对所有连接的客户端调用方法(调用了方法的客户端除外) |
| 方法 | 说明 |
|---|---|
| AllExcept | 对所有连接的客户端调用方法(指定连接除外) |
| Client | 对连接的一个特定客户端调用方法 |
| Clients | 对连接的多个特定客户端调用方法 |
| Group | 对指定组中的所有连接调用方法 |
| GroupExcept | 对指定组中的所有连接调用方法(指定连接除外) |
| Groups | 对多个连接组调用方法 |
| OthersInGroup | 对一个连接组调用方法(不包括调用了中心方法的客户端) |
| User | 对与一个特定用户关联的所有连接调用方法 |
| Users | 对与多个指定用户关联的所有连接调用方法 |
这样我们Hub里面定义的方法,就可以利用这些对象来处理了。
/// <summary> /// 前端调用发送方法(发送信息给所有人) /// </summary> /// <param name="message"></param> /// <returns></returns> public async Task ClientsSendMessagetoAll(MessageInput message { await _chatHubContext.Clients.All.ReceiveMessage(message; } /// <summary> /// 前端调用发送方法(发送消息给除了发送人的其他人) /// </summary> /// <param name="message"></param> /// <returns></returns> public async Task ClientsSendMessagetoOther(MessageInput message { var onlineuserlist = RedisHelper.Get<List<OnlineUserInfo>>(CacheConst.KeyOnlineUser; var user = onlineuserlist.Where(x => x.UserId == message.UserId.ToList(; if (user != null { await _chatHubContext.Clients.AllExcept(user[0].ConnectionId.ReceiveMessage(message; } }
基于IHubContext的接口,我们也可以定义一个常规的接口函数,用于在各个服务类中调用Hub处理函数
/// <summary> /// 封装的SignalR的常规处理实现 /// </summary> public class HubContextService : BaseService, IHubContextService
这样在服务端,注册服务后,可以使用这个自定义服务类的处理逻辑。
//使用HubContextService服务接口 builder.Services.AddSingleton<IHubContextService, HubContextService>(;
可以供一些特殊的控制器来使用Hub服务接口,如登录后台的时候,实现强制多端下线的处理方式。
/// <summary> /// 登录获取令牌授权的处理 /// </summary> [Route("api/[controller]"] [ApiController] public class LoginController : ControllerBase { private readonly IHubContextService _hubContextService;
/// <summary>
/// 登录授权处理
/// </summary>
/// <returns></returns>
[AllowAnonymous]
[HttpPost]
[Route("authenticate"]
public async Task<AuthenticateResultDto> Authenticate(LoginDto dto
{
var authResult = new AuthenticateResultDto(;
................
var loginResult = await this._userService.VerifyUser(dto.LoginName, dto.Password, ip;
if (loginResult != null && loginResult.UserInfo != null
{
var userInfo = loginResult.UserInfo;
...............
//单用户登录
await this._hubContextService.SignleLogin(userInfo.Id.ToString(;
}
else
{
authResult.Error = loginResult?.ErrorMessage;
}
return authResult;
}
2、SignalR客户端
Install-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; } } } }
可以将 HubConnection 配置为对 HubConnectionBuilder 使用 WithAutomaticReconnect 方法来自动重新连接。 默认情况下,它不会自动重新连接。
HubConnection connection= new HubConnectionBuilder( .WithUrl(new Uri("http://127.0.0.1:5000/chathub" .WithAutomaticReconnect( .Build(;
在没有任何参数的情况下,WithAutomaticReconnect( 将客户端配置为在每次尝试重新连接之前分别等待 0、2、10 和 30 秒,在四次尝试失败后停止。
/// <summary>
/// 初始化服务连接
/// </summary>
private async Task InitHub(
{
........
//创建连接对象,并实现相关事件
var url = serverUrl + $"/hubs/onlineUser?access_token={authenticateResultDto.AccessToken}";
hubConnection = new HubConnectionBuilder(
.WithUrl(url
.WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.Zero, TimeSpan.FromSeconds(10 } //自动连接
.Build(;
//接收实时信息
hubConnection.On<MessageInput>("ReceiveMessage", ReceiveMessage;
//连接上处理在线用户
hubConnection.On<OnlineUserList>("OnlineUserList", OnlineUserList;
//客户端收到服务关闭消息
hubConnection.On("ForceOffline", async (ForceOfflineInput data =>
{
await CloseHub(;
};
try
{
//开始连接
await hubConnection.StartAsync(;
var content = $"连接到服务器:{serverUrl}";
AddSystemMessage(content;
}
catch (Exception ex
{
Console.WriteLine(ex.StackTrace;
var content = $"服务器连接失败:{ex.Message}";
AddSystemMessage(content;
InitControlStatus(false;
return;
}
}
我们可以看到,客户端接收服务端的消息处理,通过下面代码进行处理。
//接收实时信息 hubConnection.On<MessageInput>("ReceiveMessage", ReceiveMessage; //连接上处理在线用户 hubConnection.On<OnlineUserList>("OnlineUserList", OnlineUserList; //客户端收到服务关闭消息 hubConnection.On("ForceOffline", async (ForceOfflineInput data =>
对于消息的接收处理,我们把它收到一个本地的集合列表中,然后统一处理即可。
/// <summary> /// 消息处理 /// </summary> /// <param name="data">JSON字符串</param> private void ReceiveMessage(MessageInput data { if (this.onlineUser != null { var info = new MessageInfo(data; ............. TryAddMessage(ownerId, info; BindTree(; } }
发送消息的时候,我们根据指向不同的用户,构造对应的消息体发送(调用服务端Hub接口)即可,调用通过InvokeAsync处理,接收相应的对象。
private async void BtnSendMessage_Click(object sender, EventArgs e { if (txtMessage.Text.Length == 0 return; var message = new MessageInput( { Title = "消息", Message = txtMessage.Text, MessageType = MessageTypeEnum.Info, UserId = this.toId, UserIds = new List<string>( }; //判断发送人,是单个发送,还是广播发送所有人 var methodName = !string.IsNullOrEmpty(this.toId ? "ClientsSendMessage" : "ClientsSendMessagetoAll"; await hubConnection.InvokeAsync(methodName, message; }
测试功能正常,我们就可以把窗体整合到Winform端的主体界面中了。
public event EventHandler<MessageInfo> SignalRMessageChanged;
因此我们可以在主界面上提供一个入口,供消息的处理操作。
/// <summary>
/// 初始化SignalR的处理
/// </summary>
private async void InitSignalR(
{
await Portal.gc.InitHub(;
}
这样就会根据相应的信息,实现HubConnection的初始化操作了,而且这个连接的生命周期是伴随整个应用的出现而出现的。
窗体可以对SignalR消息进行实时的更新相应,通过事件的实现。
public partial class FrmSignalClient : BaseDock { public FrmSignalClient( { InitializeComponent(; Portal.gc.SignalRMessageChanged += SignalRMessageChanged; }
由于篇幅的原因,后面在介绍在Vue3+Element的BS端中实现对SignalR消息整合的处理操作。