مقالات › ASP.NET Core

برنامه‌های بلادرنگ با SignalR در ASP.NET Core؛ Hub، Group و کلاینت React

قابلیت‌های بلادرنگ اضافه کنید: Hub در SignalR، ارسال به کاربران و گروه‌ها، Hub با نوع قوی، احراز هویت، کلاینت React با اتصال مجدد، مقیاس‌پذیری با Redis و اشتباهات رایج.

برخی قابلیت‌ها نمی‌توانند منتظر رفرش کاربر بمانند: اعلان‌های زنده، چت، داشبوردهایی که هر ثانیه به‌روز می‌شوند، وضعیت یک کار طولانی یا نقشه‌ای که موقعیت خودروها را نشان می‌دهد. SignalR کتابخانه‌ی ASP.NET Core برای ارتباط بلادرنگ و دوطرفه بین سرور و کلاینت‌هاست. در صورت امکان از WebSocket استفاده می‌کند و در غیر این صورت خودکار سراغ روش‌های دیگر می‌رود.

SignalR چطور کار می‌کند

  • کلاینت یک اتصال پایدار به یک Endpoint از نوع Hub باز می‌کند.
  • سرور می‌تواند هر زمان متدهای کلاینت را صدا بزند (Push) و کلاینت‌ها هم متدهای سرور را صدا می‌زنند.
  • SignalR بهترین روش انتقال را انتخاب می‌کند: WebSocket، سپس Server-Sent Events و بعد Long Polling.
  • پیام‌ها به‌طور پیش‌فرض با JSON سریالایز می‌شوند (برای Payloadهای کوچک‌تر MessagePack هم وجود دارد).

۱. ساخت Hub

public class NotificationHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, "everyone");
        await base.OnConnectedAsync();
    }

    // کلاینت‌ها می‌توانند این را صدا بزنند
    public async Task JoinProject(int projectId)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, $"project-{projectId}");
    }

    public Task SendMessage(string text) =>
        Clients.Others.SendAsync("messageReceived", Context.User?.Identity?.Name, text);
}

ثبت آن:

builder.Services.AddSignalR();

app.MapHub<NotificationHub>("/hubs/notifications");

۲. Hub با نوع قوی (Strongly Typed)

نام متدها به‌صورت رشته مستعد غلط تایپی است. به‌جای آن یک اینترفیس برای کلاینت تعریف کنید:

public interface INotificationClient
{
    Task NotificationReceived(NotificationDto notification);
    Task JobProgress(Guid jobId, int percent);
}

public class NotificationHub : Hub<INotificationClient>
{
    public Task Ping() => Clients.Caller.NotificationReceived(new NotificationDto("pong"));
}

حالا کامپایلر همه‌ی فراخوانی‌های کلاینت را بررسی می‌کند.

۳. ارسال از هر جا با IHubContext

بیشتر رویدادهای بلادرنگ بیرون از Hub رخ می‌دهند: در کنترلر، سرویس یا Worker پس‌زمینه. IHubContext را تزریق کنید:

public class OrderService(AppDbContext db, IHubContext<NotificationHub, INotificationClient> hub)
{
    public async Task MarkShippedAsync(int orderId, CancellationToken ct)
    {
        var order = await db.Orders.FindAsync([orderId], ct);
        order!.Status = OrderStatus.Shipped;
        await db.SaveChangesAsync(ct);

        await hub.Clients.User(order.CustomerId.ToString())
            .NotificationReceived(new NotificationDto($"سفارش #{order.Id} ارسال شد 🚚"));
    }
}

مقصدهایی که می‌توانید به آن‌ها پیام بفرستید:

مقصد چه کسی دریافت می‌کند
Clients.All همه‌ی اتصال‌ها
Clients.Caller / Others اتصال فراخواننده / بقیه
Clients.User(userId) همه‌ی اتصال‌های یک کاربر (همه‌ی تب‌ها و دستگاه‌ها)
Clients.Group(name) اعضای یک گروه (اتاق چت، پروژه، Tenant)
Clients.Client(connectionId) یک اتصال مشخص

۴. احراز هویت

Hubها مثل کنترلرها از [Authorize] پشتیبانی می‌کنند:

[Authorize]
public class NotificationHub : Hub<INotificationClient> { }

مرورگرها نمی‌توانند روی اتصال WebSocket هدر سفارشی تنظیم کنند؛ پس با JWT، توکن به‌صورت پارامتر Query String فرستاده می‌شود. آن را در رویدادهای JwtBearer بخوانید:

options.Events = new JwtBearerEvents
{
    OnMessageReceived = context =>
    {
        var token = context.Request.Query["access_token"];
        if (!string.IsNullOrEmpty(token) && context.HttpContext.Request.Path.StartsWithSegments("/hubs"))
            context.Token = token;
        return Task.CompletedTask;
    }
};

Clients.User(id) به‌طور پیش‌فرض از Claim NameIdentifier استفاده می‌کند؛ اگر شناسه‌ی کاربر در Claim دیگری است (مثلاً sub)، آن را با یک IUserIdProvider سفارشی کنید.

۵. کلاینت React

npm install @microsoft/signalr
import * as signalR from "@microsoft/signalr";

export function useNotifications(token) {
  const [items, setItems] = useState([]);

  useEffect(() => {
    if (!token) return;

    const connection = new signalR.HubConnectionBuilder()
      .withUrl("/hubs/notifications", { accessTokenFactory: () => token })
      .withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
      .configureLogging(signalR.LogLevel.Warning)
      .build();

    connection.on("notificationReceived", (n) => setItems((prev) => [n, ...prev].slice(0, 50)));
    connection.onreconnected(() => connection.invoke("JoinProject", 42));

    connection.start()
      .then(() => connection.invoke("JoinProject", 42))
      .catch((err) => console.error("SignalR connection failed", err));

    return () => {
      connection.stop();
    };
  }, [token]);

  return items;
}

نکات کلیدی:

  • Handlerها (on) را قبل از start() ثبت کنید تا هیچ پیامی از دست نرود.
  • withAutomaticReconnect قطعی شبکه را مدیریت می‌کند؛ اما عضویت در گروه‌ها با اتصال مجدد از بین می‌رود، پس در onreconnected دوباره عضو شوید.
  • در تابع Cleanup اتصال را متوقف کنید تا اتصال تکراری ایجاد نشود (مخصوصاً در StrictMode).

۶. گزارش پیشرفت کارهای طولانی

یک الگوی کلاسیک: یک Endpoint کاری را شروع و فوراً 202 Accepted برمی‌گرداند؛ یک Worker پس‌زمینه آن را پردازش و پیشرفت را Push می‌کند:

for (var i = 1; i <= total; i++)
{
    await ProcessAsync(items[i - 1], ct);
    await hub.Clients.User(userId).JobProgress(jobId, i * 100 / total);
}

رابط کاربری به‌جای یک اسپینر که ممکن است Timeout شود، نوار پیشرفت زنده نشان می‌دهد.

۷. مقیاس‌پذیری افقی

هر سرور اتصال‌های خودش را در حافظه نگه می‌دارد. با چند نمونه پشت Load Balancer، پیامی که روی سرور A فرستاده شود به کلاینت‌های متصل به سرور B نمی‌رسد. راه‌حل‌ها:

  • Redis Backplane — builder.Services.AddSignalR().AddStackExchangeRedis(connectionString);
  • Azure SignalR Service — همه‌ی اتصال‌ها را به یک سرویس مدیریت‌شده منتقل می‌کند.
  • Sticky Session روی Load Balancer لازم است، مگر اینکه فقط از WebSocket استفاده کنید (skipNegotiation) یا Azure SignalR داشته باشید.

روی IIS، قابلیت ویندوزی WebSocket Protocol را فعال کنید؛ وگرنه SignalR بی‌صدا سراغ روش‌های کندتر می‌رود.

اشتباهات رایج

  1. ارسال Payloadهای بزرگ — شناسه یا DTO کوچک بفرستید و بگذارید کلاینت در صورت نیاز جزئیات را از API بگیرد.
  2. نگه داشتن State در نمونه‌ی Hub — Hubها Transient هستند و هر فراخوانی یک نمونه‌ی جدید می‌سازد. از Store، کش یا دیتابیس استفاده کنید.
  3. نادیده گرفتن اتصال مجدد — بعد از اتصال مجدد دوباره عضو گروه‌ها شوید و داده‌های از دست‌رفته را بارگذاری کنید.
  4. استفاده از SignalR برای همه‌چیز — درخواست/پاسخ معمولی جایش در REST است؛ SignalR را برای به‌روزرسانی‌های واقعاً زنده استفاده کنید.
  5. اعتماد به فراخوانی‌های کلاینت — ورودی متدهای Hub را درست مثل اکشن‌های کنترلر اعتبارسنجی و مجوزدهی کنید.

جمع‌بندی

SignalR ارتباط بلادرنگ و دوطرفه با انتخاب خودکار روش انتقال را به ASP.NET Core اضافه می‌کند. Hubها (ترجیحاً با نوع قوی) تعریف کنید، از هر سرویسی با IHubContext پیام Push کنید، کاربران و گروه‌ها را هدف بگیرید، Hubها را با [Authorize] و توکن در Query String امن کنید، از React با اتصال مجدد خودکار وصل شوید و با Redis یا Azure SignalR مقیاس‌پذیر کنید. از آن برای اعلان‌ها، داشبوردهای زنده، چت و پیشرفت کارها استفاده کنید و بقیه را در REST API نگه دارید.

پرسش‌های متداول

آیا SignalR فقط برای چت است؟

نه. اعلان‌ها، داشبوردهای زنده، ویرایش هم‌زمان، اینترنت اشیا و نمایش پیشرفت کارها همگی کاربردهای رایج هستند.

آیا باید WebSocket روی سرور فعال باشد؟

SignalR بدون آن هم با روش‌های جایگزین کار می‌کند، اما WebSocket بسیار کارآمدتر است. آن را روی IIS و Reverse Proxyها فعال کنید.

یک سرور چند اتصال را تحمل می‌کند؟

بسته به حجم پیام‌ها و سخت‌افزار، هزاران تا ده‌ها هزار اتصال. بیشتر از آن از Backplane یا Azure SignalR Service استفاده کنید.