> ## Content Index
> Fetch the complete content index at: https://blog.anthea.day/llms.txt
> Use this file to discover other available public pages before exploring further.

# SignalR 的學習與應用（2）：聊天室的小房間
- URL: https://blog.anthea.day/series-dotnet-signalr-2/
- Published: 2025-02-15T19:07:51.000Z
- Updated: 2026-11-07T19:51:52.000Z
- Author: Anthea / 芫荽
- Tags: #Migrated-1792241843742, #Import 2026-10-17 12:57, Coding, .NET, SignalR 的學習與應用

### SignalR Groups 簡介

延續上一篇文章介紹的 SignalR 聊天室概念，這次要來聊聊「小房間」功能。如果你熟悉 Discord，可以把 SignalR Groups 想像成 Discord 的語音或文字頻道，每個頻道就是一個 Group，使用者可以進出不同的頻道來交流。 SignalR 的 Groups 讓我們可以把連接的使用者分成不同的「小房間」，用來實現群組聊天、特定用戶廣播等功能。每個使用者可以同時在多個房間裡，這讓訊息傳遞變得很彈性。

#### Groups 主要特點

- **動態管理**：你可以隨時讓使用者加入或退出房間，像是隨時加入或離開 Discord 頻道一樣。
- **臨時性**：房間成員資料只存在伺服器記憶體裡，伺服器重啟後就會清空，不用擔心長期維護。
- **廣播效率**：可以一次把訊息發給整個房間的人，不需要重複發送。
- **多組成員**：同一個使用者可以同時待在很多房間，就像你可以加入好幾個 Discord 頻道一樣。

---

### 實作步驟

#### 1\. 服務器端實現

```csharp
using Microsoft.AspNetCore.SignalR; 
 
namespace WebApiDemo.Hubs; 
 
public class ChatHub : Hub 
{ 
    // 使用 Dictionary 追踪用戶所在的房間 
    private static readonly Dictionary<string, string> UserRooms = new(); 
    private static readonly string[] SensitiveWords = new[] { "討厭", "笨蛋", "白痴" }; 
 
    // 加入房間的方法 
    public async Task JoinRoom(string user, string roomName) 
    { 
        // 檢查用戶是否已在其他房間 
        if (UserRooms.TryGetValue(Context.ConnectionId, out string? oldRoom)) 
        { 
            // 從舊房間移除 
            await Groups.RemoveFromGroupAsync(Context.ConnectionId, oldRoom); 
            // 通知舊房間的其他用戶 
            await Clients.Group(oldRoom).SendAsync("ReceiveSystemMessage",  
                $"用戶 {user} 離開了房間"); 
        } 
 
        // 將用戶加入新房間 
        await Groups.AddToGroupAsync(Context.ConnectionId, roomName); 
        // 更新用戶房間記錄 
        UserRooms[Context.ConnectionId] = roomName; 
        // 通知新房間的所有用戶 
        await Clients.Group(roomName).SendAsync("ReceiveSystemMessage",  
            $"用戶 {user} 加入了房間"); 
    } 
 
    public async Task SendMessage(string user, string message) 
    { 
        if (ContainsSensitiveWords(message, out string foundWord)) 
        { 
            await Clients.Caller.SendAsync("ReceiveSystemMessage",  
                $"消息包含敏感詞「{foundWord}」，已被攔截"); 
            return; 
        } 
 
        // 檢查用戶所在的房間 
        if (UserRooms.TryGetValue(Context.ConnectionId, out string? room)) 
        { 
            // 只向同一房間的用戶發送消息 
            await Clients.Group(room).SendAsync("ReceiveMessage", user, message); 
        } 
    } 
 
    private bool ContainsSensitiveWords(string message, out string foundWord) 
    { 
        foundWord = SensitiveWords.FirstOrDefault(word =>  
            message.Contains(word, StringComparison.OrdinalIgnoreCase)); 
         
        return foundWord != null; 
    } 
 
    // 當新客戶端連接時觸發 
    public override async Task OnConnectedAsync() 
    { 
        // Context.ConnectionId: 每個連接的唯一標識 
        await Clients.All.SendAsync("UserConnected", Context.ConnectionId); 
        await base.OnConnectedAsync(); 
    } 
 
    // 當客戶端斷開連接時觸發 
    public override async Task OnDisconnectedAsync(Exception? exception) 
    { 
        // 用戶斷開連接時清理房間資訊 
        if (UserRooms.TryGetValue(Context.ConnectionId, out string? room)) 
        { 
            await Groups.RemoveFromGroupAsync(Context.ConnectionId, room); 
            UserRooms.Remove(Context.ConnectionId); 
        } 
        await Clients.All.SendAsync("UserDisconnected", Context.ConnectionId); 
        await base.OnDisconnectedAsync(exception); 
    } 
}
```

關於 Groups 會使用到的幾個方法：

- **Groups.AddToGroupAsync**：將使用者加入房間
- **Groups.RemoveFromGroupAsync**：將使用者從房間中移除
- **Clients.Group**：向指定房間的使用者發送消息

#### 2\. 客戶端實現

```xml
<!DOCTYPE html> 
<html> 
<head> 
    <meta charset="UTF-8"> 
    <title>SignalR 聊天室</title> 
    <style> 
        #messagesList {  
            border: 1px solid #ccc;  
            height: 300px;  
            overflow-y: auto;  
            padding: 10px;  
            margin-bottom: 10px;  
        } 
        .form-group { 
            margin: 10px 0; 
        } 
        .form-group label { 
            display: inline-block; 
            width: 70px; 
        } 
        .form-group input { 
            padding: 5px; 
            width: 200px; 
        } 
        #sendButton { 
            padding: 5px 15px; 
            margin-top: 10px; 
        } 
        .system-message { 
            color: #ff4444; 
            font-style: italic; 
            margin: 5px 0; 
        } 
    </style> 
</head> 
<body> 
    <div class="container"> 
        <h2>SignalR 聊天室</h2> 
         
        <!-- 添加房間選擇 --> 
        <div class="form-group"> 
            <label for="roomInput">房間:</label> 
            <input type="text" id="roomInput" /> 
            <button id="joinButton">加入房間</button> 
        </div> 
 
        <div id="messagesList"></div> 
         
        <div class="form-group"> 
            <label for="userInput">用戶名:</label> 
            <input type="text" id="userInput" /> 
        </div> 
        <div class="form-group"> 
            <label for="messageInput">消息:</label> 
            <input type="text" id="messageInput" /> 
        </div> 
        <button id="sendButton">發送</button> 
    </div> 
 
    <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 的路由 
            .build(); 
 
        document.getElementById("sendButton").disabled = true; 
 
        // 註冊接收消息的處理函數 
        connection.on("ReceiveMessage", function (user, message) { 
            const msg = `${user}: ${message}`; 
            const li = document.createElement("div"); 
            li.textContent = msg; 
            document.getElementById("messagesList").appendChild(li); 
        }); 
 
        // 處理用戶連接事件 
        connection.on("UserConnected", function (connectionId) { 
            const msg = `用戶已連接 (${connectionId})`; 
            const li = document.createElement("div"); 
            li.textContent = msg; 
            li.style.color = "green"; 
            document.getElementById("messagesList").appendChild(li); 
        }); 
 
        connection.on("UserDisconnected", function (connectionId) { 
            const msg = `用戶已斷開連接 (${connectionId})`; 
            const li = document.createElement("div"); 
            li.textContent = msg; 
            li.style.color = "red"; 
            document.getElementById("messagesList").appendChild(li); 
        }); 
 
        connection.on("ReceiveSystemMessage", function (message) { 
            const div = document.createElement("div"); 
            div.textContent = `系統提示: ${message}`; 
            div.className = "system-message"; 
            document.getElementById("messagesList").appendChild(div); 
        }); 
 
        // 啟動連接 
        connection.start().then(function () { 
            document.getElementById("sendButton").disabled = false; 
        }).catch(function (err) { 
            return console.error(err.toString()); 
        }); 
 
        // 發送消息 
        document.getElementById("sendButton").addEventListener("click", function (event) { 
            const user = document.getElementById("userInput").value; 
            const message = document.getElementById("messageInput").value; 
            // 調用服務器端的 SendMessage 方法 
            connection.invoke("SendMessage", user, message).catch(function (err) { 
                return console.error(err.toString()); 
            }); 
            document.getElementById("messageInput").value = ""; 
            event.preventDefault(); 
        }); 
 
        // 添加加入房間按鈕的處理 
        document.getElementById("joinButton").addEventListener("click", function (event) { 
            const user = document.getElementById("userInput").value; 
            const room = document.getElementById("roomInput").value; 
             
            if (!user || !room) { 
                alert("請輸入用戶名和房間名"); 
                return; 
            } 
 
            connection.invoke("JoinRoom", user, room); 
            event.preventDefault(); 
        }); 
    </script> 
</body> 
</html>
```

這時候運行你的專案（你應該有記得在上一篇文章中一起設定過 `launchSettings.json` 對吧），輸入 `localhost:5000/index.html` ，就可以看到以下的畫面：

![](https://blog.anthea.day/content/images/2026/10/1-lvsl7vd1skthoiizo7qlma.png)

一進入的時候沒有特別指定房間

如果 Anthea 進入到房間（明星三缺一），這時候他的訊息只會發給同一個房間裡的人，沒有進入同一個房間的 Ensui 就收不到訊息了：

![](https://blog.anthea.day/content/images/2026/10/1-xfk4j8bnumtylqk8lxcpng.png)