- فایل ReadMe را فراموش نکنید
حتی اگر پروژه شما از یک سایت اختصاصی استفاده میکند، اولین محلی که عموم کاربران برای دریافت اطلاعات کار با پروژه، به آن مراجعه میکنند، فایل ReadMe برنامه است. این فایل میتواند حاوی مشخصات ذیل باشد:
الف) وابستگیهای پروژه را مشخص کنید
واقعیت این است که برخلاف شمای برنامه نویس، عموم استفاده کنندگان، آشنایی چندانی با جزئیات محیط و شرایط تهیه برنامه شما ندارند. به این ترتیب بسیاری از مسایلی که برای شما بدیهی هستند، برای عموم اینگونه نخواهند بود. بنابراین مسالهای که به سرعت میتواند سبب خشم کاربران و صرفنظر از کار شما گردد، مشخص نبودن نحوه نصب و وابستگیهای لازم برای اجرای برنامه است.
ب) وضعیت بلوغ پروژه خود را مشخص کنید
آیا از این برنامه، مدتی است که در محیط کاری استفاده میکنید؟ آیا به نظر شما هنوز ناتمام است؟ آیا API کتابخانه شما در نگارش بعدی کاملا دگرگون خواهد شد؟ تمام این مسایل و سؤالات را به نحو واضحی توضیح دهید و مشخص کنید. همین توضیحات کوتاه میتوانند ساعتهای بسیاری از زندگی دیگران را صرفه جویی کند.
ج) اگر پروژه شما یک کتابخانه است، نوع زبان و Runtimeهای پشتیبانی شده را مشخص کنید
برای مثال اگر یک کتابخانه دات نتی را ارائه میدهید، مشخص کنید که از کدام نگارش دات نت به بعد را پشتیبانی میکنید.
د) مجوز استفاده از پروژه را مشخص کنید
مطلب مقایسه مجوزهای سورس باز را یکبار مطالعه نمائید و سپس مجوز صحیحی را برای کار خود انتخاب کنید. همچنین آنرا به نحو واضحی در مستندات پروژه خود قید نمائید.
به علاوه بهخاطر داشته باشید که امکان ارائه مجوزهای دوگانه مانند AGPL نیز وجود دارند. در این حالت کاربر یا باید سورس محصول خودش را ارائه دهد، یا مجوز کتابخانه شما را خریداری کند. مانند RavenDB که از این نوع مجوز استفاده میکند.
- یک پروژه نیاز به مستندات دارد
مستند سازی کار، سخت و زمانبر است؛ اما بهترین لطفی است که میتوانید به کاربران خود نمائید. مستندات نه تنها زمان جستجوی بسیاری را صرفه جویی خواهند کرد، همچنین حس اطمینان خاطر را به کاربر القاء میکنند. از این جهت که احساس میکنند شما برای کارتان ارزش قائل بودهاید و احتمال اینکه این برنامه در آینده نزدیک به یک abandonware تبدیل شود، کم است (منظور یک برنامه فراموش شده و خاتمه یافته).
- به روز رسانی را ساده کنید
بالاخره زمانی نیاز خواهد بود تا نگارش جدیدی از کار خود را ارائه دهید. در این حالت نیاز است یک سری از شرایط را مدنظر داشته باشید:
الف) سازگاری قبلی را مدنظر داشته باشید
یکی از بدترین حالات به روز رسانی یک کتابخانه زمانی است که کاربر آن با دهها خطای کامپایل حاصل از به روز رسانی مواجه شود. اگر نیاز است قسمتی از کد خود را حذف کنید یا تغییر دهید، استفاده از ویژگی Obsolete را فراموش نکنید و اینکار باید مرحله به مرحله انجام شود. در یک نگارش، ویژگی Obsolete را معرفی کنید. در دو نگارش بعد، API را تغییر دهید.
ب) حتما یک Change log را تکمیل کنید
پس از ارائه یک نگارش جدید، حداقل در چند سطر مشخص کنید که چه مواردی تغییر کردهاند، چه مواردی اضافه شدهاند و چه مواردی را حذف کردهاید.
همچنین اگر مواردی تغییر کردهاند، نحوه ارتقاء کدهای قدیمی را به نگارش جدید، شرح دهید. اگر مورد جدیدی اضافه شدهاست، لینکی را به مثالی دربارهی آن ارائه دهید.
- نگارشهای جدید را اعلام کنید
برای مثال در طی ارائه یک مطلب جدید در وبلاگ خود، ارائه نگارش جدیدی از کتابخانه یا برنامه خود را به عموم اعلام کنید. در این حالت، حتما لینکی را به change log، ارائه داده و مشخص کنید که وضعیت سازگاری آن با قبل چگونه است.
- محلی را برای دریافت بازخوردهای پروژه خود مشخص کنید
نیاز است بتوانید پروژه خود را پشتیبانی کنید یا به سؤالات مربوطه پاسخ دهید. اگر سورس کنترل یا برنامه مدیریت پروژه شما، امکان پرسش و پاسخ را دارد، که بسیار خوب. اگر خیر، میتوانید مثلا یک گروه گوگل جدید و امثال آنرا برای دریافت بازخوردهای پروژه ایجاد کنید.
همچنین نیاز است لینک به این محل را در فایل ReadME پروژه به صراحت مشخص کنید.
- گذر از پروژه
بالاخره روزی فراخواهد رسید که دیگر علاقهای به نگهداری پروژه نداشته باشید. این مساله را در مکان جمع آوری بازخوردهای خود اعلام کنید یا شخص دیگری را به نگهداری پروژه دعوت نمائید. اگر این کار را انجام ندهید، سبب خواهید شد forkهای متعددی از این پروژه بیجهت ایجاد شده و در نهایت مشخص نباشد که کدامیک بهتر است و کدامیک مشکلات کمتری دارند.
اگر SQL Server و MySQL بر روی سیستم شما نصب است، روشی ساده برای انتقال اطلاعات بین این دو وجود دارد که نیازی به دخالت هیچ نوع برنامهی جانبی نداشته و با امکانات موجود قابل مدیریت است.
ایجاد یک Linked server
برای اینکه SQL Server را به MySQL متصل کنیم میتوان بین این دو یک Linked server تعریف کرد و سپس دسترسی به بانکهای اطلاعاتی MySQL همانند یک بانک اطلاعاتی محلی SQL Server خواهد شد که شرح آن در ادامه ذکر میشود.
ابتدا نیاز است تا درایور ODBC مربوط به MySQL دریافت و نصب شود. آنرا میتوانید از اینجا دریافت کنید : (+)
سپس management studio را گشوده و در قسمت Server objects ، بر روی گزینهی Linked servers کلیک راست نمائید. از منوی ظاهر شده، گزینهی New linked server را انتخاب کنید:
در ادامه، باید تنظیمات زیر را در صفحهی باز شده وارد کرد:
در قسمت Linked server و Product name ، نام دلخواهی را وارد کنید.
Provider انتخابی باید از نوع Microsoft OLE DB Provider for ODBC Drivers باشد.
مهمترین تنظیم آن، قسمت Provider string است که باید به صورت زیر وارد شود (در غیر اینصورت کار نمیکند):
DRIVER={MySQL ODBC 5.1 Driver}; SERVER=localhost; DATABASE=testdb; USER=root; PASSWORD=mypass; OPTION=3;PORT=3306; CharSet=UTF8;
پس از انجام این تنظیمات بر روی دکمهی Ok کلیک کنید تا Linked server ساخته شود:
اگر لیست بانکهای اطلاعاتی را مشاهده نمودید، یعنی اتصال به درستی برقرار شده است.
تنظیمات ثانویه:
تا اینجا اس کیوال سرور به MySQL متصل شده است، اما برای استفاده بهینه از امکانات موجود نیاز است تا یک سری تغییرات دیگر را هم اعمال کرد.
تنظیم MSDASQL Provider :
در همان قسمت Linked provider ، ذیل قسمت Providers ، گزینهی MSDASQL را انتخاب کرده و بر روی آن کلیک راست نمائید. سپس صفحهی خواص آنرا انتخاب کنید تا بتوان تنظیمات زیر را به آن اعمال کرد. این پروایدر جهت اتصال به MySQL مورد استفاده قرار میگیرد.
فعال سازی RPC :
برای اینکه بتوان از طریق SQL Server رکوردی را در یکی از جداول بانکهای اطلاعاتی MySQL متصل شده ثبت نمود، میتوان از دستور زیر استفاده کرد:
EXECUTE('insert into testdb.testtable(f1,f1) values(1,''data'')') at mysql
اینجا testdb نام بانک اطلاعاتی اتصالی MySQL است و testTable هم نام جدول مورد نظر. MySQL ایی که در آخر عبارت ذکر شده همان نام linked server ایی است که پیشتر تعریف کردیم.
به محض سعی در اجرای این کوئری خطای زیر ظاهر میشود:
Server 'mysql' is not configured for RPC.
برای رفع این مشکل، مجددا به صفحهی خواص همان liked server ایجاد شده مراجعه کنید. در قسمت Server options دو گزینه مرتبط به RPC باید فعال شوند:
و اکنون برای کوئری گرفتن از اطلاعات ثبت شده هم از عبارت زیر میتوان استفاده کرد:
SELECT * FROM OPENQUERY(mysql, 'SELECT * FROM testdb.testtable')
در این کوئری، MySQL نام Linked server ثبت شده است و testdb هم یکی از بانکهای اطلاعاتی MySQL مورد نظر.
انتقال تمام اطلاعات یک جدول از بانک اطلاعاتی MySQL به SQL Server
پس از برقراری اتصال، اکنون import کامل یک جدول MySQL به SQL Server به سادگی اجرای کوئری زیر میباشد:
SELECT * INTO MyDb.dbo.testtable FROM openquery(MYSQL, 'SELECT * FROM testdb.testtable')
در این کوئری، MySQL همان Linked server تعریف شده است. MyDB نام بانک اطلاعاتی موجود در SQL Server جاری است و testtable هم جدولی است که قرار است اطلاعات testdb.testtable بانک اطلاعاتی MySQL به آن وارد شود.
با اطلاعات فارسی هم (در سمت SQL Server) مشکلی ندارد. همانطور که مشخص است، در اطلاعات provider string ذکر شده، مقدار charset به utf8 تنظیم شده و همچنین اگر نوع collation فیلدهای تعریف شده در MySQL نیز به utf8_persian_ci تنظیم شده باشد، با مشکل ثبت اطلاعات فارسی به صورت ???? مواجه نخواهید شد.
نکته:
اگر بانک اطلاعاتی MySQL شما بر روی local host نصب نیست، جهت فعال سازی دسترسی ریموت به آن، میتوان به یکی از نکات زیر مراجعه کرد و سپس این اطلاعات جدید باید در همان قسمت provider string مرتبط با تعریف linked server وارد شوند:
مطالب مشابه:
هیچکدام از این روشها قابل استفاده نبودند چون provider string صحیحی را نهایتا تولید نمیکنند. همچنین تمام این روشها مبتنی است بر ایجاد DSN در کنترل پنل که اصلا نیازی به آن نیست و اضافی است.
پیشنیازها
برای دنبال کردن این مثال فرض بر این است که 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 اجرا خواهد کرد.
افزونه های SQL Server Reporting Services Projects و RDLC Report Designer که جدیدا توسط مایکروسافت به صورت افزونه ای که میتوان به Visual Studio 2022 اضافه کرد، ارائه شده است.
برای قسمت backend، از همان برنامهی تکمیل شدهی قسمت قبل استفاده میکنیم که به آن تولید مقدماتی JWTها نیز اضافه شدهاست. البته این سری، مستقل از قسمت سمت سرور آن تهیه خواهد شد و صرفا در حد دریافت توکن از سرور و یا ارسال مشخصات کاربر جهت لاگین، نیاز بیشتری به قسمت سمت سرور آن ندارد و تاکید آن بر روی مباحث سمت کلاینت React است. بنابراین اینکه چگونه این توکن را تولید میکنید، در اینجا اهمیتی ندارد و کلیات آن با تمام روشهای پیاده سازی سمت سرور سازگار است (و مختص به فناوری خاصی نیست). پیشنیاز درک کدهای سمت سرور قسمت JWT آن، مطالب زیر هستند:
- «معرفی JSON Web Token»
- «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»
- «پیاده سازی JSON Web Token با ASP.NET Web API 2.x»
- « آزمایش Web APIs توسط Postman - قسمت ششم - اعتبارسنجی مبتنی بر JWT»
ثبت یک کاربر جدید
فرم ثبت نام کاربران را در قسمت 21 این سری، در فایل src\components\registerForm.jsx، ایجاد و تکمیل کردیم. البته این فرم هنوز به backend server متصل نیست. برای کار با آن هم نیاز است شیءای را با ساختار زیر که ذکر سه خاصیت اول آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Users به صورت یک HTTP Post ارسال کنیم:
{ "name": "string", "email": "string", "password": "string", "isAdmin": true, "id": 0 }
اکنون نوبت به اتصال کامپوننت registerForm.jsx، به سرویس backend است. تا اینجا دو سرویس src\services\genreService.js و src\services\movieService.js را در قسمت قبل، به برنامه جهت کار کردن با endpointهای backend server، اضافه کردیم. شبیه به همین روش را برای کار با سرویس سمت سرور api/Users نیز در پیش میگیریم. بنابراین فایل جدید src\services\userService.js را با محتوای زیر، به برنامهی frontend اضافه میکنیم:
import http from "./httpService"; import { apiUrl } from "../config.json"; const apiEndpoint = apiUrl + "/users"; export function register(user) { return http.post(apiEndpoint, { email: user.username, password: user.password, name: user.name }); }
اطلاعات شیء user ای که در اینجا دریافت میشود، از خاصیت data کامپوننت RegisterForm تامین میگردد:
class RegisterForm extends Form { state = { data: { username: "", password: "", name: "" }, errors: {} };
پس از تعریف userService.js، به registerForm.jsx بازگشته و ابتدا امکانات آنرا import میکنیم:
import * as userService from "../services/userService";
import { register } userService from "../services/userService";
doSubmit = async () => { try { await userService.register(this.state.data); } catch (ex) { if (ex.response && ex.response.status === 400) { const errors = { ...this.state.errors }; // clone an object errors.username = ex.response.data; this.setState({ errors }); } } };
در این حالت برای ارسال اطلاعات یک کاربر، در بار اول، یک چنین خروجی را از سمت سرور میتوان شاهد بود که id جدیدی را به این رکورد نسبت دادهاست:
اگر مجددا همین رکورد را به سمت سرور ارسال کنیم، اینبار خطای زیر را دریافت خواهیم کرد:
که از نوع 400 یا همان BadRequest است:
بنابراین نیاز است بدنهی response را در یک چنین مواردی که خطایی از سمت سرور صادر میشود، دریافت کرده و با به روز رسانی خاصیت errors در state فرم (همان قسمت بدنهی catch کدهای فوق)، سبب درج و نمایش خودکار این خطا شویم:
پیشتر در قسمت بررسی «کار با فرمها» آموختیم که برای مدیریت خطاهای پیش بینی شدهی دریافتی از سمت سرور، نیاز است قطعه کدهای مرتبط با سرویس http را در بدنهی try/catchها محصور کنیم. برای مثال در اینجا اگر ایمیل شخصی تکراری وارد شود، سرویس یک return BadRequest("Can't create the requested record.") را بازگشت میدهد که در اینجا status code معادل BadRequest، همان 400 است. بنابراین انتظار داریم که خطای 400 را از سمت سرور، تحت شرایط خاصی دریافت کنیم. به همین دلیل است که در اینجا تنها مدیریت status code=400 را در بدنهی catch نوشته شده ملاحظه میکنید.
سپس برای نمایش آن، نیاز است خاصیت متناظری را که این خطا به آن مرتبط میشود، با پیام دریافت شدهی از سمت سرور، مقدار دهی کنیم که در اینجا میدانیم مرتبط با username است. به همین جهت سطر errors.username = ex.response.data، کار انتساب بدنهی response را به خاصیت جدید errors.username انجام میدهد. در این حالت اگر به کمک متد setState، کار به روز رسانی خاصیت errors موجود در state را انجام دهیم، رندر مجدد فرم، در صف انجام قرار گرفته و در رندر بعدی آن، پیام موجود در errors.username، نمایش داده میشود.
پیاده سازی ورود به سیستم
فرم ورود به سیستم را در قسمت 18 این سری، در فایل src\components\loginForm.jsx، ایجاد و تکمیل کردیم که این فرم نیز هنوز به backend server متصل نیست. برای کار با آن نیاز است شیءای را با ساختار زیر که ذکر هر دو خاصیت آن اجباری است، به endpoint ای با آدرس https://localhost:5001/api/Auth/Login به صورت یک HTTP Post ارسال کنیم:
{ "email": "string", "password": "string" }
var jwt = _tokenFactoryService.CreateAccessToken(user); return Ok(new { access_token = jwt });
در ادامه برای تعامل با منبع api/Auth/Login سمت سرور، ابتدا یک سرویس مختص آنرا در فایل جدید src\services\authService.js، با محتوای زیر ایجاد میکنیم:
import { apiUrl } from "../config.json"; import http from "./httpService"; const apiEndpoint = apiUrl + "/auth"; export function login(email, password) { return http.post(apiEndpoint + "/login", { email, password }); }
import * as auth from "../services/authService"; class LoginForm extends Form { state = { data: { username: "", password: "" }, errors: {} }; // ... doSubmit = async () => { try { const { data } = this.state; const { data: { access_token } } = await auth.login(data.username, data.password); console.log("JWT", access_token); localStorage.setItem("token", access_token); this.props.history.push("/"); } catch (ex) { if (ex.response && ex.response.status === 400) { const errors = { ...this.state.errors }; errors.username = ex.response.data; this.setState({ errors }); } } };
- ابتدا تمام خروجیهای ماژول authService را با نام شیء auth دریافت کردهایم.
- سپس در متد doSubmit، اطلاعات خاصیت data موجود در state را که معادل فیلدهای فرم لاگین هستند، به متد auth.login برای انجام لاگین سمت سرور، ارسال کردهایم. این متد چون یک Promise را باز میگرداند، باید await شود و پس از آن متد جاری نیز باید به صورت async معرفی گردد.
- همانطور که عنوان شد، خروجی نهایی متد auth.login، یک شیء JSON دارای خاصیت access_token است که در اینجا از خاصیت data خروجی نهایی دریافت شدهاست.
- سپس نیاز است برای استفادههای آتی، این token دریافتی از سرور را در جایی ذخیره کرد. یکی از مکانهای متداول اینکار، local storage مرورگرها است (اطلاعات بیشتر).
- در آخر کاربر را توسط شیء history سیستم مسیریابی برنامه، به صفحهی اصلی آن هدایت میکنیم.
- در اینجا قسمت catch نیز ذکر شدهاست تا خطاهای حاصل از return BadRequestهای دریافتی از سمت سرور را بتوان ذیل فیلد نام کاربری نمایش داد. روش کار آن، دقیقا همانند روشی است که برای فرم ثبت یک کاربر جدید استفاده کردیم.
اکنون اگر برنامه را ذخیره کرده و اجرا کنیم، توکن دریافتی، در کنسول توسعه دهندگان مرورگر لاگ شده و سپس کاربر به صفحهی اصلی برنامه هدایت میشود. همچنین این token ذخیره شده را میتوان در ذیل قسمت application->storage آن نیز مشاهده کرد:
لاگین خودکار کاربر، پس از ثبت نام در سایت
پس از ثبت نام یک کاربر در سایت، بدنهی response بازگشت داده شدهی از سمت سرور، همان شیء user است که اکنون Id او مشخص شدهاست. بنابراین اینبار جهت ارائهی token از سمت سرور، بجای response body، از یک هدر سفارشی در فایل Controllers\UsersController.cs استفاده میکنیم (کدهای کامل آن در انتهای بحث پیوست شدهاست):
var jwt = _tokenFactoryService.CreateAccessToken(user); this.Response.Headers.Add("x-auth-token", jwt);
در ادامه در کدهای سمت کلاینت src\components\registerForm.jsx، برای استخراج این هدر سفارشی، اگر شیء response دریافتی از سرور را لاگ کنیم:
const response = await userService.register(this.state.data); console.log(response);
برای اینکه در کدهای سمت کلاینت بتوان این هدر سفارشی را خواند، نیاز است هدر مخصوص access-control-expose-headers را نیز به response اضافه کرد:
var jwt = _tokenFactoryService.CreateAccessToken(data); this.Response.Headers.Add("x-auth-token", jwt); this.Response.Headers.Add("access-control-expose-headers", "x-auth-token");
اکنون میتوان این هدر سفارشی را در متد doSubmit کامپوننت RegisterForm، از طریق شیء response.headers خواند و در localStorage ذخیره کرد. سپس کاربر را توسط شیء history سیستم مسیریابی، به ریشهی سایت هدایت نمود:
class RegisterForm extends Form { // ... doSubmit = async () => { try { const response = await userService.register(this.state.data); console.log(response); localStorage.setItem("token", response.headers["x-auth-token"]); this.props.history.push("/"); } catch (ex) { if (ex.response && ex.response.status === 400) { const errors = { ...this.state.errors }; // clone an object errors.username = ex.response.data; this.setState({ errors }); } } };
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-26-backend.zip و sample-26-frontend.zip
کاری که اکسس در اینجا کرده یا SQLite ایی که مثال زدم، مرتب سازی بر اساس کدهای یونیکد این حروف است و کار صحیحی (در بدو امر حداقل) انجام شده: (+)
کد یونیکد "ک" عربی = 1603
کد یونیکد "ی" عربی = 1610
کد یونیکد "ک" فارسی = 1705
کد یونیکد "ی" فارسی = 1740
یعنی این مرتب سازی بر اساس منطق ریاضی صحیح است؛ اما بر اساس فرهنگ ایران خیر. به همین جهت توسعه دهندههای بانکهای اطلاعاتی مجبور شدهاند تا مفهومی به نام Collation را ارائه بدهند که در بالا در مورد آن بحث شد. این Collation دیگر صرفا بر اساس منطق ریاضی کدهای یونیکد حروف، مرتب سازی را انجام نمیدهد، بلکه بر اساس ادبیات و فرهنگ زبانهای مختلف کار مرتب سازی را انجام خواهد داد. SQL Server در این زمینه حداقل برای فارسی زبانها یک Collation مخصوص را در نگارش 2008 خودش ارائه داده تا مرتب سازی صورت گرفته روی رشتهها دقیقا مطابق فرهنگ و ادبیات ایرانی باشد (برای سایر کشورها هم این نوع Collation ها پیش بینی شده).
در اکسس که مد نظر شما است این Collation به نام General Sort order مهیا است (در اکسسهای جدید در قسمت فایل، options و سپس برگهی general قسمتی هست به نام new database sort order که همین collation است) (+)
و اگر در این مورد خاص درست کار نمیکند باید با مایکروسافت مکاتبه کرد و این مسایل را توضیح داد.
فرض کنید میخواهید یک فایل ویدیویی با قالب m4v را بر روی تلویزیون خود نمایش دهید؛ اما تلویزیون شما تنها از فایلهای mp4، پشتیبانی میکند. برای رفع این مشکل نیاز به یک نرم افزار تبدیل کنندهی فرمتهای ویدیویی را داریم و یکی از قویترینهای آنها، FFmpeg است. اگر به سایت آن مراجعه کنید، لینک دانلود آن به یک فایل tar.bz2 ختم میشود که حاوی سورس آن است! هرچند در قسمتی از آن، فایلهای نهایی کامپایل شدهی مخصوص سیستم عاملهای مختلف را نیز میتوانید پیدا کنید، اما باز هم با انبوهی از لینکها مواجه خواهید شد که دقیقا مشخص نیست کدام را باید دریافت کرد و آیا نگارش دریافت شده، با سیستم عامل فعلی سازگار است یا خیر.
همانطور که مشاهده میکنید، هنوز هم شروع به کار با نرم افزارهای مختلف برای بسیاری از کاربران، کاری مشکل و طاقتفرسا است. در اینجا شاید این سؤال مطرح شود که این موضوع چه ربطی به Docker (Docker) و کانتینرها (Containers) دارد؟ تمام هیاهویی که پیرامون Docker ایجاد شدهاست، در اصل جهت ساده سازی نصب، راه اندازی و تعامل با نرم افزارهای مختلف است.
چالشهای پیش روی یافتن نرم افزارهای مناسب
این روزها بیشتر نرم افزارهای مورد نیاز خود را از اینترنت تهیه میکنیم. اولین مرحلهی آن و اولین چالشی که در اینجا وجود دارد، یافتن نرم افزاری با مشخصات مدنظر است. برای نمونه حتی اگر با FFmpeg آشنا نیز باشید، به سادگی مشخص نیست که برای سیستم عامل و معماری خاص پردازندهی آن، دقیقا کدام نگارش آنرا از چه آدرسی میتوان دریافت کرد. پس از یافتن نرم افزار و نگارش مدنظر، مرحلهی بعد، استخراج محتویات آن از یک فایل zip و یا اجرای برنامهی نصاب آن است و مرحلهی آخر، اجرای این برنامه میباشد.
بنابراین اولین چالش، یافتن محلی برای دریافت نرم افزار است:
- این روزها برای بعضی از سکوهای کاری، App Storeهایی وجود دارند که میتوان از آنجا شروع کرد؛ اما چنین قابلیتی برای تمام سکوهای کاری پیش بینی نشدهاست.
- در لینوکس قابلیت دیگری به نام Package manager وجود دارد که کار یافتن و نصب نرم افزارها را ساده میکند؛ اما گاهی از اوقات اطلاعات آن، آنچنان به روز نیست. همچنین اگر بستهای برای توزیع خاصی از لینوکس وجود داشته باشد، الزاما به این معنا نیست که این بسته، قابلیت استفادهی در سایر توزیعهای لینوکس را نیز به همراه دارد. در ویندوز نیز وضعیت مشخص است! فاقد یک Package manager توکار و استاندارد است. هرچند یک App Store برای آن از طرف مایکروسافت ارائه شدهاست، اما آنچنان محبوبیتی پیدا نکردهاست.
- و روش متداول دیگری که وجود دارد، مراجعهی مستقیم به سایت اصلی سازندهی نرم افزار است.
- علاوه بر اینها داشتن یک سری متادیتا و آمار نیز در مورد نرم افزارها بسیار مفید هستند تا بتوانند در مورد تصمیم به استفاده، یا عدم استفادهی از نرم افزار، راهنمای کاربران باشند؛ مانند میزان محبوبیت، تعداد بار دریافت، تعداد مشکلاتی که کاربران با آن داشتهاند و آخرین باری که نرم افزار به روز شدهاست. اما با توجه به پراکندگی روشهای دریافت نرم افزار که ذکر شدند، عموما یک چنین آمارهایی را مشاهده نمیکنیم.
- چالش دیگر، مشکل سخت اطمینان کردن به روشهای مختلف توزیع نرم افزارها است. آیا سایتی که این نرم افزار را ارائه میدهد، واقعا مرتبط با نویسندهی اصلی آن است؟ همچنین آیا خود نرم افزار مشکلات امنیتی را به همراه ندارد؟ چه کاری را انجام میدهد؟
- مشکل بعدی، در دسترس بودن سایت توزیع کنندهی نرم افزار است. آیا زمانیکه به برنامهای نیاز داریم، پهنای باند سایت توزیع کنندهی آن تمام نشدهاست و میتوان به آن دسترسی داشت؟
- چالش دیگر، چگونگی پرداخت مبلغی برای دسترسی به نرم افزار است. به نظر تا به اینجا تنها App Storeها موفق شدهاند روشی یک دست را برای خرید برنامهها و همچنین ارائهی مجوزی برای استفادهی از آنها، ارائه دهند.
چالشهای پیش روی نصب نرم افزارها
زمانیکه به مرحلهی نصب نرم افزار میرسیم، هر نرم افزار، روش نصب و تنظیمات آغازین خاص خودش را دارد.
- اولین چالش پس از دریافت نرم افزار، بررسی سازگاری آن با سیستم عامل و پردازندهی فعلی است. شاید این مسایل برای توسعه دهندگان نرم افزارها پیشپا افتاده به نظر برسند، اما برای عموم کاربران، چالشی جدی به شما میروند.
- پس از مشخص شدن سازگاری یک نرم افزار با سیستم فعلی، قالب ارائهی آن نرم افزار نیز میتوان مشکلزا باشد. بعضی از برنامهها صرفا از طریق سورس کد منتشر میشوند. بعضی از آنها توسط یک فایل exe متکی به خود ارائه میشوند و بعضی دیگر به همراه یک فایل exe و تعدادی dll به همراه آنها. گاهی از اوقات این برنامهها نیاز به نصب جداگانهی NET Runtime. و یا Java Runtime را برای اجرا دارند و یا وابستگی آنها صرفا به نگارش خاصی از این کتابخانهها و فریم ورکهای ثالث است. هرچند اگر برنامهای به همراه بستهی نصاب آن باشد، به احتمال زیاد این وابستگیها را نیز نصب میکند؛ اما تمام برنامهها اینگونه ارائه نمیشوند. به علاوه خیلیها علاقهای به کار با برنامههای نصاب ندارند و از ایجاد تغییرات بسیاری که آنها در سیستم ایجاد میکنند، خشنود نیستند.
- پس از نصب نرم افزار، مشکل بعدی، نحوهی به روز رسانی آنها است. چگونه باید اینکار انجام شود؟ (تمام مراحل و چالشهایی را که تاکنون بررسی کردیم، یکبار دیگر از ابتدا مرور کنید!)
بنابراین همانطور که مشاهده میکنید، نصب، راه اندازی و به روز رسانی نرم افزارها این روزها بسیار پیچیده شدهاند و بسیاری از کاربران به سادگی از عهدهی آنها بر نمیآیند.
چالشهای پیش روی کار با نرم افزارها
مرحلهی بعد، نیاز به مستندات کافی برای کار با برنامه است. این مستندات را از کجا میتوان تهیه کرد؟ آخرین باری که به روز شده، چه زمانی بودهاست؟ بسیاری از اوقات بین مستندات تهیه شده و آخرین نگارش نرم افزار، ناسازگاری وجود دارد و به سختی قابل استفادهاست. آیا نیاز است برنامه را به PATH اضافه کرد؟ آیا نیاز است به صورت سرویس نصب شود؟ اگر بله، چگونه باید این مراحل را انجام داد؟ مجوز کار کردن با آنها چگونه است؟
مشکل مهم دیگری که حین کار با نرم افزارها، در حالت متداول آنها وجود دارد، دسترسی کامل آنها به تمام اجزای سیستم و شبکه است و درون یک sandbox (قرنطینه) امنیتی اجرا نمیشوند.
مشکل بعدی، به روز رسانی اجزای ثالث سیستم و یا حتی خود سیستم عامل، مانند به روز رسانی OpenSSL نصب شده و پس از آن، از کار افتادن برنامهای خاص است که وابستگی به نگارشی خاص از این کتابخانه را دارد.
کانتینرها در مورد برنامهها هستند و نه مجازی سازی
خوب، تا اینجا دریافتیم که مدیریت توزیع، نصب و استفادهی از برنامهها، کار سادهای نیست. اما اینها چه ارتباطی با Docker دارند؟ در بسیاری از اوقات، زمانیکه صحبت از Docker میشود، تصور بسیاری از آن، ارائهی جایگزینی برای ماشینهای مجازی است. اما ... اینگونه نیست. کانتینرها در مورد نرم افزارها هستند. برای مثال در آینده در مورد ایمیجهای (Images) کانتینرها بیشتر بحث خواهیم کرد. این ایمیجها در اصل یک بستهی حاوی برنامهها هستند. بنابراین بیشتر شبیه به فایل zip ای است که از یک وب سایت دریافت میکنیم (در قسمت یافتن نرم افزار).
یک کانتینر (Container) چیست؟
برای درک بهتر مواردی که تاکنون بحث شدند و همچنین بررسی مفهوم Containers، ابتدا MonogoDB را به صورت معمول نصب میکنیم. سپس نحوهی نصب آنرا درون یک Container بررسی خواهیم کرد. البته هدف اصلی در اینجا، بررسی مفهومی این مراحل و مقایسهی آنها با هم هستند و در قسمتهای بعدی کار نصب و استفادهی از Docker را قدم به قدم بررسی خواهیم کرد.
مراحل نصب محلی MongoDB به صورت متداول:
- ابتدا برای مثال به سایت گوگل مراجعه کرده و mongodb را جستجو میکنیم تا بتوانیم به سایت اصلی و محل دریافت بستهی آن، هدایت شویم.
- پس از ورود به سایت mongodb، در بالای صفحه اصلی آن، لینک به صفحهی دریافت بستهی mongodb را میتوان مشاهده کرد.
- با انتخاب آن، به صفحهی دریافت بستهی mongodb بر اساس سیستم عاملهای مختلفی هدایت میشویم. برای مثال در ویندوز، بستهی msi آنرا دریافت میکنیم.
- به نظر میرسد که بستهی نصاب msi آن تمام کارهای لازم برای راه اندازی اولیهی mongodb را انجام میدهد. به همین جهت آنرا اجرا کرده و پس از چندبار انتخاب گزینهی next، نصب آن به پایان میرسد.
- پس از پایان نصب، ابتدا به کنسول service.msc ویندوز مراجعه میکنیم تا مطمئن شویم که سرویس آن، توسط نصاب msi نصب شدهاست یا خیر؟ و ... خیر! این نصاب، سرویس آنرا نصب نکردهاست.
- به همین جهت به مستندات نصب آن در سایت mongodb مراجعه میکنیم (لینک Installation instructions در همان صفحهی دریافت بستهی msi وجود دارد). پس از پایان مراحل نصب، عنوان کردهاست که باید دستور md \data\db را اجرا کنید تا مسیر پیش فرض اطلاعات آن به صورت دستی ایجاد شود. اما ... این مسیر دقیقا به کجا اشاره میکند؟ چون شبیه به مسیرهای ویندوزی نیست.
- در ادامه برای آزمایش، به پوشهی program files ویندوز رفته، monogodb نصب شده را یافته و سپس فایل mongod.exe را از طریق خط فرمان اجرا میکنیم (برنامهی سرور). اگر این کار را انجام دهیم، این پروسه با نمایش خطای یافت نشدن مسیر c:\data\db، بلافاصله خاتمه پیدا میکند. به همین جهت در همین مسیری که در خط فرمان قرار داریم (جائیکه فایل mongod.exe قرار دارد)، دستور md \data\db را اجرا میکنیم. اجرای این دستور در این حالت، همان پوشهی c:\data\db را ایجاد میکند. نکتهای که شاید خیلیها با آن آشنایی نداشته باشند.
- اکنون اگر مجددا فایل mongod.exe را اجرا کنیم، اجرای آن موفقیت آمیز خواهد بود و پیام منتظر دریافت اتصالات بودن از طریق پورت 27017 را نمایش میدهد.
- مرحلهی بعد، اجرای فایل mongo.exe است تا به این دیتابیس سرور در حال اجرا متصل شویم (برنامهی کلاینت). در اینجا برای مثال میتوان دستور show dbs را اجرا کرد تا لیست بانکهای اطلاعاتی آنرا نمایش دهد.
مراحل نصب MongoDB به صورت Container توسط Docker:
- ابتدا برای مثال به سایت گوگل مراجعه کرده و اینبار mongodb docker را جستجو میکنیم تا بتوانیم به محل دریافت image آن هدایت شویم. با ورود به آن، در بالای صفحه عنوان شدهاست که official repository است که سبب اطمینان از بستهی ارائه شدهی توسط آن میشود. بنابراین در اینجا بجای مراجعه به سایت متکی به خود mongodb، به docker hub برای دریافت آن مراجعه کردهایم. در اینجا با جستجوی یک برنامه، متادیتا و اطلاعات آماری بسیاری را نیز میتوان در مورد برنامههای مختلف، مشاهده کرد که در سایت متکی به خود نرم افزارهای مختلف، در دسترس نیستند. همچنین در اینجا اگر بر روی برگهی Tags یک مخزن کلیک کنید، مشاهده میکنید که تمام فایلهای موجود در آن توسط docker hub از لحاظ مشکلات امنیتی پیشتر اسکن شدهاند و گزارش آنها قابل مشاهدهاست. علاوه بر اینها docker hub به همراه یک docker store برای برنامههای غیر رایگان نیز هست و این مورد فرآیند کار با نرم افزارهای تجاری را یک دست میکند.
- مرحلهی بعد، دریافت یک کپی از mongodb از docker hub است. اینبار بجای دریافت مستقیم یک فایل zip یا msi، از دستور docker pull mongo استفاده میشود که یک image را در نهایت دریافت میکند. این image، حاوی برنامهی مدنظر و تمام وابستگیهای آن است.
- پس از دریافت image، مرحلهی بعد، اجرای mongodb به همراه آن است. در حالت متداول، ابتدا نرم افزار داخل فایل zip یا msi استخراج شده و سپس بر روی سیستم اجرا میشوند، اما در اینجا مفهوم معادل نصب نرم افزار دریافت شدهی از بستهی zip همراه آن، یک container است. یک container دقیقا مانند یک نرم افزار از پیش نصب شده، عمل میکند و معادل اجرای فایل exe مانگو دی بی در اینجا، اجرای container آن است. بنابراین docker، از image دریافت شده، یک container را ایجاد میکند که دقیقا معادل یک نرم افزار از پیش نصب شده، رفتار خواهد کرد.
- پس از دریافت image، جهت اجرای آن به عنوان یک container، برای استفاده از نرم افزاری که دریافت کردهایم، تنها یک دستور است که باید با آن آشنا باشیم: docker run mongo. این دستور را در همان صفحهی docker hub مربوطه نیز میتوانید مشاهده کنید. پس از اجرای این دستور، دقیقا همان خروجی و پیام منتظر دریافت اتصالات بودن از طریق پورت 27017 را مشاهده خواهیم کرد. برای اجرای کلاینت آن نیز دستور docker exec -it 27 mongo را میتوان اجرا کرد. docker exec کار اجرای چندبارهی یک نرم افزار نصب شده را انجام میدهد.
این فرآیند در مورد تمام containerها یکی است و به این ترتیب به ازای هر نرم افزار مختلف، شاهد روش نصب متفاوتی نخواهیم بود.
- اجرای دستور docker stop نیز سبب خاتمهی تمام اینها میشود.
در این تصویر مقایسهای را بین مراحل متداول یافتن، دریافت، نصب و اجرای برنامهها را در دو حالت متداول و همچنین استفادهی از docker، مشاهده میکنید.
همچنین نکتهی جالبی که در مورد docker وجود دارد این است که اگر به task manager ویندوز مراجعه کنیم:
تمام پروسههایی که با job id مساوی 172 در اینجا اجرا شدهاند، متعلق به docker بوده و آنها دقیقا مانند یک پروسهی معمولی سیستم عامل جاری، در کنار سایر پروسههای موجود، اجرا میشوند. بنابراین برنامهای که از طریق docker اجرا میشود، هیچ تفاوتی با اجرای متداول آن بر روی سیستم عامل، از طریق روش مراجعهی مستقیم به فایل exe مرتبط و اجرای مستقیم آن ندارد. همانطور که پیشتر نیز عنوان شد، containerها در مورد نرم افزارها هستند و نه مجازی سازی و یک container در حال اجرا، حاوی تعدادی برنامهی در حال اجرای بر روی سیستم عامل جاری، در کنار سایر برنامههای آن میباشد.
البته containers به همراه ایزوله سازیهای بسیاری اجرا میشوند. برای مثال به روز رسانی یک کتابخانهی ثالث بر روی سیستم عامل، سبب از کار افتادن برنامهی اجرای شدهی توسط یک container نمیشود.
در قسمت بعد، نحوهی نصب Docker را بر روی ویندوز، بررسی میکنیم.
کار کردن با پروتکل Ping-back آنچنان ساده نیست؛ از این جهت که تبادل ارتباطات آن با پروتکل XML-RPC انجام میشود. XML-RPC نیز توسط PHP کارها بیشتر مورد استفاده قرار میگیرد؛ بجای استفاده از پروتکلهای استاندارد وب سرویسها مانند Soap و امثال آن. پیاده سازیهای ابتدایی Pingback نیز مرتبط است به Wordpress معروف که با PHP تهیه شدهاست. در ادامه نگاهی خواهیم داشت به جزئیات پیاده سازی ارسال ping-back توسط برنامههای ASP.NET.
یافتن آدرس وب سرویس سایت پذیرای Pingback
اولین قدم در پیاده سازی Pingback، یافتن آدرسی است که باید اطلاعات مورد نظر را به آن ارسال کرد. این آدرس عموما به دو طریق ارائه میشود:
الف) در هدری به نام x-pingback و یا pingback
ب) در قسمتی از کدهای HTML صفحه به شکل
<link rel="pingback" href="pingback server">
همانطور که ملاحظه میکنید، نیاز است Response header را آنالیز کنیم.
private Uri findPingbackServiceUri() { var request = (HttpWebRequest)WebRequest.Create(_targetUri); request.UserAgent = UserAgent; request.Timeout = Timeout; request.ReadWriteTimeout = Timeout; request.Method = WebRequestMethods.Http.Get; request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; using (var response = request.GetResponse() as HttpWebResponse) { if (response == null) return null; var url = extractPingbackServiceUriFormHeaders(response); if (url != null) return url; if (!isResponseHtml(response)) return null; using (var reader = new StreamReader(response.GetResponseStream())) { return extractPingbackServiceUriFormPage(reader.ReadToEnd()); } } } private static Uri extractPingbackServiceUriFormHeaders(WebResponse response) { var pingUrl = response.Headers.AllKeys.FirstOrDefault(header => header.Equals("x-pingback", StringComparison.OrdinalIgnoreCase) || header.Equals("pingback", StringComparison.OrdinalIgnoreCase)); return getValidAbsoluteUri(pingUrl); } private static Uri extractPingbackServiceUriFormPage(string content) { if (string.IsNullOrWhiteSpace(content)) return null; var regex = new Regex(@"(?s)<link\srel=""pingback""\shref=""(.+?)""", RegexOptions.IgnoreCase); var match = regex.Match(content); return (!match.Success || match.Groups.Count < 2) ? null : getValidAbsoluteUri(match.Groups[1].Value); } private static Uri getValidAbsoluteUri(string url) { Uri absoluteUri; return string.IsNullOrWhiteSpace(url) || !Uri.TryCreate(url, UriKind.Absolute, out absoluteUri) ? null : absoluteUri; } private static bool isResponseHtml(WebResponse response) { var contentTypeKey = response.Headers.AllKeys.FirstOrDefault(header => header.Equals("content-type", StringComparison.OrdinalIgnoreCase)); return !string.IsNullOrWhiteSpace(contentTypeKey) && response.Headers[contentTypeKey].StartsWith("text/html", StringComparison.OrdinalIgnoreCase); }
targetUri، آدرسی است از یک سایت دیگر که در سایت ما درج شدهاست. زمانیکه این صفحه را درخواست میکنیم، response.Headers.AllKeys حاصل میتواند حاوی کلید x-pingback باشد یا خیر. اگر بلی، همینجا کار پایان مییابد. فقط باید مطمئن شد که این آدرس مطلق است و نه نسبی. به همین جهت در متد getValidAbsoluteUri، بررسی بر روی UriKind.Absolute انجام شدهاست.
اگر هدر فاقد کلید x-pingback باشد، قسمت ب را باید بررسی کرد. یعنی نیاز است محتوای Html صفحه را برای یافتن link rel=pingback بررسی کنیم. همچنین باید دقت داشت که پیش از اینکار نیاز است حتما بررسی isResponseHtml صورت گیرد. برای مثال در سایت شما لینکی به یک فایل 2 گیگابایتی SQL Server درج شدهاست. در این حالت نباید ابتدا 2 گیگابایت فایل دریافت شود و سپس بررسی کنیم که آیا محتوای آن حاوی link rel=pingback است یا خیر. اگر محتوای ارسالی از نوع text/html بود، آنگاه کار دریافت محتوای لینک انجام خواهد شد.
ارسال Ping به آدرس سرویس Pingback
اکنون که آدرس سرویس pingback یک سایت را یافتهایم، کافی است ping ایی را به آن ارسال کنیم:
public void Send() { var pingUrl = findPingbackServiceUri(); if (pingUrl == null) throw new NotSupportedException(string.Format("{0} doesn't support pingback.", _targetUri.Host)); sendPing(pingUrl); } private void sendPing(Uri pingUrl) { var request = (HttpWebRequest)WebRequest.Create(pingUrl); request.UserAgent = UserAgent; request.Timeout = Timeout; request.ReadWriteTimeout = Timeout; request.Method = WebRequestMethods.Http.Post; request.ContentType = "text/xml"; request.ProtocolVersion = HttpVersion.Version11; makeXmlRpcRequest(request); using (var response = (HttpWebResponse)request.GetResponse()) { response.Close(); } } private void makeXmlRpcRequest(WebRequest request) { var stream = request.GetRequestStream(); using (var writer = new XmlTextWriter(stream, Encoding.ASCII)) { writer.WriteStartDocument(true); writer.WriteStartElement("methodCall"); writer.WriteElementString("methodName", "pingback.ping"); writer.WriteStartElement("params"); writer.WriteStartElement("param"); writer.WriteStartElement("value"); writer.WriteElementString("string", Uri.EscapeUriString(_sourceUri.ToString())); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteStartElement("param"); writer.WriteStartElement("value"); writer.WriteElementString("string", Uri.EscapeUriString(_targetUri.ToString())); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteEndElement(); writer.WriteEndElement(); } }
در اینجا sourceUri آدرس صفحهای در سایت ما است که targetUri ایی (آدرسی از سایت دیگر) در آن درج شدهاست. در یک pinback، صرفا این دو آدرس به سرویس دریافت کنندهی pingback ارسال میشوند.
سپس سایت دریافت کنندهی ping، ابتدا sourceUri را دریافت میکند تا عنوان آنرا استخراج کند و همچنین بررسی میکند که آیا targetUri، در آن درج شدهاست یا خیر (آیا spam است یا خیر)؟
تا اینجا اگر این مراحل را کنار هم قرار دهیم به کلاس Pingback ذیل خواهیم رسید:
Pingback.cs
نحوهی استفاده از کلاس Pingback تهیه شده
کار ارسال Pingback عموما به این نحو است: هر زمانیکه مطلبی یا یکی از نظرات آن، ثبت یا ویرایش میشوند، نیاز است Pingbackهای آن ارسال شوند. بنابراین تنها کاری که باید انجام شود، استخراج لینکهای خارجی یک صفحه و سپس فراخوانی متد Send کلاس فوق است.
یافتن لینکهای یک محتوا را نیز میتوان مانند متد extractPingbackServiceUriFormPage فوق، توسط یک Regex انجام داد و یا حتی با استفاده از کتابخانهی معروف HTML Agility Pack:
var doc = new HtmlWeb().Load(url); var linkTags = doc.DocumentNode.Descendants("link"); var linkedPages = doc.DocumentNode.Descendants("a") .Select(a => a.GetAttributeValue("href", null)) .Where(u => !String.IsNullOrEmpty(u));
امروز اولین دستورات MDX را خواهیم نوشت. قبل از شروع کار فراموش نکنید موارد زیر را حتما انجام داده باشید :
- نصب پایگاه داده ی Adventure Work DW 2008 و همچنین نصب پایگاه دادهی چند بعدی Adventure Work DW 2008 روی SSAS
- مطاله قسمتهای قبلی برای آشنایی با مفاهیم پایه .
در صورتیکه پیش شرایط فوق را نداشته باشید، احتمالا در ادامه با مشکلاتی مواجه خواهید شد؛ زیرا برای آموزش MDX Query ها از پایگاه دادهی Adventure Work DW 2008 استفاده شده است.
دقت داشته باشید که MDX Query ها تا حدودی شبیه T/SQL میباشند؛ اما مطلقا از نظر مفهومی با هم شباهت ندارند. به عبارت دیگر ما در T/SQL با یک مدل رابطهای سرو کار داریم در حالیکه در MDX ها با یک پایگاه داده چند بعدی کار میکنیم. به بیان دیگر در پایگاه دادههای رابطهای صحبت از جداول، ردیفها، ستونها و ضرب دکارتی مجموعهها میباشد، اما در پایگاه دادههای چند بعدی در خصوص Dimension,Fact,Cube,Tuple و ... صحبت میکنیم. البته ماکروسافت تلاش کردهاست تا حد زیادی Syntax ها شبیه به یکدیگر باشند.
نحوهی نوشتن یک Select در MDX ها به صورت زیر میباشد :
Select {} On Columns , {} On Rows From <Cube_Name> Where <Condition>
در ادامه با اجرای هر کوئری، توضیحات لازم در خصوص آن ارایه میگردد و با پیگیری این آموزشها میتوانید مفاهیم، توابع و ... را در MDX Query ها بیاموزید.
برای اجرای دستورات زیر باید Microsoft SQL Server Management Studio را باز نمایید و به سرویس SSAS متصل شوید. سپس پایگاه دادهی Adventure Works DW 2008R2 را انتخاب نمایید و از Cubes Adventure Works را انتخاب نمایید.
حال دکمهی New Query را در بالای صفحه بزنید ( Ctrl + N )
سپس در صفحهی باز شده میتوانید Cube یا SubCube های آن Cube را انتخاب کرده و کمی پایینتر Measure Group را خواهیم داشت و در انتها Measure ها و Dimension ها قرار گرفتهاند. (در هنگام نوشتن Select میتوان از عمل Drag&Drop برای آسانتر شدن نوشتن MDX Query ها نیز استفاده کنید)
متاسفانه هنوز در IDE مربوط به SQL Server کلیدی برای مرتب سازی دستورات MDX وجود ندارد و البته در نرم افزار هایی مانند SQL Toll Belt هم چنین چیزی قرار داده نشده است . بنابر این توصیه میشود در نوشتن دستورات MDX تمام تلاش خود را بکنید تا دستوراتی مرتب و خوانا را تولید کنید.
با اجرای دستور زیر اولین کوئری خود را در پایگاه دادهی چند بعدی بنویسید (برای اجرا کلید F5 مانند T/SQL کار خواهد کرد.)
Select From [Adventure Works]
شاید تعجب کنید. کوئری فاقد قسمت Projection میباشد! در MDX ها میتوان هیچ سطر یا ستونی را انتخاب نکرد. اما چگونه؟ و خروجی نمایش داده شده چیست؟
برای توضیح مطلب فوق باید در خصوص Default Measure کمی اطلاعات داشته باشید. در هنگام Deploy کردن پروژه در SSAS برای هر Cube یک Measure به عنوان Measure پیش فرض انتخاب شده. بنابر این در صورتیکه هیچ گونه Projection یا Where ایی اعمال نشده باشد، SQL Server به صورت پیش فرض مقدار Mesaure پیش فرض را بدون اعمال هیچ بعدی نمایش میدهد.
خروجی دستور بالا مشابه تصویر زیر میباشد.
حال دستور زیر را اجرا میکنیم :
Select From [Adventure Works] Where [Measures].[Reseller Sales Amount]
تصویر خروجی به صورت زیر میباشد :
شاید باز هم تعجب کنید. نوشتن نام یک شاخص به جای عبارت شرط؟! آیا خروجی عبارات شرطی نباید Boolean باشند؟
خیر. اگر چنین پرسش هایی در ذهن شما ایجاد شده باشد، به دلیل مقایسهی MDX با T/SQL میباشد. در اینجا شرط Where بر روی ردیفهای جدول مدل رابطه ای اعمال نمیشود و عملا بیانگر واکشی اطلاعات از مدل چند بعدی میباشد. با اعمال شرط فوق به SSAS اعلام کرده ایم که خروجی بر اساس شاخص [Measures].[Reseller Sales Amount] باشد. با توجه به این که شاخص انتخاب شده با شاخص پیش فرض یکی میباشد خروجی با حالت قبل تفاوتی نخواهد کرد.
برای درک بهتر، کوئری زیر را اجرا کنید :
Select From [Adventure Works] where [Measures].[Internet Sales Amount]
استفاده از این شرط سبب استفاده نشدن از شاخص پیش فرض می شود . به عبارت دیگر این کوئری دارای سرجمع مبلغ فروش اینترنتی می باشد.
دستور زیر را اجرا کنید :
Select [Measures].[Reseller Sales Amount] on columns From [Adventure Works]
با اعمال یک شاخص خاص در ستون ، عملا فیلترینگ انجام می شود
استفاده از یک دایمنشن در ستون :
دستور زیر را اجرا کنید
Select [Date].[Calendar].[Calendar Year] on columns From [Adventure Works]
خروجی به شکل زیر خواهد بود
همان طور که مشاهده میکنید خروجی دارای چندین ستون میباشد و دارای مقادیری در هر ستون. اما این مقادیر از کجا آمده اند؟
همواره این نکته را به خاطر بسپارید که در صورت عدم ذکر نام یک Measure در کوئری ، SSAS از Measure پیش فرض استفاده میکند. حال کوئری فوق میزان فروش نمایندگان ( Reseller Sales Amount ) را در هر سال نمایش میدهد.
سوال بعدی این میباشد که این سالها از کجا آمده اند؟ خوب برای درک بهتر این مورد میتوانیم مانند تصویر زیر به دایمنشن Date رفته و در ساختار سلسله مراتبی ، اعضای سطح [Date].[Calendar].[Calendar Year] را مشاهده کنیم.
ایجاد سرجمع ستونها :
کوئری زیر را اجرا نمایید
Select {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns From [Adventure Works]
بعد از اجرا
تصویر زیر را خواهید دید :
سوال اول این میباشد که کاربرد {} در انتخاب دایمنشنها چیست؟ در پاسخ میتوان گفت که اگر شاخص ها یا بعد ها ، مرتبط به یک سلسله مراتب باشند آنها را در یک {} قرار می دهیم ولی اگر سلسله مراتب متفاوت باشد، یا بعد و شاخص باشند باید در () قرار بگیرند .
خوب همان طور که مشخص است در ساختار سلسله مراتبی ابتدا سال و بعد یک سطح بالاتر را انتخاب کرده ایم این به معنی نمایش سرجمع در سطح بالاتر از سال میباشد(سرجمع تمامی سال ها).
استفاده از دایمنشن و Measure در سطر و ستون مجرا :
کوئری زیر را اجرا نمایید
Select {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns, [Product].[Product Categories].[Category] on rows From [Adventure Works]
خروجی مشابه شکل زیر میباشد
در مثال فوق از بعدها در ستون و همزمان، نمایش نوع دسته بندی محصولات در ردیفها استفاده شده است. به عبارت دیگر نتیجه عبارت است از فروش نماینگان فروش ( Reseller Sales Amount ) براساس هر سال به تفکیک نوع دسته بندی محصول فروخته شده.
(کسانی که چنین گزارشی را با استفاده از T/SQL نوشته اند، احتمالا از آسانی نوشتن این گزارش توسط MDX ها شگفت زده شده اند.)
قراردادن فیلد سرجمع در ردیف :
برای این منظور کوئری زیر را اجرا نمایید
Select {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns, {[Product].[Product Categories].[Category],[Product].[Product Categories]}on rows From [Adventure Works]
خروجی به صورت زیر میباشد
نحوهی نمایش سرجمع در ردیف، مشابه نمایش سرجمع در ستون میباشد.
استفاده از تابع non empty :
برای حذف ستون هایی که کاملا دارای مقدار null میباشند به صورت زیر عمل میکنیم :
Select non empty {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns , {[Product].[Product Categories].[Category],[Product].[Product Categories]} on rows From [Adventure Works]
خروجی به صورت زیر میباشد:
انتخاب دو دایمنشن در سطر و ستون و مشخص نمودن یک Measure خاص برای کوئری :
برای این کار به صورت زیر عمل خواهیم کرد:
Select {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns, {[Product].[Product Categories].[Category],[Product].[Product Categories]} on rows From [Adventure Works] Where [Measures].[Internet Sales Amount]
در اینجا با اعمال شرط Where عملا از SSAS خواستهایم خروجی برای شاخص مشخص شده واکشی شود.
در بالا میزان فروش اینترنتی برای دسته بندی محصولات و در سالهای مختلف ارائه و همچنین سرجمع ستون و سطر نیز نمایش داده شده است.
در صورتیکه بخواهیم ستون و سطرهایی را که دارای مقدار null در تمامی آن سطر یا ستون میباشند، حذف کنیم به صورت زیر عمل میکنیم:
Select non empty {[Date].[Calendar].[Calendar Year],[Date].[Calendar]} on columns, non empty {[Product].[Product Categories].[Category],[Product].[Product Categories]} on rows From [Adventure Works] Where [Measures].[Internet Sales Amount]
اگر در یک دایمنشن فقط یک سلسله مراتب باشد یا اصلا سلسله مراتبی وجود نداشته باشد، می توان از نام خود دایمنشن استفاده کرد
Select [Sales Channel] on columns From [Adventure Works]
و دقت داشته باشید دایمنشنی که دارای بیش از یک سلسله مراتب باشد، حتما باید در Select مشخص شود که از کدام سلسله مراتب می خواهیم استفاده کنیم .در غیر این صورت با خطا مواجه خواهیم شد.
Select [Product] on columns From [Adventure Works]
استفاده از فیلدهای یک دایمنشن که دارای سلسه مراتب می باشد نیز جایز می باشد
Select [Product].[Category] on columns From [Adventure Works]
Select [Product].[Category].[all] on columns From [Adventure Works] -- Select [Product].[Category].[All] on columns From [Adventure Works] -- Select [Product].[Category].[(all)] on columns From [Adventure Works] -- Select [Product].[Category].[all products] on columns From [Adventure Works]
برای به دست آوردن سرجمع کل روی یک صفت از دایمنشن، باید از سه حالت آخر استفاده کرد. حالت اول خطا دارد و خروجی خالی نمایش داده می شود .
در صورتی که بخواهیم از یک دایمنشن تمامی Member های آن را واکشی کنیم به صورت زیر عمل خواهیم کرد
Select {[Product].[Category].members} on columns From [Adventure Works]
استفاده از Members روی یک خصوصیت در دایمنشن به معنی دریافت سرجمع آن صفت و سپس تک تک اجزای آن صفت میباشد.
اگر از یک صفت واکشی اطلاعات انجام شود در سطح اعضای آن، در آن صورت دیگر سرجمع نمایش داده نمی شود و فقط جمع هر عضو در آن صفت نمایش داده می شود .
Select [Product].[Category].[Category].members -- dimension.hierarchy.level.members on columns From [Adventure Works]
اگر بخواهیم دو ستون را داشته باشیم که هر دو برای یک دایمنشن میباشند باید از {} استفاده کرد . دستور اول خطا خواهد داشت.
Select [Product].[Category].[Category].members,[Product].[Category].[All Products] on columns From [Adventure Works]
در دستور دوم با استفاده از {} خروجی نمایش داده میشود که عبارت است از تمامی اعضای سطح [Product].[Category].[Category]. به همراه سرجمع تمامی محصولات.
Select {[Product].[Category].[Category].members,[Product].[Category].[All Products]} on columns From [Adventure Works]
یک راه کوتاهتر برای انتخاب تمامی اعضا و سرجمع آنها
Select {[Product].[Category].[Category],[Product].[Category]} on columns From [Adventure Works]
می توان از کلمات Members, All X استفاده نکرد.
انتخاب اولین دسته بندی محصول البته این ترتیب بر اساس Key Columns در SSAS می باشد .
Select [Product].[Category].&[1] on columns From [Adventure Works]
انتخاب دقیق یک عضو در خروجی
Select [Product].[Category].[Bikes] on columns From [Adventure Works]
انتخاب دو عضو از یک دایمنشن
Select {[Product].[Category].[Bikes],[Product].[Category].[Clothing]} on columns From [Adventure Works]
واکشی تمامی دسته بندی محصولات بر اساس Measure پیش فرض :
Select [Product].[Product Categories].members on columns From [Adventure Works]
در صورتیکه بخواهیم دو Dimension مختلف را در یک ستون یا سطر بیاوریم باید از Join استفاده کنیم. بنابر این دو دستور زیر با خطا روبرو میشوند
Select [Product].[Product Categories],[Product].[Category] on columns From [Adventure Works] Go Select {[Product].[Product Categories],[Product].[Category]} on columns From [Adventure Works]
تعریف Axis : به هر کدام از ستون یا سطر یک محور یا Axis گفته میشود.
با بررسی مثال فوق به نتایج زیر خواهیم رسید.
1. امکان استفاده از دو سلسله مراتب مختلف از یک دایمنشن در یک Axis وجود ندارد . مگر اینکه آنها را باهمدیگر CrossJoin کنیم .
2. امکان استفاده از دو سلسله مراتب مختلف از یک دایمنشن در دو Axis مختلف وجود دارد .
ترتیب انتخاب Axis ها به صورت زیر میباشد:
1. Columns
2. Rows
برای مشخص شدن موضوع کوئری زیر را اجرا کنید
Select [Product].[Product Categories].members on rows From [Adventure Works]
نمیتوانیم ردیفی را واکشی کنیم بدون اینکه ستونی برای کوئری مشخص کرده باشیم.
البته میتوان ستون خالی ایجاد نماییم مانند مثال زیر :
Select {} on columns, [Product].[Product Categories].members on rows From [Adventure Works]
البته در این صورت خروجی فقط نام دسته بندی محصولات خواهد بود زیرا هیچ ستونی مشخص نشده .
در مقالات بعدی به ادامهی مطالب MDX Query خواهیم پرداخت.