[Azure雲端服務及應用開發]_創建Message Model並於Hub加入收發機制_part3
https://laptopmemo.com/facebook-messenger-for-android-gets-the-material-design-treatment-finally-c3b7d14a951b
依樣畫葫蘆
在方案下創建一個.Net Standard的Class Library專案命名為Message
代表聊天室訊息資料的結構
新增一個Message的Model Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | using System; using System.Collections.Generic; using System.Text; namespace KYChat.Messages { public class ChatMessage { public string Id { get; set; } public Type TypeInfo { get; set; } public DateTime TimeStamp { get; set; } public string Sender { get; set; } public ChatMessage() { } public ChatMessage(string sender) { Id = Guid.NewGuid().ToString(); TypeInfo = GetType(); Sender = sender; TimeStamp = DateTime.Now; } } } |
一組訊息最基本需要的配備
(1)一個可唯一識別的ID (在此使用GUID來產生唯一識別)
備註:GUID (Globally Unique Identifier)
(2)一個時間戳
(3)發送端為誰?
(4)訊息型態(純文字、圖片、檔案...等等)
->這裡用Reflection當中Type來作為欄位
Ref:
在繼承此基礎之下
新建一個Simple Text Message 的Model Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using System; using System.Collections.Generic; using System.Text; namespace KYChat.Messages { public class SimpleTextMessage : ChatMessage { public string Text { get; set; } public SimpleTextMessage() { } public SimpleTextMessage(string sender) : base(sender) { } } } |
在Chat.Functions專案之下
創建一個新的Azure Function項目,選HTTP Trigger 並命名為Messages。
Authorization Level一樣是選匿名的Anonymous
預設產生的程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace KYChat.Functions { public static class Message { [FunctionName("Message")] public static async Task<IActionResult> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, ILogger log) { log.LogInformation("C# HTTP trigger function processed a request."); string name = req.Query["name"]; string requestBody = await new StreamReader(req.Body).ReadToEndAsync(); dynamic data = JsonConvert.DeserializeObject(requestBody); name = name ?? data?.name; string responseMessage = string.IsNullOrEmpty(name) ? "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response." : $"Hello, {name}. This HTTP triggered function executed successfully."; return new OkObjectResult(responseMessage); } } } |
刪掉函數中程式內容並修改函數的部分結構
經改寫後
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | using System; using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.WebJobs; using Microsoft.Azure.WebJobs.Extensions.Http; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Microsoft.Azure.WebJobs.Extensions.SignalRService; using KYChat.Messages; namespace KYChat.Functions { public static class Messages { [FunctionName("Message")] public static async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req, [SignalR(HubName = "chat")] IAsyncCollector<SignalRMessage> signalRMessages, ILogger log) { var serializedObject = new JsonSerializer().Deserialize(new JsonTextReader(new StreamReader(req.Body))); var message = JsonConvert.DeserializeObject<SimpleTextMessage>(serializedObject.ToString()); await signalRMessages.AddAsync(new SignalRMessage { Target = "ReceiveMessage", Arguments = new[] { message } }); } } } |
Step1.我們只保留POST傳送,回傳型態改為Task 採用異步修飾
擴充傳入參數
型態為IAsyncCollector 泛型指向SignalRMessage
前綴用SignalR修飾指向自己設的SignalR Hub名稱
Step2.
首先把HTTPRequest 送post過來的body資料藉由JsonSerializer去Deserialize回Object
先透過StreamReader將Body最原始Stream處理後轉成StreamReader
再接棒給JsonTextReader處理返回JsonReader後
才最終交給JsonSerializer去Deserialize回Object
Step3.
把該Object轉字串後再做DeserializeObject動作轉為SimpleTextMessage的型態
Step4.由IAsyncCollector型態的signalRMessages (不只一則訊息)
負責去接收Message
(泛型T在此已設定pass過來資料型態為SignalRMessage)
回到Chat.Core當中的Interface IChatService
補制訂一個Send方法的Spec
記得加入對應namespace才參考的到ChatMessage
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | using KYChat.Messages; using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; namespace KYChat.Core.Services { public interface IChatService { bool IsConnected { get; } string ConnectionToken { get; set; } Task InitAsync(string userId); Task DisconnectionAsync(); Task SendMessageAsync(ChatMessage message); } } |
於Config中組態也相應擴充額外一個EndPoint
1 2 3 4 5 6 7 8 9 10 11 12 13 | using System; using System.Collections.Generic; using System.Text; namespace KYChat.Core { public static class Config { public static string MainEndPoint = "http://localhost:7071"; public static string NegotiateEndPoint = $"{MainEndPoint}/api/negotiate"; public static string MessageEndPoint = $"{MainEndPoint}/api/Messages"; } } |
由於Interface Spec有擴充新的要實作的方法
因此回到ChatSerivce補具體實作SendMessageAsync
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | using KYChat.Core.Models; using KYChat.Messages; using Microsoft.AspNetCore.SignalR.Client; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; namespace KYChat.Core.Services { public class ChatService : IChatService { public bool IsConnected { get; set; } public string ConnectionToken { get; set; } private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); private HttpClient httpClient; HubConnection hub; public async Task InitAsync(string userId) { await semaphoreSlim.WaitAsync(); if (httpClient == null) { httpClient = new HttpClient(); } var result = await httpClient.GetStringAsync($"{Config.NegotiateEndPoint}/{userId}"); var info = JsonConvert.DeserializeObject<ConnectionInfo>(result); var connectionBuilder = new HubConnectionBuilder(); connectionBuilder.WithUrl(info.Url, (obj) => { obj.AccessTokenProvider = () => Task.Run(() => info.AccessToken); }); hub = connectionBuilder.Build(); await hub.StartAsync(); ConnectionToken = hub.ConnectionId; IsConnected = true; semaphoreSlim.Release(); } public async Task DisconnectionAsync() { if (!IsConnected) return; try { await hub.DisposeAsync(); } catch (Exception ex) { Debug.WriteLine(ex); } IsConnected = false; } public async Task SendMessageAsync(ChatMessage message) { if (!IsConnected) { throw new NotImplementedException("Not connected!!"); } var strJson = JsonConvert.SerializeObject(message); var content = new StringContent(strJson, Encoding.UTF8, "application/json"); await httpClient.PostAsync(Config.MessageEndPoint, content); } } } |
在此我們完成發送功能
但需要有個handler負責hub上去接收message
調用HubConnection 信號集線器
來掛上Receive Message
methodName 比照 Message中signalRMessages的Target (hub Method Name)
其他相關參數參考如下
Ref:
補上後完整的
ChatService Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | using KYChat.Core.Models; using KYChat.Messages; using Microsoft.AspNetCore.SignalR.Client; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; namespace KYChat.Core.Services { public class ChatService : IChatService { public bool IsConnected { get; set; } public string ConnectionToken { get; set; } private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); private HttpClient httpClient; HubConnection hub; public async Task InitAsync(string userId) { await semaphoreSlim.WaitAsync(); if (httpClient == null) { httpClient = new HttpClient(); } var result = await httpClient.GetStringAsync($"{Config.NegotiateEndPoint}/{userId}"); var info = JsonConvert.DeserializeObject<ConnectionInfo>(result); var connectionBuilder = new HubConnectionBuilder(); connectionBuilder.WithUrl(info.Url, (obj) => { obj.AccessTokenProvider = () => Task.Run(() => info.AccessToken); }); hub = connectionBuilder.Build(); await hub.StartAsync(); ConnectionToken = hub.ConnectionId; IsConnected = true; hub.On<object>("ReceiveMessage", (message) => { var strJson = message.ToString(); var obj = JsonConvert.DeserializeObject<ChatMessage>(strJson); var msg = (ChatMessage)JsonConvert.DeserializeObject(strJson, obj.TypeInfo); }); semaphoreSlim.Release(); } public async Task DisconnectionAsync() { if (!IsConnected) return; try { await hub.DisposeAsync(); } catch (Exception ex) { Debug.WriteLine(ex); } IsConnected = false; } public async Task SendMessageAsync(ChatMessage message) { if (!IsConnected) { throw new NotImplementedException("Not connected!!"); } var strJson = JsonConvert.SerializeObject(message); var content = new StringContent(strJson, Encoding.UTF8, "application/json"); await httpClient.PostAsync(Config.MessageEndPoint, content); } } } |
最終我們要來設計事件
於Chat.Core專案新增目錄 EventHandlers
新增自定義的Class
名稱MessageEventArgs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | using KYChat.Messages; using System; using System.Collections.Generic; using System.Text; namespace KYChat.Core.EventHandlers { public class MessageEventArgs { public ChatMessage Message { get; set; } public MessageEventArgs(ChatMessage message) { Message = message; } } } |
隨後將ChatService也添加相應的EventHandler
Hub 掛載部分的Lambda運算式中相應去Invoke該EventHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | using KYChat.Core.EventHandlers; using KYChat.Core.Models; using KYChat.Messages; using Microsoft.AspNetCore.SignalR.Client; using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; namespace KYChat.Core.Services { public class ChatService : IChatService { public bool IsConnected { get; set; } public string ConnectionToken { get; set; } private SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); private HttpClient httpClient; HubConnection hub; public event EventHandler<MessageEventArgs> OnReceiveMessage; public async Task InitAsync(string userId) { await semaphoreSlim.WaitAsync(); if (httpClient == null) { httpClient = new HttpClient(); } var result = await httpClient.GetStringAsync($"{Config.NegotiateEndPoint}/{userId}"); var info = JsonConvert.DeserializeObject<ConnectionInfo>(result); var connectionBuilder = new HubConnectionBuilder(); connectionBuilder.WithUrl(info.Url, (obj) => { obj.AccessTokenProvider = () => Task.Run(() => info.AccessToken); }); hub = connectionBuilder.Build(); await hub.StartAsync(); ConnectionToken = hub.ConnectionId; IsConnected = true; hub.On<object>("ReceiveMessage", (message) => { var strJson = message.ToString(); var obj = JsonConvert.DeserializeObject<ChatMessage>(strJson); var msg = (ChatMessage)JsonConvert.DeserializeObject(strJson, obj.TypeInfo); OnReceiveMessage?.Invoke(this, new MessageEventArgs(msg)); }); semaphoreSlim.Release(); } public async Task DisconnectionAsync() { if (!IsConnected) return; try { await hub.DisposeAsync(); } catch (Exception ex) { Debug.WriteLine(ex); } IsConnected = false; } public async Task SendMessageAsync(ChatMessage message) { if (!IsConnected) { throw new NotImplementedException("Not connected!!"); } var strJson = JsonConvert.SerializeObject(message); var content = new StringContent(strJson, Encoding.UTF8, "application/json"); await httpClient.PostAsync(Config.MessageEndPoint, content); } } } |
最後我們要來測試純文字訊息的收發
到Client修改程式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 | using KYChat.Core.Services; using KYChat.Messages; using System; using System.Threading.Tasks; namespace KYChat.Client { class Program { static ChatService myService; static string userName; static async Task Main(string[] args) { Console.WriteLine("User Name:"); userName = Console.ReadLine(); myService = new ChatService(); myService.OnReceiveMessage += MyService_OnReceiveMessage; await myService.InitAsync(userName); Console.WriteLine("You are connected now !!"); bool IsKeepGoing = true; do { var text = Console.ReadLine(); if (text.Trim().ToLower().Equals("exit")) { await myService.DisconnectionAsync(); IsKeepGoing = false; } else { var message = new SimpleTextMessage(userName) { Text = text }; await myService.SendMessageAsync(message); } } while (IsKeepGoing); } private static void MyService_OnReceiveMessage(object sender, Core.EventHandlers.MessageEventArgs e) { if (e.Message.Sender == userName) return; if(e.Message.TypeInfo.Name == nameof(SimpleTextMessage)) { var simpleText = e.Message as SimpleTextMessage; var message = $"{simpleText.Sender}: {simpleText.Text}"; Console.WriteLine(message); } } } } |
最終就可達成一收一發的互動了
Ref:
快速入門:使用 Visual Studio 在 Azure 中建立第一個函式
留言
張貼留言