在ABP框架里面,默认会带入SignalR消息处理技术,它同时也是ABP框架里面实时消息处理、事件/通知处理的一个实现方式,SignalR消息处理本身就是一个实时很好的处理方案,我在之前在我的Winform框架中的相关随笔也有介绍过SIgnalR的一些内容《基于SignalR的服务端和客户端通讯处理》,本篇基于.net Core的ABP框架介绍SignalR的后端处理,以及基于Winform程序进行一些功能测试,以求我们对SignalR的技术应用有一些了解。

SignalR是一个.NET Core/.NET Framework的开源实时框架. SignalR的可使用Web Socket, Server Sent Events 和 Long Polling作为底层传输方式。

SignalR基于这三种技术构建, 抽象于它们之上, 它让你更好的关注业务问题而不是底层传输技术问题。

SignalR将整个信息的交换封装起来,客户端和服务器都是使用JSON来沟通的,在服务端声明的所有Hub信息,都会生成JavaScript输出到客户端,.NET则依赖Proxy来生成代理对象,而Proxy的内部则是将JSON转换成对象。

Hub类里面, 我们就可以调用所有客户端上的方法了. 同样客户端也可以调用Hub类里的方法.

SignalR可以将参数序列化和反序列化. 这些参数被序列化的格式叫做Hub 协议, 所以Hub协议就是一种用来序列化和反序列化的格式.

Hub协议的默认协议是JSON, 还支持另外一个协议是MessagePack。MessagePack是二进制格式的, 它比JSON更紧凑, 而且处理起来更简单快速, 因为它是二进制的.

此外, SignalR也可以扩展使用其它协议。

SignalR 可以与ASP.NET Core authentication一起使用,以将用户与每个连接相关联。 在中心中,可以从HubConnectionContext属性访问身份验证数据。

1、ABP框架中后端对SignalR的处理

如果需要在.net core使用SignalR,我们首先需要引入aspnetcore的SiganlR程序集包

另外由于我们需要使用ABP基础的SignalR的相关类,因此需要引入ABP的SignalR模块,如下所示。

    [DependsOn(
typeof(WebCoreModule),
typeof(AbpAspNetCoreSignalRModule))]
public class WebHostModule: AbpModule
{
private readonly IWebHostEnvironment _env;
private readonly IConfigurationRoot _appConfiguration; public WebHostModule(IWebHostEnvironment env)
{
_env = env;
_appConfiguration = env.GetAppConfiguration();
}

然后在Web.Host中发布SiganlR的服务端名称,如下所示。

public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
........................ app.UseEndpoints(endpoints =>
{
endpoints.MapHub<AbpCommonHub>("/signalr");
endpoints.MapHub<ChatHub>("/signalr-chat"); endpoints.MapControllerRoute("defaultWithArea", "{area}/{controller=Home}/{action=Index}/{id?}");
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}"); });

注册 SignalR 和 ASP.NET Core 身份验证中间件的顺序。 在 UseSignalR 之前始终调用 UseAuthentication,以便 SignalR 在 HttpContext上有用户。

在基于浏览器的应用程序中,cookie 身份验证允许现有用户凭据自动流向 SignalR 连接。 使用浏览器客户端时,无需额外配置。 如果用户已登录到你的应用,则 SignalR 连接将自动继承此身份验证。

客户端可以提供访问令牌,而不是使用 cookie。 服务器验证令牌并使用它来标识用户。 仅在建立连接时才执行此验证。 连接开启后,服务器不会通过自动重新验证来检查令牌是否撤销。

ABP框架本身提供了可用的基类OnlineClientHubBase和AbpHubBase,内置了日志、会话、配置、本地化等组件,都继承自基类Microsoft.AspNetCore.SignalR.Hub。

Abp的AbpCommonHub提供了用户链接到服务和断开链接时,ConnectionId和UserId的维护,可以在IOnlineClientManger中进行访问,IOnlineClientManger提供如下方法:

  • bool IsOnline()

而ChatHub则是我们自定义的SignalR聊天处理类,它同样继承于OnlineClientHubBase,并整合了其他一些对象接口及进行消息的处理。

例如,我们这里SendMessage发送SIgnalR消息的逻辑如下所示。

        /// <summary>
/// 发送SignalR消息
/// </summary>
/// <param name="input">发送的消息体</param>
/// <returns></returns>
public async Task<string> SendMessage(SendChatMessageInput input)
{
var sender = Context.ToUserIdentifier();
var receiver = new UserIdentifier(input.TenantId, input.UserId); try
{
using (ChatAbpSession.Use(Context.GetTenantId(), Context.GetUserId()))
{
await _chatMessageManager.SendMessageAsync(sender, receiver, input.Message, input.TenancyName, input.UserName, input.ProfilePictureId);
return string.Empty;
}
}
catch (UserFriendlyException ex)
{
Logger.Warn("Could not send chat message to user: " + receiver);
Logger.Warn(ex.ToString(), ex);
return ex.Message;
}
catch (Exception ex)
{
Logger.Warn("Could not send chat message to user: " + receiver);
Logger.Warn(ex.ToString(), ex);
return _localizationManager.GetSource("AbpWeb").GetString("InternalServerError");
}
}

而消息对象实体,如下所示

    /// <summary>
/// 发送的SignalR消息
/// </summary>
public class SendChatMessageInput
{
/// <summary>
/// 租户ID
/// </summary>
public int? TenantId { get; set; } /// <summary>
/// 用户ID
/// </summary>
public long UserId { get; set; } /// <summary>
/// 用户名
/// </summary>
public string UserName { get; set; } /// <summary>
/// 租户名
/// </summary>
public string TenancyName { get; set; } /// <summary>
/// 个人图片ID
/// </summary>
public Guid? ProfilePictureId { get; set; } /// <summary>
/// 发送的消息内容
/// </summary>
public string Message { get; set; }
}

为了和客户端进行消息的交互,我们需要存储用户发送的SignalR的消息到数据库里面,并需要知道用户的好友列表,以及获取未读消息,消息的已读操作等功能,那么我们还需要在应用层发布一个ChatAppService的应用服务接口来进行交互。

    [AbpAuthorize]
public class ChatAppService : MyServiceBase, IChatAppService
{
private readonly IRepository<ChatMessage, long> _chatMessageRepository;
private readonly IUserFriendsCache _userFriendsCache;
private readonly IOnlineClientManager<ChatChannel> _onlineClientManager;
private readonly IChatCommunicator _chatCommunicator;

客户端通过和 signalr-chat 和ChatAppService进行联合处理,前者是处理SignalR消息发送操作,后者则是应用层面的数据处理。

2、Winform程序对SignalR进行功能测试

前面说过,SignalR消息应用比较多,它主要用来处理实时的消息通知、事件处理等操作,我们这里用来介绍进行聊天回话的一个操作。

客户端使用SignalR需要引入程序集包Microsoft.AspNetCore.SignalR.Client。

首先我们建立一个小的Winform程序,设计一个大概的界面功能,如下所示。

这个主要就是先通过ABP登录认证后,传递身份,并获取用户好友列表吧,连接到服务端的SiganlR接口后,进行消息的接收和发送等操作。

首先是用户身份认证部分,先传递用户名密码,登陆认证成功后获取对应的令牌,存储在缓存中使用。

        private async void btnGetToken_Click(object sender, EventArgs e)
{
if(this.txtUserName.Text.Length == 0)
{
MessageDxUtil.ShowTips("用户名不能为空");return;
}
else if (this.txtPassword.Text.Length == 0)
{
MessageDxUtil.ShowTips("用户密码不能为空"); return;
} var data = new AuthenticateModel()
{
UserNameOrEmailAddress = this.txtUserName.Text,
Password = this.txtPassword.Text
}.ToJson(); helper.ContentType = "application/json";//指定通讯的JSON方式
helper.MaxTry = 2;
var content = helper.GetHtml(TokenUrl, data, true);
Console.WriteLine(content); var setting = new JsonSerializerSettings() { ContractResolver = new CamelCasePropertyNamesContractResolver() };
var result = JsonConvert.DeserializeObject<AbpResponse<AuthenticateResultModel>>(content, setting);
if (result != null && result.Success && !string.IsNullOrWhiteSpace(result.Result.AccessToken))
{
//获取当前用户
Cache.Instance["AccessToken"] = result.Result.AccessToken;//设置缓存,方便ApiCaller调用设置Header
currentUser = await UserApiCaller.Instance.GetAsync(new EntityDto<long>(result.Result.UserId)); Console.WriteLine(result.Result.ToJson());
Cache.Instance["token"] = result.Result; //设置缓存后,APICaller不用手工指定RequestHeaders的令牌信息 EnableConnectState(false);
} this.Text = string.Format("获取Token{0}", (result != null && result.Success) ? "成功" : "失败"); //获取用户身份的朋友列表
GetUserFriends();
}

其次是获取用户身份后,获得对应的好友列表加入到下拉列表中,如下代码所示。

        private void GetUserFriends()
{
var result = ChatApiCaller.Instance.GetUserChatFriendsWithSettings();
this.friendDict = new Dictionary<long, FriendDto>();
foreach (var friend in result.Friends)
{
this.friendDict.Add(friend.FriendUserId, friend); this.txtFriends.Properties.Items.Add(new CListItem(friend.FriendUserName, friend.FriendUserId.ToString()));
}
}

然后就是SignalR消息通道的连接了,通过HubConnection连接上代码如下所示。

connection = new HubConnectionBuilder()
.WithUrl(ChatUrl, options =>
{
options.AccessTokenProvider = () => Task.FromResult(token.AccessToken);
options.UseDefaultCredentials = true;
})
.Build();

整块创建SignalR的连接处理如下所示。

        private async Task StartConnection()
{
if (connection == null)
{
if (!Cache.Instance.ContainKey("token"))
{
MessageDxUtil.ShowTips("没有登录,请先登录");
return;
} var token = Cache.Instance["token"] as AuthenticateResultModel;
if (token != null)
{
connection = new HubConnectionBuilder()
.WithUrl(ChatUrl, options =>
{
options.AccessTokenProvider = () => Task.FromResult(token.AccessToken);
options.UseDefaultCredentials = true;
})
.Build();
//connection.HandshakeTimeout = new TimeSpan(8000);//握手过期时间 //收到消息的处理
connection.On<string>("MessageReceived", (str) =>
{
Console.WriteLine(str);
this.richTextBox.AppendText(str);
this.richTextBox.AppendText("\r\n");
this.richTextBox.ScrollToCaret();
});
await connection.StartAsync(); EnableConnectState(true);
}
}
await Task.CompletedTask;
}

客户端传递身份进行SignalR连接,连接成功后,收到消息回显在客户端。

每次用户登录并连接后,显示未读的消息到客户即可。

this.messages = new List<ChatMessageDto>();//清空数据

var result = await ChatApiCaller.Instance.GetUserChatMessages(input);
if (result != null && result.Items.Count > 0)
{
this.messages = result.Items.Concat(this.messages);
await ChatApiCaller.Instance.MarkAllUnreadMessagesOfUserAsRead(new MarkAllUnreadMessagesOfUserAsReadInput() { TenantId = 1, UserId = currentUser.Id });
} this.richTextBox.Clear();
foreach (var item in this.messages)
{
var message = string.Format("User[{0}]:{1} -{2}", item.TargetUserId, item.Message, item.CreationTime);
this.richTextBox.AppendText(message);
this.richTextBox.AppendText("\r\n");
}
this.richTextBox.ScrollToCaret();

而客户端需要发送消息给另外一个好友的时候,就需要按照消息体的对象进行属性设置,然后调用SignalR接口进行发送即可,也就是直接调用服务端的方法了。

//当前用户id为2,发送给id为8的
var data = new SendChatMessageInput()
{
Message = this.txtMessage.Text,
UserId = friend.FriendUserId,
TenantId = friend.FriendTenantId,
UserName = friend.FriendUserName,
TenancyName = friend.FriendTenancyName,
ProfilePictureId = Guid.NewGuid()
}; try
{
//调用服务chathub接口进行发送消息
var result = await connection.InvokeAsync<string>("SendMessage", data);
Console.WriteLine(result); if (!string.IsNullOrWhiteSpace(result))
{
MessageDxUtil.ShowError(result);
}
else
{
await GetUserChatMessages(); //刷新消息
this.txtMessage.Text = ""; //清空输入
this.Text = string.Format("消息发送成功:{0}", DateTime.Now.ToString());
}
}
catch (Exception ex)
{
MessageDxUtil.ShowTips(ex.Message);
}

最后我们看看程序的效果,如下所示。

消息已经被 序列化到ABP的系统表里面了,我们可以在表中查看到。

用户的好友列表在表AppFriendships中,发送的消息则存储在AppChatMessages中

基于ABP框架的SignalR,使用Winform程序进行功能测试的更多相关文章

  1. 利用代码生成工具生成基于ABP框架的代码

    在前面随笔,我介绍了整个ABP优化过框架的分层模型,包括尽量简化整个ABP框架的各个层的关系,以及纳入一些基类的辅助处理,使得我们对应业务分层类或者接口尽可能减少代码,并具有生产环境所需要的基类接口, ...

  2. 基于abp框架的数据库种子数据初始化

    目录 基于abp框架的数据库种子数据初始化 1.背景 2.参照 3.解决方案 3.1 初始化数据 3.2 依赖注入方法容器里获取数据库上下文 3.3 封装创建初始化数据列表方法 3.4 数据库中没有的 ...

  3. 使用代码生成工具快速生成基于ABP框架的Vue+Element的前端界面

    世界上唯一不变的东西就是变化,我们通过总结变化的规律,以规律来应付变化,一切事情处理起来事半功倍.我们在开发后端服务代码,前端界面代码的时候,界面都是依照一定的规律进行变化的,我们通过抽取数据库信息, ...

  4. 在基于ABP框架的前端项目Vue&Element项目中采用电子签名的处理

    在前面随笔介绍了<在基于ABP框架的前端项目Vue&Element项目中采用电子签章处理文件和打印处理>的处理,有的时候,我们在流程中或者一些文件签署的时候,需要签上自己的大名,一 ...

  5. 在基于ABP框架的前端项目Vue&Element项目中采用日期格式处理,对比Moment.js和day.js的处理

    Day.js 是一个轻量的处理时间和日期的 JavaScript 库,和 Moment.js 的 API 设计保持完全一样. 如果您曾经用过 Moment.js, 那么您已经知道如何使用 Day.js ...

  6. 基于Struts2框架实现登录案例 之 程序国际化

    国际化牵涉的知识非常多,这里只能简单的介绍,程序国际化的一般做法是:在jsp页面时, 不是直接输出信息,而是输出一个key值,该key值在不同语言环境下找到对应资源文件下的 对应信息,因此首先要创建满 ...

  7. 基于NopCommerce框架开发的微信小程序UrShop

    Urshop小程序商城 介绍 UrShop小程序商城 2.0发布啦,发布地址https://gitee.com/urselect/urshop UrShop 根据NopCommerce框架开发的,基于 ...

  8. 基于ABP框架的权限设置

    需求:在界面展示中,"定向包管理","竞价管理","竞拍管理","发布定向资源","添加竞价资源", ...

  9. 在基于ABP框架的前端项目Vue&Element项目中采用电子签章处理文件和打印处理

    在一些内部OA或者流转的文件,或者给一些客户的报价文件.合同,或者一些医院出示的给保险机构的病历资料等,有时候可能都希望快速的使用电子签章的处理方式来给文件盖上特定的印章,本篇随笔介绍基于Vue&am ...

随机推荐

  1. Java进阶专题(二十五) 分布式锁原理与实现

    前言 ​ 现如今很多系统都会基于分布式或微服务思想完成对系统的架构设计.那么在这一个系统中,就会存在若干个微服务,而且服务间也会产生相互通信调用.那么既然产生了服务调用,就必然会存在服务调用延迟或失败 ...

  2. 微服务架构Day03-SpringBoot之web开发配置

    概述 SpringBoot开发: 1.创建SpringBoot应用,选中需要的场景模块. 2.SpringBoot已经默认将场景模块配置好,只需要在配置文件中指定少量的配置(数据库地址,用户名,密码) ...

  3. webpack4.0源码解析之打包后js文件分析

    首先,init之后创建一个简单的webpack基本的配置,在src目录下创建两个js文件(一个主入口文件和一个非主入口文件)和一个html文件,package.json,webpack.config. ...

  4. Oh My Zsh All In One

    Oh My Zsh All In One https://ohmyz.sh/ install # CURL $ sh -c "$(curl -fsSL https://raw.github. ...

  5. Get your site working on Google Search Console , 在 Google Search Console中运行您的网站, Google Search Console

    1 1 https://support.google.com/webmasters/topic/4564315? Search Console Help SEARCH CONSOLEHELP FORU ...

  6. React.memo All In One

    React.memo All In One https://reactjs.org/docs/react-api.html#components React.memo const MyComponen ...

  7. 图解 H5 与 WebView 数据通信原理

    图解 H5 与 WebView 数据通信原理 Android / iOS / RN / Flutter H5 接受数据 自定义 schema H5 调用原生 API 拍照,扫码 原生 调用 H5 AP ...

  8. JavaScript Inheritance All in One

    JavaScript Inheritance All in One constructor inheritance prototype chain inheritance "use stri ...

  9. React & Didact

    React & Didact A DIY guide to build your own React https://github.com/pomber/didact https://gith ...

  10. 炒币亏损一万美金?不如抢SPC空投!

    币圈的市场可以用风云变幻来形容,1月9日的时候比特币震荡,其他币种争先上涨,连平时都不涨的币种都拉出了10%-20%的领先涨幅,市场惊呼牛市来了,但喜悦还没有维持一天,1月10日(昨天)市场就走向另一 ...