[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 的 Azure 資料表儲存體繫結

Azure Table計價

Introduction to Table storage in Azure

Azure Storage documentation

Quickstart: Create an Azure Storage table in the Azure portal

留言

這個網誌中的熱門文章

何謂淨重(Net Weight)、皮重(Tare Weight)與毛重(Gross Weight)

Architecture(架構) 和 Framework(框架) 有何不同?_軟體設計前的事前規劃的藍圖概念

(2021年度)駕訓學科筆試準備題庫歸納分析_法規是非題