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

WebSocket 與即時通訊

為什麼需要即時通訊?

傳統 HTTP 是「一問一答」模式:客戶端發請求,伺服器才回應。 但有些場景需要伺服器主動推送資料給客戶端:

需要即時通訊的場景:
├── 聊天室(LINE、Discord)
├── 股票即時報價
├── 多人線上遊戲
├── 通知系統
└── 協作編輯(Google Docs)

四種即時通訊方案比較

1. 短輪詢(Short Polling)

客戶端                     伺服器
  |--- 有新訊息嗎? -------->|
  |<-- 沒有 -----------------|
  |(等 3 秒)                |
  |--- 有新訊息嗎? -------->|
  |<-- 沒有 -----------------|
  |(等 3 秒)                |
  |--- 有新訊息嗎? -------->|
  |<-- 有!這是新訊息 --------|

缺點:浪費資源,大部分請求都是空的

2. 長輪詢(Long Polling)

客戶端                     伺服器
  |--- 有新訊息嗎? -------->|
  |    (伺服器先不回應...     |
  |     等到有新訊息才回)     |
  |<-- 有!這是新訊息 --------|
  |--- 有新訊息嗎? -------->|  ← 馬上再問
  |    (繼續等...)           |

優點:減少空的回應
缺點:每次收到訊息都要重新建立連線

3. Server-Sent Events(SSE)

客戶端                     伺服器
  |--- 我要訂閱 ------------>|
  |<== 保持連線 ============>|
  |<-- 新訊息 1 -------------|
  |<-- 新訊息 2 -------------|
  |<-- 新訊息 3 -------------|

優點:伺服器可以持續推送
缺點:單向(只能伺服器 → 客戶端)

4. WebSocket(雙向即時通訊)

客戶端                     伺服器
  |--- HTTP 升級請求 -------->|
  |<-- 101 Switching ---------|
  |<=== WebSocket 連線 =====>|
  |<--> 雙向即時通訊 <------->|
  |--- 我說哈囉 ------------>|
  |<-- 伺服器說嗨 ------------|
  |<-- 伺服器推通知 ----------|

優點:雙向、低延遲、省頻寬

方案比較表

方案           方向     延遲    複雜度   適用場景
───────────────────────────────────────────────────
短輪詢         單向     高      低       簡單通知
長輪詢         單向     中      中       即時通知
SSE            單向     低      中       股票報價、新聞推播
WebSocket      雙向     最低    高       聊天室、遊戲

WebSocket 握手過程

# 客戶端發送升級請求(從 HTTP 升級到 WebSocket)
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
# 伺服器回應同意升級
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
握手完成後:
├── 不再是 HTTP 協議了
├── 變成 WebSocket 協議(ws:// 或 wss://)
├── 連線保持開啟,雙方可以隨時傳資料
└── 資料以「Frame」為單位傳輸(比 HTTP 輕量很多)

C# WebSocket 客戶端

using System.Net.WebSockets;
using System.Text;

// 建立 WebSocket 客戶端
var ws = new ClientWebSocket();

// 連接到 WebSocket 伺服器
await ws.ConnectAsync(
    new Uri("wss://echo.websocket.org"),
    CancellationToken.None
);
// 確認連線狀態
Console.WriteLine($"連線狀態:{ws.State}");  // Open

// 傳送訊息
var message = "你好,WebSocket!";
// 把字串轉成 byte 陣列
var bytes = Encoding.UTF8.GetBytes(message);
// 送出訊息(Text 類型,EndOfMessage 表示這是完整的一筆)
await ws.SendAsync(
    new ArraySegment<byte>(bytes),
    WebSocketMessageType.Text,
    endOfMessage: true,
    CancellationToken.None
);

// 接收訊息
var buffer = new byte[1024];
// 等待伺服器回傳訊息
var result = await ws.ReceiveAsync(
    new ArraySegment<byte>(buffer),
    CancellationToken.None
);
// 把收到的 byte 轉回字串
var received = Encoding.UTF8.GetString(buffer, 0, result.Count);
Console.WriteLine($"收到:{received}");

// 關閉連線(禮貌地告訴對方要斷線了)
await ws.CloseAsync(
    WebSocketCloseStatus.NormalClosure,
    "再見",
    CancellationToken.None
);

SignalR — 更好用的即時通訊框架

💡 比喻 WebSocket 像是自己接水管——你要處理連線、斷線、重連、序列化... SignalR 像是請水電師傅來裝——幫你處理好所有細節,你只要開水龍頭就好。

SignalR Hub(伺服器端)

using Microsoft.AspNetCore.SignalR;

// 定義一個聊天 Hub(類似 Controller)
public class ChatHub : Hub
{
    // 當客戶端呼叫 SendMessage 時觸發
    public async Task SendMessage(string user, string message)
    {
        // 廣播給所有連線的客戶端
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // 加入特定群組(例如聊天室)
    public async Task JoinRoom(string roomName)
    {
        // 把目前的連線加入指定群組
        await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
        // 通知群組內的其他人
        await Clients.Group(roomName).SendAsync(
            "ReceiveMessage", "系統", $"{Context.ConnectionId} 加入了 {roomName}"
        );
    }

    // 當客戶端斷線時觸發
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        // 可以在這裡做清理工作
        Console.WriteLine($"使用者 {Context.ConnectionId} 已斷線");
        await base.OnDisconnectedAsync(exception);
    }
}

SignalR 客戶端(C#)

using Microsoft.AspNetCore.SignalR.Client;

// 建立 SignalR 連線
var connection = new HubConnectionBuilder()
    // 指定 Hub 的 URL
    .WithUrl("https://localhost:5001/chatHub")
    // 啟用自動重連(斷線後自動嘗試重新連線)
    .WithAutomaticReconnect()
    .Build();

// 監聽伺服器推送的 ReceiveMessage 事件
connection.On<string, string>("ReceiveMessage", (user, message) =>
{
    // 收到訊息時顯示在 Console
    Console.WriteLine($"{user}: {message}");
});

// 監聽重連事件
connection.Reconnecting += error =>
{
    Console.WriteLine("正在重新連線...");
    return Task.CompletedTask;
};

// 開始連線
await connection.StartAsync();
Console.WriteLine("已連線到 SignalR Hub");

// 發送訊息
await connection.InvokeAsync("SendMessage", "小明", "大家好!");

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:沒有處理 WebSocket 斷線重連

// ❌ 錯誤:連線斷了就整個掛掉
var ws = new ClientWebSocket();
await ws.ConnectAsync(uri, CancellationToken.None);
// 如果網路中斷,下面的 ReceiveAsync 會拋出例外,程式就結束了
var result = await ws.ReceiveAsync(buffer, CancellationToken.None);

// ✅ 正確:加入斷線重連機制
async Task ConnectWithRetry(Uri uri)
{
    // 最多重試 5 次
    var maxRetries = 5;
    for (int i = 0; i < maxRetries; i++)
    {
        try
        {
            var ws = new ClientWebSocket();
            // 嘗試連線
            await ws.ConnectAsync(uri, CancellationToken.None);
            Console.WriteLine("連線成功!");
            // 連線成功後開始接收訊息
            await ReceiveLoop(ws);
        }
        catch (Exception ex)
        {
            // 連線失敗或斷線,等待後重試
            Console.WriteLine($"連線失敗:{ex.Message},{i + 1}/{maxRetries} 次重試");
            // 指數退避:每次等待時間加倍
            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
        }
    }
}

❌ 錯誤 2:WebSocket 記憶體洩漏

// ❌ 錯誤:每次接收都建立新的 byte 陣列,造成 GC 壓力
while (ws.State == WebSocketState.Open)
{
    // 每次迴圈都分配新的記憶體(浪費!)
    var buffer = new byte[4096];
    await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
}

// ✅ 正確:重複使用同一個 buffer
// 在迴圈外面就分配好 buffer
var buffer2 = new byte[4096];
while (ws.State == WebSocketState.Open)
{
    // 重複使用同一塊記憶體
    var result2 = await ws.ReceiveAsync(
        new ArraySegment<byte>(buffer2),
        CancellationToken.None
    );
    // 只處理實際收到的資料長度
    var msg = Encoding.UTF8.GetString(buffer2, 0, result2.Count);
}

❌ 錯誤 3:沒有正確關閉 WebSocket 連線

// ❌ 錯誤:直接斷線,沒有通知對方
ws.Dispose();  // 粗暴地斷開,對方不知道發生什麼事

// ✅ 正確:先發送 Close 訊框,再關閉
// 禮貌地告訴對方要關閉連線
await ws.CloseAsync(
    WebSocketCloseStatus.NormalClosure,
    "正常關閉",
    CancellationToken.None
);
// 關閉後再釋放資源
ws.Dispose();

💡 重點整理

概念 說明
Short Polling 定時發請求問有沒有新資料(浪費資源)
Long Polling 請求掛著等有新資料才回應
SSE 伺服器單向推送(適合通知)
WebSocket 雙向即時通訊(適合聊天、遊戲)
SignalR ASP.NET Core 的即時通訊框架,簡化 WebSocket 使用
重連機制 WebSocket 必須處理斷線重連,建議用指數退避

💡 大家的想法 · 0

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