zoukankan      html  css  js  c++  java
  • 基于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("
    ");
                            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("
    ");
    }
    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框架代码生成

    主要研究技术:代码生成工具、会员管理系统、客户关系管理软件、病人资料管理软件、Visio二次开发、酒店管理系统、仓库管理系统等共享软件开发
    专注于Winform开发框架/混合式开发框架Web开发框架Bootstrap开发框架微信门户开发框架的研究及应用
      转载请注明出处:
    撰写人:伍华聪  http://www.iqidi.com 
        
  • 相关阅读:
    Broadcasting
    TensorFlow2-维度变换
    TensorFlow2教程(目录)
    SQL Server 加密层级
    Windows 打开防火墙上的指定端口
    SQL Server 查看对象之间的引用关系
    SQL Server 查看实例配置情况的 2 方法
    Linux 快捷键
    MYSQL 二进制日志
    SQL Server 存储过程自启动
  • 原文地址:https://www.cnblogs.com/wuhuacong/p/14516675.html
Copyright © 2011-2022 走看看