مهمترین عیب این روش این است که در این حالت تمام متدهای این سرویس رو هم به صورت Sync و هم به صورت Async تولید میکنه در حالی که ما فقط نیاز به یک متد Async داریم.
در این پست قصد دارم پیاده سازی این متد رو بدون استفاده از Async&Await و Code Generation توکار دات نت شرح بدم که با دات نت 3.5 هم سازگار است.
ابتدا یک پروژه از نوع WCF Service Application ایجاد کنید.
یک ClassLibrary جدید به نام Model بسازید و کلاس زیر را به عنوان مدل در آن قرار دهید.(این اسمبلی باید هم به پروژههای کلاینت و هم به پروژههای سرور رفرنس داده شود)
[DataContract] public class Book { [DataMember] public int Code { get; set; } [DataMember] public string Title { get; set; } [DataMember] public string Author { get; set; } }
#Class Library به نام Contract بسازید. قصد داریم از این لایه به عنوان قراردادهای سمت کلاینت و سرور استفاده کنیم. اینترفیس زیر را به عنوان BookContract در آن بسازید.
[ServiceContract] public interface IBookService { [OperationContract( AsyncPattern = true )] IAsyncResult BeginGetAllBook( AsyncCallback callback, object state ); IEnumerable<Book> EndGetAllBook( IAsyncResult asyncResult ); }
[OperationContract] IEnumerable<Book> GetAllBook(int code , AsyncCallback callback, object state );
public class CompletedAsyncResult<TEntity> : IAsyncResult where TEntity : class , new() { public IList<TEntity> Result { get { return _result; } set { _result = value; } } private IList<TEntity> _result; public CompletedAsyncResult( IList<TEntity> data ) { this.Result = data; } public object AsyncState { get { return ( IList<TEntity> )Result; } } public WaitHandle AsyncWaitHandle { get { throw new NotImplementedException(); } } public bool CompletedSynchronously { get { return true; } } public bool IsCompleted { get { return true; } } }
public class BookService : IBookService { public BookService() { ListOfBook = new List<Book>(); } public List<Book> ListOfBook { get; private set; } private List<Book> CreateListOfBook() { Parallel.For( 0, 10000, ( int counter ) => { ListOfBook.Add( new Book() { Code = counter, Title = String.Format( "Book {0}", counter ), Author = "Masoud Pakdel" } ); } ); return ListOfBook; } public IAsyncResult BeginGetAllBook( AsyncCallback callback, object state ) { var result = CreateListOfBook(); return new CompletedAsyncResult<Book>( result ); } public IEnumerable<Book> EndGetAllBook( IAsyncResult asyncResult ) { return ( ( CompletedAsyncResult<Book> )asyncResult ).Result; } }
#کدهای سمت کلاینت:
اکثر برنامه نویسان با استفاده از روش AddServiceReference یک سرویس کلاینت در اختیار خواهند داشت که با وهله سازی از این کلاس یک ChannelFactory ایجاد میشود. در این پست به جای استفاده از Code Generation توکار دات نت برای ساخت ChannelFactory از روش دیگری استفاده خواهیم کرد. به عنوان برنامه نویس باید بدانیم که در پشت پرده عملیات ساخت ChannelFactory چگونه است.
بعد از اضافه شدن سرویس سمت کلاینت کدهای زیر برای سرویس Book به صورت زیر تولید میشود.
[System.Diagnostics.DebuggerStepThroughAttribute()] [System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "4.0.0.0")] public partial class BookServiceClient : System.ServiceModel.ClientBase<UI.BookService.IBookService>, UI.BookService.IBookService { public BookServiceClient() { } public BookServiceClient(string endpointConfigurationName) : base(endpointConfigurationName) { } public BookServiceClient(string endpointConfigurationName, string remoteAddress) : base(endpointConfigurationName, remoteAddress) { } public BookServiceClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : base(endpointConfigurationName, remoteAddress) { } public BookServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : base(binding, remoteAddress) { } public UI.BookService.Book[] BeginGetAllBook() { return base.Channel.BeginGetAllBook(); } }
System.ServiceModel.ClientBase<UI.BookService.IBookService>
یک پروژه ConsoleApplication سمت کلاینت ایجاد کنید. برای فراخوانی متدهای سرویس سمت سرور باید ابتدا تنظیمات EndPoint رو به درستی انجام دهید. سپس با استفاده از EndPoint به راحتی میتوانیم Channel مربوطه را بسازیم.
کلاسی به نام ServiceMapper ایجاد میکنیم که وظیفه آن ساخت ChannelFactory به ازای درخواستها است.
public class ServiceMapper<TChannel> { public static TChannel CreateChannel() { TChannel proxy; var endPointAddress = new EndpointAddress( "http://localhost:7000/" + typeof( TChannel ).Name.Remove( 0, 1 ) + ".svc" ); var httpBinding = new BasicHttpBinding(); ChannelFactory<TChannel> factory = new ChannelFactory<TChannel>( httpBinding, endPointAddress ); proxy = factory.CreateChannel(); return proxy; } }
"http://localhost:7000/" + "BookService.svc"
بعد از اعمال تغییرات زیر در فایل Program پروژه Console و اجرای آن، خروجی به صورت زیر میباشد.
var channel = ServiceMapper<Contract.IBookService>.CreateChannel(); channel.BeginGetAllBook( new AsyncCallback( ( asyncResult ) => { channel.EndGetAllBook( asyncResult ).ToList().ForEach( _record => { Console.WriteLine( _record.Title ); } ); } ) , null ); Console.WriteLine( "Loading..." ); Console.ReadLine();
خروجی :
نکته: برای اینکه مطمئن شوید که سرویس مورد نظر در آدرس "http"//localhost:7000/" هاست شده است(یعنی همان آدرسی که در EndPointAddress از آن استفاه کردیم) کافیست از پنجره Project Properties برای پروژه سرویس وارد برگه Web شده و از بخش Servers گزینه Use Visual Studio Development Server و Specific Port 7000 رو انتخاب کنید.
OutputCaching باعث میشود خروجیِ یک اکشن متد در حافظه نگهداری شود. با اعمال این نوع کشینگ، ASP.NET در خواستهای بعدی به این اکشن را تنها با بازگرداندن همان مقدار قبلی ِ نگهداری شده در کش، پاسخ میدهد. در حقیقت با OutputCaching از تکرار چند باره کد درون یک اکشن در فراخوانیهای مختلف جلوگیری کردهایم. کش کردن باعث میشود که کارایی و سرعت سایت افزایش یابد؛ اما باید دقت کنیم که چه موقع و چرا از کَش کردن استفاده میکنیم و چه موقع باید از این کار امتناع کرد.
فواید کَش کردن
- انجام عملیات هزینه دار فقط یکبار صورت میگیرد. (هزینه از لحاظ فشار روی حافظه سرور و کاهش سرعت بالا آمدن سایت)
- بار روی سرور در زمانهای پیک کاهش مییابد.
- سرعت بالا آمدن سایت بیشتر میشود.
چه زمانی باید کَش کرد؟
- وقتی محتوای نمایشی برای همه کاربران یکسان است.
- وقتی محتوای نمایشی برای نمایش داده شدن، فشار زیادی روی سرور تحمیل میکند.
- وقتی محتوای نمایشی به شکل مکرر در طول روز باید نمایش داده شود.
- وقتی محتوای نمایشی به طور مکرر آپدیت نمیشود. (در مورد تعریف کیفیت "مکرر"، برنامه نویس بهترین تصمیم گیرنده است)
طرح مساله
فرض کنید صفحه اول سایت شما دارای بخشهای زیر است :
خلاصه اخبار بخش علمی، خلاصه اخبار بخش فرهنگی ، ده کامنت آخر، لیستی از کتگوریهای موجود در سایت.
روشهای مختلفی برای کوئری گرفتن وجود دارد، به عنوان مثال ما به کمک یک یا چند کوئری و توسط یک ViewModel جامع، میخواهیم اطلاعات را به سمت ویو ارسال کنیم. پس در اکشن متد Index ، حجم تقریبا کمی از اطلاعات را باید به کمک کوئری(کوئری های) تقریبا پیچیده ای دریافت کنیم و اینکار به ازای هر ریکوئست هزینه دارد و فشار به سرور وارد خواهد شد. از طرفی میدانیم صفحه اول ممکن است در طول یک یا چند روز تغییر نکند و همچنین شاید در طول یکساعت چند بار تغییر کند! به هر حال در جایی از سایت قرار داریم که کوئری (کوئری های) مورد نظر زیاد صدا زده میشوند ، در حقیقت صفحه اول احتمالا بیشترین فشار ترافیکی را در بین صفحات ما دارد، البته این فقط یک احتمال است و ما دقیقا از این موضوع اطلاع نداریم.
یکی از راههای انجام یک کش موفق و دانستن لزوم کش کردن، این است که دقیقا بدانیم ترافیک سایت روی چه صفحه ای بیشتر است. در واقع باید بدانیم در کدام صفحه "هزینهی اجرای عملیات موجود در کد" بیشترین است.
فشار ترافیکی(ریکوئستهای زیاد) و آپدیتهای روزانهی دیتابیس را، در دو کفه ترازو قرار دهید؛ چه کار باید کرد؟ این تصمیمی است که شما باید بگیرید. نگرانی خود را در زمینه آپدیتهای روزانه و ساعتی کمتر کنید؛ در ادامه راهی را معرفی میکنیم که آپدیتهای هر از گاهِ شما، در پاسخِ ریکوئستها دیده شوند. کمی کفهی کش کردن را سنگین کنید.
به هر حال، فعال کردن قابلیت کش کردن برای یک اکشن، بسیار ساده است، کافیست ویژگی ( attribute ) آن را بالای اکشن بنویسید :
[OutputCache(Duration = "60", Location = OutputCacheLocation.Server)] public ActionResult Index() { //کوئری یا کوئریهای لازم برای استفاده در صفحه اصلی و تبدیل آن به یک ویو مدل جامع }
[OutputCache(CacheProfile = "FirstPageIndex",Location=OutputCacheLocation.Server)] public ActionResult Index() { //کوئری یا کوئریهای لازم برای استفاده در صفحه اصلی و تبدیل آن به یک ویو مدل جامع }
دو روش فوق برای کش کردن خروجی Index از لحاظ عملکرد یکسان است، به شرطی که در حالت دوم در وب کانفیگ و در بخش system.web آن ، یک پروفایل ایجاد کنیم کنیم :
<caching> <outputCacheSettings> <outputCacheProfiles> <add name="FirstPageIndex" duration="60"/> </outputCacheProfiles> </outputCacheSettings> </caching>
در حالت دوم ما یک پروفایل برای کشینگ ساخته ایم و در ویژگی بالای اکشن متد، آن پروفایل را صدا زده ایم. از لحاظ منطقی در حالت دوم، چون امکان استفاده مکرر از یک پروفایل در جاهای مختلف فراهم شده، روش بهتری است. محل ذخیره کش نیز در هر دو حالت سرور تعریف شده است.
برای تست عملیات کشینگ، کافیست یک BreakPoint درون Index قرار دهید و برنامه را اجرا کنید. پس از اجرا، برنامه روی Break Point میایستد و اگر F5 را بزنیم، سایت بالا میآید. بار دیگر صفحه را رفرش کنیم، اگر این "بار دیگر" در کمتر از 60 ثانیه پس از رفرش قبلی اتفاق افتاده باشد برنامه روی Break Point متوقف نخواهد شد، چون خروجی اکشن، در کش بر روی سرور ذخیره شده است و این یعنی ما فشار کمتری به سرور تحمیل کرده ایم، صفحه با سرعت بالاتری در دسترس خواهد بود.
ما از تکرار اجرای کد جلوگیری کرده ایم و عدم اجرای کد بهترین نوع بهینه سازی برای یک سایت است. [اسکات الن، پلورال سایت]
چطور زمان مناسب برای کش کردن یک اکشن را انتخاب کنیم؟
- کشینگ با زمان کوتاه؛ فرض کنید زمان کش را روی 1 ثانیه تنظیم کرده اید. این یعنی اگر ریکوئست هایی به یک اکشن ارسال شود و همه در طول یک ثانیه اتفاق بیفتد، آن اکشن فقط برای بار اول اجرا میشود، و در بارهای بعد(در طول یک ثانیه) فقط محتوای ذخیره شده در آن یک اجرا، بدون اجرای جدید، نمایش داده میشود. پس سرور شما فقط به یک ریکوئست در ثانیه در طول روز جواب خواهد داد و ریکوئستهای تقریبا همزمان دیگر، در طول همان ثانیه، از نتایج آن ریکوئست (اگر موجود باشد) استفاده خواهند کرد
- کشینگ با زمان طولانی؛ ما در حقیقت با اینکار منابع سرور را حفاظت میکنیم، چون عملیاتِ هزینه دار(مثل کوئریهای حجیم) تنها یکبار در طول زمان کشینگ اجرا خواهند شد. مثلا اگر تنظیم زمان روی عدد 86400 تنظیم شود(یک روز کامل)، پس از اولین ریکوئست به اکشن مورد نظر، تا 24 ساعت بعد، این اکشن اجرا نخواهد شد و فقط خروجی آن نمایش داده خواهد شد. آیا دلیلی دارد که یک کوئری هزینه دار را که قرار نیست خروجی اش در طول روز تغییر کند به ازای هر ریکوئست یک بار اجرا کنیم؟
اگر اطلاعات موجود در دیتابیس را تغییر دهیم چه کار کنیم که کشینگ رفرش شود؟
فرض کنید در همان مثال ابتدای این مقاله، شما یک پست به دیتابیس اضافه کرده اید، اما چون مثلا duration مربوط به کشینگ را روی 86400 تعریف کرده اید تا 24 ساعت از زمان ریکوئست اولیه نگذرد، سایت آپدیت نخواهد شد و محتوا همان چیزهای قبلی باقی خواهند ماند. اما چاره چیست؟
کافیست در بخش ادمین، وقتی که یک پست ایجاد میکنید یا پستی را ویرایش میکند در اکشنهای مرتبط با Create یا Edit یا Delete چنین کدی را پس از فرمان ذخیره تغییرات در دیتابیس، بنویسید:
Response.RemoveOutputCacheItem(Url.Action("index", "home"));
واضح است که ما داریم کشینگ مرتبط با یک اکشن متد مشخص را پاک میکنیم. با اینکار در اولین ریکوئست پس از تغییرات اعمال شده در دیتابیس، ASP.NET MVC چون میبیند کَشی برای این اکشن وجود ندارد، متد را اجرا میکند و کوئریهای درونش را خواهد دید و اولین ریکوئست پیش از کَش شدن را انجام خواهد داد. با اینکار کشینگ ریست شده است و پس از این ریکوئست و استخراج اطلاعات جدید، زمان کشینگ صفر شده و آغاز میشود.
میتوانید یک دکمه در بخش ادمین سایت طراحی کنید که هر موقع دلتان خواست کلیه کشها را به روش فوق پاک کنید! تا اپلیکیشن منتظر ریکوئستهای جدید بماند و کشها دوباره ایجاد شوند.
جمع بندی
ویژگی OutputCatch دارای پارامترهای زیادیست و در این مقاله فقط به توضیح عملکرد این اتریبیوت اکتفا شده است. بطور کلی این مبحث ظاهر ساده ای دارد، ولی نحوه استفاده از کشینگ کاملا وابسته به هوش برنامه نویس است و پیچیدگیهای مرتبط با خود را دارد. در واقع خیلی مشکل است که بتوانید یک زمان مناسب برای کش کردن تعیین کنید. باید برنامه خود را در یک محیط شبیه سازی تحت بار قرار دهید و به کمک اندازه گیری و محاسبه به یک قضاوت درست از میزان زمان کش دست پیدا کنید. گاهی متوجه خواهید شد، از مقدار زیادی از حافظه سیستم برای کش کردن استفاده کرده اید و در حقیقت آنقدر ریکوئست ندارید که احتیاج به این هزینه کردن باشد.
یکی از روشهای موثر برای دستیابی به زمان بهینه برای کش کردن استفاده از CacheProfile درون وب کانفیگ است. وقتی از کشینگ استفاده میکنید، در همان ابتدا مقدار زمانی مشخص برای آن در نظر نگرفته اید(در حقیقت مقدار زمان مشخصی نمیدانید) پس مجبور به آزمون و خطا و تست و اندازه گیری هستید تا بدانید چه مقدار زمانی را برای چه پروفایلی قرار دهید. مثلا پروفایل هایی به شکل زیر تعریف کرده اید و نام آنها را به اکشنهای مختلف نسبت داده اید. به راحتی میتوانید از طریق دستکاری وب کانفیگ مقادیر آن را تغییر دهید تا به حالت بهینه برسید، بدون آنکه کد خود را دستکاری کنید.
<caching> <outputCacheSettings> <outputCacheProfiles> <add name="Long" duration="86400"/> <add name="Average" duration="43600"/> <add name="Short" duration="600"/> </outputCacheProfiles> </outputCacheSettings> </caching>
برای مطالعه جزئیات بیشتر در مورد OutputCaching مقالات زیر منابع مناسبی هستند.
در نرم افزارهای بزرگ و چند کاربره، اتصال به بانک اطلاعاتی کامپیوتر سرور، یکی از نیازهای اساسی برنامه نویسان محسوب میگردد. در این بخش با دو اصطلاح بسیار مهم سروکار داریم.
1. کلاینت (Client): منظور از کلاینت کامپیوتری است که میخواهد به سرور متصل گردد و از SQL کامپیوتر سرور خدماتی را دریافت نماید.
2. سرور (Server): کامپیوتری است که میخواهیم به آن متصل شویم و دادهها را بصورت متمرکز بر روی آن ذخیره و بازیابی نماییم.
به دو روش میتوان به سرور متصل شد:
1. Windows Authentication
در این روش جهت اتصال به بانک اطلاعاتی، کامپیوتر مبدا یا Client باید عضو شبکه ای باشد کهServer در آن وجود دارد. در واقع برای شبکه هایی استفاده میشوند که دارای Domain می باشند وClient به عنوان یک کاربر شناخته شده در سرور تعریف شده است.
2. SQL Authentication
در این روش کلاینت به عنوان یک کاربر یا Login در SQL تعریف شده است و دارای نام کاربری و رمز عبور میباشد.
جهت اتصال از راه دور به یک سرور دارای SQL، باید تنظیمات زیر را برای کامپیوتر سرور انجام دهیم:
1. به SQL Server کامپیوتر سرور متصل شوید.
2. در پنجره Object Explorer بر روی نام سرور (اولین آیتم موجود در لیست) کلیک راست کنید و گزینهProperties را انتخاب نمایید.
3. در پنجره ظاهر شده (Server Properties) و در قسمت Select a page (سمت چپ پنجره) بر رویSecurity کلیک کنید.
4. در سمت راست پنجره گزینه SQL Server and Windows Authentication mode را انتخاب کنید.
5. دکمه OK را انتخاب کنید. پنجره پیغامی مبنی بر Restart کردن سرور نمایش داده میشود. این پنجره را تایید کنید.
6. مجددا بر روی نام سرور کلیک راست کنید و گزینه Restart را انتخاب نموده و در پیغام ظاهر شده Yesرا انتخاب نمایید.
تا به اینجا سرور آماده پذیرش اتصال از راه دور بصورت SQL Authentication می باشد. حال نوبت به تعریف یک Login می باشد تا توسط این Login بتوانید به سرور از راه دور متصل شوید. مراحل زیر را برای تعریفLogin دنبال کنید:
1. در پنجره Object Explorer به مسیر Security > Logins بروید.
2. بر روی پوشه Logins کلیک راست نموده و گزینه New Login… را انتخاب نمایید.
3. در پنجره ظاهر شده در بخش Login name نامی را به کاربر اختصاص دهید. (به عنوان مثال user1)
4. گزینه SQL Server authentication را انتخاب نموده و در بخش Password و Confirm password رمز عبوری را به این کاربر اختصاص دهید. (به عنوان مثال abc123)
5. گزینه Enforce password policy را از حالت انتخاب خارج کنید تا رمز عبور را از قید سیاستهای رمزگذاری ویندوز خارج کنید.
6. در قسمت Select a page (سمت چپ پنجره) بر روی Server Roles کلیک کنید.
7. در سمت راست پنجره گزینه sysadmin یا هر نوع دسترسی دیگری را که مایل هستید انتخاب نمایید.
توجه: با انتخاب sysadmin کاربر ایجاد شده به کل سرور و بانکهای اطلاعاتی دسترسی کامل یاAdmin دارد. اگر نمیخواهید کاربر چنین دسترسی داشته باشد، در بخش فوق فقط گزینه public انتخاب شده باشد.
8. در قسمت Select a page (سمت چپ پنجره) بر روی User Mapping کلیک کنید. در این بخش نحوه دسترسی کاربر را به بانکهای اطلاعاتی موجود، مشخص میکنیم.
9. در سمت راست پنجره و در بخش Users mapped to this login یک یا چند بانک اطلاعاتی را که میخواهید توسط این Login قابل دسترسی باشند را انتخاب نمایید.
10. پس از انتخاب هر بانک اطلاعاتی، در قسمت پایین (Database role membership for:) نوع دسترسی کاربر به آن Database را انتخاب کنید. در اینجا من db_owner را انتخاب میکنم تا کاربر دسترسی کامل به بانک اطلاعاتی انتخاب شده را داشته باشد.
11. دکمه OK را انتخاب کنید تا Login مورد نظر ساخته شود.
حالا میتوانید از راه دور و حتی از روی خود سرور با کاربر ایجاد شده به سرور متصل شوید. برای این منظور SQL را Disconnect نمایید و یا یکبار SQL Server Management Studio (SSMS) را ببندید و دوباره اجرا نمایید. در پنجره Connect to Server اطلاعات زیر را وارد نمایید:
Server name :نام یا IP سرور (به عنوان مثال 192.168.0.1)
Authentication: انتخاب گزینه SQL Server Authentication
Login: طبق مثال user1
Password: طبق مثال abc123
پس از ورود با مشخصات فوق فقط میتوانید به بانک اطلاعاتی دسترسی باشید که در قسمت User Mapping انتخاب کرده بودید. اگر sysadmin را انتخاب کرده باشید به تمامی بانکهای اطلاعاتی موجود دسترسی دارید.
برخی مشکلات اتصال از راه دور
ممکن است در زمان اتصال از راه دور با مشکل عدم امکان اتصال به سرور مواجه شوید. برای این منظور و اطمینان از صحت تنظیمات سرور، موارد زیر را در سرور بررسی نمایید تا بدرستی تنظیم شده باشند:
1. به مسیر Start > All Programs > Microsoft SQL Server 2008/2005 > Configuration Tools > SQL Server Configuration Manager مراجعه کنید و موارد زیر را بررسی نمایید:
1.1. بر روی SQL Server Services کلیک کنید و در سمت راست پنجره بررسی کنید که ستون Stateمربوط به SQL Server Browser و SQL Server در وضعیت Running باشد.
1.2. بر روی آیتمهای زیر مجموعه SQL Server Network Configuration کلیک کنید و در سمت راست پنجره بررسی کنید که آیتم های Shared Memory، Named Pipes و TCP/IP در وضعیت Enabled باشند.
1.3. بر روی آیتم SQL Native Client Configuration > Client Protocols کلیک کنید و در سمت راست پنجره بررسی کنید که آیتم های Shared Memory، Named Pipes و TCP/IP در وضعیت Enabled باشند.
2. بررسی کنید که فایروال سیستم سرور غیر فعال باشد و یا SQL Server به برنامه های Trust فایروال اضافه شده باشد.
پیشنیازها
برای دنبال کردن این مثال فرض بر این است که NET Core 2.0 SDK. و همچنین Angular CLI را نیز پیشتر نصب کردهاید. مابقی بحث توسط خط فرمان و ابزارهای dotnet cli و angular cli ادامه داده خواهند شد و الزامی به نصب هیچگونه IDE نیست و این مثال تنها توسط VSCode پیگیری شدهاست.
تدارک ساختار ابتدایی مثال جاری
ساخت برنامهی وب، توسط dotnet cli
ابتدا یک پوشهی جدید را به نام SignalRCore2Sample ایجاد میکنیم. سپس داخل این پوشه، پوشهی دیگری را به نام SignalRCore2WebApp ایجاد خواهیم کرد (تصویر فوق). از طریق خط فرمان به این پوشه وارد شده (در ویندوز، در نوار آدرس، دستور cmd.exe را تایپ و enter کنید) و سپس فرمان ذیل را صادر میکنیم:
dotnet new mvc
ساخت برنامهی کلاینت، توسط angular cli
سپس از طریق خط فرمان به پوشهی SignalRCore2Sample بازگشته و دستور ذیل را صادر میکنیم:
ng new SignalRCore2Client
اکنون که در پوشهی ریشهی SignalRCore2Sample قرار داریم، اگر در خط فرمان، دستور . code را صادر کنیم، VSCode هر دو پوشهی وب و client را با هم در اختیار ما قرار میدهد:
تکمیل پیشنیازهای برنامهی وب
پس از ایجاد ساختار اولیهی برنامههای وب ASP.NET Core و کلاینت Angular، اکنون نیاز است وابستگی جدید AspNetCore.SignalR را به آن معرفی کنیم. به همین جهت به فایل SignalRCore2WebApp.csproj مراجعه کرده و تغییرات ذیل را به آن اعمال میکنیم:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp2.0</TargetFramework> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.All" Version="2.0.0" /> <PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.0.0-alpha1-final" /> </ItemGroup> <ItemGroup> <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="2.0.0" /> <DotNetCliToolReference Include="Microsoft.DotNet.Watcher.Tools" Version="2.0.0" /> </ItemGroup> </Project>
پس از این تغییرات، دستور ذیل را در خط فرمان صادر میکنیم تا وابستگیهای پروژه نصب شوند:
dotnet restore
یک نکته: نگارش فعلی افزونهی #C مخصوص VSCode، با تغییر فایل csproj و restore وابستگیهای آن نیاز دارد یکبار آنرا بسته و سپس مجددا اجرا کنید، تا اطلاعات intellisense خود را به روز رسانی کند. بنابراین اگر VSCode بلافاصله کلاسهای مرتبط با بستههای جدید را تشخیص نمیدهد، علت صرفا این موضوع است.
پس از بازیابی وابستگیها، به ریشهی پروژهی برنامهی وب وارد شده و دستور ذیل را صادر کنید:
dotnet watch run
تکمیل برنامهی وب جهت ارسال پیامهایی به کلاینتهای متصل به آن
پس از افزودن وابستگیهای مورد نیاز، بازیابی و build برنامه، اکنون نوبت به تعریف یک Hub است، تا از طریق آن بتوان پیامهایی را به کلاینتهای متصل ارسال کرد. به همین جهت یک پوشهی جدید را به نام Hubs به پروژهی وب افزوده و سپس کلاس جدید MessageHub را به صورت ذیل به آن اضافه میکنیم:
using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR; namespace SignalRCore2WebApp.Hubs { public class MessageHub : Hub { public Task Send(string message) { return Clients.All.InvokeAsync("Send", message); } } }
پس از تعریف این Hub، نیاز است به کلاس Startup مراجعه کرده و دو تغییر ذیل را اعمال کنیم:
الف) ثبت و معرفی سرویس SignalR
ابتدا باید SignalR را فعالسازی کرد. به همین جهت نیاز است سرویسهای آنرا به صورت یکجا توسط متد الحاقی AddSignalR در متد ConfigureServices به نحو ذیل معرفی کرد:
public void ConfigureServices(IServiceCollection services) { services.AddSignalR(); services.AddMvc(); }
ب) ثبت مسیریابی دسترسی به Hub
پس از تعریف Hub، مرحلهی بعدی، مشخص سازی نحوهی دسترسی به آن است. به همین جهت در متد Configure، به نحو ذیل Hub را معرفی کرده و سپس یک path را برای آن مشخص میکنیم:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseSignalR(routes => { routes.MapHub<MessageHub>(path: "message"); });
http://localhost:5000/message
انتشار پیامهایی به تمام کاربران متصل به برنامه
آدرس فوق به تنهایی کار خاصی را انجام نمیدهد. از آن جهت اتصال کلاینتهای برنامه استفاده میشود و این کلاینتها پیامهای رسیدهی از طرف برنامه را از این آدرس دریافت خواهند کرد. بنابراین مرحلهی بعد، ارسال تعدادی پیام به سمت کلاینتها است. برای این منظور به HomeController برنامهی وب مراجعه کرده و آنرا به نحو ذیل تغییر میدهیم:
using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.SignalR; using SignalRCore2WebApp.Hubs; namespace SignalRCore2WebApp.Controllers { public class HomeController : Controller { private readonly IHubContext<MessageHub> _messageHubContext; public HomeController(IHubContext<MessageHub> messageHubContext) { _messageHubContext = messageHubContext; } public IActionResult Index() { return View(); // show the view } [HttpPost] public async Task<IActionResult> Index(string message) { await _messageHubContext.Clients.All.InvokeAsync("Send", message); return View(); } } }
در این مثال ابتدا View ذیل نمایش داده میشود:
@{ ViewData["Title"] = "Home Page"; } <form method="post" asp-action="Index" asp-controller="Home" role="form"> <div class="form-group"> <label label-for="message">Message: </label> <input id="message" name="message" class="form-control"/> </div> <button class="btn btn-primary" type="submit">Send</button> </form>
تکمیل برنامهی کلاینت Angular جهت نمایش پیامهای رسیدهی از طرف سرور
تا اینجا ساختار ابتدایی برنامهی Angular را توسط Angular CLI ایجاد کردیم. اکنون نیاز است وابستگی سمت کلاینت SignalR Core را نصب کنیم. به همین جهت از طریق خط فرمان به پوشهی SignalRCore2Client وارد شده و دستور ذیل را صادر کنید:
npm install @aspnet/signalr-client --save
کلاینت رسمی signalr، هم جاوا اسکریپتی است و هم تایپاسکریپتی. به همین جهت به سادگی توسط یک برنامهی تایپ اسکریپتی Angular قابل استفاده است. کلاسهای آنرا در مسیر node_modules\@aspnet\signalr-client\dist\src میتوانید مشاهده کنید.
در ابتدا، فایل app.component.ts را به نحو ذیل تغییر میدهیم:
import { Component, OnInit } from "@angular/core"; import { HubConnection } from "@aspnet/signalr-client"; @Component({ selector: "app-root", templateUrl: "./app.component.html", styleUrls: ["./app.component.css"] }) export class AppComponent implements OnInit { hubPath = "http://localhost:5000/message"; messages: string[] = []; ngOnInit(): void { const connection = new HubConnection(this.hubPath); connection.on("send", data => { this.messages.push(data); }); connection.start().then(() => { // connection.invoke("send", "Hello"); console.log("connected."); }); } }
آرایهی messages را به نحو ذیل توسط یک حلقه در قالب این کامپوننت نمایش خواهیم داد:
<div> <h1> The messages from the server: </h1> <ul> <li *ngFor="let message of messages"> {{message}} </li> </ul> </div>
ng serve -o
همانطور که مشاهده میکنید، پیام خطای ذیل را صادر کردهاست:
Failed to load http://localhost:5000/message: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:4200' is therefore not allowed access.
برای این منظور به فایل آغازین برنامهی وب مراجعه کرده و سرویسهای AddCors را به مجموعهی سرویسهای برنامه اضافه میکنیم:
public void ConfigureServices(IServiceCollection services) { services.AddSignalR(); services.AddCors(options => { options.AddPolicy("CorsPolicy", builder => builder .AllowAnyOrigin() .AllowAnyMethod() .AllowAnyHeader() .AllowCredentials()); }); services.AddMvc(); }
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseCors(policyName: "CorsPolicy");
در آخر برای آزمایش برنامه، به آدرس http://localhost:5000 یا همان برنامهی وب، مراجعه کرده و پیامی را ارسال کنید. بلافاصله مشاهده خواهید کرد که این پیام توسط کلاینت Angular دریافت شده و نمایش داده میشود:
کدهای کامل این مثال را از اینجا میتوانید دریافت کنید: SignalRCore2Sample.zip
برای اجرا آن، ابتدا به پوشهی SignalRCore2WebApp مراجعه کرده و دو فایل bat آنرا به ترتیب اجرا کنید. اولی وابستگیهای برنامه را بازیابی میکند و دومی برنامه را بر روی پورت 5000 ارائه میدهد.
سپس به پوشهی SignalRCore2Client مراجعه کرده و در آنجا نیز دو فایل bat ابتدایی آنرا به ترتیب اجرا کنید. اولی وابستگیهای برنامهی Angular را بازیابی میکند و دومی برنامهی Angular را بر روی پورت 4200 اجرا خواهد کرد.
public boolean isNetworkAvailable(Context context) { ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo activeNetworkInfo = connectivityManager.getActiveNetworkInfo(); return activeNetworkInfo != null && activeNetworkInfo.isConnected(); }
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
البته توصیه ما نیز استفاده از این کد هست و توصیه میشود که استثناءها یا وضعیت خروجیها را کنترل نمایید. روشهای زیر تنها تحلیل کوچکی برای بررسی وضعیت اینترنت است یا اینکه واقعا به این بررسیهای نیاز داشته باشید. در غیر این صورت برای بسیاری از برنامهها همین کد کفایت میکند. راه حلهای پایین مواردی است که از تجربه خود به دست آوردهام تا شاید راهنمایی برای افرادی باشد که میخواهند این کار را آغاز کنند تا در وقتشان صرفه جویی شود.
مثالهای زیر وضعیتهایی را نشان میدهند که شبکه موجود است ولی اینترنتی در دسترس نیست:
- مودم وای فای را فعال کرده است، ولی اینترنت را در اختیار ندارد که این علل میتواند عدم ثابت شدن چراغ DSL یا راه اندازی مجدد مودم باشد که وای فای زودتر از DSL فعال میشود و یا اینکه اشتراک شما تمام شده است یا در شبکه به مشکل برخورد کردهاید.
- شما از طریق mobile data قصد اتصال دارید. در این حالت یا اعتبار شما پایان یافته است یا شبکه آنان دچار اختلال است.
- شما در یک محیط اداری هستید که به عنوان مثال سیستمشان توسط روترهای میکروتیک هدایت میشود. در این حالت شما میتوانید وارد شبکه بدون کلمه عبور آنان شوید. ولی نیاز دارید که حتما صفحه لاگین را رد نمایید تا اینترنت در اختیار شما قرار بگیرد.
در همه مثالهای بالا اینترنتی وجود ندارد، ولی تکه کد بالا true را برخواهد گرداند.
public boolean IsInternetConnected() { try { Process ipProcess = Runtime.getRuntime().exec("/system/bin/ping -c 1 4.2.2.4"); int exitValue = ipProcess.waitFor(); return (exitValue == 0); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } return false; }
System.Diagnostics.Process.Start("processName");
فرمان یا دستور بالا به شرح زیر است:
در سیستم عامل لینکوس تمام برنامههای سیستمی مورد نیاز، در شاخه bin قرار میگیرند و پینگ، یکی از آن هاست. سوییچ c هم که مخفف count است، به معنی تعداد درخواستهای یک اکو است که در این دستور گفتهایم تنها یک درخواست اکو echo ارسال کن. (ااطلاعات بیشتر در مورد دستور پینگ ).
در خط بعدی از آنجا که این دستور، یک دستور زمان بر است، باید مدتی در این کد توقف شود تا مقدار مورد نظر دریافت شود. در صورتی که مقدار 0 بازگردانده شود، اکو پاسخ داده شده است و یعنی اینکه شما به اینترنت متصلید. (مشاهده کدهای وضعیتی ICMP )
وجود catchهای بالا الزامی است از آنجا که متدهای استفاده شده توسط استثناءهای زیر throw شدهاند، جاوا شما را ملزم به استفاده از catchهای این استثناها خواهد کرد.
public Process exec(String prog) throws java.io.IOException { return exec(prog, null, null); }
public abstract int waitFor() throws InterruptedException;
برای اجرا این تکه کد شما نیاز به مجوز اتصال به اینترنت دارید:
<uses-permission android:name="android.permission.INTERNET"/>
نکته مهم اینکه نگران اجرای این دستور در گوشی کاربر نباشید. این دستور نیاز به مجوز روت ندارد.
تکه کد زیر صفحه گوگل را درخواست میکند و در نهایت وضعیت کد http آن را دریافت میکنیم و اگر این کد وضعیت برابر 200 بود به این معنی است که اینترنت متصل میباشد. ولی در یک سیستم میکروتیک که هنوز وارد سیستم آن نشده باشید، به صفحه لاگین هدایت میشوید و وضعیت دیگری را دریافت خواهید مانند آدرس درخواستی شما redirect شده است یا اینکه باز هم کد 200 را دریافت میکنید که در بیشتر حالات هم به همین شکل است. برای رفع این مسئله بهتر است url فعلی را با url درخواستی مطابقت دهیم. برای این قضیه گوگل در بخش Handling Network Sign-On این صفحه چنین کدی را پیشنهاد داده است:
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); try { InputStream in = new BufferedInputStream(urlConnection.getInputStream()); if (!url.getHost().equals(urlConnection.getURL().getHost())) { // we were redirected! Kick the user out to the browser to sign on? ... } finally { urlConnection.disconnect(); } }
ولی با توجه به تحقیقات و مشاهداتی که کردهام، در بعضی از گوشیها این کد کارکرد مناسبی ندارد و برای خودم هم پاسخی دریافت نکردم. اگر واقعا باز هم مصر هستید که این وضعیت را بررسی کنید، میتواند بررسی یک url با محتوای خاص باشد و بعد از دریافت این صفحه محتوای آن را بررسی کنید.
ولی با این همه بسیاری از برنامهها از همان تکه کد بالا استفاده میکنند و با مدیریت استثناءها سعی در جلوگیری از خطا دارند. در غیر این صورت شما باید مدام در حال بررسی وضعیت اینترنت به شکل بالا باشید که بسیار زمان بر خواهد شد.
در صورتی که شما روش بهتری را برای بررسی وضعیت اینترنت دارید یا راه حل خاصی به نظرتان میرسد بسیار عالی خواهد بود که آن را با ما به اشتراک بگذارید.
EF Code First #7
با استفاده از این template ویژوال استدیو می تونید خیلی سریع پروژههای خودتون رو براساس لایه بندی معماری تمیز شروع کنید و وقتتون رو صرف لایه بندی پروژه نکنید. همچنین در این template با پوشه بندی هایی که برای هر لایه انجام شده، سعی شده به شما نشون بده که هر یک از آیتمهای پروژه رو در کدوم قسمت باید قرار بدید.