امروز، کاربر انتظار دارد که دستیار هوش مصنوعی به صورت آنی (Real-time) پاسخ دهد، متنها را به صورت استریم (Stream) و کلمه به کلمه بنویسد، و فرآیندهای پیچیده پردازش تصویر یا داده را در همان لحظه گزارش کند. اگر بخواهید برای یک اپلیکیشن AI که پردازش آن ممکن است چندین ثانیه یا حتی دقیقه طول بکشد، از پروتکلهای سنتی HTTP استفاده کنید، با دو چالش بزرگ روبرو خواهید شد:
Timeout شدن درخواستها: کانکشنهای معمولی HTTP برای باز ماندن در طولانیمدت طراحی نشدهاند.
تجربه کاربری (UX) فاجعهبار: کاربر در مقابل یک صفحه قفلشده با یک لودینگ طولانی رها میشود، بدون اینکه بداند سیستم در حال پردازش است یا کرش کرده است.
اینجاست که به عنوان یک مهندس نرمافزار ارشد، باید به سراغ ابزارهای ارتباطی بلادرنگ رفت. در اکوسیستم .NET، بیرقیبترین و قدرتمندترین ابزار برای این کار SignalR است. در این مقاله تخصصی، معماری، نحوه پیادهسازی و چالشهای مهندسی استفاده از SignalR برای مدیریت ارتباطات بلادرنگ در اپلیکیشنهای AI را بررسی میکنیم.
قبل از اینکه به کدنویسی بپردازیم، باید بدانیم چرا SignalR انتخاب اول ماست. برای ایجاد ارتباطات دوطرفه (Bi-directional) و بلادرنگ، پروتکلهای مختلفی وجود دارند:
WebSockets: پروتکل استاندارد و بهینه برای ارتباطات دوطرفه و کمهزینه (Low-overhead).
Server-Sent Events (SSE): یک پروتکل یکطرفه از سرور به کلاینت که برای استریم متن (مانند خروجی ChatGPT) بسیار عالی است.
Long Polling: یک راهکار قدیمی و پرهزینه که در صورت عدم پشتیبانی از پروتکلهای بالا استفاده میشود.
SignalR یک فریمورک پکیجشده و هوشمند است که روی تمام این پروتکلها قرار میگیرد. شما نیازی ندارید خودتان را درگیر پیچیدگیهای مدیریت کانکشنهای WebSocket یا سناریوهای Fallback (سقوط به پروتکلهای پایینتر در صورت قطع ارتباط) کنید. SignalR به صورت خودکار بهترین پروتکل موجود میان کلاینت و سرور را انتخاب میکند (ترجیحاً WebSocket) و اگر به هر دلیلی قطع شد، فرآیند اتصال مجدد (Reconnection) را مدیریت میکند.
مزایای کلیدی SignalR در پروژههای AI:
Hub Absorption: مفهوم Hub در SignalR به ما اجازه میدهد کلاینتها و سرور را به صورت انتزاعی متصل کنیم، گویی کلاینت در حال صدا زدن یک متد روی سرور است و بالعکس (RPC - Remote Procedure Call).
پشتیبانی بومی از Streaming: نسخههای مدرن SignalR (از .NET Core 3.0 به بعد) از مفهوم IAsyncEnumerable پشتیبانی میکنند که کاملاً با خروجیهای استریمینگ مدلهای AI همخوانی دارد.
مدیریت گروهی (Groups): به سادگی میتوان کاربران را در گروههای مختلف (مثلاً اتاقهای چت با AI یا کانالهای مانیتورینگ پردازشهای سنگین) دستهبندی کرد.
در اپلیکیشنهای هوش مصنوعی در مقیاس بزرگ، شما هرگز نباید پردازشهای سنگین AI (مانند استنتاج مدل، پردازش تصویر یا راهاندازی پایپلاینهای RAG) را مستقیماً درون ترد (Thread) اصلی وبسرور یا خود Hub انجام دهید. انجام این کار باعث مسدود شدن (Blocking) منابع سرور و کاهش شدید دسترسیپذیری سیستم میشود.
معماری پیشنهادی و استاندارد به صورت زیر است:
کلاینت (Client): درخواست خود را از طریق SignalR Hub ارسال میکند.
هاب (SignalR Hub): درخواست را دریافت کرده، سریعاً یک شناسه (Job ID) به کلاینت برمیگرداند و کار را به یک صف پیام (Message Queue مانند RabbitMQ) یا یک کانال داخلی (System.Threading.Channels) میسپارد.
پردازشگر پسزمینه (Background Worker): کارها را از صف برمیدارد، با مدل AI (مثلاً از طریق APIهای OpenAI، HuggingFace یا مدلهای محلی مانیتور شده توسط Semantic Kernel) ارتباط برقرار میکند.
ارسال بلادرنگ خروجی: پردازشگر پسزمینه، تکههای پاسخ (Tokens) یا درصد پیشرفت کار را به صورت متوالی به SignalR Hub میفرستد و Hub آن را به کلاینت مربوطه تزریق میکند.
بیایید یک سناریوی واقعی را پیادهسازی کنیم: کاربر سوالی از هوش مصنوعی میپرسد و سرور پاسخ را کلمه به کلمه (Token-by-Token) به کلاینت استریم میکند.
۱. تعریف SignalR Hub
ابتدا هاب خود را تعریف میکنیم. این هاب وظیفه مدیریت اتصالات و فراهم کردن متدهای ارتباطی را دارد.
using Microsoft.AspNetCore.SignalR;
using System.Runtime.CompilerServices;
namespace AIRealTimeApp.Hubs
{
public class AiChatHub : Hub
{
private readonly IAiService _aiService;
public AiChatHub(IAiService aiService)
{
_aiService = aiService;
}
// متد استریمینگ برای ارسال سوال و دریافت تکه تکه پاسخ
public async IAsyncEnumerable<string> StreamAiResponse(string prompt, [EnumeratorCancellation] CancellationToken cancellationToken)
{
// فراخوانی سرویس هوش مصنوعی که خروجی را به صورت IAsyncEnumerable برمیگرداند
await foreach (var token in _aiService.GenerateTextStreamAsync(prompt, cancellationToken))
{
// هر توکن به محض آماده شدن به سمت کلاینت فرستاده میشود
yield return token;
}
}
// متدی برای کارهای طولانی مدت (مانند تولید تصویر) که وضعیت را گزارش میدهد
public async Task StartImageGeneration(string prompt)
{
var jobId = Guid.NewGuid().ToString();
// ارسال سریع شناسه به کاربر تا منتظر نماند
await Clients.Caller.SendAsync("JobStarted", jobId);
// سپردن کار به یک سرویس پسزمینه غیرمسدودکننده
_ = _aiService.EnqueueImageJob(jobId, prompt, Context.ConnectionId);
}
}
}
۲. پیادهسازی سرویس AI (شبیهسازی استریمینگ)
در این بخش، سرویسی را مینویسیم که متون را به صورت ناهمگام (Asynchronous) تولید میکند. در دنیای واقعی، اینجا جایی است که به لایبرریهایی مثل Semantic Kernel یا مستقیم به OpenAI SDK متصل میشوید.
using System.Runtime.CompilerServices;
namespace AIRealTimeApp.Services
{
public interface IAiService
{
IAsyncEnumerable<string> GenerateTextStreamAsync(string prompt, CancellationToken cancellationToken);
Task EnqueueImageJob(string jobId, string prompt, string connectionId);
}
public class AiService : IAiService
{
private readonly IHubContext<AiChatHub> _hubContext;
public AiService(IHubContext<AiChatHub> hubContext)
{
_hubContext = hubContext;
}
public async IAsyncEnumerable<string> GenerateTextStreamAsync(string prompt, [EnumeratorCancellation] CancellationToken cancellationToken)
{
// شبیهسازی اتصال به یک LLM و دریافت پاسخ کلمه به کلمه
string[] dummyWords = $"پاسخ هوش مصنوعی به پردازش درخواست شما ({prompt}): این یک متن استریم شده بلادرنگ توسط سیگنالآر است.".Split(' ');
foreach (var word in dummyWords)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(150, cancellationToken); // شبیهسازی تاخیر پردازش مدل
yield return word + " ";
}
}
public async Task EnqueueImageJob(string jobId, string prompt, string connectionId)
{
// شبیهسازی یک پردازش سنگین پسزمینه (مثلاً Stable Diffusion)
Task.Run(async () =>
{
try
{
for (int i = 10; i <= 100; i += 30)
{
await Task.Delay(1000); // زمانبر بودن فرآیند
// ارسال درصد پیشرفت به کلاینت خاص از طریق ConnectionId
await _hubContext.Clients.Client(connectionId).SendAsync("JobProgress", jobId, i);
}
string fakeImageUrl = "https://images.ai.com/generated/123.png";
await _hubContext.Clients.Client(connectionId).SendAsync("JobCompleted", jobId, fakeImageUrl);
}
catch (Exception ex)
{
await _hubContext.Clients.Client(connectionId).SendAsync("JobFailed", jobId, ex.Message);
}
});
}
}
}
۳. کانفیگ و راهاندازی در Program.cs
تنظیمات مربوط به تزریق وابستگیها و مپ کردن آدرس هاب در فایل Program.cs به شکل زیر خواهد بود:
using AIRealTimeApp.Hubs;
using AIRealTimeApp.Services;
var builder = WebApplication.CreateBuilder(args);
// اضافه کردن خدمات SignalR به کانتینر DI
builder.Services.AddSignalR();
builder.Services.AddSingleton<IAiService, AiService>();
builder.Services.AddCors(options =>
{
options.AddPolicy("CorsPolicy", policy =>
{
policy.AllowAnyHeader()
.AllowAnyMethod()
.WithOrigins("http://localhost:3000") // آدرس فرانتاند شما (مثلاً رکت)
.AllowCredentials();
});
});
var app = builder.Build();
app.UseCors("CorsPolicy");
// مپ کردن Endpoint مربوط به هاب سیگنالآر
app.MapHub<AiChatHub>("/chathub");
app.Run();
۴. پیادهسازی کلاینت (JavaScript / TypeScript)
حال ببینیم فرانتاند چگونه از این قابلیت استریمینگ فوقالعاده با استفاده از پکیج @microsoft/signalr استفاده میکند:
import * as signalR from "@microsoft/signalr";
const connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5000/chathub")
.withAutomaticReconnect() // تلاش مجدد خودکار در صورت قطع شبکه
.build();
async function start() {
try {
await connection.start();
console.log("SignalR Connected.");
} catch (err) {
console.log(err);
setTimeout(start, 5000);
}
}
connection.onclose(async () => {
await start();
});
// شروع اتصال
start();
// ۱. سناریوی استریمینگ متن
async function askAi(prompt) {
try {
// فراخوانی متد استریمینگ سرور
connection.stream("StreamAiResponse", prompt)
.subscribe({
next: (item) => {
// این متد با آمدن هر کلمه جدید اجرا میشود
document.getElementById("chatBox").innerText += item;
},
complete: () => {
console.log("استریم پایان یافت.");
},
error: (err) => {
console.error("خطا در استریم:", err);
},
});
} catch (err) {
console.error(err);
}
}
// ۲. سناریوی کارهای طولانی (تولید تصویر)
connection.on("JobStarted", (jobId) => {
console.log(`پردازش آغاز شد. شناسه کار: ${jobId}`);
});
connection.on("JobProgress", (jobId, progress) => {
console.log(`کار ${jobId} به میزان ${progress}% پیش رفته است.`);
document.getElementById("progressBar").style.width = `${progress}%`;
});
connection.on("JobCompleted", (jobId, imageUrl) => {
console.log(`کار ${jobId} با موفقیت پایان یافت.`);
document.getElementById("resultImage").src = imageUrl;
});
در سناریوهای پیچیدهتر هوش مصنوعی، مانند پردازش ویدیو با بینایی ماشین (Computer Vision) یا ساختارهای زنجیرهای هوش مصنوعی (Agentic Workflows)، قضیه فراتر از استریم کردن چند کلمه متن است. یک Agent ممکن است ابتدا در وب سرچ کند، سپس یک دیتابیس گرافی را تحلیل کند و در نهایت خروجی را جمعآوری کند.
برای نمایش این مراحل به کاربر، ما از الگوی State/Event Notification استفاده میکنیم:
public async Task ExecuteComplexAgentWorkflow(string taskDescription)
{
var connectionId = Context.ConnectionId;
// مرحله اول: تجزیه تحلیل تسک
await Clients.Client(connectionId).SendAsync("WorkflowStatus", "Analyzing", "در حال تحلیل ساختار درخواست شما...");
await Task.Delay(1000);
// مرحله دوم: جستجوی منابع
await Clients.Client(connectionId).SendAsync("WorkflowStatus", "Searching", "در حال جستجو در پایگاه داده دانش...");
await Task.Delay(1500);
// مرحله سوم: تولید پاسخ نهایی
await Clients.Client(connectionId).SendAsync("WorkflowStatus", "Generating", "در حال ساختاربندی پاسخ نهایی توسط LLM...");
// در نهایت خروجی اصلی فرستاده میشود
}
این مدل از طراحی نرمافزار، اصطکاک روانی کاربر با سیستم (Cognitive Load) را به شدت کاهش میدهد، چرا که کاربر دقیقاً میداند هوش مصنوعی در هر ثانیه روی چه چیزی تمرکز کرده است.
وقتی اپلیکیشن شما از محیط توسعه فراتر رفته و میزبان هزاران کاربر همزمان میشود، با چالشهای جدی در لایه زیرساخت و مموری مواجه خواهید شد. در ادامه به مهمترین چالشها و راهحلهای معماری آنها میپردازیم.
۱. چالش ارتباطات ایالتی (Stateful) و نیاز به Redis Backplane
پروتکل HTTP یک پروتکل بدون حالت (Stateless) است؛ هر سروری در پشت Load Balancer میتواند به هر درخواستی پاسخ دهد. اما SignalR بر پایه اتصالات مداوم (Persistent Connections) کار میکند. کلاینت به سرور شماره ۱ متصل است. اگر یک پردازشگر پسزمینه که به سرور شماره ۲ متصل است بخواهد پیامی به کلاینت بفرستد، سرور شماره ۲ هیچ ایدهای ندارد که کلاینت کجاست!
راهکار: استفاده از SignalR Redis Backplane. با اضافه کردن این لایه، تمام سرورهای شما از طریق یک کانتینر یا کلاستر Redis با یکدیگر از طریق الگوی Pub/Sub ارتباط برقرار میکنند. وقتی سرور ۲ میخواهد پیامی به کلاینت بفرستد، پیام را در Redis منتشر میکند و سرور ۱ آن را دریافت کرده و به کلاینت تحویل میدهد.
builder.Services.AddSignalR().AddStackExchangeRedis("localhost:6379", options => {
options.Configuration.ChannelPrefix = "AiApp_SignalR";
});
۲. مدیریت Backpressure (فشار برگشتی) در استریم دادهها
سرعت تولید داده توسط سرور یا مدل AI ممکن است بسیار سریعتر از سرعت پردازش و رندرینگ مرورگر یا اپلیکیشن موبایل کلاینت باشد. این پدیده باعث انباشتگی دادهها در بافر کلاینت و در نهایت کندی شدید یا کرش کردن کلاینت میشود.
راهکار: * تجميع دادهها (Batching): به جای فرستادن کلمه به کلمه دادهها در هر میلیثانیه، کلمات را در دستههای کوچکتر (مثلاً جملات یا بخشهای ۵ کلمهای) بستهبندی کرده و سپس ارسال کنید.
مکانیزم پینگ-پونگ کنترلشده: کلاینت پس از رندر کردن هر بخش از داده، یک سیگنال تایید کوچک به سرور بفرستد تا سرور پارت بعدی داده را آزاد کند (البته این روش سرعت کلی را کاهش میدهد و باید با دقت پیادهسازی شود).
۳. مدیریت امنیت و احراز هویت (Authentication)
از آنجا که دسترسی به کانالهای هوش مصنوعی هزینهبر است (استفاده از توکنهای API مدلهای تجاری گران است)، نباید به کاربران ناشناس اجازه باز کردن کانالهای استریمینگ SignalR را داد.
راهکار: سیگنالآر اتصال اولیهاش را از طریق چنل HTTP ایجاد میکند، بنابراین میتوانید توکن JWT خود را از طریق Query String یا هدرهای درخواست ارسال کنید:
const connection = new signalR.HubConnectionBuilder()
.withUrl("http://localhost:5000/chathub", {
accessTokenFactory: () => "YOUR_JWT_ACCESS_TOKEN"
})
.build();
در سمت سرور نیز به سادگی با استفاده از اتریبیوت [Authorize] روی هاب، امنیت آن را تضمین میکنید.
برای درک بهتر جایگاه SignalR در پروژههای بزرگ هوش مصنوعی، جدول زیر مقایسهای جامع بین متدهای مختلف ارائه میدهد:
| شاخص ارزیابی | SignalR (.NET) | WebSockets خام | gRPC Streams | HTTP SSE |
| نوع ارتباط | دوطرفه هوشمند | دوطرفه خام | دوطرفه مالتیپلکس | یکطرفه (سرور به کلاینت) |
| پشتیبانی از مرورگر | عالی (فوقالعاده) | عالی | محدود (نیاز به grpc-web) | عالی |
| مدیریت قطع اتصال | خودکار و بومی | نیاز به کدنویسی دستی | نیاز به مدیریت دستی | خودکار (توسط مرورگر) |
| فرمت دادهها | JSON و MessagePack | متن و باینری | باینری (Protobuf) | متن (Text/Event-Stream) |
| مناسب برای اپلیکیشن AI | بسیار عالی (جامع) | خوب (پیچیدگی بالا) | عالی برای ارتباط سرور با سرور | عالی برای چتهای متنی ساده |
همانطور که مشخص است، اگرچه gRPC برای ارتباطات بین مایکروسرویسها (مثلاً اتصال وبسرور داتنتی به سرور پایتونی که مدل هوش مصنوعی را هاست کرده) به دلیل سرعت باینری خیرهکنندهاش بهترین گزینه است، اما برای ارتباط سرور با اپلیکیشنهای فرانتاند وب و موبایل، SignalR به دلیل سازگاری بالا و سادگی در پیادهسازی، برنده مطلق است.
تلفیق قدرت پردازشی مدلهای هوش مصنوعی با توانمندی بلادرنگ SignalR، به مهندسان نرمافزار این امکان را میدهد تا سیستمهایی با تجربه کاربری بینقص، پویا و زنده خلق کنند. در این مقاله آموختیم که چگونه از فرآیندهای مسدودکننده (Blocking) دوری کنیم، چطور خروجی یک LLM را به صورت نامتقارن استریم کنیم و چگونه معماری خود را برای پاسخگویی به حجم وسیعی از کاربران با استفاده از ابزارهایی مانند Redis مجهز کنیم.
به عنوان یک قانون طلایی در مهندسی نرمافزار هوش مصنوعی به خاطر داشته باشید: هر چقدر زمان پردازش مدل شما طولانیتر است، ارتباط شما با کاربر باید شفافتر و بلادرنگتر باشد. SignalR پلی مستحکم برای عبور از این چالش معماری است.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.