عنوان:

‫پیاده‌سازی Server-Sent Events در ASP.NET Core و NET 10.


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۲/۰۳ ۰۸:۴۴
آدرس: www.dntips.ir
مقدمه
در دنیای توسعه وب، ارائه به‌روزرسانی‌های بلادرنگ (Real-Time Updates) به کاربران یکی از نیازهای کلیدی بسیاری از برنامه‌های کاربردی مدرن است. از فیدهای خبری زنده گرفته تا نمایش لحظه‌ای تغییرات قیمت سهام، فناوری‌هایی که امکان ارسال داده از سرور به کلاینت را به‌صورت یک‌طرفه فراهم می‌کنند، جایگاه ویژه‌ای دارند. یکی از این فناوری‌ها، رویدادهای ارسالی از سرور (Server-Sent Events یا SSE) است که به دلیل سادگی و استفاده از پروتکل HTTP، گزینه‌ای سبک و کارآمد برای پیاده‌سازی ارتباطات بلادرنگ محسوب می‌شود. با انتشار .NET 10 و بهبودهای اعمال‌شده در ASP.NET Core، پشتیبانی داخلی از SSE در Minimal APIs، توسعه‌دهندگان را قادر ساخته است تا با کمترین پیچیدگی، این فناوری را در پروژه‌های خود به کار گیرند. این مقاله به بررسی نحوه پیاده‌سازی SSE در ASP.NET Core با استفاده از .NET 10 می‌پردازد.

Server-Sent Events چیست؟
رویدادهای ارسالی از سرور (Server-Sent Events) یک استاندارد وب است که امکان برقراری یک کانال ارتباطی یک‌طرفه از سرور به کلاینت را فراهم می‌کند. برخلاف فناوری وب‌سوکت (WebSocket) که ارتباط دوطرفه را پشتیبانی می‌کند، SSE برای سناریوهایی طراحی شده است که سرور نیاز دارد داده‌ها را به‌صورت مداوم و بدون درخواست مستقیم از کلاینت به آن ارسال کند. این فناوری بر پایه پروتکل HTTP عمل می‌کند و از فرمت متنی ساده‌ای با نوع محتوا (Content-Type) text/event-stream استفاده می‌کند.

مزایای اصلی SSE شامل موارد زیر است:
  • سادگی پیاده‌سازی: نیازی به پروتکل‌های پیچیده مانند WebSocket ندارد و با ابزارهای استاندارد HTTP قابل پیاده‌سازی است.
  • سبک بودن: به دلیل عدم نیاز به ارتباط دوطرفه، منابع کمتری مصرف می‌کند.
  • پشتیبانی گسترده: تمامی مرورگرهای مدرن از SSE پشتیبانی می‌کنند.

در مقایسه با SignalR، که معمولاً از WebSocket به‌عنوان پروتکل پیش‌فرض استفاده می‌کند، SSE گزینه‌ای سبک‌تر برای سناریوهایی است که تنها نیاز به ارسال داده از سرور به کلاینت دارند، مانند به‌روزرسانی‌های لحظه‌ای در فیدهای خبری یا داشبوردهای داده.

پیاده‌سازی SSE در ASP.NET Core با NET 10.
NET 10. با معرفی قابلیت‌های جدید در ASP.NET Core Minimal APIs، امکان استفاده مستقیم از SSE را از طریق کلاس TypedResults فراهم کرده است. این قابلیت به توسعه‌دهندگان اجازه می‌دهد تا با حداقل کد، یک نقطه پایانی (Endpoint) برای ارسال رویدادهای سرور ایجاد کنند. در ادامه، مراحل پیاده‌سازی یک مثال ساده با استفاده از Minimal APIs، یک سرویس پس‌زمینه (Background Service)، و جاوااسکریپت برای کلاینت توضیح داده می‌شود.

1. تنظیم پروژه و نقطه پایانی SSE
برای شروع، یک پروژه Minimal API در ASP.NET Core ایجاد می‌کنیم. کد زیر ساختار اولیه پروژه را نشان می‌دهد:
using System.ComponentModel;
using System.Runtime.CompilerServices;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<FoodService>();
builder.Services.AddHostedService<FoodServiceWorker>();

var app = builder.Build();
app.UseDefaultFiles().UseStaticFiles();

app.MapGet("/orders", (FoodService foods, CancellationToken token) =>
    TypedResults.ServerSentEvents(
        foods.GetCurrent(token),
        eventType: "order")
);

app.Run();
در این کد:
  • سرویس FoodService به‌عنوان یک سرویس Singleton ثبت می‌شود تا مدیریت داده‌های مشترک بین کلاینت‌ها را بر عهده گیرد.
  • سرویس پس‌زمینه FoodServiceWorker برای به‌روزرسانی دوره‌ای داده‌ها ثبت می‌شود.
  • نقطه پایانی /orders با استفاده از متد TypedResults.ServerSentEvents تعریف شده است که یک جریان (Stream) از نوع IAsyncEnumerable<string> را از متد GetCurrent دریافت می‌کند.
  • پارامتر eventType نوع رویداد را مشخص می‌کند (در اینجا order) که در سمت کلاینت برای شناسایی رویدادها استفاده می‌شود.
  • CancellationToken امکان لغو اشتراک کلاینت را فراهم می‌کند، که برای مدیریت منابع سرور ضروری است.

2. پیاده‌سازی سرویس FoodService با IAsyncEnumerable
کلاس FoodService مسئول تولید جریان داده‌ها و همگام‌سازی کلاینت‌ها با یک منبع داده واحد است. این کلاس از الگوی اعلان تغییرات (INotifyPropertyChanged) استفاده می‌کند تا تمام کلاینت‌ها به‌روزرسانی‌های یکسانی دریافت کنند:
public class FoodService : INotifyPropertyChanged
{
    public FoodService()
    {
        Current = Foods[Random.Shared.Next(Foods.Length)];
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    private static readonly string[] Foods = ["🍔", "🍟", "🥤", "🍤", "🍕", "🌮", "🥙"];

    private string Current
    {
        get => field;
        set
        {
            field = value;
            OnPropertyChanged();
        }
    }

    public async IAsyncEnumerable<string> GetCurrent(
        [EnumeratorCancellation] CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            yield return Current;
            var tcs = new TaskCompletionSource();
            PropertyChangedEventHandler handler = (_, _) => tcs.SetResult();
            PropertyChanged += handler;
            try
            {
                await tcs.Task.WaitAsync(ct);
            }
            finally
            {
                PropertyChanged -= handler;
            }
        }
    }

    public void Set()
    {
        Current = Foods[Random.Shared.Next(Foods.Length)];
    }

    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
نکات کلیدی در این کد:
  • آرایه Foods شامل اموجی‌هایی است که به‌صورت تصادفی به کلاینت‌ها ارسال می‌شوند.
  • متد GetCurrent یک جریان IAsyncEnumerable تولید می‌کند که در هر چرخه، مقدار Current را بازمی‌گرداند و منتظر تغییر بعدی می‌ماند.
  • استفاده از TaskCompletionSource و رویداد PropertyChanged تضمین می‌کند که تمام کلاینت‌ها به‌روزرسانی‌های هم‌زمان دریافت کنند.
  • CancellationToken امکان توقف جریان را در صورت قطع ارتباط کلاینت فراهم می‌کند.

3. سرویس پس‌زمینه برای به‌روزرسانی داده‌ها
برای شبیه‌سازی به‌روزرسانی‌های دوره‌ای، از یک سرویس پس‌زمینه استفاده می‌کنیم:
public class FoodServiceWorker(FoodService foodService) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            foodService.Set();
            await Task.Delay(1000, stoppingToken);
        }
    }
}
این سرویس هر ثانیه متد Set را فراخوانی می‌کند تا مقدار Current در FoodService به‌روزرسانی شود، که به‌طور خودکار تمام کلاینت‌های متصل را از طریق جریان IAsyncEnumerable مطلع می‌کند.

4. کلاینت جاوااسکریپت برای دریافت رویدادها
در سمت کلاینت، از شیء EventSource برای اشتراک در نقطه پایانی SSE استفاده می‌کنیم. کد HTML و جاوااسکریپت زیر این فرآیند را نشان می‌دهد:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Khalid's Fast-Food Fair</title>
</head>
<style>
    ul {
        display: flex;
        flex-direction: row;
        list-style: none;
        flex-wrap: wrap;
        width: 90%;
        gap: 1rem;
        padding: 0;
    }
    li {
        font-size: 2rem;
    }
</style>
<body>
    <h1>Khalid's Fast-Food Fair</h1>
    <ul id="orders"></ul>
    <script>
        const eventSource = new EventSource('/orders');
        const angelsList = document.getElementById('orders');

        eventSource.addEventListener('order', event => {
            const li = document.createElement('li');
            li.textContent = event.data;
            angelsList.appendChild(li);
        });

        eventSource.onerror = error => {
            console.error('EventSource failed:', error);
            eventSource.close();
        };
    </script>
</body>
</html>
در این کد:
  • شیء EventSource به نقطه پایانی /orders متصل می‌شود.
  • رویداد order توسط addEventListener شنیده می‌شود و داده دریافتی (اموجی غذا) به لیست ul اضافه می‌شود.
  • در صورت بروز خطا، اتصال بسته می‌شود و پیام خطا در کنسول نمایش داده می‌شود.

5. نکات پیشرفته و ارزش افزوده
برای بهبود این پیاده‌سازی در سناریوهای واقعی، می‌توان موارد زیر را در نظر گرفت:
  • مدیریت خطاها: افزودن مکانیزم‌های Retry در سمت کلاینت با استفاده از ویژگی reconnect در EventSource می‌تواند پایداری ارتباط را افزایش دهد.
  • پیکربندی سرور: در صورت استفاده از پروکسی‌هایی مانند Nginx، باید تنظیمات مربوط به Buffering غیرفعال شود تا جریان SSE به درستی کار کند.
  • استفاده از کتابخانه‌ها: کتابخانه‌هایی مانند Lib.AspNetCore.ServerSentEvents می‌توانند پیاده‌سازی SSE را ساده‌تر کنند، به‌ویژه برای مدیریت چندین نقطه پایانی یا سناریوهای پیچیده‌تر.
  • مقایسه با SignalR: در پروژه‌هایی که نیاز به ارتباط دوطرفه وجود دارد، SignalR همچنان گزینه مناسب‌تری است، اما برای سناریوهای ساده‌تر، SSE به دلیل کاهش سربار، ارجحیت دارد.

نتیجه‌گیری
رویدادهای ارسالی از سرور (Server-Sent Events) یک ابزار قدرتمند و ساده برای پیاده‌سازی به‌روزرسانی‌های بلادرنگ در برنامه‌های وب است. با معرفی پشتیبانی داخلی در .NET 10 و ASP.NET Core Minimal APIs، توسعه‌دهندگان می‌توانند با حداقل کد، نقاط پایانی SSE را ایجاد کرده و کلاینت‌ها را به‌روزرسانی کنند. مثال ارائه‌شده در این مقاله، که شامل یک سرویس پس‌زمینه، جریان IAsyncEnumerable، و کلاینت جاوااسکریپت است، نشان‌دهنده سادگی و انعطاف‌پذیری این فناوری است. برای توسعه‌دهندگانی که به دنبال راه‌حل‌های سبک و کارآمد برای ارتباطات بلادرنگ هستند، SSE جایگزینی مناسب برای فناوری‌های پیچیده‌تر مانند WebSocket و SignalR است. با این حال، انتخاب فناوری مناسب باید بر اساس نیازهای خاص پروژه انجام شود.


مشاهده مطلب اصلی