基于ASP.NET Core SignalR 可以实现客户端和服务器之间进行即时通信。本篇随笔介绍一些SignalR的基础知识,以及结合对SqlSugar的开发框架的支持,实现SignalR的多端处理整合,从而实现Winform客户端,基于Vue3+ElementPlus的BS端整合,后面也可以实现对移动端的SignalR的整合通讯。

适合 SignalR 的应用场景:

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

SignalR 自动选择服务器和客户端能力范围内的最佳传输方法,如
WebSockets
、Server-Sent Events、长轮询。Hub 是一种高级管道,允许客户端和服务器相互调用方法。 SignalR 自动处理跨计算机边界的调度,并允许客户端调用服务器上的方法,反之亦然。SignalR 提供两个内置中心协议:基于 JSON 的文本协议和基于 MessagePack 的二进制协议。

客户端负责通过
HubConnection
对象建立到服务器终结点的连接。 Hub 连接在每个目标平台中表示:

当中心连接实例成功启动后,消息可以自由地双向流动。 用户可以自由地将通知发送到服务器,以及从服务器接收通知。 客户端是任何已连接的应用程序,例如(但不限于)Web 浏览器、移动应用或桌面应用。

1、SignalR服务端

在.net core的Web API上,我们首先需要注册SignalR的服务,然后创建对应的Hub进行使用。一般可以在启动类中添加如下代码即可。

builder.Services.AddSignalR();//即时通讯
app.UseEndpoints(endpoints=>{//注册集线器
    endpoints.MapHub<OnlineUserHub>("/hubs/onlineUser");
});

定义集线器只需要继承
Hub

Hub<TStrongType>
泛型基类即可。

public classChatHub : Hub
{
public async Task SendMessage(string user, stringmessage)=> await Clients.All.SendAsync("ReceiveMessage", user, message);
}

泛型强类型方法是使用
Hub<T>
的强类型
Hub
类。在以下示例中
ChatHub
,客户端方法已提取到名为 的
IChatClient
接口中:

public interfaceIChatClient
{
Task ReceiveMessage(
string user, stringmessage);
}

此接口可用于将前面的
ChatHub
示例重构为强类型:

public class ChatHub : Hub<IChatClient>{public async Task SendMessage(string user, stringmessage)=> awaitClients.All.ReceiveMessage(user, message);public async Task SendMessageToCaller(string user, stringmessage)=> awaitClients.Caller.ReceiveMessage(user, message);public async Task SendMessageToGroup(string user, stringmessage)=> await Clients.Group("SignalR Users").ReceiveMessage(user, message);
}

这样Clients的对象都具备了接口定义的
ReceiveMessage
方法调用,实际这个就是客户端的方法。

使用
Hub<IChatClient>
可以对客户端方法进行编译时检查。 这可以防止使用字符串引起的问题,因为
Hub<T>
只能提供对 接口中定义的方法的访问权限。 使用强类型
Hub<T>
会禁止使用
SendAsync

Hub服务端中心

public interfaceIClient
{
Task
<string>GetMessage();
}
public class ChatHub : Hub<IClient>{public async Task<string> WaitForMessage(stringconnectionId)
{
string message = awaitClients.Client(connectionId).GetMessage();returnmessage;
}
}

.NET 客户端

客户端在其
.On(...)
处理程序中返回结果,如下所示:

hubConnection.On("GetMessage", async () =>{
Console.WriteLine(
"Enter message:");var message = awaitConsole.In.ReadLineAsync();returnmessage;
});

Typescript 客户端

hubConnection.on("GetMessage", async () =>{
let promise
= new Promise((resolve, reject) =>{
setTimeout(()
=>{
resolve(
"message");
},
100);
});
returnpromise;
});

Java 客户端

hubConnection.onWithResult("GetMessage", () ->{return Single.just("message");
});

在框架中整合SignalR的Hub的时候,我们定义一个接口IOnlineUserHub,以便强类型对客户端接口方法的调用,减少错误。

然后在定义一个Hub的对象类,如下所示 。

public class OnlineUserHub : Hub<IOnlineUserHub>{private readonlyIOnlineUserService _onlineUserService;private readonly IHubContext<OnlineUserHub, OnlineUserHub>_chatHubContext;publicOnlineUserHub(IOnlineUserService onlineUserService,
IHubContext
<OnlineUserHub, IOnlineUserHub>onlineUserHubContext)
{
_onlineUserService
=onlineUserService;
_chatHubContext
=onlineUserHubContext;
}
}

对象Hub<T>本身可以通过注入一个
IHubContext<OnlineUserHub, OnlineUserHub>
接口来获得对它的调用,如上面构造函数所示。该Hub一般还需要重写连接和断开的处理操作,如下代码所示。

如对于用户的SignalR连接发起,我们需要判断用户的令牌及相关身份信息,如果成功,则通过给客户端提供在线用户列表。

        /// <summary>
        ///连接后处理/// </summary>
        /// <returns></returns>
        public override asyncTask OnConnectedAsync()
{
var httpContext =Context.GetHttpContext();var token = httpContext!.Request.Query["access_token"];if (string.IsNullOrWhiteSpace(token)) return;

................
//向客户端提供在线用户信息 await _chatHubContext.Clients.Groups(groupName).OnlineUserList(newOnlineUserList
{
ConnectionId
=user.ConnectionId,
RealName
= user.RealName + $"({client.UA.Family})", //加上实际终端 Online = true,
UserList
=userList.Items.ToList()
});
//更新在线用户缓存 awaitRedisHelper.SetAsync(CacheConst.KeyOnlineUser, userList.Items.ToList());
}

上下文对象


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 中止连接。

客户端对象


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

属性 说明
All 对所有连接的客户端调用方法
Caller 对调用了中心方法的客户端调用方法
Others 对所有连接的客户端调用方法(调用了方法的客户端除外)

Hub.Clients
还包含以下方法:

方法 说明
AllExcept 对所有连接的客户端调用方法(指定连接除外)
Client 对连接的一个特定客户端调用方法
Clients 对连接的多个特定客户端调用方法
Group 对指定组中的所有连接调用方法
GroupExcept 对指定组中的所有连接调用方法(指定连接除外)
Groups 对多个连接组调用方法
OthersInGroup 对一个连接组调用方法(不包括调用了中心方法的客户端)
User 对与一个特定用户关联的所有连接调用方法
Users 对与多个指定用户关联的所有连接调用方法

这样我们Hub里面定义的方法,就可以利用这些对象来处理了。

        /// <summary>
        ///前端调用发送方法(发送信息给所有人)/// </summary>
        /// <param name="message"></param>
        /// <returns></returns>
        public asyncTask ClientsSendMessagetoAll(MessageInput message)
{
await_chatHubContext.Clients.All.ReceiveMessage(message);
}
/// <summary> ///前端调用发送方法(发送消息给除了发送人的其他人)/// </summary> /// <param name="message"></param> /// <returns></returns> public asyncTask 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 classLoginController : ControllerBase
{
private readonly IHubContextService _hubContextService;
        /// <summary>
        ///登录授权处理/// </summary>
        /// <returns></returns>
[AllowAnonymous]
[HttpPost]
[Route(
"authenticate")]public async Task<AuthenticateResultDto>Authenticate(LoginDto dto)
{
var authResult = newAuthenticateResultDto();
................
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;
}
returnauthResult;
}

2、SignalR客户端

.net客户端在对接Hub中心服务端的时候,需要添加Microsoft.AspNetCore.SignalR.Client的引用。

Install-Package Microsoft.AspNetCore.SignalR.Client

若要建立连接,请创建
HubConnectionBuilder
并调用
Build
。 在建立连接期间,可以配置中心 URL、协议、传输类型、日志级别、标头和其他选项。 可通过将任何
HubConnectionBuilder
方法插入
Build
中来配置任何必需选项。 使用
StartAsync
启动连接。

usingSystem;usingSystem.Threading.Tasks;usingSystem.Windows;usingMicrosoft.AspNetCore.SignalR.Client;namespaceSignalRChatClient
{
public partial classMainWindow : Window
{
HubConnection connection;
publicMainWindow()
{
InitializeComponent();

connection
= newHubConnectionBuilder()
.WithUrl(
"http://localhost:53353/ChatHub")
.Build();

connection.Closed
+= async (error) =>{await Task.Delay(new Random().Next(0,5) * 1000);awaitconnection.StartAsync();
};
}
private async void connectButton_Click(objectsender, RoutedEventArgs e)
{
connection.On
<string, string>("ReceiveMessage", (user, message) =>{this.Dispatcher.Invoke(() =>{var newMessage = $"{user}: {message}";
messagesList.Items.Add(newMessage);
});
});
try{awaitconnection.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(objectsender, RoutedEventArgs e)
{
try{await connection.InvokeAsync("SendMessage",
userTextBox.Text, messageTextBox.Text);
}
catch(Exception ex)
{
messagesList.Items.Add(ex.Message);
}
}
}
}

可以将
HubConnection
配置为对
HubConnectionBuilder
使用
WithAutomaticReconnect
方法来自动重新连接。 默认情况下,它不会自动重新连接。

HubConnection connection= newHubConnectionBuilder()
.WithUrl(
new Uri("http://127.0.0.1:5000/chathub"))
.WithAutomaticReconnect()
.Build();

在没有任何参数的情况下,
WithAutomaticReconnect()
将客户端配置为在每次尝试重新连接之前分别等待 0、2、10 和 30 秒,在四次尝试失败后停止。

为了测试Winform客户端对服务端的连接,我们可以新建一个小案例Demo,来测试信息处理的效果。

创建一个测试的窗体如下所示(实际测试效果)。

创建连接Hub中心的代码如下所示。

        /// <summary>
        ///初始化服务连接/// </summary>
        private asyncTask InitHub()
{
........
//创建连接对象,并实现相关事件 var url = serverUrl + $"/hubs/onlineUser?access_token={authenticateResultDto.AccessToken}";
hubConnection
= newHubConnectionBuilder()
.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) =>{awaitCloseHub();
});
try{//开始连接 awaithubConnection.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 voidReceiveMessage(MessageInput data)
{
if (this.onlineUser != null)
{
var info = newMessageInfo(data);
.............
TryAddMessage(ownerId, info);
BindTree();
}
}

发送消息的时候,我们根据指向不同的用户,构造对应的消息体发送(调用服务端Hub接口)即可,调用通过
InvokeAsync
处理,接收相应的对象。

private async void BtnSendMessage_Click(objectsender, EventArgs e)
{
if (txtMessage.Text.Length == 0)return;var message = newMessageInput()
{
Title
= "消息",
Message
=txtMessage.Text,
MessageType
=MessageTypeEnum.Info,
UserId
= this.toId,
UserIds
= new List<string>()
};
//判断发送人,是单个发送,还是广播发送所有人 var methodName = !string.IsNullOrEmpty(this.toId) ? "ClientsSendMessage" : "ClientsSendMessagetoAll";awaithubConnection.InvokeAsync(methodName, message);}

测试功能正常,我们就可以把窗体整合到Winform端的主体界面中了。

在Winform端的登陆处理的时候,我们把SignarR的主要处理逻辑放在全局类GlobalControl 中,方便调用,并定义好几个常用的对象,如连接,在线用户信息,消息列表等。

并通过定义事件的方式,在消息变化的时候,通知界面进行更新处理。

public event EventHandler<MessageInfo> SignalRMessageChanged;

因此我们可以在主界面上提供一个入口,供消息的处理操作。

主窗体在界面初始化的时候,调用一下全局类的初始化SignalR的Hub连接即可。

        /// <summary>
        ///初始化SignalR的处理/// </summary>
        private async voidInitSignalR()
{
awaitPortal.gc.InitHub();
}

这样就会根据相应的信息,实现HubConnection的初始化操作了,而且这个连接的生命周期是伴随整个应用的出现而出现的。

打开就可以展示在线用户,并可以和系统相关用户发送实时消息了。如果可以,我们也可以把消息存储在数据库端,然后离线也可以收到存储起来,供下次登录后进行查看。

窗体可以对SignalR消息进行实时的更新相应,通过事件的实现。

    public partial classFrmSignalClient : BaseDock
{
publicFrmSignalClient()
{
InitializeComponent();

Portal.gc.SignalRMessageChanged
+=SignalRMessageChanged;
}

由于篇幅的原因,后面在介绍在Vue3+Element的BS端中实现对SignalR消息整合的处理操作。

标签: none

添加新评论