1. 程式人生 > >基於ABP框架的SignalR,使用Winform程式進行功能測試

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

&n