HTTP 無狀態,使用者關閉瀏覽器不會發送任何請求至伺服器,導致 Session_End(InProc 模式)或分散式 Session 清理無法即時執行,線上名單殘留「鬼魂使用者」。
| 方案 | 原理 | 優點 | 缺點 |
|---|---|---|---|
| 定時 AJAX 心跳 | 前端 setInterval 定期呼叫後端 API 更新最後活動時間 | 即時、可精確判斷離線 | 增加伺服器負載、頻寬 |
| SignalR / WebSocket | 長連線,連線斷開即觸發離線邏輯 | 真即時、雙向、低延遲 | 需支援 WebSocket 基礎設施、較複雜 |
| Session 超時掃描 | 背景工作定期掃描 Session 最後存取時間,超時標記離線 | 無前端改動、相容性佳 | 延遲 = 掃描間隔 + Session Timeout |
| beforeunload / unload 事件送 Beacon | 頁面卸載時發送 navigator.sendBeacon 通知伺服器 | 輕量、不阻塞卸載 | 瀏覽器支援度、可能被攔截 |
let heartbeatTimer = null;
const HEARTBEAT_INTERVAL = 30000; // 30 秒
const MAX_MISSED = 2; // 容許遺漏次數
function startHeartbeat() {
let missed = 0;
function tick() {
fetch('/api/heartbeat', { method: 'POST', keepalive: true })
.then(r => { if (r.ok) missed = 0; else missed++; })
.catch(() => missed++);
if (missed < MAX_MISSED) {
heartbeatTimer = setTimeout(tick, HEARTBEAT_INTERVAL);
} else {
// 連續失敗,視為離線(伺服器端同步清理)
console.log('Heartbeat stopped');
}
}
tick();
}
// 頁面載入啟動
document.addEventListener('DOMContentLoaded', startHeartbeat);
// 可選:頁面可見性變更時重置
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') startHeartbeat();
});
[HttpPost("api/heartbeat")]
public IActionResult Heartbeat() {
var userId = User.Identity.Name; // 或 Session["UserId"]
if (string.IsNullOrEmpty(userId)) return Unauthorized();
// 更新 Redis / 資料庫 / MemoryCache 中的 LastActiveTime
_cache.Set($"user:{userId}:lastActive", DateTimeOffset.UtcNow, TimeSpan.FromMinutes(30));
return Ok();
}
public class OnlineUserCleanupService : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken ct) {
while (!ct.IsCancellationRequested) {
var cutoff = DateTimeOffset.UtcNow.AddMinutes(-35); // 比 Session Timeout 稍長
var staleKeys = _cache.Keys("user:*:lastActive").Where(k => _cache.Get(k) < cutoff);
foreach (var key in staleKeys) {
var userId = key.Split(':')[1];
RemoveFromOnlineList(userId); // 你的清理邏輯
_cache.Remove(key);
}
await Task.Delay(TimeSpan.FromMinutes(5), ct);
}
}
}
<sessionState mode="InProc" timeout="30" />
<sessionState mode="SQLServer" timeout="30" sqlConnectionString="..." />
timeout 單位為分鐘,建議大於心跳間隔的 2~3 倍。Session_End 只有在 IIS 回收或 Session 真正過期才觸發,不可靠作為即時離線依據。// Hub
public class PresenceHub : Hub {
public override async Task OnConnectedAsync() {
await OnlineTracker.UserConnected(Context.UserIdentifier, Context.ConnectionId);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception e) {
await OnlineTracker.UserDisconnected(Context.UserIdentifier, Context.ConnectionId);
await base.OnDisconnectedAsync(e);
}
}
// 前端
const connection = new signalR.HubConnectionBuilder().withUrl("/presenceHub").build();
connection.start();
SignalR 內建重連、心跳、斷線偵測,是現代 ASP.NET Core 專案的首選方案。
ConcurrentDictionary、Redis Hash 或資料表,支援多伺服器擴展。