پیاده سازی سمت سرور SignalR

بعد از بیان مطالب تئوری مربوط به سیگنال، در این مقاله قصد داریم تا به پیاده سازی سمت سرور با استفاده از asp.net core نسخه 6 بپردازیم.(در نسخه های مختلف .net core عملیات مشابهی داریم و جزئیات ممکن است کمی متفاوت باشد.)سورس کد مربوط به مجموعه مقالات SignalR شامل پیاده سازی سمت سرور و کلاینت، جهت یادگیری بهتر شما عزیزان در انتهای این مقاله قابل دانلود و مشاهده است.Hub در SignalR در توسعه یک نرم افزار RealTime، گاها سرور نقش یک واسط میان چند کلاینت و گاها نقش یک منبع تزریق کننده اطلاعات به کلاینت ها مورد استفاده قرار میگیرد.کلاسهای Hub نقطه اتصال کلاینت ها به سرور سیگنال هستند. هر کلاس hub از ادرس مشخصی در شبکه (لوکال یا اینترنت) برخوردار است و کلاینت ها با استفاده از این ادرس به هاب به عنوان دروازه ای در سرور سیگنال متصل میشوند. بنابراین ابتدا باید به نصب فریم ورک مورد نیاز و پیکربندی پروژه و هاب ها بپردازیم.1) ابتدا یک پروژه از نوع Asp.net core WebApp با نام SignalRSample ایجاد میکنیم.(در این مثال جهت توضیح بهتر از mvc استفاده شده است، شما میتوانید به شکل مشابه در یک پروژه web api پیکربندی را انجام دهید.)میتوانید جهت ایجاد پروژه، مطابق تصویر زیر عمل کنید.2) بعد از ایجاد پروژه فولدر Pages را از طریق Solution Explorer در صورت وجود حذف کنید.3) فریم ورک SignalR را از طریق Nuget Package Manager به پروژه اضافه کنید.میتوانید با استفاده از دستور زیر در Package Manager Console نیز فریم ورک مد نظر را اضافه کنید.Install-Package Microsoft.AspNetCore.SignalR -Version 1.1.04) سپس باید از طریق کلاس Program (Startup در نسخه کور 5 به قبل) مشخص کنیم که در پروژه قصد داریم تا سرویس دهی سیگنال انجام دهیم. (ترتیب قرار دادن کد در کلاس Program اهمیت دارد و باید قبل از فراخوانی متد builder.Build() باشد.)builder.Services.AddSignalR();5) پس از افزودن فریم ورک، باید به ایجاد هاب بپردازیم.a. ابتدا یک فولدر با عنوان ServerHub در سورس پروژه ایجاد کنید.b. سپس کلاس زیر را در فولدر ایجاد کنید.usingMicrosoft.AspNetCore.SignalR; namespace SignalRSample.ServerHub { public class SignalHub:Hub { } }همانطور که میبینید، کلاس فوق از Hub که یک کلاس مربوط به فریم ورک سیگنال است مشتق شده است. در حال حاضر پیاده سازی خاصی از متدهای کلاس هاب نداریم و صرفا پیکربندی های ابتدایی را انجام میدهیم.6) اکنون باید یک ادرس EndPoint به هاب ایجاد شده اختصاص دهیم.در نسخه Core6 کد زیر را به کلاس Program اضافه میکنیم.app.MapHub<SignalHub>(&quot/Hub&quot);· کد بالا باید حتما قبل از فراخوانی متد app.Run() باشد.· در کد بالا مشخص کردیم که از طریق ادرس زیر، هاب همواره در دسترس است.{BaseUrl}/Hub· آدرس پایه (BaseUrl) در فایل زیر مشخص شده است./Properties/ launchSettings.jsonهمانطور که میبینید دو ادرس در تصویر فوق وجود دارد که در صورت دیباگ پروژه از طریق iis ادرس بالا و در صورت استفاده از kestrel ادرس پایین به عنوان ادرس پایه استفاده میشود.نکته مهم: ممکن است کلاینت ها در زمان اتصال به سرور با خطای Cors Error مواجه شوند. جهت جلوگیری از این خطا میتوانید تنظیمات Cors را در کلاس Program یا Startup انجام دهید.(در انتهای مقاله، سورس کد پروژه جهت دانلود قرار گرفته و تنظیمات Cors در آن قابل مشاهده است.)بعد از تنظیمات فوق و با جست و جوی آدرس هاب در مرورگر با خطای "Connection Id required" مواجه میشویم که نشان از درستی انجام پیکربندی فوق است.7) در این مرحله نوبت به تکمیل هاب و اضافه کردن متدهای لازم میرسید.در این مقاله دو نوع ارسال پیام زیر را مورد بررسی قرار میدهیم.1) Broadcast: ارسال یک پیام از سرور به تمامی کلاینت ها2) One-To-One: ارسال پیام از سرور به یک مقصد مشخصاز آنجا که سناریوهای متفاوتی به صورت تجربی وجود دارد، در ادامه مقاله ابتدا به بیان سناریو و سپس کدنویسی میپردازیم.سناریوی شماره یکارسال پیام به صورت Broadcast بر اساس درخواست یک Clientدر این سناریوی ساده، متدی به نام AllertToAll را پیاده سازی میکنیم. متد مد نظر میتواند توسط هر یکی از کلاینت ها فراخوانی شود و پس از فراخوانی شدن ، یک پیام برودکست به تمامی کلاینت ها ارسال میشود.از آنجا که این متد باید توسط یکی از کلاینت ها قابل فراخوانی باشد و سپس Broadcast انجام شود، باید آن را در کلاس هاب که دروازه ارتباطی کلاینت ها با سرور است پیاده سازی کنیم. بنابراین در کلاس هاب مطابق کد زیر، متد AllertToAll را مینویسیم.public class SignalHub:Hub { public async Task AllertToAll(string clientMessage) { await Clients.All.SendAsync(&quotAlertEvent&quot, clientMessage); } }همانطور که میبینید متد AllertToAll در کلاس هاب اضافه شده و متشکل از چند قسمت مختلف است.1) ورودی متد: کلاینت میتواند در زمان فراخوانی این متد، یک مقدار ورودی نیز به آن ارسال کند که از طریق متغیر clientMessage در دسترس است.2) بلاک دستورات متد: شامل کدی که مشخص میکند به تمامی کلاینت ها پیامی با عنوان AlertEvent و مقدار داخل متغیر clientMessage به عنوان بدنه پیام ارسال شود. (طبیعتا کلاینت در زبان برنامه نویسی خود باید متدی جهت دریافت پیام AllertEvent پیاده و ساختار ورودی مناسب پیاده سازی کند.)یک کلاینت میتواند با استفاده از ادرس هاب به سرور متصل شده و ارتباط دائمی را ایجاد کند و سپس در بستر ارتباط ایجاد شده، به متد AllertToAll در هاب پیامی ارسال کند. سپس این پیام به تمامی کلاینت ها برودکست میشود.سناریوی شماره دوارسال پیام به صورت Broadcast بر اساس یک Event در سروربدیهی است که ما الزاما نیاز نداریم تا یک کلاینت شروع کننده یک Broadcast یا هر گونه ارسال پیام باشد. گاها ما نیاز داریم تا پس از یک رویداد در سیستم، یک پیام به کاربران ارسال شود. به طور مثال کدی داشته باشیم تا در ساعت 00:00 هر شب، یک پیام به تمامی کاربران بفرستد.قاعدتا در سناریوی مطرح شده نوشتن متد در هاب کاربردی ندارد، چرا که متدهای هاب با فراخوانی یک کلاینت قابل استفاده هستند و در این سناریو یک کلاینت آغازکننده broadcast نیست. بنابراین نیاز داریم تا بتوانیم به شکل دیگری در هر نقطه از کد سمت سرور خود(خارج از هاب) یک پیام به کاربران ارسال کنیم.برای این منظور میتوانید از قطعه کدی مشابه کد زیر استفاده کنید. در این قطعه کد از طریق کلاسی غیر از هاب(در کد شما میتواند هر کلاسی در هر نقطه از یک سیستم باشد.) یک پیام ارسال میکنیم.public class SampleClass { private IHubContext<SignalHub> _hubContext; public SampleClass(IHubContext<SignalHub> hubContext) { _hubContext = hubContext; } public void SendBroadcastMessageSample() { _hubContext.Clients.All.SendAsync(&quotHelloMessage&quot, &quotHelloWord&quot); } }کلاس Sample مثالی از هر نوع کلاس در هر نقطه از پروژه است. آبجکتی از نوع IHubContext<SignalHub> در کلاس Inject شده است و با استفاده از این آبجکت میتوان همانند سناریوی قبل یک پیام به تمام کاربران برودکست نمود.اطلاعات اضافی: در سورس کد پروژه که در انتهای مقاله قرار داده شده،SampleClass در کنترلر Home به عنوان نمونه Inject و در اکشن SendTest، متد SendBroadcastSample استفاده شده است. از آنجا که مباحث Dependency Injectionخارج از موضوع سیگنال است، از توضیح آن خودداری نموده اما نمونه ای از آن در سورس کد قرار داده ایم. همچنین در صورتی که قصد دارید کلاس SampleClass قابل اینجکت در کلاسهای دیگر باشد باید قطعه کد زیر را به کلاس Programاضافه کنید.builder.Services.AddScoped<SampleClass>();سناریوی شماره سهارسال پیام از یک کلاینت به کلاینتی دیگر بر اساس UserIdدر این سناریو، ما نیاز داریم تا پیام یک کلاینت را مستقیما به کلاینت دیگری بفرستیم. بنابراین نیاز داریم تا بدون broadCast این اتفاق انجام شود بنابراین به کدی برای سازماندهی کلاینت های متصل به سرور نیاز داریم. ابتدا اشاره ای به کاری که باید انجام شود میکنیم:1) گاها کاربران در سیستم ما دارای یک شناسه منحصر به فرد(در این مثال UserId هستند) که صرف نظر از سیگنال، برای ذخیره سازی کاربران در پایگاه داده ها و مدیریت آنها از این آیدی استفاده میشود.2) کاربر پس از اتصال به سیگنال یک ConnectionId منحصر به فرد خواهد داشت.با توجه به دو مورد بالا، ما به لیستی نیاز داریم که پس از اتصال هر کاربر به سیستم، ConnectionId و UserId او را در کنار هم نگهداری کند تا در هر زمان که نیاز بود، با استفاده از UserId به ConnectionId کاربر دسترسی پیدا کنیم و سپس پیامی را به کاربر ارسال کنیم.· نکته بسیار مهم: از آنجا که مبحث توکن دهی به کاربران خارج از موضوع سیگنال است ، در این مثال به هر کلاینت یک Guid که رشته ای منحصر به فرد است به عنوان UserId اختصاص میدهیم. در مثال های واقعی معمولا UserId باید از طریق Token دریافت شود.ابتدا یک کلاس برای نگهداری UserId و ConnectionId کاربران درکنار هم ایجاد میکنیم. طبیعتا این کلاس دارای دو فیلد از نوع رشته است.public class HubConnections { public string UserId { set; get; } public string ConnectionId { set; get; } }سپس یک لیست استاتیک شامل آبجکت هایی از جنس کلاس فوق در کلاس هاب ایجاد میکنیم.public static List<HubConnections> Connections = new List<HubConnections>();پس از اتصال هر کابر به هاب، باید یک آبجکت متناسب به لیست فوق اضافه شود و با قطع اتصال از لیست فوق حذف شود. اکنون به پیاده سازی متدهای اتصال و قطع اتصال از هاب میپردازیم. برای این منظور دو متد OnConnectedAsync را OnDisconnectedAsync که متدهایی از کلاس پایه هستند override میکنیم. این دو متد به صورت خودکار با اتصال هر کاربر به هاب و قطع اتصال کاربر از هاب فراخوانی میشوند.public class SignalHub : Hub { public static List<HubConnections> Connections = new List<HubConnections>(); ........ public override Task OnConnectedAsync() { var ConnectionId = Context.ConnectionId; //در مثالهای واقعی میتوانیم یوزر آیدی را از توکن کاربر دریافت کنیم var UserId = Guid.NewGuid().ToString(); Connections.Add(new HubConnections { ConnectionId = ConnectionId, UserId = UserId, }); return base.OnConnectedAsync(); } public override Task OnDisconnectedAsync(Exception? exception) { var ConnectionId = Context.ConnectionId; Connections.RemoveAll(a => a.ConnectionId == ConnectionId); return base.OnDisconnectedAsync(exception); } }همانطور که ذکر شد متدهای مد نظر override شده و با اتصال کلاینت به هاب، مشخصات او به لیست اضافه شده و با قطع ارتباط از لیست حذف میشود. اگر میخواستیم userId را از Token دریافت کنیم میتوانستیم از کد زیر استفاده کنیم که در این مثال چون سیستم توکن دهی پیاده سازی نشده و این موضوع خارج از مبحث سیگنال است، به یوزر یک آیدی فیک به صورت Guid اختصاص دادیم.public override Task OnConnectedAsync() { var ConnectionId = Context.ConnectionId; string UserId = Context.User.Identity.Name; Connections.Add(new HubConnections { ConnectionId = ConnectionId, UserId = UserId, }); return base.OnConnectedAsync(); }با آماده شدن کانکشن میتوانیم متدهای هاب جهت تبادل پیام از یک کلاینت به کلاینت دیگر را بنویسیم.به این منظور متد SendMessage را در هاب پیاده سازی میکنیم. متد SendMessage، یک رشته Json شامل متن پیام و آیدی مقصد را از کلاینت مبدا دریافت میکند.ابتدا کلاس زیر را شامل فیلدهای آیدی کاربر مقصد و متن پیام جهت دیسریالایز کردن Json ورودی، ایجاد میکنیم.public class SendMessageInput { public string DestinationUserId { set; get; } public string Message { set; get; } }سپس متد را به شکل زیر در هاب پیاده سازی میکنیم.public class SignalHub : Hub { public static List<HubConnections> Connections = new List<HubConnections>(); ......... public async Task SendMessage(string input) { var inputObject = JsonConvert.DeserializeObject<SendMessageInput>(input); var destinationConnectionId = Connections.FirstOrDefault(a => a.UserId == inputObject.DestinationUserId).ConnectionId; awaitClients.Client(destinationConnectionId).SendAsync(&quotSendMessageEvent&quot,inputObject.Message); } }همانطور که میبینید، ابتدا رشته Json به آبجکت سیشارپی دیسریالایز شده،سپس ConnectionId مقصد بر اساس یوزر آیدی ورودی استخراج شده و سرانجام پیام مد نظر مستقیما به مقصد ارسال میشود. (به طریق مشابه با مثالهای قبل، کلاینت باید در کد خود متدی جهت دریافت پیام SendMessageEvent را پیاده سازی کند.)سناریوی شماره چهارارسال پیام از سرور به یک کلاینت خاص بر اساس UserIdبه طریق مشابه با سناریوی شماره دو، گاها ما نیاز داریم تا بدون دخالت یک کلاینت مبدا، پیامی را بر اساس ایونتی خاص در هر نقطه از سیستم(خارج از هاب) به یک کلاینت مشخص ارسال کنیم. این متد را در کلاس SampleClass با همان توضیحات سناریوی شماره دو اضافه میکنیم.public class SampleClass { private IHubContext<SignalHub> _hubContext; public SampleClass(IHubContext<SignalHub> hubContext) { _hubContext = hubContext; } ....... public void SendMessage(string userId,string message) { var destinationConnectionId =SignalHub.Connections.FirstOrDefault(a => a.UserId == userId).ConnectionId; _hubContext.Clients.Client(destinationConnectionId).SendAsync(&quotSendMessageEvent&quot, message); } }تنها تفاوت با پیاده سازی متد در هاب آن است که ارسال پیام به کاربران را با استفاده از آبجکتی که در کلاس SampleClass اینجکت شده است انجام میدهیم.همانطور که در مقاله قبلی اشاره شد، ما نیاز داشتیم تا در هر زمان بتوانیم متدی را سمت کلاینت فراخوانی کنیم، و همانطور که در کدهای فوق میبینید، ما در حال ارسال یک پیام به کلاینت هستیم.توضیحات تکمیلیدر این مقاله با عمده مطالب مربوط به پیاده سازی سیگنال سمت سرور آشنا شدیم. نکات تکمیلی زیر قابل توجه است:· فرآیند ارسال پیام به کلاینت توسط سرور را، اصطلاحا پوش کردن نیز میگویند.· با استفاده از سیگنال میتوان پروژه هایی در Scale متوسط را پیاده سازی کرد و برای پروژه های بسیار بزرگ بهتر است به سراغ گزینه های دیگر یا حتی پیاده سازی پایه ای سوکت بروید.· در این مقاله به موضوع امنیت اشاره ای نشد، چرا که بحث امنیت فراتر از سیگنال است و باید به صورت جداگانه بررسی شود.· در این مقاله به معماری کدنویسی سمت سرور اشاره ای نشد. شما میتوانید مطالب ذکر شده را به شکل متناسب با معماری خود استفاده کنید.· یکی از امکانات دیگر SignalR ایجاد گروه ها و ارسال پیام به گروه است. گروه ها متشکل از چند کلاینت هستند و هر گروه شناسه منحصر به فرد خود را دارد. میتوانید در این مورد تحقیق کنید و آموزش های لازم را فراگیرید.مشاهده و دانلود سورس کد پروژه در گیت هابمقاله مرتبط قبلی (آشنایی با فریم ورک SignalR)مقاله مرتبط بعدی (پیاده سازی SignalR به عنوان کلاینت در JavaScript)شما میتوانید این مقاله را در وبسایت آموزشی من به آدرس زیر مشاهده نمایید.https://classicode.org/B-3117برای پیگیری آموزش ها و مقالات بیشتر، به صفحه لینکدین و وبسایت من سر بزنید.www.classicode.orgwww.linkedin.com/in/mrabiee1996