Comet (長輪詢):客戶端發起請求 → 伺服器暫存請求直到有事件或逾時 → 回傳事件 → 客戶端處理後立即發起下一輪。
ASP.NET MVC 4 (.NET 4.5) 引入 async/await,配合 Task<ActionResult> 可輕鬆實作非同步長輪詢,不阻塞 IIS 執行緒池。
public enum EntityEventType { Create = 0, Update = 1, Deleted = 2 }
public interface IEntityEvent
{
TEntity[] Entities { get; }
EntityEventType Type { get; }
}
public class EntityEvent : IEntityEvent
{
public TEntity[] Entities { get; set; }
public EntityEventType Type { get; set; }
}
public sealed class EntityEventSubscriber
{
private static readonly Lazy _instance
= new Lazy(() => new EntityEventSubscriber());
public static EntityEventSubscriber Instance => _instance.Value;
// 每個實體類型一個佇列
private readonly ConcurrentDictionary>>> _waiters
= new ConcurrentDictionary>>>();
// 發布事件 (由業務層呼叫,如 Repository.SaveChanges 後)
public void Publish(EntityEventType type, params TEntity[] entities)
{
var eventObj = new EntityEvent { Type = type, Entities = entities };
var queue = _waiters.GetOrAdd(typeof(TEntity), _ => new Queue>>());
lock (queue)
{
while (queue.Count > 0)
{
var tcs = queue.Dequeue();
tcs.TrySetResult(eventObj);
}
}
}
// 非同步等待事件
public async Task> WaitForEventAsync(int timeoutMs)
{
var tcs = new TaskCompletionSource>();
var queue = _waiters.GetOrAdd(typeof(TEntity), _ => new Queue>>());
lock (queue) queue.Enqueue(tcs);
var completedTask = await Task.WhenAny(tcs.Task, Task.Delay(timeoutMs));
if (completedTask == tcs.Task)
return (IEntityEvent)tcs.Task.Result;
return null; // 逾時
}
}
[SessionState(SessionStateBehavior.ReadOnly)] // 關鍵:避免 Session 鎖死其他請求
public class EventController : Controller
{
// GET /event/ImageSet?millisecondsTimeout=10000
public async Task ImageSet(int millisecondsTimeout = 10000)
{
var evt = await EntityEventSubscriber.Instance.WaitForEventAsync(millisecondsTimeout);
return Json(new
{
HasEvent = evt != null,
EntityEvent = evt
}, JsonRequestBehavior.AllowGet);
}
}
// 實體資料類別
public class ImageSetData
{
public int Id { get; set; }
public string Name { get; set; }
public DateTime UpdatedAt { get; set; }
}
預設 MVC Controller 對 Session 有寫入鎖定。長輪詢請求持有 Session 鎖 30 秒會導致同使用者其他頁面請求排隊阻塞。標記 ReadOnly 釋放鎖定,允許並行請求。
await Task.Delay 或 await tcs.Task 釋放執行緒回執行緒池,單一 IIS 能支援數萬並發長連線。
TaskCompletionSource 必須在逾時或完成時移出佇列 (上方 Dequeue 已處理)。Task.Delay 逾時自然釋放。單機 ConcurrentDictionary 無法跨節點共享事件。生產環境必須替換為:
TaskCompletionSource。
// ASP.NET Core + SignalR Hub
public class NotificationHub : Hub
{
public async Task Subscribe(string entityType)
=> await Groups.AddToGroupAsync(Context.ConnectionId, entityType);
}
public class ImageSetService
{
private readonly IHubContext _hub;
public async Task NotifyCreated(ImageSetData data)
=> await _hub.Clients.Group("ImageSet").SendAsync("Created", data);
}
// 前端
const connection = new signalR.HubConnectionBuilder()
.withUrl("/notificationHub").build();
connection.on("Created", (data) => addToList(data));
await connection.start();
await connection.invoke("Subscribe", "ImageSet");
優點:WebSocket/Server-Sent Events/長輪詢自動協商、自動重連、分組廣播、Scaleout 開箱即用。
| 方案 | 單機並發連線 | 跨節點 | 實作複雜度 | 瀏覽器相容 |
|---|---|---|---|---|
| MVC Async 長輪詢 | ~20,000 (受限記憶體) | 需自建 Redis Pub/Sub | 中 | 全瀏覽器 |
| SignalR (Core) | ~50,000+ (WebSocket) | 內建 Scaleout | 低 | IE10+ (需 Polyfill) |
| WebSocket 手工 | ~100,000 | 自建 | 高 | IE10+ |