SignalR 即時通訊
SignalR 是什麼?
想像 LINE 群組——當有人發訊息時,群組裡的每個人都會即時收到通知,不需要一直重新整理。SignalR 就是讓伺服器能主動推送訊息給瀏覽器的技術。
傳統 HTTP(輪詢):
瀏覽器:「有新訊息嗎?」 → 伺服器:「沒有」
瀏覽器:「有新訊息嗎?」 → 伺服器:「沒有」
瀏覽器:「有新訊息嗎?」 → 伺服器:「有!」
(浪費很多次查詢)
SignalR(即時推送):
瀏覽器:「我要連線」 → 伺服器:「OK,建立連線」
... 過了一段時間 ...
伺服器:「有新訊息給你!」 → 瀏覽器立刻收到
(伺服器主動推送,不需要輪詢)
Hub 類別
// Hubs/ChatHub.cs
using Microsoft.AspNetCore.SignalR;
// Hub 是 SignalR 的核心,像是聊天室的「伺服器端管理員」
public class ChatHub : Hub
{
// 當客戶端呼叫 SendMessage 時觸發
public async Task SendMessage(string user, string message)
{
// 廣播給所有連線的客戶端
await Clients.All.SendAsync(
"ReceiveMessage", // 客戶端要監聽的方法名稱
user, // 發送者
message); // 訊息內容
}
// 發送給特定使用者(私訊)
public async Task SendPrivateMessage(
string targetUser,
string message)
{
await Clients.User(targetUser).SendAsync(
"ReceivePrivateMessage", // 私訊事件
Context.User?.Identity?.Name, // 發送者名稱
message); // 訊息內容
}
// 當客戶端連線時
public override async Task OnConnectedAsync()
{
var user = Context.User?.Identity?.Name ?? "匿名"; // 取得使用者名稱
await Clients.All.SendAsync(
"UserJoined", // 通知所有人
user); // 誰加入了
await base.OnConnectedAsync(); // 呼叫基底方法
}
// 當客戶端斷線時
public override async Task OnDisconnectedAsync(Exception? exception)
{
var user = Context.User?.Identity?.Name ?? "匿名"; // 取得使用者名稱
await Clients.All.SendAsync(
"UserLeft", // 通知所有人
user); // 誰離開了
await base.OnDisconnectedAsync(exception); // 呼叫基底方法
}
}
// Program.cs - 註冊 SignalR
builder.Services.AddSignalR(); // 註冊 SignalR 服務
var app = builder.Build();
app.MapHub<ChatHub>("/chatHub"); // 設定 Hub 端點路徑
客戶端 JavaScript
<!-- 引入 SignalR 客戶端函式庫 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.0/signalr.min.js"></script>
<script>
// 建立連線
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub") // 連線到 Hub 端點
.withAutomaticReconnect() // 自動重新連線
.build();
// 監聽伺服器推送的訊息
connection.on("ReceiveMessage", (user, message) => {
// 收到訊息時在頁面上顯示
const li = document.createElement("li"); // 建立清單項目
li.textContent = `${user}: ${message}`; // 設定內容
document.getElementById("messages")
.appendChild(li); // 加到訊息列表
});
// 監聽使用者加入
connection.on("UserJoined", (user) => {
console.log(`${user} 加入了聊天室`); // 記錄到主控台
});
// 啟動連線
connection.start()
.then(() => console.log("已連線到 SignalR")) // 連線成功
.catch(err => console.error("連線失敗:", err)); // 連線失敗
// 發送訊息
function sendMessage() {
const user = document.getElementById("userInput").value; // 取得使用者名稱
const message = document.getElementById("messageInput").value; // 取得訊息
connection.invoke("SendMessage", user, message) // 呼叫 Hub 方法
.catch(err => console.error("發送失敗:", err)); // 錯誤處理
}
</script>
群組 Groups
// Hubs/ChatHub.cs - 群組功能
public class ChatHub : Hub
{
// 加入群組
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(
Context.ConnectionId, // 目前連線的 ID
roomName); // 群組名稱
await Clients.Group(roomName).SendAsync(
"RoomNotification", // 群組通知
$"{Context.User?.Identity?.Name} 加入了 {roomName}"); // 訊息
}
// 離開群組
public async Task LeaveRoom(string roomName)
{
await Groups.RemoveFromGroupAsync(
Context.ConnectionId, // 目前連線的 ID
roomName); // 群組名稱
await Clients.Group(roomName).SendAsync(
"RoomNotification", // 群組通知
$"{Context.User?.Identity?.Name} 離開了 {roomName}"); // 訊息
}
// 發送訊息到特定群組
public async Task SendToRoom(
string roomName,
string message)
{
await Clients.Group(roomName).SendAsync(
"ReceiveRoomMessage", // 群組訊息事件
Context.User?.Identity?.Name, // 發送者
roomName, // 群組名稱
message); // 訊息內容
}
}
強型別 Hub
// 定義客戶端介面
public interface IChatClient
{
Task ReceiveMessage(string user, string message); // 接收訊息
Task UserJoined(string user); // 使用者加入
Task UserLeft(string user); // 使用者離開
Task RoomNotification(string notification); // 群組通知
}
// 使用強型別 Hub(有編譯期檢查!)
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(string user, string message)
{
// 有智慧提示!不會打錯方法名稱
await Clients.All.ReceiveMessage(user, message); // 編譯期就能檢查
}
public override async Task OnConnectedAsync()
{
var name = Context.User?.Identity?.Name ?? "匿名";
await Clients.All.UserJoined(name); // 強型別,不會拼錯
await base.OnConnectedAsync();
}
}
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:沒有處理重新連線
// ❌ 沒有自動重連,斷線後就再也收不到訊息
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.build(); // ❌ 沒有 withAutomaticReconnect
connection.start(); // 連上後斷線就沒了
// ✅ 啟用自動重連 + 斷線處理
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withAutomaticReconnect([0, 2000, 5000, 10000]) // 重連間隔(毫秒)
.build();
connection.onreconnecting((error) => {
console.log("正在重新連線..."); // 通知使用者
document.getElementById("status")
.textContent = "重新連線中..."; // 更新 UI 狀態
});
connection.onreconnected((connectionId) => {
console.log("重新連線成功!"); // 連線恢復
document.getElementById("status")
.textContent = "已連線"; // 更新 UI 狀態
});
connection.onclose((error) => {
console.log("連線已關閉"); // 完全斷線
// 可以嘗試手動重連
setTimeout(() => connection.start(), 5000); // 5 秒後重試
});
connection.start(); // 啟動連線
為什麼? 網路不穩定時連線會中斷,沒有重連機制的話用戶就收不到新訊息。
withAutomaticReconnect會自動嘗試重新連線。
❌ 錯誤 2:Hub 方法名稱拼錯(字串魔術)
// ❌ 方法名稱用字串,容易拼錯
await Clients.All.SendAsync("RecieveMessage", user, message); // 拼錯了!
// 客戶端監聽 "ReceiveMessage",永遠收不到!
// ✅ 使用強型別 Hub(編譯期檢查)
public class ChatHub : Hub<IChatClient> // 使用介面
{
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message); // 拼錯會編譯失敗!
}
}
為什麼? 字串沒有編譯期檢查,拼錯也不會報錯,但客戶端就是收不到訊息。用強型別 Hub 可以在編譯時就發現錯誤。
❌ 錯誤 3:在 Hub 中保存狀態
// ❌ 在 Hub 中存使用者列表(Hub 是 Transient 的!)
public class ChatHub : Hub
{
private List<string> _onlineUsers = new(); // ❌ 每次呼叫都會重建!
public async Task SendMessage(string user, string message)
{
_onlineUsers.Add(user); // ❌ 加了也白加,下次呼叫就沒了
}
}
// ✅ 用 Singleton 服務或靜態欄位管理狀態
public class OnlineUserService
{
private readonly ConcurrentDictionary<string, string> _users = new(); // 執行緒安全
public void AddUser(string connectionId, string name) =>
_users.TryAdd(connectionId, name); // 新增使用者
public void RemoveUser(string connectionId) =>
_users.TryRemove(connectionId, out _); // 移除使用者
public List<string> GetAll() =>
_users.Values.ToList(); // 取得所有線上使用者
}
// Program.cs 註冊為 Singleton
builder.Services.AddSingleton<OnlineUserService>(); // 全域唯一
為什麼? Hub 是 Transient 的,每次方法呼叫都會建立新的 Hub 實例。存在 Hub 欄位中的資料下次呼叫就消失了。