基於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