# [實作] .Net Framework WebAPI 實作 SignalR - 實作功能篇 ### :arrow_forward: **綱要** 1. **建構 SignalR 環境** 2. **資料庫 Schema 設定** 3. **SignalR 連線人數計算 - 程式碼實作** 4. **SignalR User Group - 程式碼實作** 5. **SignalR 後端 Method 呼叫 Web API - 程式碼實作** --- ### 1. 前情提要 - 如何建構 SignalR 環境 - 有關如何於 .Net Framework Web API 中建構 SignalR 環境,以及如何於前後端互相呼叫特定 SignalR 的方法,請參閱下方 HackMD 連結: - **[.Net Framework WebAPI 實作 SignalR - 建立環境篇](https://hackmd.io/@David0799/SykNjmcmA)** - 本文將實作一個顯示全站人數和具有特定 群組(Group) 的 1對1 即時聊天功能 --- ### 2. 資料庫 Schema 設定 #### (1) User 會員表單 ```csharp public class User { [Key] [Display(Name = "編號")] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } ... } ``` #### (2) ChatRoom 聊天室表單 ```csharp [Table("ChatRooms")] public class ChatRooms { [Key] [Display(Name = "編號")] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [Display(Name = "聊天室擁有者ID")] public int UserIdOwner { get; set; } [Display(Name = "聊天室對談者ID")] public int UserIdTalker { get; set; } [Display(Name = "訊息表單虛擬欄位")] public virtual ICollection<Record> Record { get; set; } ... } ``` #### (3) Records 聊天訊息紀錄表單 ```csharp [Table("Records")] public class Records { [Key] [Display(Name = "編號")] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [Display(Name = "發訊者使用者Id")] public int UserIdSender { get; set; } [Required] [MaxLength(500)] public string Message { get; set; } [Display(Name = "聊天室外鍵")] public int ChatRoomId { get; set; } [JsonIgnore] [ForeignKey("ChatRoomId")] [Display(Name = "聊天室表單虛擬欄位")] public virtual ChatRoom ChatRoom { get; set; } ... } ``` --- ### 3. SignalR 全域連線人數計算 #### (1) 於後端 SRHub.cs 檔中擴增以下程式 ```csharp public class SRHub : Hub { // 加入以下 private static int OnlineCount = 0; // 宣告目前進站人數 public override Task OnConnected() // 若有人加入 Hub,SignalR 會自動執行此方法 { OnlineCount++; return base.OnConnected(); } public override Task OnDisconnected(bool stopCalled) // 若有人離開 Hub,SignalR 會自動執行此方法 { OnlineCount--; return base.OnDisconnected(stopCalled); } public override Task OnReconnected() // 若有人重新加入 Hub,SignalR 會自動執行此方法 { OnlineCount++; return base.OnReconnected(); } public int GetOnlineCount() // 前端可以呼叫此方法,取得目前連上 Hub 的總人數 { return OnlineCount; } // 加入以上 } ``` #### (2) 於前端 SRPage.cshtml 檔中的擴增以下程式 ```javascript $.connection.hub.start().done(function () { // 省略程式碼 // 加入以下 // 呼叫後端的 getOnlineCount 方法,若成功,會印出目前線上人數 // Note:前端呼叫後端方法要使用 小駝峰(第一個單字小寫) chat.server.getOnlineCount().done(function (result) { console.log('目前線上人數: ' + result); }).fail(function (error) { console.error('呼叫方法失敗,Error為: ' + error); }); // 加入以上 }); }); ``` #### (3) 進行連線人數測試 運行專案,並使用多個無痕視窗開啟 `https://localhost:你自己的port號/Home/SRPage` 路由,打開瀏覽器開發者工具並選擇 Console,若不同的無痕視窗會顯示 `目前線上人數:X` 即代表前端頁面已經可以取得 Hub 線上總人數;可以再把其中的一個瀏覽器關閉(模擬使用者離開網站),可以發現 `目前線上人數:X-1` ![image](https://hackmd.io/_uploads/ryGpa5TX0.png) --- ### 4. SignalR User Group 將使用者加入特定的 **群組(Group)** ,可以達到建立 `1對1` 或 `多對多` 的訊息房間; 在 **SignalR** 中,可以使用 `Groups.Add(Context.ConnectionId,房間名稱)`,其中 `房間名稱` 為可以自定義的字串型別,本文將使用 `Table("ChatRooms")` 中的 `Id` 作為房間名稱 #### (1) 於後端 SRHub.cs 檔中擴增以下程式 當前端呼叫 `JoinChatRoom()`,就會把使用者加入特定的 `chatroomId` 訊息房間 ```csharp! public async Task<string> JoinChatRoom(int chatroomId) { // 將當前使用者加入特定的聊天室(Groups),使用 Context.ConnectionId 和 自行指定的 chatroomId 參數 await Groups.Add(Context.ConnectionId, chatroomId.ToString()); return $"Return by Backend:Client join into chatroom - {chatroomId} successfully"; } ``` #### (2) 於前端 SRPage.cshtml 檔中的擴增以下程式 ```javascript $.connection.hub.start().done(function () { // 省略程式碼 // 加入以下 // 呼叫後端的 joinChatRoom 方法,並傳入 100 作為 chatroomId, // 若成功,會印出後端 return 傳出的訊息 // Note:前端呼叫後端方法要使用 小駝峰(第一個單字小寫) chat.server.joinChatRoom(100).done(function (result) { console.log('已經被成功加入聊天室: ' + result); }).fail(function (error) { console.error('呼叫方法失敗,Error為: ' + error); }); // 加入以上 }); }); ``` #### (3) 進行加入聊天房間測試 ![image](https://hackmd.io/_uploads/rJlFVrB40.png) #### (4) 後續規劃、衍生問題與 API 規劃 - 後續規劃: 當前使用者被加入到特定的 `chatroomId` 訊息房間,接下來就要讓房間內的使用者可以**互相傳送和讀取彼此**的訊息 - 衍生問題: 1.應該要讓特定的使用者(`User.Id`), (=> 需透過 JWT 解析當前使用者 `User.Id`,並於資料庫 2.才能被加入特定的 `chatroomId` 訊息房間, (=> 找到其所對應的`chatroomId` 訊息房間 3.以及,如何才能傳送訊息給這個房間內的人, (=> 透過 API 捕捉相關訊息,存入資料庫 4.並於前端畫面進行顯示 (=> 再透過 前端呼叫後端 SignalR Function 5.和,如何讀取過去的聊天室訊息? (=> 前端透過 呼叫 API 即可解決) 以上的衍生問題,需要由後端 SRHub.cs 檔案中,和後端的 API 互動,才能解決上述的衍生問題, - API 規劃:共有三支 A.取得當前使用者的所有對應的聊天房間`chatroomId` B.取得當前使用者的特定聊天房間`chatroomId`內部的歷史訊息 C.當前使用者於特定聊天房間`chatroomId`傳送訊息 本文僅針對 C-API 進行說明,A-API 和 B-API 可透過簡單的 API 實作即可完成 --- ### 5. SignalR 後端 Method 呼叫 Web API (C-API) #### (1) 架構一支 建立和取得 房間號碼的 API ```csharp public class ChatController : ApiController { private DbModel db = new DbModel(); [HttpPost] [Route("api/chat/SendMessageToChatroom")] // input 為 View Model,型別於下方 class public IHttpActionResult SendMessageToChatroom(ChatSendMessageCheck input) { // Note:有進行簡化,建議先確認 chatroomId 是否存在 // 準備於 Records 資料表新增訊息留言資料 var newRoomSet = new Records { UserIdSender = input.userIdSender, Message = input.message, ChatRoomId = input.chatroomId, }; db.Records.Add(newRoomSet); db.SaveChanges(); // 撈取聊天室資料 var messageReturn = db.Records.Where(x => x.ChatRoomId == input.chatroomId).AsEnumerable(); var result = new { statusCode = 200, status = "success", message = "訊息傳送成功", chatcontent = messageReturn?.Select(x => new { senderId = x.UserIdSender, message = x.Message, }).ToList() }; return Content(HttpStatusCode.OK, result); } } public class ChatSendMessageCheck // API 的 View Model { [Display(Name = "chatroom")] public int chatroomId { get; set; } [Display(Name = "使用者Id=>發訊者")] public int userIdSender { get; set; } [Required] [MaxLength(500)] public string message { get; set; } } ``` #### (2) 於後端 SRHub.cs 檔中擴增以下程式: SendMessageToApi() 於 SRHub 中建構一個 SendMessageToApi 的方法,其會回傳一個物件 ```csharp public class SRHub : Hub { // 建立變數,稍後會使用此變數去發送 Post 給自己的後端 API private readonly HttpClient _httpClient; // 建立建構子 public SRHub() { _httpClient = new HttpClient(); _httpClient.BaseAddress = new Uri(@"http://4.224.41.94"); // 修改為後端domain路由 } // 省略其他程式碼 // 建構一個 SendMessageToApi 方法,傳入 (C-API) 所需要的相關參數 public async Task<object> SendMessageToApi(int chatroomId, int userIdSender, string message) { var postData = new { chatroomId, userIdSender, message, }; // 將資料序列化為 JSON 字串 var jsonContent = JsonConvert.SerializeObject(postData); // 發送 POST 請求到 Web API(C-API) var response = await _httpClient.PostAsync("api/chat/SendMessageToChatroom", new StringContent(jsonContent, Encoding.UTF8, "application/json")); // 檢查是否成功 if (response.IsSuccessStatusCode) { // 讀取成功的回應資料 string responseBody = await response.Content.ReadAsStringAsync(); // 將回應資料作為 JSON 物件格式返回給前端 var data = JsonConvert.DeserializeObject(responseBody); return data; } else { // 讀取失敗的回應資料 string responseBody = await response.Content.ReadAsStringAsync(); // 將回應資料作為 JSON 物件格式返回給前端 var data = JsonConvert.DeserializeObject(responseBody); return data; } } } ``` #### (3) 於後端 SRHub.cs 檔中擴增以下程式:SendMessageToRoom() 於 SRHub 中建構一個 SendMessageToRoom 的方法,其會呼叫 (2)中的 SendMessageToApi 方法 屆時,前端可以呼叫 SendMessageToRoom 方法,就會驅動整個 Function 的連動 ```csharp public class SRHub : Hub { // 省略其他程式碼 // 建構一個 SendMessageToRoom 方法,其內部會呼叫 SendMessageToApi 方法 public async Task<string> SendMessageToRoom(int chatroomId, int userIdSender, string message) { // 透過 Clients.Group(chatroomId.ToString()) 指定特定聊天房間 // 並呼叫前端所寫定的 receiveMessage()方法,將 SendMessageToApi 方法回傳物件傳送給他 var SendMessageToGroup = await Clients.Group(chatroomId.ToString()).receiveMessage(await SendMessageToApi(chatroomId, userIdSender, message)); } } ``` #### (4) 於前端 SRPage.cshtml 檔中的擴增以下程式 ```javascript <script> $(function () { var chat = $.connection.sRHub; // 建立前端方法 receiveMessage(),後端會把訊息傳送給這個方法,讓前端捕獲 chat.client.receiveMessage = function (returnData) { // 以下需要前端修改,捕獲 returnData ,並於前端渲染 console.log(returnData); }; $.connection.hub.start().done(function () { // ... 省略相關程式碼 // 建立一個呼叫後端的方法,傳入相關參數 // 相關參數需要透過 JWT(User.Id)、API(chatroomId)和前端捕獲(message內容) // 以下假設 chatroomId=100 ; User.Id=1 ; message="我是David" chat.server.SendMessageToRoom(100,1,'我是David').done(function () { // 呼叫完成後端方法後,甚麼事情都不做 }).fail(function (error) { console.error('呼叫方法失敗,Error為: ' + error); }); }); }); </script> ``` ---