.NET 6 Web API 使用SignalR實作WebSocket技術實現即時通訊-2
前言
🔗上一篇已經介紹了全推播的使用方法,只要有連線上的使用者都能接收到訊息,接下來要模擬對特定使用者進行推播的功能。
原理
🔗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,過程中可以開啟中段點,查看執行的過程。
以上為使用 Web API 簡單的實踐即時通訊功能方式。過程包含綁定使用者、連線ID 並儲存,且提供對特定使用者即時通訊的API。雖然前端畫面有點簡陋,但主要是為了解其中原理,我想應該是任何功能都可以實現了吧。
專案已上傳至 Github。
Alvin
軟體工程師,喜歡金融知識、健康觀念、心理哲學、自助旅遊與系統設計。
相關文章
留言區 (0)
尚無留言