[Azure雲端服務及應用開發]_創建Azure Storage帳戶使用Table存取(NoSQL)_不同地點會咖秀(台語)_添加至特定群組機制_part4
圖片摘自:站在巨人的肩膀上看世界
牛頓:「如果我能看得更遠, 那是因為站在巨人的肩膀上。」
波頓:「侏儒站在巨人的肩膀上,便能比巨人看得更遠。」
目前雖然已經達成能透過SignalR收發機制
但是仍缺少一些比方群組設定,有各自隱私的交談功能。
因為目前還是一個廣播給所有都連上SignalR Hub的User
都會看到的粗略設計
在此對ChatMessage擴充一個屬性叫作GroupName
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 | 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 string GroupName { get; set; } public ChatMessage() { } public ChatMessage(string sender) { Id = Guid.NewGuid().ToString(); TypeInfo = GetType(); Sender = sender; TimeStamp = DateTime.Now; } } } |
依樣畫葫蘆這次我們要擴充新的加入群組的Function
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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"; public static string AddToGroupEndPoint = $"{MainEndPoint}/api/AddToGroup"; } } |
在Chat.Functions專案之下
創建一個新的Azure Function項目,選HTTP Trigger 並命名為AddToGroup。
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 | 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; namespace KYChat.Functions { public static class AddToGroup { [FunctionName("AddToGroup")] public static async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, [SignalR(HubName = "chat")] IAsyncCollector<SignalRGroupAction> signalRGroupActions, ILogger log) { } } } |
Step1.回傳型態改為Task 用async修飾
回傳型態改為Task 採用異步修飾
擴充傳入參數
型態為IAsyncCollector 泛型指向SignalRGroupAction
前綴用SignalR修飾指向自己設的SignalR Hub名稱
在此由於之前的ChatMessage 資料型態不太足夠
因此我們需要再額外擴充新的自訂型態
於Chat.Message專案下新增一個新的Message Type
名字為 UserConnectionMessage 繼承自之前寫的ChatMessage
for識別
某User是否已進到群組了還是出群組了呢....
有該群組特定的Token (不好意思進來之前請先講通關密語的概念喔~~)
UserConnectionMessage程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | using System; using System.Collections.Generic; using System.Text; namespace KYChat.Messages { public class UserConnectedMessage : ChatMessage { public string Token { get; set; } public bool IsEntered { get; set; } public UserConnectedMessage(){} public UserConnectedMessage(string userName,string groupName) : base(userName) { GroupName = groupName; } } } |
Step2.由於我們需要存用戶群組相關資料因此需要使用到
Azure Storage的 帳戶當中的Table來做存取
Azure Table本身採用一種NoSQL存取模式(Key-Attribute Value)
跟筆者之前研究的Redis有點類似概念
(更多Redis介紹可以參考如下連結共寫了四篇介紹)
屬於Schema Free你不需要先花時間去規劃Table Schema
一種 NoSQL PaaS服務
一個Table就好比如很多Entity 的集合體
一個Entity就是很多屬性的集合體(Class Model物件實體)
每一筆Entity最大可高達1MB
如果是更高階的使用需求(或有更多預算)
可採用Cosmos DB 每一筆Entity可到2MB
選擇左側Create a resource
移到最下方選擇Storage Account
這裡選擇
一般用途 v2 帳戶
然後Replication 選LRS ---> 因為咖秀(台語) 備份機制會都只能在同一Data Center
Hot Access
地區這裡選美國東部、當然日本東部也都可
這裡筆者也都用定價計算查過這幾個點的Data Center都 咖秀(台語)
(注意!!! 地區不同收費是有差異的)
價格估算資料(微軟Azure官網):
12 個月免費項目當中看起來是不包含Storage Account當中的Table 項目呀
也不含儲存體帳戶 QQ
這裡操作方式都很一致性、簡單
創建好後我們到Resources
去建立一個Table 命名為Users
Step3.將AddToGroup的Azure Function程式完善
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 | 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 AddToGroup { [FunctionName("AddToGroup")] public static async Task Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, [SignalR(HubName = "chat")] IAsyncCollector<SignalRGroupAction> signalRGroupActions, ILogger log) { var message = new JsonSerializer() .Deserialize<UserConnectedMessage>(new JsonTextReader(new StreamReader(req.Body))); await signalRGroupActions.AddAsync(new SignalRGroupAction { ConnectionId = message.Token, UserId = message.Sender, GroupName = message.GroupName, Action = GroupAction.Add }) ; } } } |
Message的回傳資料處理和上一篇Part3依樣畫葫蘆
先透過StreamReader將Body最原始Stream處理後轉成StreamReader
再接棒給JsonTextReader處理返回JsonReader後
才最終交給JsonSerializer去Deserialize回Object
再把該Object轉字串後
再做DeserializeObject動作轉為UserConnectedMessage的型態
進行相關屬性的設置
最後透過IAsyncCollector<SignalRGroupAction> (代表群組物件的集合)
調用AddAsync
加入的資料型態為 SignalRGroupAction
將使用者新增至群組
Step4.接著我們要來下載安裝Azure Storage套件
對Chat.Functions專案右鍵開啟Nuget套件管理
搜尋
microsoft.azure.webjobs.extensions.storage
然後下載安裝(這裡用之前的3.0.1版本,看到Preview版本可能會有不太穩定成熟的風險。)
回到AddToGroup
添加回傳修飾字串
TableAttributes
在此設置好我們想access的Table名稱跟連線字串
連線字串首先到azure portal面板
Home -> All resources (秀出目前所有創建的資源) -> 點選方才建立Storage Account
->選擇 Access Keys
copy到我們的Chat.Function專案下的local.settings.json
新增額外連線用的字串
Step5.到專案下建立Models folder
新增UserEntity Class
繼承TableEntity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | using Microsoft.WindowsAzure.Storage.Table; using System; using System.Collections.Generic; using System.Text; namespace KYChat.Functions.Models { public class UserEntity : TableEntity { public string UserId { get; set; } public string Room { get; set; } public string Color { get; set; } public string Avatar { get; set; } } } |
主要設置幾個基本屬性
用戶識別ID、進入的聊天群組名稱、對話字顏色、大頭照存放檔案名
AddToGroup最終程式
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 | 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; using KYChat.Functions.Models; namespace KYChat.Functions { public static class AddToGroup { [FunctionName("AddToGroup")] [return: Table("Users",Connection = "StorageConnectionString")] public static async Task<UserEntity> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req, [SignalR(HubName = "chat")] IAsyncCollector<SignalRGroupAction> signalRGroupActions, ILogger log) { var message = new JsonSerializer() .Deserialize<UserConnectedMessage>(new JsonTextReader(new StreamReader(req.Body))); await signalRGroupActions.AddAsync(new SignalRGroupAction { ConnectionId = message.Token, UserId = message.Sender, GroupName = message.GroupName, Action = GroupAction.Add }) ; Random r = new Random(); var red = r.Next(0, 255).ToString("X2"); var green = r.Next(0, 255).ToString("X2"); var blue = r.Next(0, 255).ToString("X2"); var item = new UserEntity { UserId = message.Sender, Room = message.GroupName, Color = $"#{red}{green}{blue}", Avatar = $"image_{r.Next(1,51)}.png", PartitionKey = message.GroupName, RowKey = message.Sender }; return item; } } } |
之後是針對Chat.Core專案的擴充微調
Step1.到Config多增加EndPoint for 加入群組
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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"; public static string AddToGroupEndPoint = $"{MainEndPoint}/api/AddToGroup"; } } |
Step2.到IChatService補擴充規格 JoinChannelAsync
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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); Task JoinChannelAsync(UserConnectedMessage message); } } |
Step3.ChatService補實作具體加入群組行動
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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | 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); } public async Task JoinChannelAsync(UserConnectedMessage message) { if (!IsConnected) return; message.Token = ConnectionToken; message.IsEntered = true; var strJson = JsonConvert.SerializeObject(message); var content = new StringContent(strJson, Encoding.UTF8, "application/json"); await httpClient.PostAsync(Config.AddToGroupEndPoint, content); await SendMessageAsync(message); } } } |
目前我們已將添加至聊天室群組功能函數完成
但還要能夠取得目前有的群組(有點像聊天包廂列表)
到Chat.Core專案的Models目錄再新增
Room的實體Class
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | using System; using System.Collections.Generic; using System.Text; namespace KYChat.Core.Models { public class Room { public string Name { get; set; } public string Image { get; set; } public int UsersNumber { get; set; } } } |
一項聊天群組可能需要
一特定群組名稱
一個特定的照片(參考Line的設計)
其他可能用戶數等等
到IChatService補擴充Spec 再定義 GetRooms()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | using KYChat.Core.Models; 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); Task JoinChannelAsync(UserConnectedMessage message); Task<List<Room>> GetRooms(); } } |
到ChatService程式區塊補實作
這裡暫時先寫死自定義幾個群組
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 | public async Task<List<Room>> GetRooms() { var rooms = new List<Room> { new Room { Name = "公司", Image = "company.png" }, new Room { Name = "家庭", Image = "family.png" }, new Room { Name = "朋友", Image = "friend.png" }, new Room { Name = "部門群組", Image = "department.png" } }; return rooms; } |
最終我們回到Chat.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 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 | 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 string room; 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 !!"); await JoinRoom(); 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 async Task JoinRoom() { var rooms = await myService.GetRooms(); Console.WriteLine("===聊天群組列表==="); foreach (var room in rooms) { Console.WriteLine(room.Name); } Console.WriteLine("請輸入想進入的聊天群組:"); room = Console.ReadLine(); var message = new UserConnectedMessage(userName, room); await myService.JoinChannelAsync(message); } 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); } else if (e.Message.TypeInfo.Name == nameof(UserConnectedMessage)) { var userConnected = e.Message as UserConnectedMessage; string message = string.Empty; if (userConnected.IsEntered) { message = $"{userConnected.Sender} 已進入群組"; } else { message = $"{userConnected.Sender} 已離開群組"; } Console.WriteLine(message); } } } } |
主要於已連線後多一個副程式JoinRoom()
進行聊天群組列表的打印
對OnReceiveMessage 委派指向的副程式MyService_OnReceiveMessage
當中再穿插一段判斷是否進入群組的邏輯
當傳入UserConnectedMessage資料型態的時候
之後回到Chat.Functions專案下去修改
傳入的型別部分擴充判斷
因為之前是用SimpleTextMessage
並沒有我們新增的Bool Flag IsEntered屬性
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 | 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("Messages")] 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());// before var message = JsonConvert.DeserializeObject<ChatMessage>(serializedObject.ToString()); if (message.TypeInfo.Name == nameof(SimpleTextMessage)) { message = JsonConvert.DeserializeObject<SimpleTextMessage>(serializedObject.ToString()); } else if (message.TypeInfo.Name == nameof(UserConnectedMessage)) { message = JsonConvert.DeserializeObject<UserConnectedMessage>(serializedObject.ToString()); } await signalRMessages.AddAsync(new SignalRMessage { Target = "ReceiveMessage", Arguments = new[] { message } }); } } } |
最終就能達成Console版本的聊天群組控管程式雛形了
Ref:
Azure Functions 的 SignalR Service 輸出系結
Azure Functions 的 Azure 資料表儲存體繫結
Azure Table計價
Introduction to Table storage in Azure
Azure Storage documentation
Quickstart: Create an Azure Storage table in the Azure portal
留言
張貼留言