Server-Sent Events Thời Gian Thực trong ASP.NET Core và .NET 10

22 tháng 7, 2025

Bạn có thể cần tích hợp cập nhật thời gian thực trong ứng dụng .NET của mình từ backend đến frontend. Bạn có một số tùy chọn để triển khai điều này:

  • Polling — frontend liên tục kiểm tra server để tìm dữ liệu mới
  • SignalR — frontend đăng ký một sự kiện, và server gửi sự kiện này bằng WebSockets
  • Server-Sent Events (đã có sẵn trong .NET 10 preview)

Polling endpoint mỗi vài giây có thể làm quá tải server và lãng phí băng thông, trong khi WebSockets full-duplex có thể quá phức tạp cho các cập nhật đơn hướng đơn giản.

Server-Sent Events (SSE) cung cấp một cách nhẹ nhàng, đáng tin cậy để các ứng dụng ASP.NET Core đẩy luồng dữ liệu liên tục mà không cần sự phức tạp của các giao thức hai chiều.

Server-Sent Events là gì?

Server-Sent Events (SSE) là một chuẩn web cho phép server đẩy dữ liệu thời gian thực đến các client web qua một kết nối HTTP đơn. Không giống như các mẫu request-response truyền thống nơi client phải liên tục polling server để nhận cập nhật, SSE cho phép server khởi tạo giao tiếp và gửi dữ liệu bất cứ khi nào có thông tin mới.

Đặc điểm chính của SSE:

  • Giao tiếp đơn hướng: Dữ liệu chỉ chảy từ server đến client
  • Xây dựng trên HTTP/1.1: SSE hoạt động qua HTTP thông thường, sử dụng kiểu MIME text/event-stream. Không cần bắt tay WebSocket đặc biệt.
  • Tự động kết nối lại: Trình duyệt tự động kết nối lại nếu kết nối bị mất
  • Nhẹ nhàng: Chi phí tối thiểu so với các giải pháp thời gian thực khác

Các trường hợp sử dụng phổ biến:

  • Nguồn cấp dữ liệu trực tiếp: Giá cổ phiếu, điểm thể thao, cập nhật tin tức
  • Thông báo thời gian thực: Thông báo mạng xã hội, cảnh báo hệ thống, cập nhật trạng thái
  • Theo dõi tiến trình: Tải lên tệp, các hoạt động chạy dài
  • Bảng điều khiển trực tiếp: Hệ thống giám sát, hiển thị phân tích

Triển khai SSE trong ASP.NET Core 10

Bắt đầu từ .NET 10 preview 4, ASP.NET Core đã thêm hỗ trợ cho Server-Sent Events. Bên dưới, nó thiết lập Content-Type thành text/event-stream, xử lý flushing và tích hợp với cancellation.

Hãy tạo một StockService tạo ra luồng Async của các cập nhật giá cổ phiếu:

public record StockPriceEvent(string Id, string Symbol, decimal Price, DateTime Timestamp);

public class StockService
{
    public async IAsyncEnumerable<StockPriceEvent> GenerateStockPrices(
       [EnumeratorCancellation] CancellationToken cancellationToken)
    {
       var symbols = new[] { "MSFT", "AAPL", "GOOG", "AMZN" };

       while (!cancellationToken.IsCancellationRequested)
       {
          // Chọn một ký hiệu và giá ngẫu nhiên
          var symbol = symbols[Random.Shared.Next(symbols.Length)];
          var price  = Math.Round((decimal)(100 + Random.Shared.NextDouble() * 50), 2);

          var id = DateTime.UtcNow.ToString("o");

          yield return new StockPriceEvent(id, symbol, price, DateTime.UtcNow);

          // Đợi 2 giây trước khi gửi cập nhật tiếp theo
          await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
       }
    }
}

Chúng ta có thể sử dụng kết quả TypedResults.ServerSentEvents để gửi Server-Sent Events.

Tạo một endpoint Minimal API gửi cập nhật SSE về giá cổ phiếu:

builder.Services.AddSingleton<StockService>();

app.MapGet("/stocks", (StockService stockService, CancellationToken ct) =>
{
    return TypedResults.ServerSentEvents(
       stockService.GenerateStockPrices(ct),
       eventType: "stockUpdate"
    );
});

Logic kết nối lại và Header Last-Event-ID

Một trong những tính năng mạnh mẽ nhất của SSE là kết nối lại tự động. Khi kết nối bị ngắt, trình duyệt tự động thử kết nối lại và có thể tiếp tục từ nơi đã dừng bằng cách sử dụng header Last-Event-ID.

Dưới đây là cách triển khai logic như vậy:

app.MapGet("/stocks2", (
    StockService stockService,
    HttpRequest httpRequest,
    CancellationToken ct) =>
{
    // 1. Đọc Last-Event-ID (nếu có)
    var lastEventId = httpRequest.Headers.TryGetValue("Last-Event-ID", out var id)
       ? id.ToString()
       : null;

    // 2. Tùy chọn ghi log hoặc xử lý logic tiếp tục
    if (!string.IsNullOrEmpty(lastEventId))
    {
       app.Logger.LogInformation("Đã kết nối lại, client cuối cùng thấy ID {LastId}", lastEventId);
    }

    // 3. Stream SSE với lastEventId và retry
    var stream = stockService.GenerateStockPricesSince(lastEventId, ct)
       .Select(evt =>
       {
          var sseItem = new SseItem<StockPriceEvent>(evt, "stockUpdate")
          {
             EventId = evt.Id
          };

          return sseItem;
       });

    return TypedResults.ServerSentEvents(
       stream,
       eventType: "stockUpdate"
    );
});

Cách đăng ký Server-Sent Events từ Frontend

Bạn có thể sử dụng Server-Sent Events trên frontend bằng cách sử dụng EventSource API gốc.

Chúng ta có thể sử dụng EventSource để đăng ký sự kiện “stockUpdate” trong JavaScript:

// 1. Kết nối đến endpoint SSE
const source = new EventSource('http://localhost:5000/stocks');

// 2. Lắng nghe sự kiện "stockUpdate" đã đặt tên của chúng ta
source.addEventListener('stockUpdate', e => {
    // Phân tích payload JSON
    const { symbol, price, timestamp } = JSON.parse(e.data);

    // Tạo và thêm mục danh sách mới với các lớp Tailwind
    const li = document.createElement('li');
    li.classList.add('new', 'flex', 'justify-between', 'items-center');

    // Tạo phần tử thời gian
    const timeSpan = document.createElement('span');
    timeSpan.classList.add('text-gray-500', 'text-sm');
    timeSpan.textContent = new Date(timestamp).toLocaleTimeString();

    // Tạo phần tử ký hiệu
    const symbolSpan = document.createElement('span');
    symbolSpan.classList.add('font-medium', 'text-gray-800');
    symbolSpan.textContent = symbol;

    // Tạo phần tử giá
    const priceSpan = document.createElement('span');
    priceSpan.classList.add('font-bold', 'text-green-600');
    priceSpan.textContent = `$${price}`;

    // Thêm tất cả các phần tử vào mục danh sách
    li.appendChild(timeSpan);
    li.appendChild(symbolSpan);
    li.appendChild(priceSpan);

    const list = document.getElementById('updates');
    list.prepend(li);

    // Loại bỏ highlight sau một lúc
    setTimeout(() => li.classList.remove('new'), 2000);
});

// 3. Xử lý lỗi & kết nối lại tự động
source.onerror = err => {
    console.error('Lỗi kết nối SSE:', err);
};

// 4. (Tùy chọn) Kiểm tra ID sự kiện cuối cùng nhận được
source.onmessage = e => {
    console.log('Last Event ID now:', source.lastEventId);
};

Để có thể kiểm tra cục bộ, chúng ta cần cho phép chính sách CORS trong Program.cs ở chế độ phát triển:

if (builder.Environment.IsDevelopment())
{
    builder.Services.AddCors(options =>
    {
       options.AddPolicy("AllowFrontend", policy =>
       {
          policy.WithOrigins("*")
             .AllowAnyHeader()
             .AllowAnyMethod();
       });
    });
}

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseCors("AllowFrontend");
}

SSE vs. SignalR (WebSockets)

Trong khi cả Server-Sent Events (SSE) và SignalR đều cho phép nhắn tin thời gian thực trong ASP.NET Core, chúng nhắm đến các kịch bản khác nhau và đánh đổi độ phức tạp, tính năng và sử dụng tài nguyên.

Dưới đây là sự khác biệt giữa chúng:

Giao thức:

  • SSE: HTTP/1.1 streaming (text/event-stream)
  • SignalR: WebSocket (với giao thức fallback HTTP)

Hướng giao tiếp:

  • SSE: Đơn hướng (chỉ server → client)
  • SignalR: Full-duplex (hai chiều)

Hỗ trợ trình duyệt:

  • SSE: Gốc trong hầu hết trình duyệt hiện đại
  • SignalR: WebSocket gốc + fallback qua Long Polling

Chi phí kết nối:

  • SSE: Một request HTTP đơn, định dạng tối thiểu
  • SignalR: Bắt tay WebSocket + quản lý frame

Tóm tắt

Server-Sent Events là một giải pháp thay thế dễ tích hợp cho SignalR khi bạn chỉ cần đẩy cập nhật từ server đến client.

Khi nào chọn SSE:

  • Bạn chỉ cần cập nhật server → client. Nếu client của bạn không bao giờ cần gửi tin nhắn lại qua cùng kênh, SSE đơn giản hơn.
  • Streaming nhẹ nhàng. Đối với bảng điều khiển, số liệu trực tiếp, log hoặc nguồn cấp dữ liệu giá cổ phiếu, định dạng tối thiểu và cơ sở HTTP của SSE làm cho nó dễ quản lý và gỡ lỗi.
  • Triển khai không phức tạp. Hỗ trợ trình duyệt gốc và các tiện ích lớp ASP.NET Core giúp bạn xây dựng các ứng dụng điều khiển SSE một cách dễ dàng.

Khi nào chọn SignalR:

  • Giao tiếp hai chiều. Phòng chat, bảng trắng cộng tác hoặc bất kỳ kịch bản nào mà client đẩy tin nhắn đến server và ngược lại yêu cầu WebSockets.
  • Tính năng nâng cao. SignalR Hubs cho phép bạn gọi phương thức trên các nhóm kết nối, quản lý danh tính người dùng và phát sóng đến tập hợp con của client với mã tối thiểu.
  • Hỗ trợ mở rộng quy mô. Nếu bạn mong đợi chạy ứng dụng của mình trên nhiều server hoặc trong môi trường đám mây, tích hợp Redis hoặc Azure backplane của SignalR xử lý định tuyến kết nối và phân phối tin nhắn tự động.
Chỉ mục