.NET SignalR 的學習與應用(1)

簡單網頁聊天室

SignalR 簡介

如果你是第一次接觸 SignalR,可以想像它是一個幫你處理即時通訊的工具。它會自動幫你搞定誰在線上、誰下線了,還能讓你輕鬆把訊息送給所有人或指定的人,而且可以應付很多人同時使用。簡單來說,不用煩惱那些複雜的底層細節,SignalR 幫你包好了!

傳輸協議

你可能會想問,SignalR 到底怎麼做到讓訊息這麼即時的呢?其實它會自動幫你選擇最適合的傳輸方式:

  • WebSocket:就像一直開著的專線,速度最快,也是首選。
  • Server-Sent Events (SSE):伺服器主動推消息給你,像訂閱新聞一樣。
  • Long Polling:不斷問伺服器「有新消息嗎?」來達成即時效果。

不用擔心怎麼挑,SignalR 會幫你搞定!

實作步驟

1. 建立專案並添加 SignalR

在 .NET Core/.NET 5+ 中,SignalR 已經被整合到 ASP.NET Core 框架中了,所以我們不需要額外安裝 SignalR 的服務器端套件,只需要在 Program.cs 中配置 SignalR:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using WebApiDemo.Hubs;

var builder = WebApplication.CreateBuilder(args);

// 添加 SignalR 服務到 DI 容器
builder.Services.AddSignalR();

var app = builder.Build();

app.UseStaticFiles();
app.UseRouting();     // 啟用路由

// 配置 SignalR Hub 路由
app.MapHub<ChatHub>("/chatHub");

// 設置默認頁面
app.MapGet("/", () => Results.File("wwwroot/index.html", "text/html"));

app.Run();

2. 創建 Hub

Hub 是 SignalR 的核心角色,可以想成「訊息小幫手」,負責在用戶的瀏覽器和伺服器之間傳遞訊息。基本的 Hub 需要做的事很簡單:收到使用者的訊息並轉送給其他人,處理使用者連線和斷線的通知。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using Microsoft.AspNetCore.SignalR;

namespace WebApiDemo.Hubs;

public class ChatHub : Hub
{
    // 這個方法用來接收使用者的訊息並廣播給所有人
    public async Task SendMessage(string user, string message)
    {
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // 使用者連線時會觸發這裡,把新加入的使用者通知給大家
    public override async Task OnConnectedAsync()
    {
        await Clients.All.SendAsync("UserConnected", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    // 使用者斷線時會執行這裡,把誰離開的通知發給所有人
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await Clients.All.SendAsync("UserDisconnected", Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
}

Hub 重要屬性

Clients:負責把訊息發送給誰,例如:

  • Clients.All:送給所有人,不管是誰
  • Clients.Caller:只回傳給發送訊息的人
  • Clients.Others:送給除了發訊息的人以外的所有人

Context:提供關於這次連線的資訊,例如:

  • Context.ConnectionId:每個使用者連上來後都有一個獨一無二的編號
  • Context.User:使用者的身份資訊,適用在需要登入或認證的應用程式

3. 客戶端實現

在 root 目錄底下建立 wwwroot 的資料夾,並且建立 index.html 輸入以下內容:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<!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; }
    </style>
</head>
<body>
    <div class="container">
        <h2>SignalR 聊天室</h2>
        <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.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();
        });
    </script>
</body>
</html>

這時候運行你的專案(記得設定 launchSettings.json),輸入 localhost:5000/index.html,就可以看到以下的畫面。

如果你再多開一個新的 tab,就會看到第一個 tab 多了一行連接的用戶:

兩個不同的人可以輸入訊息,按下發送後,另外一邊也會收到:

有人離開的話就可以送出通知:

Built with Hugo
Theme Stack designed by Jimmy