☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
ASP.NET Core 進階

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 欄位中的資料下次呼叫就消失了。

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊