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

多人連線與 Session 管理

同步 vs 非同步處理

💡 比喻:點餐方式

  • 同步處理:你在櫃台點餐,站在那裡等做好才離開。後面的人都在排隊等你。
  • 非同步處理:你在櫃台點餐,拿到號碼牌就去坐下。櫃台可以繼續服務下一位。

非同步不是「更快」,而是「不浪費等待時間」。 做菜(I/O 操作)的時間沒有變短,但櫃台(執行緒)可以服務更多人。

同步 vs 非同步程式碼

// ❌ 同步寫法:執行緒在等待資料庫回應時被卡住
public IActionResult GetUsers()                    // 同步方法,沒有 async
{
    var users = _db.Users.ToList();                // 等待資料庫回應(執行緒被佔用)
    return Ok(users);                              // 資料庫回應後才執行
}
// 問題:如果有 100 個請求同時來,需要 100 個執行緒
// 執行緒是有限的資源,用完就會出現 503 錯誤

// ✅ 非同步寫法:執行緒在等待時可以去處理其他請求
public async Task<IActionResult> GetUsers()        // 非同步方法,回傳 Task
{
    var users = await _db.Users.ToListAsync();     // await 等待時釋放執行緒
    return Ok(users);                              // 資料庫回應後繼續
}
// 好處:100 個請求可能只需要 10 幾個執行緒就能處理
// 因為大部分時間都在等 I/O,執行緒可以重複利用

效能差異圖解

// 同步模式:4 個執行緒處理 4 個請求
Thread 1: [===處理===][等待DB..........][===完成===]         // 執行緒被卡住
Thread 2: [===處理===][等待DB..........][===完成===]         // 無法服務新請求
Thread 3: [===處理===][等待DB..........][===完成===]         // 資源浪費
Thread 4: [===處理===][等待DB..........][===完成===]         // 全部都在等
Request 5: 排隊中...等不到執行緒...超時...503 錯誤           // 第 5 個請求失敗

// 非同步模式:2 個執行緒處理 4+ 個請求
Thread 1: [處理1][等1→處理3][等3→處理5][完成1][完成3][完成5]  // 一個執行緒輪流處理
Thread 2: [處理2][等2→處理4][完成2][完成4]                   // 充分利用等待時間
// 少量執行緒就能處理大量請求!

Connection Pool 概念

💡 比喻:共用腳踏車

  • 沒有 Connection Pool:每次出門都買一台新腳踏車,用完丟掉(太浪費!)
  • 有 Connection Pool:社區有一批公共腳踏車,需要時借,用完歸還

建立資料庫連線很花時間(像買腳踏車),Connection Pool 讓你重複使用已建立的連線。

資料庫 Connection Pool

// 連線字串中設定 Connection Pool 參數
var connectionString = new StringBuilder()               // 建立連線字串
    .Append("Server=localhost;")                       // 資料庫伺服器位址
    .Append("Database=MyDb;")                          // 資料庫名稱
    .Append("Min Pool Size=5;")                        // 最少保持 5 個連線
    .Append("Max Pool Size=100;")                      // 最多建立 100 個連線
    .Append("Connection Timeout=30;")                  // 等待連線的逾時時間(秒)
    .Append("Connection Lifetime=300;")                // 連線存活最長時間(秒)
    .ToString();                                         // 轉成字串

// 在 Program.cs 中註冊 DbContext
builder.Services.AddDbContext<AppDbContext>(options =>    // 註冊資料庫上下文
    options.UseSqlServer(connectionString)                // 使用 SQL Server
);
// EF Core 會自動管理 Connection Pool
// 取得連線 → 使用 → 歸還,不需要手動管理

HttpClient Connection Pool

// ❌ 錯誤做法:每次都 new HttpClient
public async Task<string> CallApi()                       // 呼叫外部 API
{
    using var client = new HttpClient();                   // 每次 new 會建立新連線
    return await client.GetStringAsync("https://api.example.com");  // 用完就丟
}
// 問題:頻繁建立/關閉連線會耗盡 Socket(Socket Exhaustion)

// ✅ 正確做法:使用 IHttpClientFactory
// 在 Program.cs 中註冊
builder.Services.AddHttpClient("ExternalApi", client =>  // 註冊具名 HttpClient
{
    client.BaseAddress = new Uri("https://api.example.com");  // 基底位址
    client.Timeout = TimeSpan.FromSeconds(30);                  // 逾時設定
    client.DefaultRequestHeaders.Add("Accept", "application/json"); // 預設標頭
});

// 在 Service 中使用
public class MyService                                     // 服務類別
{
    private readonly IHttpClientFactory _factory;           // HttpClient 工廠

    public MyService(IHttpClientFactory factory)            // 透過 DI 注入
    {
        _factory = factory;                                 // 儲存工廠實例
    }

    public async Task<string> CallApi()                    // 呼叫 API
    {
        var client = _factory.CreateClient("ExternalApi"); // 從工廠取得 HttpClient
        return await client.GetStringAsync("/data");       // 連線會被自動管理和重用
    }
}

Session vs Token 狀態管理

💡 比喻:身分識別方式

  • Session:像是遊樂園的手環——入園時套在手上,工作人員看到手環就知道你買過票。但手環資訊存在遊樂園的系統裡。
  • Token (JWT):像是護照——你自己帶著,上面寫著你的身分資訊。任何人都能讀取,但偽造不了(有防偽鋼印)。

Session 方式

// Program.cs 設定 Session
builder.Services.AddDistributedMemoryCache();              // 記憶體快取(開發用)
builder.Services.AddSession(options =>                     // 設定 Session 選項
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);        // 閒置 30 分鐘過期
    options.Cookie.HttpOnly = true;                        // Cookie 只能透過 HTTP 存取
    options.Cookie.IsEssential = true;                     // 標記為必要 Cookie
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;  // 只在 HTTPS 傳送
});

var app = builder.Build();                                 // 建構應用程式
app.UseSession();                                          // 啟用 Session 中介軟體

// 在 Controller 中使用 Session
public class CartController : Controller                   // 購物車控制器
{
    public IActionResult AddItem(int productId)            // 加入商品
    {
        var cart = HttpContext.Session.GetString("Cart"); // 從 Session 取得購物車
        // 處理購物車邏輯...                                // 業務邏輯
        HttpContext.Session.SetString("Cart", updatedCart); // 更新 Session
        return Ok();                                       // 回傳成功
    }
}

JWT Token 方式

// Program.cs 設定 JWT 驗證
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)  // 設定 JWT 驗證
    .AddJwtBearer(options =>                               // JWT Bearer 選項
    {
        options.TokenValidationParameters = new TokenValidationParameters   // 驗證參數
        {
            ValidateIssuer = true,                         // 驗證發行者
            ValidateAudience = true,                       // 驗證對象
            ValidateLifetime = true,                       // 驗證有效期限
            ValidateIssuerSigningKey = true,               // 驗證簽名金鑰
            ValidIssuer = "myapp.com",                   // 合法的發行者
            ValidAudience = "myapp.com",                 // 合法的對象
            IssuerSigningKey = new SymmetricSecurityKey(    // 簽名金鑰
                Encoding.UTF8.GetBytes("YourSuperSecretKey123!"))  // 金鑰字串(生產環境要用環境變數)
        };
    });

// 產生 JWT Token
public string GenerateToken(User user)                     // 產生 Token 方法
{
    var claims = new[]                                     // 宣告(Token 中的資訊)
    {
        new Claim(ClaimTypes.Name, user.Username),         // 使用者名稱
        new Claim(ClaimTypes.Role, user.Role),             // 使用者角色
        new Claim("UserId", user.Id.ToString())          // 使用者 ID
    };

    var key = new SymmetricSecurityKey(                     // 建立簽名金鑰
        Encoding.UTF8.GetBytes("YourSuperSecretKey123!") // 金鑰字串
    );
    var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);  // 簽名憑證

    var token = new JwtSecurityToken(                       // 建立 JWT Token
        issuer: "myapp.com",                             // 發行者
        audience: "myapp.com",                           // 對象
        claims: claims,                                    // 宣告內容
        expires: DateTime.Now.AddHours(1),                 // 1 小時後過期
        signingCredentials: creds                          // 簽名憑證
    );

    return new JwtSecurityTokenHandler().WriteToken(token); // 序列化為字串
}

WebSocket 長連線

💡 比喻:打電話 vs 傳簡訊

  • HTTP:像傳簡訊——每次都要重新撥號、說完就掛斷
  • WebSocket:像打電話——接通後保持連線,雙方可以隨時說話

適合需要即時更新的場景:聊天室、股票報價、線上遊戲、通知推播。

SignalR(ASP.NET Core 的 WebSocket 框架)

// ChatHub.cs - 定義 SignalR Hub
using Microsoft.AspNetCore.SignalR;                        // SignalR 命名空間

public class ChatHub : Hub                                 // 繼承 Hub 基底類別
{
    // 當客戶端連線時觸發
    public override async Task OnConnectedAsync()          // 連線事件
    {
        var username = Context.User?.Identity?.Name;       // 取得使用者名稱
        await Clients.All.SendAsync(                       // 通知所有人
            "UserJoined", $"{username} 加入了聊天室"   // 發送加入訊息
        );
        await base.OnConnectedAsync();                     // 呼叫基底方法
    }

    // 當客戶端發送訊息時觸發
    public async Task SendMessage(string message)          // 接收客戶端的訊息
    {
        var username = Context.User?.Identity?.Name;       // 取得發送者名稱
        await Clients.All.SendAsync(                       // 廣播給所有連線的客戶端
            "ReceiveMessage", username, message          // 傳送使用者名稱和訊息內容
        );
    }

    // 發送訊息給特定群組
    public async Task SendToGroup(string group, string msg)  // 群組訊息
    {
        await Clients.Group(group).SendAsync(              // 只發給特定群組
            "ReceiveMessage", Context.User?.Identity?.Name, msg  // 訊息內容
        );
    }

    // 加入群組
    public async Task JoinGroup(string groupName)          // 加入群組方法
    {
        await Groups.AddToGroupAsync(                      // 將連線加入群組
            Context.ConnectionId, groupName                // 使用連線 ID 和群組名稱
        );
        await Clients.Group(groupName).SendAsync(          // 通知群組成員
            "UserJoined", $"{Context.User?.Identity?.Name} 加入了 {groupName}"
        );
    }

    // 當客戶端斷線時觸發
    public override async Task OnDisconnectedAsync(Exception? ex)  // 斷線事件
    {
        await Clients.All.SendAsync(                       // 通知所有人
            "UserLeft", $"{Context.User?.Identity?.Name} 離開了"  // 離開訊息
        );
        await base.OnDisconnectedAsync(ex);                // 呼叫基底方法
    }
}

// Program.cs 中註冊 SignalR
var builder = WebApplication.CreateBuilder(args);          // 建構器
builder.Services.AddSignalR();                             // 註冊 SignalR 服務

var app = builder.Build();                                 // 建構應用程式
app.MapHub<ChatHub>("/chatHub");                         // 將 Hub 對應到 /chatHub 路徑
app.Run();                                                 // 啟動

限流(Rate Limiting)與防護

💡 比喻:夜店的門口管制

  • 限流就像夜店門口的保鏢:
    • 「一次只能進 100 人」(並發限制)
    • 「每個人每小時只能進出 3 次」(速率限制)
    • 「VIP 不受限制」(白名單)
  • 目的是防止惡意攻擊者(DDoS)或是爬蟲把你的服務搞垮

ASP.NET Core 內建限流

// Program.cs 設定限流
using System.Threading.RateLimiting;                       // 限流命名空間

var builder = WebApplication.CreateBuilder(args);          // 建構器

builder.Services.AddRateLimiter(options =>                  // 新增限流服務
{
    // 全域固定視窗限流
    options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(  // 全域限流器
        context => RateLimitPartition.GetFixedWindowLimiter(                     // 固定視窗演算法
            partitionKey: context.Connection.RemoteIpAddress?.ToString() ?? "unknown",  // 用 IP 分區
            factory: _ => new FixedWindowRateLimiterOptions                      // 限流選項
            {
                PermitLimit = 100,                         // 每個視窗允許 100 個請求
                Window = TimeSpan.FromMinutes(1),          // 視窗大小:1 分鐘
                QueueLimit = 10,                           // 排隊等待的請求數
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst  // 先進先出
            }
        )
    );

    // 針對特定端點的限流策略
    options.AddFixedWindowLimiter("ApiPolicy", opt =>     // 建立具名策略
    {
        opt.PermitLimit = 30;                              // 每個視窗允許 30 個請求
        opt.Window = TimeSpan.FromMinutes(1);              // 視窗:1 分鐘
        opt.QueueLimit = 5;                                // 排隊數量
    });

    // 被限流時的回應
    options.OnRejected = async (context, token) =>         // 被拒絕時的處理
    {
        context.HttpContext.Response.StatusCode = 429;      // 回傳 429 Too Many Requests
        await context.HttpContext.Response.WriteAsync(      // 寫入回應內容
            "請求太頻繁,請稍後再試。", cancellationToken: token  // 中文錯誤訊息
        );
    };
});

var app = builder.Build();                                 // 建構應用程式
app.UseRateLimiter();                                      // 啟用限流中介軟體

// 在 Controller 上套用特定策略
app.MapGet("/api/data", () => "OK")                    // API 端點
    .RequireRateLimiting("ApiPolicy");                   // 套用 ApiPolicy 限流策略

SignalR Scale-Out(Redis Backplane)

💡 比喻:連鎖餐廳的廣播系統 如果你的聊天室跑在多台伺服器上:

  • 使用者 A 連到 Server 1,使用者 B 連到 Server 2
  • A 發訊息給 B,但 Server 1 不知道 B 在 Server 2 上
  • Redis Backplane 就像連鎖餐廳的內部廣播系統
  • Server 1 把訊息發到 Redis,Redis 廣播給所有 Server
  • Server 2 收到後轉發給使用者 B

設定 Redis Backplane

// 安裝 NuGet 套件
// dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis  // 安裝 Redis 套件

// Program.cs 設定 SignalR + Redis
var builder = WebApplication.CreateBuilder(args);          // 建構器

builder.Services.AddSignalR()                              // 註冊 SignalR
    .AddStackExchangeRedis(                                // 加入 Redis Backplane
        "localhost:6379",                                // Redis 連線位址
        options =>                                         // Redis 選項
        {
            options.Configuration.ChannelPrefix =          // 頻道前綴
                RedisChannel.Literal("MyApp");           // 避免不同應用衝突
        }
    );

var app = builder.Build();                                 // 建構應用程式
app.MapHub<ChatHub>("/chatHub");                         // 對應 Hub 路徑
app.Run();                                                 // 啟動
// Redis Backplane 運作流程
使用者 A ──→ Server 1 ──┐
                         ├──→ Redis(訊息中繼站)──→ 廣播給所有 Server
使用者 B ──→ Server 2 ──┘                          │
                                                    ├──→ Server 1 ──→ 使用者 A(收到)
                                                    └──→ Server 2 ──→ 使用者 B(收到)
# 安裝 Redis(Ubuntu)
sudo apt update                                    # 更新套件清單
sudo apt install redis-server -y                   # 安裝 Redis

# 設定 Redis
sudo nano /etc/redis/redis.conf                    # 編輯設定檔
# 將 bind 127.0.0.1 改為 bind 0.0.0.0             # 如果需要外部存取
# 設定 requirepass YourRedisPassword               # 設定密碼保護

sudo systemctl restart redis                       # 重啟 Redis 套用設定
sudo systemctl enable redis                        # 設定開機自動啟動

# 測試 Redis 連線
redis-cli ping                                     # 應該回傳 PONG
redis-cli info clients                             # 查看連線數量

🤔 我這樣寫為什麼會錯?

❌ 錯誤一:在非同步方法中使用同步呼叫

// 錯誤:在 async 方法裡使用 .Result 或 .Wait()
public async Task<IActionResult> GetData()                 // 非同步方法
{
    var result = _httpClient.GetStringAsync("https://api.example.com").Result;  // 用 .Result 同步等待!
    return Ok(result);
}
// 問題:.Result 會阻塞執行緒,可能造成死結(Deadlock)
// 在 ASP.NET Core 中,這會導致應用程式凍結

// ✅ 正確做法:全程使用 await
public async Task<IActionResult> GetData()                 // 非同步方法
{
    var result = await _httpClient.GetStringAsync("https://api.example.com");  // 用 await 非同步等待
    return Ok(result);                                     // 不會阻塞執行緒
}

❌ 錯誤二:每次請求都 new HttpClient

// 錯誤:在 Controller 中直接 new HttpClient
public class MyController : Controller
{
    public async Task<IActionResult> CallApi()
    {
        using var client = new HttpClient();               // 每次都建立新的 HttpClient!
        var result = await client.GetStringAsync("https://api.example.com");
        return Ok(result);
    }
}
// 問題:HttpClient 的 Dispose 不會立即釋放 Socket
// 大量請求會導致 Socket Exhaustion 錯誤

// ✅ 正確做法:使用 IHttpClientFactory
public class MyController : Controller
{
    private readonly IHttpClientFactory _factory;          // 注入工廠

    public MyController(IHttpClientFactory factory)        // 建構函式注入
    {
        _factory = factory;                                // 儲存參考
    }

    public async Task<IActionResult> CallApi()
    {
        var client = _factory.CreateClient();              // 從工廠取得(連線會被重用)
        var result = await client.GetStringAsync("https://api.example.com");
        return Ok(result);
    }
}

❌ 錯誤三:JWT 金鑰太短或硬編碼

// 錯誤:使用太短的金鑰
var key = new SymmetricSecurityKey(
    Encoding.UTF8.GetBytes("123")                        // 金鑰太短!至少要 256 位元(32 字元)
);

// 錯誤:金鑰直接寫在程式碼中
var secretKey = "MySecretKeyHardcodedInSourceCode123!";  // 程式碼可能被推到 GitHub!

// ✅ 正確做法:從環境變數或 User Secrets 讀取
var secretKey = builder.Configuration["Jwt:SecretKey"];  // 從設定檔讀取
// 金鑰長度至少 32 字元以上
// 生產環境存在環境變數中,不要提交到版本控制

💡 大家的想法 · 0

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