6

基于ABP框架的SignalR,使用Winform程序进行功能测试

 3 years ago
source link: https://www.cnblogs.com/wuhuacong/p/14516675.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

基于ABP框架的SignalR,使用Winform程序进行功能测试

在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开发框架的基础上,完善了Winform端的界面,以及Vue&Element的前端界面,并结合代码生成工具的快速辅助,使得利用ABP框架开发项目,更加方便和高效。

ABP框架代码生成


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK