.NET 6 Web API 使用SignalR實作WebSocket技術實現即時通訊-2


June 16, 2023 程式語言

.NET 6 Web API 使用SignalR實作WebSocket技術實現即時通訊-2


使用.NET 6 Web API 建立 WebSocket 通訊,本篇紀錄模擬將使用者對應至連線ID並儲存與發送訊息功能的測試過程。

前言

🔗

上一篇已經介紹了全推播的使用方法,只要有連線上的使用者都能接收到訊息,接下來要模擬對特定使用者進行推播的功能。

原理

🔗

WebSocket 通訊,只要使用者連線就會自動生成一個連線ID,若程式邏輯是使用者執行某項操作時,就對其推播訊息,是非常容易的,只要針對當下的連線ID發送推播即可。但實際上邏輯可能更複雜,通常會是使用者A執行某項操作,或是想直接對另一使用者B發送推播,這時就必須針對特定的使用者ID與連線ID進行綁定,這樣在進行推播時,才能藉由已知的使用者ID取得連線ID並推播。

將使用者綁定至連線ID

🔗

通常在使用者連線時即需同時儲存使用者ID與連線ID,在實際要推播時才能查詢使用者對應的連線ID,方法主要分為儲存至記憶體與資料庫,儲存在記憶體的優點是效能較好,但若伺服器重啟相關資訊則會遺失;儲存在資料庫則可以永久儲存資料,缺點則是效能會較差一點。至於要選擇使用哪一種則需要考慮實際的應用情況。

詳細資訊可以查閱教程: Mapping SignalR Users to Connections

實作

🔗

預設使用者連線時需輸入使用者名稱,在按下登入時,會將使用者名稱傳給後端,後端立即將使用者名稱綁定此連線ID。後續若要傳遞給特定使用者,只要從儲存的數據中查詢使用者對應的連線ID,再傳送資訊即可。

過程大致如下:


輸入使用者名稱 ➜ 綁定使用者名稱與連線ID並儲存

使用者B傳遞訊息給A ➜ 搜尋已儲存的使用者名稱A ➜ 取得連線ID並傳送

使用者離線 ➜ 刪除已儲存的連線ID


在 Hub/IMessageHub.cs 新增兩個介面:

C#
namespace SignalR_Example.Hub
{
    public interface IMessageHub
    {
        Task sendToAllConnections(List<string> message);
        Task JsonDataTransfer(dynamic message);
        Task StringDataTransfer(string message);
    }
}

JsonDataTransfer 用來傳遞 JSON 物件

StringDataTransfer 用來傳遞字串


在 Hub/MessageHub.cs 新增幾個功能如下:

C#
public static Dictionary<string, string> userInfoDict = new Dictionary<string, string>();

因為只是模擬,資料的儲存僅使用記憶體儲存。

userInfoDict 的 key 為使用者名稱,value 為 連線ID。

C#
public async Task LoadUserInfo(dynamic message)
{
    dynamic dynParam = JsonConvert.DeserializeObject(Convert.ToString(message));
    string userID = dynParam.userId;
    var ID = Context.ConnectionId;
    userInfoDict[userID] = ID;
    await Clients.Client(ID).StringDataTransfer("Login successfully.");
}

模擬前端在使用者登入時,傳送Json物件給後端,儲存在userInfoDict後,會呼叫StringDataTransfer傳遞登入成功給前端。

Context.ConnectionId 是 SignalR 套件中的一個屬性,用於取得目前連線的 Connection ID。

C#
public async Task SendToConnection(string userID, string message)
{
    if (userInfoDict.ContainsKey(userID))
    {
        await Clients.Client(userInfoDict[userID]).StringDataTransfer(message);
    }
}

模擬前端使用者傳訊息給另一使用者,藉由 userID,查詢userInfoDict是否有連線ID,若有則藉由StringDataTransfer傳遞字串。

C#
/// <summary>
/// Automatically obtaining the connection ID
/// </summary>
/// <returns></returns>
public override Task OnConnectedAsync()
{
    //string userId = Context.User.Identity.Name;
    string connectionId = Context.ConnectionId;
    return base.OnConnectedAsync();
}

此為SignalR內建的方法,若使用者連線,即可進入此方法。因模擬為使用者需先輸入userID,所以並沒有在這部分測試。

C#
/// <summary>
/// Disconnecting and automatically removing the connection ID
/// </summary>
/// <param name="exception"></param>
/// <returns></returns>
public override Task OnDisconnectedAsync(Exception exception)
{
    string ID = Context.ConnectionId;
    string userID = string.Empty;
    if (userInfoDict.ContainsValue(ID))
    {
        string key = userInfoDict.FirstOrDefault(x => x.Value == ID).Key;
        userInfoDict.Remove(key);
    }
    return base.OnDisconnectedAsync(exception);
}

此為SignalR內建的方法,若使用者離線,即可進入此方法,模擬使用者離線,即移除儲存在userInfoDict的連線資料。

以上兩種內建方法,可以自行下中斷點測試。

Hub/MessageHub.cs 完整程式碼:

C#
namespace SignalR_Example.Hub
{
    public class MessageHub: Hub<IMessageHub>
    {
        public async Task sendToAllConnections(List<string> message)
        {
            await Clients.All.sendToAllConnections(message);
        }

        public static Dictionary<string, string> userInfoDict = new Dictionary<string, string>();
        public async Task LoadUserInfo(dynamic message)
        {
            dynamic dynParam = JsonConvert.DeserializeObject(Convert.ToString(message));
            string userID = dynParam.userId;
            var ID = Context.ConnectionId;
            userInfoDict[userID] = ID;
            await Clients.Client(ID).StringDataTransfer("Login successfully.");
        }
        public async Task SendToConnection(string userID, string message)
        {
            if (userInfoDict.ContainsKey(userID))
            {
                await Clients.Client(userInfoDict[userID]).StringDataTransfer(message);
            }
        }

        /// <summary>
        /// Automatically obtaining the connection ID
        /// </summary>
        /// <returns></returns>
        public override Task OnConnectedAsync()
        {
            //string userId = Context.User.Identity.Name;
            string connectionId = Context.ConnectionId;
            return base.OnConnectedAsync();
        }

        /// <summary>
        /// Disconnecting and automatically removing the connection ID
        /// </summary>
        /// <param name="exception"></param>
        /// <returns></returns>
        public override Task OnDisconnectedAsync(Exception exception)
        {
            string ID = Context.ConnectionId;
            string userID = string.Empty;
            if (userInfoDict.ContainsValue(ID))
            {
                string key = userInfoDict.FirstOrDefault(x => x.Value == ID).Key;
                userInfoDict.Remove(key);
            }
            return base.OnDisconnectedAsync(exception);
        }
    }
}

在Controllers/MsgController.cs 新增一個對特定使用者傳資訊的API,模擬藉由API啟動即時通訊。

C#
[HttpPost]
[Route("toUser")]
public string toUser([FromBody] JsonElement jobj)
{
    var userID = jobj.GetProperty("userID").GetString();
    var Msg = jobj.GetProperty("msg").GetString();
    if (MessageHub.userInfoDict.ContainsKey(userID))
    {
        messageHub.Clients.Client(MessageHub.userInfoDict[userID]).StringDataTransfer(Msg);
        return "Msg sent successfully to user!";
    }
    else return "Msg sent failed to user!";

}

前端程式碼

🔗

接下來,就是前端的畫面顯示,因為主要是測試連線的功能,前端所收到的訊息為了方便都會顯示在console上。

HTML
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>SignalR TEST </title>
    <script src="https://cdn.jsdelivr.net/npm/@microsoft/[email protected]/dist/browser/signalr.min.js"></script>
    <script>
        // 建立 SignalR Hub 連線
        const hubConnection = new signalR.HubConnectionBuilder()
            .withUrl("https://localhost:7013/messageHub/")
            .build();

        hubConnection.start()
            .then(() => {
                console.log("Connection started");
            });
        


        // 使用者點擊Login按鈕時的處理函式
        function onLoginClick() {
            // 取得使用者輸入的使用者ID
            const userId = document.getElementById("userIdInput").value;

            // 將使用者ID包成 JSON 格式
            const jsonData = {
                "userId": userId
            };
            
            // 使用 SignalR Hub 的 LoadUserInfo 方法,將 JSON 資料傳送至後端
            hubConnection.invoke("LoadUserInfo", jsonData)
                .then(() => {
                    console.log("Data sent successfully!");
                })
                .catch((error) => {
                    console.error(error);
                });
        }


        // 使用者點擊Send按鈕時的處理函式
        function onSendClick() {
            // 取得使用者輸入的使用者ID
            const userId = document.getElementById("msgUserIdInput").value;
            // 取得使用者輸入的訊息
            const msg = document.getElementById("msgInput").value;

            // 使用 SignalR Hub 的 SendToConnection 方法,將資料傳送至另一使用者
            hubConnection.invoke("SendToConnection", userId, msg)
                .then(() => {
                    console.log("Msg sent successfully!");
                })
                .catch((error) => {
                    console.error(error);
                });
        }

        // 註冊 MessageHub 的事件
        hubConnection.on("sendToAllConnections", function (msgs) {
            console.log("To All Connections:", msgs);
        });

        hubConnection.on("StringDataTransfer", (response) => {
            console.log("Received Msg:", response);
        });
        

    </script>
</head>
<body>
    SignalR TEST
    <hr>
    <label for="userIdInput">Please enter user ID:</label>
    <input type="text" id="userIdInput">
    <button onclick="onLoginClick()">Login</button>
    <hr>
    <label for="msgUserIdInput">User ID:</label>
    <input type="text" id="msgUserIdInput">
    <label for="msgInput">msg:</label>
    <input type="text" id="msgInput">
    <button onclick="onSendClick()">Send</button>

</body>
</html>

測試結果

🔗

測試時,可以開啟多個網頁,在登入時使用不同的userID,過程中可以開啟中段點,查看執行的過程。

使用者 B 登入,並傳訊息給使用者 A
使用者 A 的介面,確認收到訊息

以上為使用 Web API 簡單的實踐即時通訊功能方式。過程包含綁定使用者、連線ID 並儲存,且提供對特定使用者即時通訊的API。雖然前端畫面有點簡陋,但主要是為了解其中原理,我想應該是任何功能都可以實現了吧。

專案已上傳至 Github

WebCsharpJavaScript



Avatar

Alvin

軟體工程師,喜歡金融知識、健康觀念、心理哲學、自助旅遊與系統設計。

相關文章






留言區 (0)



  or   

尚無留言