基於ABP框架的SignalR,使用Winform程序進行功能測試

在ABP框架裏面,默認會帶入SignalR消息處理技術,它同時也是ABP框架裏面實時消息處理、事件/通知處理的一個實現方式,SignalR消息處理自己就是一個實時很好的處理方案,我在以前在個人Winform框架中的相關隨筆也有介紹過SIgnalR的一些內容《基於SignalR的服務端和客戶端通信處理》,本篇基於.net Core的ABP框架介紹SignalR的後端處理,以及基於Winform程序進行一些功能測試,以求咱們對SignalR的技術應用有一些瞭解。html

SignalR是一個.NET Core/.NET Framework的開源實時框架. SignalR的可以使用Web Socket, Server Sent Events 和 Long Polling做爲底層傳輸方式。前端

SignalR基於這三種技術構建, 抽象於它們之上, 它讓你更好的關注業務問題而不是底層傳輸技術問題。數據庫

SignalR將整個信息的交換封裝起來,客戶端和服務器都是使用JSON來溝通的,在服務端聲明的全部Hub信息,都會生成JavaScript輸出到客戶端,.NET則依賴Proxy來生成代理對象,而Proxy的內部則是將JSON轉換成對象。json

Hub類裏面, 咱們就能夠調用全部客戶端上的方法了. 一樣客戶端也能夠調用Hub類裏的方法.後端

SignalR能夠將參數序列化和反序列化. 這些參數被序列化的格式叫作Hub 協議, 因此Hub協議就是一種用來序列化和反序列化的格式.api

Hub協議的默認協議是JSON, 還支持另一個協議是MessagePack。MessagePack是二進制格式的, 它比JSON更緊湊, 並且處理起來更簡單快速, 由於它是二進制的.瀏覽器

此外, SignalR也能夠擴展使用其它協議。緩存

SignalR 能夠與ASP.NET Core authentication一塊兒使用,以將用戶與每一個鏈接相關聯。 在中心中,能夠從HubConnectionContext屬性訪問身份驗證數據。服務器

一、ABP框架中後端對SignalR的處理

若是須要在.net core使用SignalR,咱們首先須要引入aspnetcore的SiganlR程序集包cookie

另外因爲咱們須要使用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消息發送操做,後者則是應用層面的數據處理。

 

二、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框架代碼生成

相關文章
相關標籤/搜索