[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 中建立第一個函式





留言

這個網誌中的熱門文章

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

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

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