基於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框架代碼生成

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM