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