Skip to content

4. مبانی توسعه ASP.NET

4.1. مفهوم یک برنامه وب ASP.NET

4.1.1. مقدمه

یک برنامه وب، برنامه‌ای است که شامل اسناد مختلفی (HTML، کدهای NET، تصاویر، صداها و غیره) می‌شود. این اسناد باید در یک دایرکتوری ریشه واحد قرار گیرند که به آن ریشه برنامه وب گفته می‌شود. یک مسیر مجازی بر روی وب‌سرور با این ریشه مرتبط است. ما پیش از این با مفهوم دایرکتوری مجازی برای وب‌سرور Cassini آشنا شده‌ایم. این مفهوم برای وب‌سرور IIS نیز صدق می‌کند. تفاوت کلیدی بین این دو سرور این است که، در هر زمان، سرور IIS می‌تواند هر تعداد دایرکتوری مجازی داشته باشد، در حالی که سرور وب Cassini تنها یک دایرکتوری مجازی دارد—دایرکتوری‌ای که هنگام راه‌اندازی مشخص شده است. این بدان معناست که سرور IIS می‌تواند همزمان چندین برنامه وب را ارائه دهد، در حالی که سرور Cassini تنها در هر زمان یک برنامه را ارائه می‌دهد. در مثال‌های قبلی، سرور Cassini همیشه با پارامترهای (<webroot>,/aspnet) راه‌اندازی می‌شد، که پوشه مجازی /aspnet را به پوشه فیزیکی <webroot> نگاشت می‌کرد. بنابراین وب‌سرور همیشه همان وب‌اپلیکیشن را ارائه می‌داد. این موضوع مانع از نوشتن و آزمایش صفحات مختلف و مستقل در داخل این وب‌اپلیکیشن واحد نمی‌شد. هر وب‌اپلیکیشن دارای منابع خاص خود است که در زیر ریشه فیزیکی آن یعنی <webroot> قرار دارند:

  • یک پوشه به نام [bin] که می‌توان کلاس‌های پیش‌کامپایل‌شده را در آن قرار داد
  • یک فایل [global.asax] که برای راه‌اندازی کل برنامه وب و همچنین محیط زمان اجرا برای هر یک از کاربران آن استفاده می‌شود
  • یک فایل [web.config] که برای پیکربندی رفتار برنامه استفاده می‌شود
  • یک فایل [default.aspx] که به‌عنوان نقطهٔ ورود برنامه عمل می‌کند
  • ...

به محض اینکه یک برنامه از این سه منبع استفاده کند، به مسیرهای فیزیکی و مجازی خود نیاز پیدا می‌کند. در واقع، هیچ دلیلی وجود ندارد که دو برنامه وب مختلف به یک شکل پیکربندی شوند. تمام مثال‌های قبلی ما می‌توانستند در یک برنامه واحد (<webroot>,/aspnet) قرار گیرند، زیرا از هیچ‌یک از منابع فوق استفاده نمی‌کردند.

بیایید به معماری MVC که در ابتدای این فصل برای توسعه یک برنامه وب توصیه شد، بازگردیم:

Image

برنامه وب از فایل‌های کلاسی (کنترلرها، کلاس‌های کسب‌وکار، کلاس‌های دسترسی به داده) و فایل‌های ارائه (اسناد HTML، تصاویر، صداها، صفحات سبک و غیره) تشکیل شده است. تمام این فایل‌ها در یک دایرکتوری ریشه واحد قرار خواهند گرفت که گاهی اوقات از آن با نام <application-path> یاد خواهیم کرد. این دایرکتوری ریشه با یک مسیر مجازی <application-vpath> مرتبط خواهد بود. نگاشت بین این مسیر مجازی و مسیر فیزیکی از طریق تنظیمات وب‌سرور پیکربندی می‌شود. ما دیده‌ایم که برای سرور Cassini، این نگاشت هنگام راه‌اندازی سرور انجام می‌شود. به عنوان مثال، در یک پنجره DOS، Cassini با استفاده از دستور زیر راه‌اندازی می‌شود:

webserver.exe /port:80 /path:<application-path> /vpath:<application-vpath>

در پوشه <application-path>، بسته به نیازهایمان، موارد زیر را خواهیم یافت:

  • پوشه [bin]، برای ذخیره کلاس‌های پیش‌کامپایل‌شده (DLLها)
  • فایل [global.asax] زمانی که نیاز به انجام عملیات اولیه باشد، چه هنگام شروع برنامه و چه هنگام آغاز یک جلسه کاربری
  • فایل [web.config] زمانی که نیاز به پیکربندی برنامه داریم
  • فایل [default.aspx] زمانی که به یک صفحه پیش‌فرض در داخل برنامه نیاز داریم

برای پایبندی به این مفهوم وب‌اپلیکیشن، تمام مثال‌های زیر در پوشه‌ای با نام <application-path> که مختص این اپلیکیشن است، قرار داده خواهند شد و یک پوشه مجازی <application-vpath> به آن مرتبط می‌شود، زیرا سرور Cassini به گونه‌ای راه‌اندازی می‌شود که این دو پارامتر را به هم متصل کند.

4.1.2. پیکربندی یک برنامه وب

اگر <application-path> ریشه یک برنامه به نام ASP.NET باشد، می‌توانید از فایل <application-path>\web.config برای پیکربندی آن استفاده کنید. این فایل در قالب XML است. در اینجا یک مثال آورده شده است:

<?xml version="1.0" encoding="UTF-8" ?>

<configuration>
  <appSettings>
    <add key="nom" value="tintin"/>
    <add key="age" value="27"/>
  </appSettings>   
</configuration>

لطفاً توجه داشته باشید که تگ‌های XML به حروف بزرگ و کوچک حساس هستند. تمام اطلاعات پیکربندی باید بین تگ‌های <configuration> و </configuration> قرار گیرند. بخش‌های پیکربندی متعددی در دسترس هستند. در اینجا تنها یکی را معرفی می‌کنیم: بخش <appSettings> که امکان инициализация داده‌ها با استفاده از تگ <add> را فراهم می‌کند. نحو این تگ به شرح زیر است:

<add key="identificateur" value="valeur"/>

هنگامی که وب سرور یک برنامه را راه‌اندازی می‌کند، بررسی می‌کند که آیا فایلی به نام web.config در <مسیر-برنامه> وجود دارد یا خیر. در صورت وجود، آن را می‌خواند و اطلاعات آن را در یک شیء از نوع [ConfigurationSettings] ذخیره می‌کند که در تمام صفحات برنامه در حین فعال بودن در دسترس خواهد بود. کلاس [ConfigurationSettings] دارای یک متد استاتیک [AppSettings] است:

Image

برای بازیابی مقدار کلید C از فایل پیکربندی، بنویسید ConfigurationSettings.AppSettings("C"). این یک رشته را بازمی‌گرداند. برای استفاده از فایل پیکربندی قبلی، بیایید صفحه‌ای به نام [default.aspx] ایجاد کنیم. کد VB از فایل [default.aspx.vb] به شرح زیر خواهد بود:


Imports System.Configuration

Public Class _default
    Inherits System.Web.UI.Page

    Protected nom As String
    Protected age As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'در حال بازیابی اطلاعات پیکربندی است
        nom = ConfigurationSettings.AppSettings("nom")
        age = ConfigurationSettings.AppSettings("age")
    End Sub

End Class

می‌توانیم ببینیم که هنگام بارگذاری صفحه، مقادیر پارامترهای پیکربندی [nom] و [age] بازیابی می‌شوند. این مقادیر توسط کد ارائه برای [default.aspx] نمایش داده خواهند شد:


<%@ Page src="default.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="_default" %>
<html>
    <head>
        <title>Configuration</title>
    </head>
    <body>
        Nom :
        <% =nom %><br/>
        Age :
        <% =age %><br/>
    </body>
</html>

برای آزمایش، فایل‌های [web.config]، [default.aspx] و [default.aspx.vb] را در یک پوشه قرار دهید:

D:\data\devel\aspnet\poly\chap2\config1>dir
30/03/2004  15:06                  418 default.aspx.vb
30/03/2004  14:57                  236 default.aspx
30/03/2004  14:53                  186 web.config

فرض کنید <application-path> پوشه‌ای است که شامل سه فایل برنامه می‌باشد. سرور Cassini با پارامترهای (<application-path>,/aspnet/config1) راه‌اندازی می‌شود. ما فایل‌های URL و [http://localhost/aspnet/config1] را درخواست می‌کنیم. از آنجا که [config1] یک پوشه است، وب‌سرور در داخل آن به دنبال فایلی به نام [default.aspx] می‌گردد و در صورت یافتن آن را نمایش می‌دهد. در این مورد، آن را پیدا خواهد کرد:

Image

4.1.3. برنامه، جلسه، زمینه

4.1.3.1. فایل global.asax

کد موجود در فایل [global.asax] همیشه قبل از بارگذاری صفحه‌ای که توسط درخواست جاری درخواست شده است، اجرا می‌شود. این فایل باید در ریشه <application-path> برنامه قرار داشته باشد. اگر این فایل وجود داشته باشد، فایل [global.asax] در زمان‌های مختلف توسط وب‌سرور استفاده می‌شود:

  1. هنگامی که برنامه وب شروع یا بسته می‌شود
  2. هنگامی که یک جلسه کاربری شروع یا پایان می‌یابد
  3. هنگامی که یک درخواست کاربر آغاز می‌شود

مانند صفحات .aspx، فایل [global.asax] را می‌توان به روش‌های مختلفی نوشت، به‌ویژه با تفکیک کد VB به یک کلاس کنترل‌کننده و کد ارائه. این انتخاب پیش‌فرض ویژوال استودیو است و ما نیز در اینجا همین کار را انجام خواهیم داد. معمولاً هیچ ارائه (presentation) برای مدیریت وجود ندارد، زیرا این وظیفه به صفحات .aspx محول شده است. بنابراین، محتوای فایل [global.asax] به یک دستور (directive) که به فایلی حاوی کد کنترل‌کننده ارجاع می‌دهد، محدود می‌شود:


<%@ Application src="Global.asax.vb" Inherits="Global" %>

توجه داشته باشید که دستور دیگر [Page] نیست، بلکه [Application] است. کد کنترل‌کننده مرتبط [global.asax.vb] که توسط ویژوال استودیو تولید شده، به شرح زیر است:


Imports System
Imports System.Web
Imports System.Web.SessionState

Public Class Global
    Inherits System.Web.HttpApplication

  Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام راه‌اندازی برنامه فعال می‌شود
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام شروع جلسه فعال می‌شود
    End Sub

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' در شروع هر درخواست فعال می‌شود
    End Sub

    Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام تلاش برای احراز هویت کاربر فعال می‌شود
    End Sub

    Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام رخ دادن خطا فعال می‌شود
    End Sub

    Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام پایان جلسه فعال می‌شود
    End Sub

    Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام بسته‌شدن برنامه فعال می‌شود
    End Sub

End Class

توجه داشته باشید که کلاس کنترلر از کلاس [HttpApplication] مشتق شده است. در طول عمر یک برنامه، چندین رویداد مهم وجود دارد. این رویدادها توسط رویه‌هایی مدیریت می‌شوند که ساختار کلی آن‌ها در بالا نشان داده شده است.

  • [Application_Start]: باید به خاطر داشت که یک برنامه وب در یک مسیر مجازی «دربرگرفته شده» است. برنامه به محض اینکه یک صفحه واقع در این مسیر مجازی توسط یک کلاینت درخواست شود، شروع به کار می‌کند. سپس رویه [Application_Start] اجرا می‌شود. این تنها باری است که این رویه اجرا خواهد شد. در این رویه، هرگونه راه‌اندازی اولیه مورد نیاز برنامه، مانند ایجاد اشیایی که چرخه عمرشان با چرخه عمر برنامه مطابقت دارد، انجام خواهد شد.
  • [Application-End]: زمانی اجرا می‌شود که برنامه به پایان رسیده باشد. هر برنامه با یک زمان‌بندی اتمام عدم‌فعالیت مرتبط است که در [web.config] قابل تنظیم است و پس از آن، برنامه به‌عنوان پایان‌یافته در نظر گرفته می‌شود. بنابراین، این وب‌سرور است که بر اساس تنظیمات برنامه این تصمیم را می‌گیرد. زمان‌بندی اتمام عدم‌فعالیت یک برنامه به دوره‌ای گفته می‌شود که در آن هیچ کلاینتی درخواستی برای یک منبع از برنامه ارسال نکرده است.
  • [Session-Start]/[Session_End]: یک جلسه به هر کلاینت متصل می‌شود، مگر اینکه برنامه برای کار بدون جلسات پیکربندی شده باشد. یک کلاینت با کاربرانی که پشت یک صفحه نمایش نشسته‌اند یکسان نیست. اگر یک کاربر دو مرورگر برای دسترسی به برنامه باز کرده باشد، این دو به عنوان دو کلاینت شمرده می‌شوند. یک کلاینت با یک توکن جلسه (session token) شناسایی می‌شود که باید آن را در هر یک از درخواست‌های خود بگنجاند. این توکن جلسه یک رشته کاراکتری منحصربه‌فرد است که به‌طور تصادفی توسط وب‌سرور تولید می‌شود. هیچ دو کلاینتی نمی‌توانند توکن جلسه یکسانی داشته باشند. این توکن به شرح زیر کلاینت را دنبال می‌کند:
    • کلاینتی که اولین درخواست خود را ارسال می‌کند، توکن جلسه را ارسال نمی‌کند. وب‌سرور این موضوع را تشخیص داده و یکی را برای آن تعیین می‌کند. این امر آغاز جلسه را مشخص می‌کند و رویه [Session_Start] اجرا می‌شود. این تنها یک بار اتفاق می‌افتد.
    • کلاینت با ارسال توکنی که هویت آن را مشخص می‌کند، درخواست‌های بعدی خود را انجام می‌دهد. این امر به سرور وب امکان می‌دهد تا اطلاعات مرتبط با آن توکن را بازیابی کند. این به سرور اجازه می‌دهد تا درخواست‌های مختلف کلاینت را ردیابی کند.
    • برنامه کاربردی ممکن است فرم پایان جلسه را در اختیار کلاینت قرار دهد. در این صورت، خود کلاینت است که پایان جلسه را درخواست می‌کند. رویه [Session_End] اجرا خواهد شد. این فقط یک بار اتفاق می‌افتد.
    • ممکن است مشتری هرگز شخصاً درخواست پایان جلسه خود را نکند. در این صورت، پس از یک دوره عدم فعالیت جلسه—که می‌توان آن را از طریق [web.config] نیز پیکربندی کرد—جلسه توسط وب سرور خاتمه داده می‌شود. سپس رویه [Session_End] اجرا خواهد شد.
  • [Application_BeginRequest]: این رویه به محض دریافت یک درخواست جدید اجرا می‌شود. بنابراین برای هر درخواست از هر کلاینتی اجرا می‌گردد. این مکان مناسبی برای بررسی درخواست قبل از ارسال آن به صفحه مورد نظر است. حتی می‌توان تصمیم گرفت که آن را به صفحه دیگری هدایت (redirect) کرد.
  • [Application_Error]: هر زمان که خطایی رخ دهد که به صراحت توسط کد در کنترل‌کننده [global.asax.vb] مدیریت نشده باشد، اجرا می‌شود. در اینجا، درخواست مشتری می‌تواند به صفحه‌ای که علت خطا را توضیح می‌دهد، هدایت شود.

اگر هیچ‌کدام از این رویدادها نیاز به رسیدگی نداشته باشند، آنگاه می‌توان فایل [global.asax] را نادیده گرفت. این کاری است که در مثال‌های اول این فصل انجام شد.

4.1.3.2. مثال ۱

بیایید یک برنامه کاربردی توسعه دهیم تا سه مرحله را بهتر درک کنیم: شروع برنامه، شروع جلسه و شروع درخواست مشتری. فایل [global.asax] به شرح زیر خواهد بود:

<%@ Application src="Global.asax.vb" Inherits="global" %>

فایل مرتبط [global.asax.vb] به شرح زیر خواهد بود:


Imports System
Imports System.Web
Imports System.Web.SessionState

Public Class global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام راه‌اندازی برنامه فعال می‌شود
        'زمان را ثبت می‌کند
        Dim startApplication As String = Date.Now.ToString("T")
        ' آن را در زمینهٔ برنامه ذخیره می‌کند
        Application.Item("startApplication") = startApplication
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام شروع جلسه فعال می‌شود
        'زمان ثبت می‌شود
        Dim startSession As String = Date.Now.ToString("T")
        ' در جلسه قرار می‌گیرد
        Session.Item("startSession") = startSession
    End Sub

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        'زمان ثبت می‌شود
        Dim startRequest As String = Date.Now.ToString("T")
        ' آن را به جلسه اضافه کنید
        Context.Items("startRequest") = startRequest
    End Sub
End Class

نکات کلیدی کد به شرح زیر است:

  • سرور وب تعدادی شیء را از [global.asax.vb] در اختیار کلاس [HttpApplication] قرار می‌دهد:
    • یک برنامه کاربردی از نوع [HttpApplicationState] – که نماینده برنامه کاربردی وب است – دسترسی به یک فرهنگ لغت از اشیاء [Application.Item] را فراهم می‌کند که برای همه کلاینت‌های برنامه در دسترس است – اشتراک‌گذاری اطلاعات بین کلاینت‌های مختلف را ممکن می‌سازد – دسترسی همزمان چندین کلاینت به خواندن/نوشتن یکسان به داده‌ها نیازمند همگام‌سازی کلاینت است.
    • جلسه از نوع [HttpSessionState] – نمایانگر یک مشتری خاص است – دسترسی به یک فرهنگ لغت شیء [Session.Item] را که برای تمام درخواست‌های آن مشتری قابل دسترسی است، فراهم می‌کند – امکان ذخیره اطلاعات در مورد یک مشتری را فراهم می‌آورد، که سپس می‌توان آن را هنگام ارسال درخواست‌های بیشتر توسط مشتری بازیابی کرد.
    • درخواست از نوع [HttpRequest] – نمایانگر درخواست فعلی مشتری است، HTTP
    • پاسخ از نوع [HttpResponse] – نمایانگر پاسخی است که در حال حاضر توسط سرور برای مشتری تولید می‌شود HTTP
    • نوع سرور [HttpServerUtility] – متدهای کاربردی را فراهم می‌کند، به‌ویژه برای هدایت درخواست به صفحه‌ای غیر از صفحهٔ اصلی مورد نظر.
    • زمینه‌ای از نوع [HttpContext] – این شیء با هر درخواست جدید دوباره ایجاد می‌شود اما توسط همه صفحات درگیر در پردازش درخواست مشترک است – امکان انتقال اطلاعات از صفحه‌ای به صفحه دیگر را در حین پردازش یک درخواست از طریق فرهنگ لغت Items آن فراهم می‌کند.
  • روند [Application_Start] شروع برنامه را در متغیری که در دیکشنری قابل دسترسی در سطح برنامه ذخیره شده است، ثبت می‌کند
  • روند [Session_Start] شروع جلسه را در متغیری که در دیکشنری قابل دسترسی در سطح جلسه ذخیره شده است، ثبت می‌کند
  • رویه [Application_BeginRequest] شروع درخواست را در یک متغیر ذخیره شده در دیکشنری قابل دسترسی در سطح درخواست ثبت می‌کند (c.a.d در تمام مدت پردازش در دسترس است اما در پایان آن از بین می‌رود)

صفحه مقصد صفحه زیر خواهد بود: [main.aspx]:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
    <head>
        <title>global.asax</title>
    </head>
    <body>
        jeton de session  :
        <% =jeton %><br/>
        début Application  :
        <% =startApplication %><br/>
        début Session  :
        <% =startSession %><br/>
        début Requête  :
        <% =startRequest %><br/>        
    </body>
</html>

این صفحهٔ نمای کلی، مقادیری را که توسط کنترل‌کننده‌اش [main.aspx.vb] محاسبه شده‌اند، نمایش می‌دهد:

Public Class main
    Inherits System.Web.UI.Page

    Protected startApplication As String
    Protected startSession As String
    Protected startRequest As String
    Protected jeton as String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
         'بازیابی اطلاعات برنامه و جلسه
        jeton=Session.SessionId
        startApplication = Application.Item("startApplication").ToString
        startSession = Session.Item("startSession").ToString
        startRequest = Context.Items("startRequest").ToString
    End Sub

End Class

کنترلر به سادگی سه بخش اطلاعات را که به ترتیب توسط [global.asax.vb] در برنامه، جلسه و زمینه قرار داده شده‌اند، بازیابی می‌کند.

ما برنامه را به شرح زیر تست می‌کنیم:

  1. فایل‌ها در یک پوشه واحد به نام <application-path> قرار داده شده‌اند.

Image

  1. سرور Cassini با پارامترهای (<application-path>,/aspnet/globalasax1) راه‌اندازی می‌شود
  2. یک کلاینت اول URL [http://localhost/aspnet/globalasax1/main.aspx] را درخواست می‌کند و نتیجه زیر را دریافت می‌کند:

Image

  1. همان کلاینت یک درخواست جدید ارسال می‌کند (با استفاده از گزینه Reload مرورگر):

Image

مشاهده می‌شود که تنها زمان درخواست تغییر کرده است. این دو نکته را نشان می‌دهد:

  • روندهای [Application_Start] و [Session_Start] از [global.asax] در طول درخواست دوم اجرا نشدند.
  • ابجکت‌های [Application] و [Session] که زمان‌های شروع برنامه و جلسه را ذخیره کرده‌اند، برای درخواست دوم نیز در دسترس هستند.
  1. ما یک مرورگر دوم راه‌اندازی می‌کنیم تا یک کلاینت دوم ایجاد کنیم و دوباره همان URL را درخواست می‌کنیم:

Image

این بار می‌توانیم ببینیم که زمان جلسه تغییر کرده است. مرورگر دوم، اگرچه روی همان ماشین است، به‌عنوان یک مشتری دوم در نظر گرفته شده و یک جلسه جدید برای آن ایجاد شده است. می‌توانیم ببینیم که دو مشتری توکن جلسه یکسانی ندارند. زمان شروع برنامه تغییر نکرده است، که به این معنی است که:

  • روال [Application_Start] از [global.asax.vb] اجرا نشد
  • شیء [Application]، که زمان شروع برنامه در آن ذخیره شده بود، برای کلاینت دوم قابل دسترسی است. بنابراین این شیء است که باید اطلاعاتی را که باید در میان کلاینت‌های مختلف برنامه به اشتراک گذاشته شود، ذخیره کند، در حالی که شیء [Session] برای ذخیره اطلاعاتی استفاده می‌شود که باید در میان درخواست‌های یک کلاینت واحد به اشتراک گذاشته شود.

4.1.3.3. نمای کلی

با آنچه تاکنون آموخته‌ایم، اکنون می‌توانیم یک نمودار اولیه تهیه کنیم که نحوه کار یک سرور وب و برنامه‌های وبی را که به آنها خدمت‌رسانی می‌کند، توضیح می‌دهد:

Image

نمودار بالا یک سرور را نشان می‌دهد که به دو برنامه با نام‌های A و B، هر کدام با دو کلاینت، خدمت‌رسانی می‌کند. یک وب سرور قادر است به طور همزمان به چندین برنامه وب خدمت‌رسانی کند. این برنامه‌ها کاملاً از یکدیگر مستقل هستند. ما بر روی برنامه A تمرکز خواهیم کرد. پردازش یک درخواست از کلاینت-1A به برنامه A به شرح زیر انجام خواهد شد:

  • کلاینت 1A منبعی را از وب سرور درخواست می‌کند که به دامنهٔ برنامهٔ A تعلق دارد. این بدان معناست که یک URL از نوع [http://machine:port/VA/ressource] درخواست می‌کند، که در آن VA مسیر مجازی برنامه A است.
  • اگر وب‌سرور تشخیص دهد که این اولین درخواست برای یک منبع از برنامه A است، رویداد [Application_Start] را در فایلی به نام [global.asax] که متعلق به برنامه A است، فعال می‌کند. یک شیء از نوع [HttpApplicationState] با نام [ApplicationA] ایجاد خواهد شد. کدهای مختلف برنامه‌ها داده‌ها را با دامنه‌های [Application] و c.a.d در این شیء ذخیره خواهند کرد که شامل داده‌های مربوط به همه کاربران است. شیء [ApplicationA] تا زمانی که وب‌سرور برنامه A را متوقف کند، وجود خواهد داشت.
  • اگر سرور وب همچنین تشخیص دهد که در حال کار با یک مشتری جدید برای برنامه A است، رویداد [Session_Start] را در فایل [global.asax] برای برنامه A فعال خواهد کرد. یک شیء از نوع [HttpSessionState] با نام [Session-1A] ایجاد خواهد شد. این شیء به برنامهٔ A امکان می‌دهد اشیاء دامنهٔ [Session] و c.a.d متعلق به یک مشتری خاص را ذخیره کند. شیء [Session-1A] تا زمانی که کلاینت 1A به ارسال درخواست ادامه دهد، وجود خواهد داشت. این شیء امکان ردیابی این کلاینت را فراهم می‌کند. وب‌سرور در دو حالت تشخیص می‌دهد که با کلاینت جدیدی سر و کار دارد:
    • کلاینت توکن جلسه را در هدرهای HTTP درخواست خود ارسال نکرده است
    • کلاینت یک توکن جلسه ارسال کرده است که وجود ندارد (نقص عملکرد کلاینت یا تلاش برای هک) یا دیگر وجود ندارد. یک توکن جلسه پس از یک دوره عدم فعالیت کلاینت منقضی می‌شود (به طور پیش‌فرض ۲۰ دقیقه با IIS). این محدودیت زمانی قابل تنظیم است.
  • در تمام موارد، وب‌سرور رویداد [Application_BeginRequest] را از فایل [global.asax] فراخوانی می‌کند. این رویداد پردازش درخواست کلاینت را آغاز می‌کند. رویه رایج این است که این رویداد پردازش نشود و در عوض کنترل به صفحه‌ای که کلاینت درخواست کرده است واگذار شود تا آن صفحه سپس درخواست را مدیریت کند. این رویداد همچنین می‌تواند برای تجزیه و تحلیل درخواست، پردازش آن و تصمیم‌گیری در مورد اینکه کدام صفحه باید به عنوان پاسخ ارسال شود، استفاده شود. ما از این تکنیک برای پیاده‌سازی برنامه‌ای استفاده خواهیم کرد که با معماری MVC که در مورد آن بحث کردیم، مطابقت دارد.
  • پس از عبور از فیلتر [global.asax]، درخواست کلاینت به یک صفحه .aspx ارسال می‌شود که درخواست را پردازش خواهد کرد. بعداً خواهیم دید که امکان ارسال درخواست از طریق فیلتری متشکل از چندین صفحه نیز وجود دارد. صفحه نهایی مسئول ارسال پاسخ به کلاینت خواهد بود. صفحات می‌توانند اطلاعاتی را که محاسبه کرده‌اند به درخواست اولیه کلاینت اضافه کنند. آن‌ها می‌توانند این اطلاعات را در مجموعه Context.Items ذخیره کنند. در واقع، تمام صفحات درگیر در پردازش درخواست کلاینت به این مخزن داده دسترسی دارند.
  • کد صفحات مختلف به مخازن داده، یعنی اشیاء [ApplicationA]، [Session-1A] و ... دسترسی دارد. مهم است به یاد داشته باشیم که وب سرور همزمان چندین کلاینت را برای برنامه A پردازش می‌کند. تمام این کلاینت‌ها به شیء [Application A] دسترسی دارند. اگر نیاز به اصلاح داده‌ها در این شیء داشته باشند، همگام‌سازی مشتری لازم است. هر مشتری XA همچنین به استخر داده‌های [Session-XA] دسترسی دارد. از آنجایی که این استخر برای آن مشتری رزرو شده است، در اینجا نیازی به همگام‌سازی نیست.
  • سرور وب چندین برنامه وب را به‌طور همزمان ارائه می‌دهد. بین کلاینت‌های این برنامه‌های مختلف تداخلی وجود ندارد.

از این توضیحات، نکات زیر باید مورد توجه قرار گیرد:

  • در هر لحظه، یک وب سرور به طور همزمان به چندین کلاینت خدمت‌رسانی می‌کند. این بدان معناست که برای پردازش درخواست دیگر، منتظر اتمام درخواست قبلی نمی‌ماند. بنابراین در زمان T، چندین درخواست متعلق به کلاینت‌های مختلف برای برنامه‌های کاربردی مختلف در حال پردازش هستند. کدی که به طور همزمان در داخل وب سرور اجرا می‌شود، گاهی اوقات به آن رشته‌های اجرایی (execution threads) گفته می‌شود.
  • رشته‌های اجرایی مشتریانِ استفاده‌کننده از برنامه‌های وب مختلف با یکدیگر تداخل ندارند. جداسازی وجود دارد.
  • رشته‌های اجرایی برای مشتریان یک برنامه یکسان ممکن است نیاز به اشتراک‌گذاری داده‌ها داشته باشند:
    • رشته‌های اجرایی برای درخواست‌های دو مشتری متفاوت (با توکن جلسه یکسان) ممکن است از طریق شیء [Application] داده‌ها را به اشتراک بگذارند.
    • رشته‌های اجرایی برای درخواست‌های متوالی از یک مشتری واحد ممکن است از طریق شیء [Session] داده‌ها را به اشتراک بگذارند.
    • رشته‌های اجرایی صفحات متوالی که همان درخواست را از یک مشتری مشخص پردازش می‌کنند، می‌توانند از طریق شیء [Context] داده‌ها را به اشتراک بگذارند.

4.1.3.4. مثال ۲

بیایید یک مثال جدید برای روشن‌تر کردن آنچه تاکنون دیده‌ایم توسعه دهیم. فایل‌های زیر را در همان پوشه قرار می‌دهیم:

[global.asax]

<%@ Application src="Global.asax.vb" Inherits="global" %>

[global.asax.vb]


Imports System
Imports System.Web
Imports System.Web.SessionState

Public Class global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام راه‌اندازی برنامه اجرا می‌شود
        ' ابتدایی‌سازی شمارندهٔ مشتری
        Application.Item("nbRequêtes") = 0
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام شروع جلسه فعال می‌شود
        ' ابتدایی‌سازی شمارنده درخواست
        Session.Item("nbRequêtes") = 0
    End Sub
End Class

هدف این برنامه شمارش تعداد کل درخواست‌های ارسال‌شده به برنامه و تعداد درخواست‌های هر مشتری است. وقتی برنامه اجرا می‌شود ([Application_Start])، شمارنده درخواست‌های ارسال‌شده به برنامه روی 0 تنظیم می‌شود. این شمارنده در دامنه [Application] قرار دارد زیرا باید توسط همه کلاینت‌ها افزایش یابد. وقتی یک کلاینت برای اولین بار به [Session_Start] متصل می‌شود، شمارنده درخواست‌های انجام‌شده توسط آن کلاینت روی 0 تنظیم می‌شود. این شمارنده در دامنه [Session] قرار می‌گیرد زیرا فقط به یک کلاینت خاص مربوط است.

پس از اجرای [global.asax]، فایل بعدی، [main.aspx]، اجرا خواهد شد:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
    <head>
        <title>application-session</title>
    </head>
    <body>
        jeton de session :
        <% =jeton %>
        <br />
        requêtes Application :
        <% =nbRequêtesApplication %>
        <br />
        requêtes Client :
        <% =nbRequêtesClient %>
        <br />
    </body>
</html>

این سه مورد اطلاعات را که توسط کنترلر آن محاسبه شده‌اند، نمایش می‌دهد:

  1. هویت مشتری از طریق توکن جلسهٔ او: [jeton]
  2. تعداد کل درخواست‌های ارسال‌شده به برنامه: [nbRequêtesApplication]
  3. تعداد کل درخواست‌های ارسال‌شده توسط کلاینتی که با شناسه ۱ شناسایی شده است: [nbRequêtesClient]

این سه مورد اطلاعات در [main.aspx.vb] محاسبه می‌شوند:

Public Class main
    Inherits System.Web.UI.Page

    Protected nbRequêtesApplication As String
    Protected nbRequêtesClient As String
    Protected jeton As String

    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
         ' یک درخواست دیگر برای برنامه
        Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
         ' یک درخواست دیگر در جلسه
        Session.Item("nbRequêtes") = CType(Session.Item("nbRequêtes"), Integer) + 1
         ' ابتدایی‌سازی متغیرهای ارائه
        nbRequêtesApplication = Application.Item("nbRequêtes").ToString
        jeton = Session.SessionID
        nbRequêtesClient = Session.Item("nbRequêtes").ToString
    End Sub
End Class

وقتی [main.aspx.vb] اجرا می‌شود، در حال حاضر در حال پردازش درخواستی از یک مشتری خاص هستیم. ما از شیء [Application] برای افزایش شمار درخواست‌ها برای برنامه و از شیء [Session] برای افزایش شمار درخواست‌ها برای مشتری‌ای که در حال پردازش درخواستش هستیم، استفاده می‌کنیم. به یاد داشته باشید که در حالی که همه کلاینت‌های یک برنامه یکسان از شیء [Application] مشترک استفاده می‌کنند، هر یک از آنها شیء [Session] مخصوص به خود را دارند.

ما اپلیکیشن را با قرار دادن چهار فایل مذکور در پوشه‌ای به نام <application-path> و راه‌اندازی سرور Cassini با پارامترها (<application-path>,/aspnet/webapplia) تست می‌کنیم. یک مرورگر باز کرده و URL [http://localhost/aspnet/webapplia/main.aspx] را درخواست می‌کنیم:

Image

یک درخواست دوم را با استفاده از دکمه [Reload] انجام می‌دهیم:

Image

ما یک مرورگر دوم را برای درخواست همان URL باز می‌کنیم. برای وب‌سرور، این یک کلاینت جدید است:

Image

می‌توانیم ببینیم که توکن جلسه تغییر کرده است، به این معنی که اکنون یک کلاینت جدید داریم. این موضوع در تعداد درخواست‌های ارسال‌شده از سوی کلاینت منعکس شده است. اکنون به مرورگر اول بازمی‌گردیم و دوباره همان URL را درخواست می‌کنیم:

Image

تعداد درخواست‌های ارسال‌شده به برنامه در واقع به‌درستی شمارش شده است.

4.1.3.5. نیاز به همگام‌سازی کلاینت‌های یک برنامه

در برنامه قبلی، شمارنده درخواست‌های ارسال‌شده به برنامه در رویه [Form_Load] در صفحه [main.aspx] به شرح زیر افزایش می‌یابد:

         'یک درخواست دیگر برای برنامه
        Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1

اگرچه این دستور ساده است، اما نیازمند اجرای چندین دستور پردازنده است. فرض کنیم که سه دستور لازم است:

  1. خواندن شمارنده
  2. افزایش شمارنده
  3. بازنویسی شمارنده

سرور وب روی یک ماشین چندوظیفه‌ای اجرا می‌شود، به این معنی که به هر وظیفه برای چند میلی‌ثانیه پردازنده اختصاص داده می‌شود و سپس آن را از دست می‌دهد، و پس از آنکه سایر وظایف نیز نوبت خود را دریافت کردند، دوباره آن را به دست می‌آورد. فرض کنیم دو مشتری، A و B، همزمان درخواستی به سرور وب ارسال می‌کنند. فرض کنید مشتری A ابتدا وارد رویه [Form_Load] از [main.aspx.vb] می‌شود، مقدار کانتر را (=100) می‌خواند و سپس به دلیل اتمام نوبت زمانی‌اش متوقف می‌شود. حال فرض کنید نوبت به کلاینت B می‌رسد و آنها نیز با همان سرنوشت مواجه می‌شوند: آنها موفق می‌شوند مقدار شمارنده را (=100) بخوانند اما فرصتی برای افزایش آن ندارند. کلاینت‌های A و B هر دو مقدار شمارنده 100 را دارند. فرض کنید دوباره نوبت مشتری A است: او شمارنده خود را افزایش داده، آن را روی 101 تنظیم می‌کند و سپس پایان می‌یابد. اکنون نوبت مشتری B است که هنوز مقدار شمارنده قدیمی را به جای مقدار جدید دارد. بنابراین او نیز مقدار شمارنده را روی 101 تنظیم کرده و پایان می‌دهد. مقدار شمارنده درخواست در برنامه اکنون نادرست است.

برای تشریح این مشکل، برنامه قبلی را بازنگری کرده و آن را به شرح زیر اصلاح می‌کنیم:

  • فایل‌های [global.asax]، [global.asax.vb] و [main.aspx] بدون تغییر باقی می‌مانند
  • فایل [main.aspx.vb] به شکل زیر درمی‌آید:

Imports System.Threading

Public Class main
    Inherits System.Web.UI.Page

    Protected nbRequêtesApplication As Integer
    Protected nbRequêtesClient As Integer
    Protected jeton As String

    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'یک درخواست دیگر برای برنامه و جلسه
        ' خواندن شمارنده‌ها
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' منتظر ۵ ثانیه بمانید
        Thread.Sleep(5000)
        ' شمارنده‌های افزایشی
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' ذخیره شمارنده‌ها
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' ابتدایی‌سازی متغیرهای نمایش
        jeton = Session.SessionID
    End Sub
End Class

فرآیند قرائت کنتور به چهار مرحله تقسیم شده است:

  1. خواندن شمارنده
  2. تعلیق نخ اجرای
  3. افزایش شمارنده
  4. بازنویسی شمارنده

بیایید بار دیگر دو مشتری A و B را در نظر بگیریم. بین فاز خواندن و فازی که شمارنده‌های درخواست افزایش می‌یابند، اجرای نخ را به مدت ۵ ثانیه متوقف می‌کنیم. نتیجهٔ فوری این کار آن است که نخ اجرای برنامه کنترل پردازنده را از دست می‌دهد و پردازنده به وظیفه‌ای دیگر اختصاص می‌یابد. فرض کنیم که مشتری A ابتدا اجرا می‌شود. این مشتری مقدار N را از شمارنده می‌خواند و به مدت ۵ ثانیه متوقف می‌شود. اگر در این مدت، مشتری B به پردازنده دسترسی داشته باشد، باید همان مقدار N را از شمارنده بخواند. در نهایت، هر دو مشتری باید همان مقدار شمارنده را نمایش دهند که این امر غیرعادی است.

ما برنامه را با قرار دادن چهار فایل قبلی در پوشه‌ای به نام <application-path> و راه‌اندازی سرور Cassini با پارامترها (<application-path>,/aspnet/webapplib) آزمایش می‌کنیم. ما دو مرورگر مختلف را با URL [http://localhost/aspnet/webapplib/main.aspx] راه‌اندازی می‌کنیم. اولین مرورگر را برای درخواست URL اجرا می‌کنیم، سپس، بدون انتظار برای پاسخ—که ۵ ثانیه بعد دریافت می‌شود—مرورگر دوم را اجرا می‌کنیم. پس از کمی بیش از ۵ ثانیه، نتیجه زیر را به دست می‌آوریم:

Image

می‌توانیم ببینیم:

  • که دو کلاینت متفاوت وجود دارند (با توکن جلسه یکسان نیستند)
  • که هر کلاینت یک درخواست انجام داده است
  • که شمارنده درخواست‌های ارسال‌شده به برنامه باید در یکی از دو مرورگر روی عدد ۲ باشد. اما این‌طور نیست.

حالا، بیایید یک آزمایش دیگر انجام دهیم. با استفاده از همان مرورگر، پنج درخواست به URL [http://localhost/aspnet/webapplib/main.aspx] ارسال می‌کنیم. دوباره، آن‌ها را یکی پس از دیگری بدون انتظار برای نتایج ارسال می‌کنیم. پس از اجرای تمام درخواست‌ها، برای آخرین مورد نتیجه زیر را دریافت می‌کنیم:

Image

می‌توانیم ببینیم که:

  • که پنج درخواست به‌عنوان درخواست‌هایی از یک کلاینت واحد در نظر گرفته شده‌اند، زیرا شمارنده درخواست‌های کلاینت روی عدد ۵ قرار دارد. اگرچه در بالا نشان داده نشده است، می‌توانیم ببینیم که توکن جلسه در واقع برای هر پنج درخواست یکسان است.
  • که شمارنده درخواست‌های ارسال‌شده به برنامه صحیح است.

از این چه نتیجه‌ای می‌توان گرفت؟ هیچ چیز قطعی. شاید وب‌سرور پردازش یک درخواست از یک کلاینت را آغاز نمی‌کند اگر آن کلاینت قبلاً یک درخواست در حال اجرا داشته باشد؟ بنابراین هرگز اجرای همزمان درخواست‌ها از یک کلاینت وجود نخواهد داشت. آن‌ها یکی پس از دیگری پردازش می‌شوند. این نکته باید تأیید شود. در واقع ممکن است به نوع کلاینت مورد استفاده بستگی داشته باشد.

4.1.3.6. همگام‌سازی کلاینت

مشکل مطرح‌شده در برنامه قبلی، یک مشکل کلاسیک (اما حل‌کردن آن چندان ساده نیست) در مورد دسترسی انحصاری به یک منبع است. در مورد خاص ما، باید اطمینان حاصل کنیم که دو کلاینت، A و B، نمی‌توانند همزمان در توالی کد زیر باشند:

  1. خواندن شمارنده
  2. افزایش شمارنده
  3. بازنویسی شمارنده

چنین توالی کدی به عنوان یک بخش بحرانی شناخته می‌شود. این بخش نیازمند همگام‌سازی نخ‌هایی است که همزمان آن را اجرا می‌کنند. پلتفرم .NET ابزارهای مختلفی را برای تضمین این امر ارائه می‌دهد. در اینجا، از کلاس [Mutex] استفاده خواهیم کرد.

Image

در اینجا، ما فقط از سازندها و متدهای زیر استفاده خواهیم کرد:

public Mutex()
ایجاد یک شی همگام‌سازی M
public bool WaitOne()
رشته‌ی T1 که عملیات M.WaitOne() را اجرا می‌کند، مالکیت شیء همگام‌سازی M را درخواست می‌کند. اگر مابکس M توسط هیچ رشته‌ای در اختیار گرفته نشده باشد (که در ابتدا همین‌طور است)، به نخ T1 که آن را درخواست کرده است، «اعطا» می‌شود. اگر کمی بعد، نخی با شناسه T2 همان عملیات را امتحان کند، مسدود خواهد شد. این به این دلیل است که یک موتکس در هر لحظه فقط می‌تواند متعلق به یک نخ باشد. هنگامی که نخ T1 موتکس M را که در اختیار دارد آزاد کند، آن آزاد خواهد شد. بنابراین، ممکن است چندین نخ در حین انتظار برای موتکس M مسدود شوند.
public void ReleaseMutex()
رشته‌ی T1 که عملیات M.ReleaseMutex() را انجام می‌دهد، مالکیت متاکس M را واگذار می‌کند. وقتی رشته‌ی T1 پردازنده را از دست می‌دهد، سیستم ممکن است آن را به یکی از رشته‌هایی که منتظر متاکس M هستند اختصاص دهد. در نهایت تنها یکی آن را به‌دست می‌آورد؛ سایر نخ‌هایی که منتظر M هستند مسدود باقی خواهند ماند

یک مایتکس M دسترسی به یک منبع مشترک R را مدیریت می‌کند. یک نخ از طریق M.WaitOne() منبع R را درخواست می‌کند و از طریق M.ReleaseMutex() آن را آزاد می‌کند. یک بخش بحرانی کد که باید همزمان فقط توسط یک نخ اجرا شود، یک منبع مشترک است. همگام‌سازی اجرای بخش بحرانی را می‌توان به شرح زیر انجام داد:

M.WaitOne()
' تنها این نخ وارد اینجا می‌شود
'بخش بحرانی
....
M.ReleaseMutex()

که در آن M یک شیء از نوع Mutex است. البته، ضروری است که هرگز فراموش نکنید Mutex را که دیگر مورد نیاز نیست آزاد کنید، تا نخ دیگر بتواند به نوبه خود وارد بخش بحرانی شود؛ در غیر این صورت، نخ‌هایی که منتظر mutex هستند که هرگز آزاد نمی‌شود، هرگز به پردازنده دسترسی پیدا نخواهند کرد. علاوه بر این، باید از وضعیت بن‌بست (deadlock) که در آن دو نخ منتظر یکدیگر هستند، اجتناب کرد. توالی رویدادهای زیر را در نظر بگیرید:

  • یک نخ T1 یک متکس M1 را برای دسترسی به یک منبع مشترک R1 به دست می‌آورد
  • یک نخ T2 برای دسترسی به یک منبع مشترک R2، یک متکس M2 را به دست می‌آورد.
  • رشته T1 مَیتکس M2 را درخواست می‌کند. مسدود شده است.
  • رشته T2 مایتکس M1 را درخواست می‌کند. مسدود شده است.

در اینجا، نخ‌های T1 و T2 منتظر یکدیگر هستند. این وضعیت زمانی پیش می‌آید که رشته‌ها به دو منبع مشترک نیاز دارند: منبع R1 که توسط موتکس M1 کنترل می‌شود، و منبع R2 که توسط موتکس M2 کنترل می‌شود. یک راه‌حل ممکن این است که با استفاده از یک موتکس واحد M، هر دو منبع را به‌طور همزمان درخواست کنیم. با این حال، این همیشه ممکن نیست، به‌ویژه اگر منجر به این شود که منبعی که به‌دست آوردنش پرهزینه است برای مدت طولانی درگیر شود. یک راه‌حل دیگر این است که رشته‌ای که M1 را در اختیار دارد و قادر به به‌دست‌آوردن M2 نیست، برای جلوگیری از بن‌بست، M1 را آزاد کند.

اگر آنچه را که به تازگی آموخته‌ایم به کار ببریم، برنامه ما به شکل زیر درمی‌آید:

  • فایل‌های [global.asax] و [main.aspx] بدون تغییر باقی می‌مانند
  • فایل [global.asax.vb] به شکل زیر درمی‌آید:

Imports System
Imports System.Web
Imports System.Web.SessionState
Imports System.Threading

Public Class global
    Inherits System.Web.HttpApplication

    Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام راه‌اندازی برنامه فعال می‌شود
        ' ابتدایی‌سازی شمارنده مشتری
        Application.Item("nbRequêtes") = 0
        'ایجاد قفل همگام‌سازی
        Application.Item("verrou") = New Mutex
    End Sub

    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' هنگام شروع جلسه فعال می‌شود
        ' ابتدایی‌سازی شمارنده درخواست
        Session.Item("nbRequêtes") = 0
    End Sub
End Class

تنها ویژگی جدید ایجاد یک [Mutex] است که توسط کلاینت‌ها برای همگام‌سازی استفاده خواهد شد. از آنجا که باید برای همه کلاینت‌ها قابل دسترسی باشد، در داخل شیء [Application] قرار داده شده است.

  • فایل [main.aspx.vb] به شکل زیر درمی‌آید:

Imports System.Threading

Public Class main
    Inherits System.Web.UI.Page

    Protected nbRequêtesApplication As Integer
    Protected nbRequêtesClient As Integer
    Protected jeton As String

    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' یک درخواست دیگر برای برنامه و جلسه
        ' ورود به بخش بحرانی – به‌دست‌آوردن قفل همگام‌سازی
        Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
        ' درخواست برای ورود تنها به بخش بحرانی زیر
        verrou.WaitOne()
        ' خواندن شمارنده‌ها
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' منتظر ۵ ثانیه
        Thread.Sleep(5000)
        ' افزایش شمارنده‌ها
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        'ذخیرهٔ شمارنده‌ها
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        'اجازه دسترسی به بخش بحرانی را بدهید
        verrou.ReleaseMutex()
        ' ابتدایی‌سازی متغیرهای نمایش
        jeton = Session.SessionID
    End Sub
End Class

می‌توانیم ببینیم که کلاینت:

  • درخواست می‌کند که به‌تنهایی وارد بخش بحرانی شود. برای این کار، مالکیت انحصاری موتکس [verrou] را درخواست می‌کند.
  • در پایان بخش بحرانی، متک [verrou] را آزاد می‌کند تا مشتری دیگری نیز بتواند به نوبه خود وارد بخش بحرانی شود.

ما با قرار دادن چهار فایل مذکور در پوشه‌ای به نام <application-path> و راه‌اندازی سرور Cassini با پارامترهای (<application-path>,/aspnet/webapplic) برنامه را آزمایش می‌کنیم. ما دو مرورگر مختلف را با URL [http://localhost/aspnet/webapplic/main.aspx] راه‌اندازی می‌کنیم. اولی را برای درخواست URL اجرا می‌کنیم و سپس، بدون انتظار برای پاسخ—که ۵ ثانیه بعد خواهد رسید—مرورگر دوم را اجرا می‌کنیم. پس از کمی بیش از ۵ ثانیه، نتیجه زیر را دریافت می‌کنیم:

Image

این بار، شمارنده درخواست‌های برنامه صحیح است.

نکته کلیدی این نمایش طولانی، ضرورت مطلق همگام‌سازی کلاینت‌های یک برنامه وب واحد است، اگر قرار باشد عناصر مشترکی را که بین همه کلاینت‌ها مشترک هستند، به‌روزرسانی کنند.

4.1.3.7. مدیریت توکن جلسه

ما در موارد متعدد به توکن جلسه که بین کلاینت و وب‌سرور مبادله می‌شود اشاره کرده‌ایم. بیایید مرور کنیم که چگونه کار می‌کند:

  • کلاینت درخواست اولیه را به سرور ارسال می‌کند. توکن جلسه ارسال نمی‌شود.
  • از آنجا که این درخواست حاوی توکن جلسه نیست، سرور یک مشتری جدید را تشخیص داده و برای آن یک توکن اختصاص می‌دهد. همراه با این توکن، یک شیء به نام [Session] وجود دارد که برای ذخیره اطلاعات خاص آن مشتری استفاده خواهد شد. این توکن تمام درخواست‌های بعدی این کلاینت را همراهی خواهد کرد. این توکن در سربرگ‌های HTTP پاسخ به اولین درخواست کلاینت گنجانده خواهد شد.
  • اکنون کلاینت توکن جلسه خود را می‌داند. این توکن را در هدرهای HTTP هر درخواست بعدی که به وب سرور ارسال می‌کند، بازمی‌فرستد. با استفاده از این توکن، سرور قادر خواهد بود شیء [Session] مرتبط با کلاینت را بازیابی کند.

برای تشریح این مکانیزم، اپلیکیشن قبلی را بازنگری می‌کنیم و تنها یک فایل [main.aspx.vb] را تغییر می‌دهیم:


Imports System.Threading

Public Class main
    Inherits System.Web.UI.Page

    Protected nbRequêtesApplication As Integer
    Protected nbRequêtesClient As Integer
    Protected jeton As String

    Private Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'یک درخواست دیگر برای برنامه و جلسه
        ' ورود به بخش بحرانی – به‌دست‌آوردن قفل همگام‌سازی
        Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
        'درخواست ورود به بخش بعدی به‌تنهایی
        verrou.WaitOne()
        ' خواندن شمارنده‌ها
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' منتظر ۵ ثانیه
        Thread.Sleep(5000)
        ' افزایش شمارنده‌ها
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' ذخیره شمارنده‌ها
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' اجازه دسترسی به بخش بحرانی را بده
        verrou.ReleaseMutex()
        ' ابتدایی‌سازی متغیرهای نمایش
        jeton = Session.SessionID
    End Sub

    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
        'ذخیره درخواست مشتری در request.txt در پوشه برنامه
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub
End Class

هنگامی که رویداد [Page_Init] رخ می‌دهد، درخواست مشتری را در پوشهٔ برنامه ذخیره می‌کنیم. بیایید چند نکته را مرور کنیم:

  • [TemplateSourceDirectory] نشان‌دهنده مسیر مجازی صفحه‌ای است که در حال حاضر در حال اجراست،
  • MapPath (TemplateSourceDirectory) مسیر فیزیکی متناظر را نشان می‌دهد. این امر به ما امکان می‌دهد تا مسیر فیزیکی فایلی را که قرار است ایجاد شود، بسازیم،
  • [Request] یک شیء است که نمایانگر درخواستی است که در حال پردازش است. این شیء با پردازش درخواست خام ارسال‌شده توسط کلاینت، c.a.d، که شامل دنباله‌ای از خطوط متن به شکل زیر است، ساخته شده است:

Image

  • Request.Save([FileName]) کل درخواست کلاینت (سرورهای HTTP و در صورت لزوم، سند پیرو آن) را در فایلی که مسیری برای آن به عنوان پارامتر ارسال می‌شود، ذخیره می‌کند.

بنابراین قادر خواهیم بود دقیقاً ببینیم درخواست کلاینت چه بوده است. ما با قرار دادن چهار فایل قبلی در پوشه‌ای به نام <application-path> و راه‌اندازی سرور Cassini با پارامترها (<application-path>,/aspnet/session1) برنامه را تست می‌کنیم. سپس با استفاده از یک مرورگر، URL را درخواست می‌کنیم.

[http://localhost/aspnet/session1/main.aspx]. نتیجهٔ زیر را به‌دست می‌آوریم:

Image

ما از فایل [request.txt]، که توسط [main.aspx.vb] ذخیره شده است، برای دسترسی به درخواست مرورگر استفاده می‌کنیم:

GET /aspnet/session1/main.aspx HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7b) Gecko/20040316

می‌توانیم ببینیم که مرورگر درخواستی برای URL و [/aspnet/session1/main.aspx] ارسال کرده و اطلاعات دیگری را که در فصل قبلی در مورد آن بحث کردیم، فرستاده است. در اینجا هیچ توکن جلسه‌ای قابل مشاهده نیست. با این حال، صفحه‌ای که در پاسخ دریافت شد نشان می‌دهد که سرور یک توکن جلسه ایجاد کرده است. هنوز نمی‌دانیم که آیا مرورگر آن را دریافت کرده است یا خیر. اکنون بیایید با استفاده از همان مرورگر یک درخواست دوم انجام دهیم (بارگذاری مجدد). پاسخ جدید زیر را دریافت می‌کنیم:

Image

ردیابی جلسه در واقع در حال انجام است، زیرا تعداد درخواست‌های مربوط به جلسه به درستی افزایش یافته است. اکنون بیایید به محتویات فایل [request.txt] نگاهی بیندازیم:

GET /aspnet/session1/main.aspx HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Cookie: ASP.NET_SessionId=y153tk45sise0lrhdzrf22m3
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.0; en-US; rv:1.7b) Gecko/20040316

می‌توانیم ببینیم که، برای این درخواست دوم، مرورگر یک هدر جدید HTTP [Cookie:] را برای سرور ارسال کرده است که یک قطعه اطلاعات به نام [ASP.NET_SessionId] را تعریف می‌کند، که مقدار آن توکن جلسه است که در پاسخ به درخواست اول ظاهر شد. وب‌سرور با استفاده از این توکن، این درخواست جدید را به شیء [Session] که با توکن [y153tk45sise0lrhdzrf22m3] شناسایی می‌شود، مرتبط کرده و شمارنده درخواست مربوطه را بازیابی می‌کند.

ما هنوز مکانیزم ارسال توکن توسط سرور به کلاینت را نمی‌دانیم، زیرا به پاسخ سرور HTTP دسترسی نداریم. شایان ذکر است که این پاسخ همان ساختار درخواست کلاینت را دارد، یعنی مجموعه‌ای از خطوط متن به شکل:

Image

ما قبلاً از یک کلاینت وب استفاده کردیم که دسترسی به پاسخ وب سرور HTTP را برای ما فراهم می‌کرد: کلاینت curl. ما دوباره از آن، در یک پنجره DOS، برای پرس‌وجوی همان URL که در مرورگر قبلی استفاده شده بود، استفاده می‌کنیم:

E:\curl>curl --include http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:31:42 GMT
X-AspNet-Version: 1.1.4322
Set-Cookie: ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close


<HTML>
        <HEAD>
                <title>application-session</title>
        </HEAD>
        <body>
                jeton de session :
                qxnxmqmvhde3al55kzsmx445
                <br>
                requêtes Application :
                3
                <br>
                requêtes Client :
                1
                <br>
        </body>
</HTML>

ما پاسخ سؤال خود را داریم. سرور وب توکن جلسه را در قالب یک هدر ارسال می‌کند: HTTP [Set-Cookie:]:

Set-Cookie: ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445; path=/

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

E:\curl>curl --include http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:36:06 GMT
X-AspNet-Version: 1.1.4322
Set-Cookie: ASP.NET_SessionId=cs2p12mehdiz5v55ihev1kaz; path=/
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close


<HTML>
        <HEAD>
                <title>application-session</title>
        </HEAD>
        <body>
                jeton de session :
                cs2p12mehdiz5v55ihev1kaz
                <br>
                requêtes Application :
                4
                <br>
                requêtes Client :
                1
                <br>
        </body>
</HTML>

از آنجایی که توکن جلسه را بازنفرستادیم، سرور نتوانست ما را شناسایی کند و یک توکن جدید صادر کرد. برای ادامه یک جلسه موجود، کلاینت باید توکن جلسه‌ای را که دریافت کرده است به سرور بازگرداند. ما این کار را در اینجا با استفاده از گزینه [--cookie clé=valeur] در curl انجام می‌دهیم، که هدر HTTP [Cookie: clé=valeur] را تولید می‌کند. ما دیدیم که مرورگر این هدر HTTP را در درخواست دوم خود ارسال کرد.

E:\curl>curl --include --cookie ASP.NET_SessionId=cs2p12mehdiz5v55ihev1kaz http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:40:20 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close


<HTML>
        <HEAD>
                <title>application-session</title>
        </HEAD>
        <body>
                jeton de session :
                cs2p12mehdiz5v55ihev1kaz
                <br>
                requêtes Application :
                5
                <br>
                requêtes Client :
                2
                <br>
        </body>
</HTML>

چند نکته قابل توجه است:

  • شمارنده درخواست مشتری واقعاً افزایش یافته است، که نشان می‌دهد سرور توکن ما را به درستی تشخیص داده است.
  • توکن جلسه نمایش‌داده‌شده توسط صفحه، دقیقاً همان توکنی است که ما ارسال کردیم
  • توکن جلسه دیگر در هدرهای HTTP که توسط وب سرور ارسال می‌شود، وجود ندارد. این به آن دلیل است که سرور آن را فقط یک بار ارسال می‌کند: زمانی که توکن در ابتدای یک جلسه جدید ایجاد می‌شود. پس از اینکه کلاینت توکن خود را دریافت کرد، این وظیفه کلاینت است که هر زمان که می‌خواهد شناسایی شود، از آن استفاده کند.

هیچ چیزی مانع از استفاده یک کلاینت از چندین توکن جلسه نمی‌شود، همانطور که در مثال زیر با [curl] نشان داده شده است، که در آن از توکن به دست آمده در طول اولین درخواست خود (درخواست شماره ۱) استفاده می‌کنیم:

E:\curl>curl --include --cookie ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445 http://localhost/aspnet/session1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 07:48:47 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 228
Connection: Close


<HTML>
        <HEAD>
                <title>application-session</title>
        </HEAD>
        <body>
                jeton de session :
                qxnxmqmvhde3al55kzsmx445
                <br>
                requêtes Application :
                6
                <br>
                requêtes Client :
                2
                <br>
        </body>
</HTML>

این مثال چه معنایی دارد؟ ما توکنی را ارسال کردیم که کمی پیش به‌دست آورده بودیم. وقتی وب‌سرور توکنی ایجاد می‌کند، آن را تا زمانی که کلاینت مرتبط با آن توکن به ارسال درخواست ادامه دهد، نگه می‌دارد. پس از یک دوره عدم فعالیت مشخص (به‌طور پیش‌فرض ۲۰ دقیقه با IIS)، توکن حذف می‌شود. مثال قبلی نشان می‌دهد که ما از توکنی استفاده کردیم که هنوز فعال بود.

شاید برایتان کنجکاو باشد که ببینید درخواست‌های HTTP از سمت کلاینت [curl] در طول تمام این عملیات‌ها چه بوده‌اند. می‌دانیم که این درخواست‌ها در فایل [request.txt] ثبت شده‌اند. در اینجا آخرین مورد را می‌بینید:

GET /aspnet/session1/main.aspx HTTP/1.1
Pragma: no-cache
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Cookie: ASP.NET_SessionId=qxnxmqmvhde3al55kzsmx445
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4

سربرگ HTTP که توکن جلسه را ارسال می‌کند، در واقع موجود است.

اطلاعاتی که توسط سرور از طریق هدرهای HTTP و [Set-Cookie:] منتقل می‌شود، کوکی‌ها نامیده می‌شود. سرور می‌تواند از این مکانیزم برای انتقال اطلاعاتی غیر از توکن جلسه استفاده کند. وقتی سرور S یک کوکی را به کلاینت ارسال می‌کند، همچنین طول عمر کوکی D و U مرتبط با URL را مشخص می‌کند. برای کلاینت، این بدان معناست که وقتی یک URL از شکل /U/path را از سرور S درخواست می‌کند، اگر کوکی را برای مدتی بیش از D دریافت نکرده باشد، می‌تواند آن را مجدداً ارسال کند. هیچ چیزی مانع از آن نمی‌شود که کلاینت این ضابطه رفتاری را نادیده بگیرد. با این حال، مرورگرها از آن پیروی می‌کنند. برخی مرورگرها به کاربران اجازه می‌دهند محتوای کوکی‌هایی را که دریافت می‌کنند مشاهده کنند. این موضوع در مورد مرورگر موزیلا نیز صدق می‌کند. در اینجا، به عنوان مثال، اطلاعاتی که مربوط به کوکی ارسال شده توسط سرور در مثال قبلی است، آورده شده است:

Image

این شامل:

  • نام کوکی، [ASP.NET_SessionId]
  • مقدار آن: [y153...m3]
  • ماشینی که با آن مرتبط است: [localhost]
  • URL مرتبط با آن: [/]
  • دوره عمر آن: [at end of session]

بنابراین مرورگر هر بار که یک URL را در قالب [http://localhost/...c.a.d درخواست می‌کند، توکن جلسه را ارسال خواهد کرد. هر زمان که یک URL را از سرور وب روی ماشین [localhost] درخواست می‌کند. عمر کوکی برابر با عمر جلسه است. برای مرورگر، این بدان معناست که کوکی هرگز منقضی نمی‌شود. مرورگر هر زمان که یک URL را از ماشین [localhost] درخواست کند، آن را ارسال خواهد کرد. بنابراین، اگر مرورگر در روز D توکن جلسه را دریافت کند، بسته شود و روز بعد دوباره استفاده شود، توکن جلسه را (که در یک فایل ذخیره شده است) مجدداً ارسال خواهد کرد. سرور این توکن را دریافت خواهد کرد، در حالی که دیگر آن را در اختیار ندارد، زیرا توکن جلسه در سرور عمر محدودی دارد (۲۰ دقیقه در IIS). در نتیجه، یک جلسه جدید آغاز خواهد شد.

امکان غیرفعال کردن کوکی‌ها در مرورگر وجود دارد. در این صورت، کلاینت توکن جلسه را دریافت می‌کند اما آن را بازنمی‌فرستد، که این امر از ردیابی جلسه جلوگیری می‌کند. برای نشان دادن این موضوع، ما کوکی‌ها را در مرورگر خود (در این مورد موزیلا) غیرفعال می‌کنیم:

Image

علاوه بر این، ما تمام کوکی‌های موجود را حذف می‌کنیم:

Image

پس از انجام این کار، سرور Cassini را برای شروع مجدد از ابتدا راه‌اندازی می‌کنیم و با استفاده از مرورگر، دوباره URL [http://localhost/aspnet/session1/main.aspx] را درخواست می‌کنیم:

Image

بیایید بررسی کنیم که آیا مرورگر ما کوکی را ذخیره کرده است:

Image

می‌توانیم ببینیم که مرورگر کوکی توکن جلسه را که سرور برایش ارسال کرده بود، ذخیره نکرده است. بنابراین می‌توانیم انتظار داشته باشیم که هیچ ردیابی جلسه‌ای انجام نخواهد شد. همان URL را دوباره درخواست می‌کنیم (بارگیری مجدد):

Image

این دقیقاً همان چیزی است که انتظار داشتیم. مرورگر توکن جلسه را بازنمی‌گرداند، هرچند آن را دریافت کرده بود اما ذخیره نکرده بود. بنابراین، سرور یک جلسه جدید با یک توکن جدید آغاز کرده است. درس قابل یادگیری از این مثال این است که اگر کاربر کوکی‌ها را در مرورگر خود غیرفعال کرده باشد، سیاست ردیابی جلسه ما به خطر می‌افتد. با این حال، راه دیگری به غیر از کوکی‌ها برای تبادل توکن جلسه بین سرور و کلاینت وجود دارد. در واقع، می‌توان به سرور وب اطلاع داد که برنامه بدون کوکی‌ها در حال اجرا است. این کار از طریق فایل پیکربندی [web.config] انجام می‌شود:


<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <system.web>
        <sessionState cookieless="true" timeout="10" />
    </system.web>
</configuration>

فایل پیکربندی بالا مشخص می‌کند که برنامه بدون کوکی (cookieless="true") کار خواهد کرد و حداکثر دوره عدم فعالیت برای توکن جلسه ۱۰ دقیقه (timeout="10") است. پس از این دوره، جلسه مرتبط با توکن خاتمه می‌یابد. فرآیند تبادل توکن جلسه بین سرور و کلاینت به شرح زیر است:

  1. کلاینت URL [http://machine:port/V/chemin] را درخواست می‌کند، که در آن V یک دایرکتوری مجازی روی وب‌سرور است
  2. سرور یک توکن J تولید می‌کند و به کلاینت دستور می‌دهد که به URL [http://machine:port/V/(J)/chemin] هدایت (redirect) شود. بنابراین، توکن را در URL مورد درخواست، بلافاصله پس از دایرکتوری مجازی V قرار داده است.
  3. کلاینت این هدایت را دنبال می‌کند و URL جدید URL [http://machine:port/V/(J)/chemin] را درخواست می‌کند.
  4. سرور به این درخواست پاسخ می‌دهد و یک صفحه پاسخ ارسال می‌کند.

بیایید این نکات مختلف را تشریح کنیم. ما کل برنامه قبلی را در پوشه جدید <application-path> قرار می‌دهیم. فایل قبلی [web.config] را در همین پوشه قرار می‌دهیم. علاوه بر این، کد نمایش [main.aspx] را برای درج یک لینک اصلاح می‌کنیم:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <HEAD>
        <title>application-session</title>
    </HEAD>
    <body>
        jeton de session :
        <% =jeton %>
        <br>
        requêtes Application :
        <% =nbRequêtesApplication %>
        <br>
        requêtes Client :
        <% =nbRequêtesClient %>
        <br>
        <a href="main.aspx">Recharger l'application</a>
    </body>
</HTML>

این لینک به صفحه [main.aspx] اشاره می‌کند و بنابراین معادل دکمه (بارگذاری مجدد) مرورگر است. سرور Cassini با پارامترهای (<application-path>,/session2) راه‌اندازی می‌شود. ما از رویه معمول خود در ثبت دایرکتوری مجازی [/aspnet/XX] عدول می‌کنیم. این به این دلیل است که به دلیل درج توکن جلسه در URL، دایرکتوری مجازی باید فقط یک عنصر داشته باشد: /XX. ما ابتدا از کلاینت [curl] برای درخواست URL [http://localhost/session2/main.aspx] استفاده می‌کنیم:

E:\curl>curl --include http://localhost/session2/main.aspx
HTTP/1.1 302 Found
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Thu, 01 Apr 2004 13:52:36 GMT
X-AspNet-Version: 1.1.4322
Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 163
Connection: Close

<html><head><title>Object moved</title></head><body>
<h2>Object moved to <a href='/session2/(hinadjag3bt0u155g5hqe245)/main.aspx'>here
</body></html>

می‌توانیم ببینیم که سرور به جای [HTTP/1.1 200 OK] با هدر HTTP [HTTP/1.1 302 Found] پاسخ می‌دهد. این یک هدر است که به کلاینت دستور می‌دهد به URL مشخص‌شده توسط هدر Location [Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx] هدایت کند. می‌توانیم توکن جلسه را که در URL هدایت درج شده است ببینیم. مرورگری که این پاسخ را دریافت می‌کند، درخواست جدید را به‌طور شفاف برای کاربر ارسال می‌کند، به‌طوری‌که کاربر درخواست جدید را نمی‌بیند. در صورتی که مرورگر خود هدایت را مدیریت نکند، سندی به نام HTML پس از کد HTTP فوق ارسال می‌شود. این سند حاوی پیوندی به URL هدایت است که کاربر می‌تواند روی آن کلیک کند.

اکنون، بیایید همین کار را با مرورگری که کوکی‌ها در آن غیرفعال شده‌اند انجام دهیم. بار دیگر، URL [http://localhost/session2/main.aspx] را درخواست می‌کنیم. پاسخ زیر را از سرور دریافت می‌کنیم:

Image

ابتدا توجه کنید که URL نمایش‌داده‌شده توسط مرورگر، همان چیزی نیست که ما درخواست کرده‌ایم. این نشان می‌دهد که یک هدایت (redirection) رخ داده است. در واقع، مرورگر همیشه URL سند آخر دریافت‌شده یعنی URL را نمایش می‌دهد. بنابراین، اگر URL [http://localhost/session2/main.aspx] را نمایش ندهد، به این دلیل است که به آن دستور داده شده تا به یک URL دیگر هدایت (redirect) شود. ممکن است چندین هدایت وجود داشته باشد. URL نمایش‌داده‌شده توسط مرورگر، URL آخرین هدایت است. ما می‌توانیم ببینیم که توکن جلسه در URL نمایش‌داده‌شده توسط مرورگر وجود دارد. ما می‌توانیم این را ببینیم زیرا این توکن نیز توسط برنامه ما در صفحه نمایش داده می‌شود.

بیایید کد لینک قرار داده شده در صفحه را به یاد بیاوریم:


        <a href="main.aspx">Recharger l'application</a>

این یک لینک نسبی است زیرا با یک اسلش (/) شروع نمی‌شود، که در این صورت آن را به یک لینک مطلق تبدیل می‌کند. نسبت به چه چیزی؟ برای درک این موضوع، باید به URL سند فعلی که نمایش داده می‌شود نگاه کنیم: [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx]. هر لینک نسبی که در این سند یافت شود، نسبت به مسیر [http://localhost/session2/(gu5ee455pkpffn554e3b1a32)] خواهد بود. بنابراین، لینک ما در بالا معادل لینک زیر است:


        <a href=" http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx">Recharger l'application</a>

این چیزی است که مرورگر هنگام قرار دادن نشانگر ماوس روی لینک به ما نشان می‌دهد:

Image

اگر روی لینک [Recharger l'application] کلیک کنیم، URL

[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx] که فراخوانی می‌شود. بنابراین سرور توکن جلسه را دریافت کرده و قادر خواهد بود اطلاعات مرتبط با آن را بازیابی کند. این چیزی است که پاسخ سرور به ما نشان می‌دهد:

Image

شایان ذکر است که اگر نیاز باشد جلسات را در یک برنامه وب ردیابی کنیم و مطمئن نباشیم که مرورگرهای کلاینت‌ها اجازه استفاده از کوکی‌ها را می‌دهند، در این صورت

  • باید برنامه را طوری پیکربندی کنیم که بدون کوکی‌ها کار کند
  • صفحات برنامه باید حاوی لینک‌های نسبی به جای لینک‌های مطلق باشند

4.2. بازیابی اطلاعات از یک درخواست کلاینت

4.2.1. چرخه درخواست-پاسخ وب مبتنی بر مدل کلاینت-سرور

بیایید در اینجا زمینهٔ کلاینت-سرور یک برنامهٔ وب را به یاد آوریم:

Image

درخواست یک کلاینت به یک برنامه وب به شرح زیر پردازش می‌شود:

  1. کلاینت یک اتصال TCP/IP به پورت P سرویس وب روی ماشین M که میزبان برنامه وب است برقرار می‌کند
  2. سپس مجموعه‌ای از خطوط متن را از طریق این اتصال مطابق پروتکل HTTP ارسال می‌کند. این مجموعه خطوط چیزی را تشکیل می‌دهد که به آن درخواست کلاینت گفته می‌شود. این درخواست به شکل زیر است:

Image

پس از ارسال درخواست، کلاینت منتظر پاسخ خواهد ماند.

  1. اولین خط هدرها، HTTP، عملی را که از سرور وب درخواست می‌شود مشخص می‌کند. این عمل می‌تواند چندین شکل داشته باشد:
    1. GET url HTTP/<نسخه>، که در حال حاضر <نسخه> برابر با 1.0 یا 1.1 است. در این حالت، درخواست شامل بخش [Document] نیست
    2. POST url HTTP/<version>. در این حالت، درخواست شامل بخشی [Document] است که اغلب فهرستی از اطلاعات مورد نظر برای برنامه وب می‌باشد
    3. PUT url HTTP/<version>. کلاینت یک سند را در بخش [Document] ارسال می‌کند و می‌خواهد آن را در سرور به آدرس url ذخیره کند

وقتی کلاینت می‌خواهد اطلاعاتی را به اپلیکیشن وبی که به آن متصل است ارسال کند، دو روش اصلی در اختیار دارد:

  • (ادامه)
    1. درخواست او [GET url_enrichie HTTP/<version>] است، که در آن url_enrichie به شکل [url?param1=val1&param2=val2&...] است. علاوه بر URL، کلاینت مجموعه‌ای از اطلاعات را به شکل [clé=valeur] ارسال می‌کند.
    2. درخواست کلاینت [POST url HTTP/<version>] است. در بخش [Document]، اطلاعات را به همان فرمت قبلی ارسال می‌کند: [param1=val1&param2=val2&...].
  1. در سمت سرور، کل زنجیرهٔ پردازش درخواست مشتری از طریق یک شیء جهانی به نام Request به درخواست دسترسی دارد. وب‌سرور کل درخواست مشتری را در قالبی که به‌زودی بررسی خواهیم کرد، در این شیء قرار داده است. برنامه کاربردی درخواست‌شده این شیء را پردازش کرده و پاسخی برای کلاینت ایجاد می‌کند. این پاسخ در یک شیء سراسری به نام Response در دسترس است. نقش برنامه کاربردی وب این است که یک شیء [Response] را از روی شیء دریافت‌شده [Request] بسازد. زنجیره پردازش همچنین از اشیاء سراسری [Application] و [Session] استفاده می‌کند که پیش از این در مورد آنها بحث کرده‌ایم و به آن امکان اشتراک‌گذاری داده‌ها بین کلاینت‌های مختلف (Application) یا بین درخواست‌های متوالی از یک کلاینت (Session) را می‌دهند.
  2. برنامه پاسخ خود را با استفاده از شیء [Response] به سرور ارسال خواهد کرد. هنگامی که این پاسخ وارد شبکه شود، شکل زیر را خواهد داشت: HTTP:

Image

پس از ارسال این پاسخ، سرور اتصال شبکه ورودی را قطع خواهد کرد (مگر اینکه کلاینت به آن دستور عدم قطع را داده باشد).

  1. کلاینت پاسخ را دریافت می‌کند و در عوض (در حین انتقال) اتصال را قطع می‌کند. آنچه با این پاسخ رخ می‌دهد به نوع کلاینت بستگی دارد. اگر کلاینت یک مرورگر وب باشد و سند دریافت‌شده یک سند HTML باشد، آن سند نمایش داده خواهد شد. اگر کلاینت یک برنامه باشد، پاسخ تحلیل و پردازش می‌شود.
  2. اینکه پس از چرخه درخواست-پاسخ، اتصال بین کلاینت و سرور بسته می‌شود، پروتکل HTTP را به یک پروتکل بدون حالت تبدیل می‌کند. در درخواست بعدی، کلاینت یک اتصال شبکه جدید به همان سرور برقرار خواهد کرد. از آنجایی که این دیگر همان اتصال شبکه نیست، سرور هیچ راهی (در سطوح TCP/IP و HTTP) برای مرتبط کردن این اتصال جدید با اتصال قبلی ندارد. این سیستم توکن جلسه (session token system) است که این ارتباط را امکان‌پذیر می‌سازد.

4.2.2. بازیابی اطلاعات ارسال‌شده توسط کلاینت

اکنون برخی از ویژگی‌ها و متدهای شیء [Request] را بررسی می‌کنیم که به کد برنامه اجازه می‌دهد به درخواست کلاینت و در نتیجه اطلاعاتی که ارسال کرده است دسترسی پیدا کند. شیء [Request] از نوع [HttpRequest] است:

Image

این کلاس دارای خواص و متدهای متعددی است. ما به خواص HttpMethod، QueryString، Form و Params علاقه‌مند هستیم که به ما امکان دسترسی به عناصر رشته اطلاعاتی [param1=val1&param2=val2&...] را می‌دهند.

HttpMethod as String
روش‌های درخواست کلاینت: GET, POST, HEAD, ...
QueryString as NameValueCollection
مجموعه‌ای از عناصر از رشتهٔ پرس‌وجو param1=val1&param2=val2&.. از خط اول HTTP [méthode]?param1=val1&param2=val2&... که در آن [méthode] ممکن است GET، POST، HEAD باشد.
Form as NameValueCollection
مجموعه‌ای از عناصر رشتهٔ پرس‌وجو param1=val1&param2=val2&... که در بخش [Document] پرس‌وجو (روش POST) یافت می‌شوند.
Params as NameValueCollection
چندین مجموعه را ترکیب می‌کند: QueryString، فرم، ServerVariables، کوکی‌ها، در یک مجموعه واحد.

4.2.3. مثال ۱

بیایید این عناصر را در یک مثال اولیه پیاده‌سازی کنیم. برنامه تنها یک عنصر [main.aspx] خواهد داشت. کد نمایش [main.aspx] به شرح زیر خواهد بود:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<html>
    <head>
        <title>Requête client</title>
    </head>
    <body>
        Requête :
        <% = méthode %>
        <br />
        nom :
        <% = nom %>
        <br />
        âge :
        <% = age %>
        <br />
    </body>
</html>

این صفحه سه مورد اطلاعات را نمایش می‌دهد، [méthode, nom, age]، که توسط مؤلفه کنترل‌کننده آن، [main.aspx.vb]، محاسبه شده است:

Public Class main
    Inherits System.Web.UI.Page

    Protected nom As String = "xx"
    Protected age As String = "yy"
    Protected méthode As String

    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
         'درخواست مشتری در request.txt در پوشهٔ برنامه ذخیره می‌شود
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
         'پارامترهای درخواست بازیابی می‌شوند
        méthode = Request.HttpMethod.ToLower
        If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString
        If Not Request.QueryString("age") Is Nothing Then age = Request.QueryString("age").ToString
        If Not Request.Form("nom") Is Nothing Then nom = Request.Form("nom").ToString
        If Not Request.Form("age") Is Nothing Then age = Request.Form("age").ToString
    End Sub

End Class

هنگامی که صفحه بارگیری می‌شود (Form_Load)، اطلاعات از [nom, age] از درخواست مشتری بازیابی می‌شود. ما آن را در دو مجموعه [QueryString] و [Form] جستجو می‌کنیم. . علاوه بر این، در [Page_Init]، درخواست کلاینت را ذخیره می‌کنیم تا بتوانیم بررسی کنیم که چه چیزی ارسال کرده‌اند. این دو فایل را در پوشه‌ای به نام <application-path> قرار می‌دهیم و سرور Cassini را با پارامترها (<application-path>,/request1) راه‌اندازی می‌کنیم، سپس با استفاده از یک مرورگر URL را درخواست می‌کنیم

[http://localhost/request1/main.aspx?nom=tintin&age=27]. ما پاسخ زیر را دریافت می‌کنیم:

Image

اطلاعات ارسال‌شده توسط کلاینت به‌درستی بازیابی شده است. درخواست مرورگر که در فایل [request.txt] ذخیره شده است، به شرح زیر است:

GET /request1/main.aspx?nom=tintin&age=27 HTTP/1.1
Cache-Control: max-age=0
Connection: keep-alive
Keep-Alive: 300
Accept: application/x-shockwave-flash,text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,image/jpeg,image/gif;q=0.2,*/*;q=0.1
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Accept-Encoding: gzip,deflate
Accept-Language: en-us,en;q=0.5
Host: localhost
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7b) Gecko/20040316

می‌توانیم ببینیم که مرورگر درخواستی برای GET ارسال کرده است. برای ارسال درخواست برای POST، از کلاینت [curl] استفاده خواهیم کرد. در یک پنجره DOS، دستور زیر را تایپ می‌کنیم:

C:\curl>curl --include --data nom=tintin --data age=27 http://localhost/request1/main.aspx
--include
برای نمایش سربرگ‌های HTTP از پاسخ
--data param=valeur
برای ارسال اطلاعات 'param=value' با استفاده از POST

پاسخ سرور به شرح زیر است:

HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Fri, 02 Apr 2004 09:27:25 GMT
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 178
Connection: Close


<html>
        <head>
                <title>Requête client</title>
        </head>
        <body>
                Requête :
                post
                <br />
                nom :
                tintin
                <br />
                âge :
                27
                <br />
        </body>
</html>

بار دیگر، سرور با موفقیت پارامترهای ارسال‌شده این بار توسط POST را بازیابی کرد. برای تأیید این موضوع، می‌توانید محتویات فایل [request.txt] را بررسی کنید:

POST /request1/main.aspx HTTP/1.1
Pragma: no-cache
Content-Length: 17
Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4

nom=tintin&age=27

کلاینت [curl] در واقع یک POST انجام داده است. اکنون بیایید دو روش انتقال اطلاعات را ترکیب کنیم. ما [age] را در URL درخواستی و [nom] را در سند ارسال‌شده قرار می‌دهیم:

E:\curl>curl --include --data nom="tintin" http://localhost/request1/main.aspx?age=27

درخواست ارسال‌شده توسط [curl] به شرح زیر است (request.txt):

POST /request1/main.aspx?age=27 HTTP/1.1
Pragma: no-cache
Content-Length: 10
Content-Type: application/x-www-form-urlencoded
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Host: localhost
User-Agent: curl/7.10.8 (win32) libcurl/7.10.8 OpenSSL/0.9.7a zlib/1.1.4

nom=tintin

می‌توانیم ببینیم که سن در URL درخواست‌شده ارسال شده است. آن را از مجموعه [QueryString] بازیابی خواهیم کرد. نام، در همین حال، در سند ارسال‌شده به این URL ارسال شده است. آن را از مجموعه [Form] بازیابی خواهیم کرد. پاسخ دریافتی توسط کلاینت [curl]:

<html>
        <head>
                <title>Requête client</title>
        </head>
        <body>
                Requête :
                post
                <br />
                nom :
                tintin
                <br />
                âge :
                27
                <br />
        </body>
</html>

در نهایت، بیایید هیچ اطلاعاتی را به سرور ارسال نکنیم:

E:\curl>curl --include http://localhost/request1/main.aspx
HTTP/1.1 200 OK
Server: Microsoft ASP.NET Web Matrix Server/0.6.0.0
Date: Fri, 02 Apr 2004 12:43:14 GMT
X-AspNet-Version: 1.1.4322
Cache-Control: private
Content-Type: text/html; charset=utf-8
Content-Length: 173
Connection: Close


<html>
        <head>
                <title>Requête client</title>
        </head>
        <body>
                Requête :
                get
                <br />
                nom :
                xx
                <br />
                âge :
                yy
                <br />
        </body>
</html>

به خوانندگان توصیه می‌شود برای درک این پاسخ به کد کنترل‌کننده [main.aspx.vb] مراجعه کنند.

4.2.4. مثال ۲

ممکن است کلاینت مقادیر متعددی را برای یک کلید ارسال کند. پس اگر در مثال قبلی، ما URL [http://localhost/request1/main.aspx?nom=tintin&age=27&nom=milou] را درخواست کنیم، در حالی که کلید [nom] دو بار ظاهر می‌شود، چه اتفاقی می‌افتد؟ بیایید آن را در یک مرورگر امتحان کنیم:

Image

برنامهٔ ما به‌درستی دو مقدار مرتبط با کلید [nom] را بازیابی کرده است. نمایش کمی گمراه‌کننده است. این با استفاده از دستور به دست آمده است


        If Not Request.QueryString("nom") Is Nothing Then nom = Request.QueryString("nom").ToString

متد [ToString] رشته [tintin,milou] را تولید کرد که نمایش داده شد. این کار این واقعیت را پنهان می‌کند که در واقع، شیء [Request.QueryString("nom")] آرایه‌ای از رشته‌ها {"tintin","milou"} است. مثال زیر این نکته را روشن می‌کند. صفحه نمایش [main.aspx] به شکل زیر خواهد بود:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <HEAD>
        <title>Requête client</title>
    </HEAD>
    <body>
        <P>Informations passées par le client :</P>
        <form runat="server">
            <P>QueryString :</P>
            <P><asp:listbox id="lstQueryString" runat="server" EnableViewState="False" Rows="6"></asp:listbox></P>
            <P>Form :</P>
            <P><asp:listbox id="lstForm" runat="server" EnableViewState="False" Rows="2"></asp:listbox></P>
        </form>
    </body>
</HTML>

در این صفحه چند ویژگی جدید وجود دارد که از آنچه کنترل‌های سرور نامیده می‌شوند استفاده می‌کنند. این‌ها با ویژگی [runat="server"] مشخص می‌شوند. هنوز برای معرفی مفهوم کنترل‌های سرور زود است. فعلاً کافی است بدانید که در اینجا:

  • این صفحه دو لیست دارد (برچسب‌های <asp:listbox>)
  • این لیست‌ها اشیایی (lstQueryString, lstForm) از نوع [ListBox] هستند که توسط کنترل‌کننده صفحه ایجاد خواهند شد
  • این اشیاء فقط روی سرور وب وجود دارند. هنگامی که پاسخ ارسال می‌شود، آن‌ها به تگ‌های استاندارد HTML تبدیل خواهند شد که کلاینت بتواند آن‌ها را درک کند. یک شیء [listbox] در نتیجه به تگ‌های HTML <select> و <option> تبدیل (یا همانندسازی) می‌شود.
  • هدف اصلی این اشیاء، حذف تمام کدهای VB از کدهای نمایشی است، که در کنترلر محدود می‌مانند.

کنترلر [main.aspx.vb] که مسئول ساخت دو شیء [lstQueryString] و [lstForm] است، به شرح زیر است:


Imports System.Collections
Imports System
Imports System.Collections.Specialized

Public Class main
    Inherits System.Web.UI.Page

    Protected infosQueryString As ArrayList
    Protected WithEvents lstQueryString As System.Web.UI.WebControls.ListBox
    Protected WithEvents lstForm As System.Web.UI.WebControls.ListBox
    Protected infosForm As ArrayList

    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
        'درخواست کلاینت در request.txt در پوشهٔ برنامه ذخیره می‌شود
        Dim requestFileName As String = Me.MapPath(Me.TemplateSourceDirectory) + "\request.txt"
        Me.Request.SaveAs(requestFileName, True)
    End Sub

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'کل مجموعه اطلاعات را از QueryString بازیابی می‌کند
        infosQueryString = getValeurs(Request.QueryString)
        lstQueryString.DataSource = infosQueryString
        lstQueryString.DataBind()
        infosForm = getValeurs(Request.Form)
        lstForm.DataSource = infosForm
        lstForm.DataBind()
    End Sub

    Private Function getValeurs(ByRef data As NameValueCollection) As ArrayList
        'در ابتدا، یک لیست خالی از اطلاعات
        Dim infos As New ArrayList
        'کلیدها را از مجموعه بازیابی می‌کند
        Dim clés() As String = data.AllKeys
        ' ما در آرایه کلیدها تکرار می‌کنیم
        Dim valeurs() As String
        For Each clé As String In clés
            'مقادیر مرتبط با کلید
            valeurs = data.GetValues(clé)
            ' یک مقدار واحد؟
            If valeurs.Length = 1 Then
                infos.Add(clé + "=" + valeurs(0))
            Else
                ' چندین مقدار
                For ivalue As Integer = 0 To valeurs.Length - 1
                    infos.Add(clé + "(" + ivalue.ToString + ")=" + valeurs(ivalue))
                Next
            End If
        Next
        ' نتیجه را برمی‌گرداند
        Return infos
    End Function
End Class

نکات کلیدی این کد به شرح زیر است:

  • در [Form_Load]، صفحه دو مجموعه [QueryString] و [Form] را بازیابی می‌کند. این کد از تابع [getValeurs] برای قرار دادن محتویات این دو مجموعه در دو شیء از نوع [ArrayList] استفاده می‌کند که در صورت مرتبط بودن کلید مجموعه با یک مقدار واحد، حاوی رشته‌های متنی از نوع [clé=valeur] خواهند بود، یا [clé(i)=valeur] اگر کلید با چندین مقدار مرتبط باشد.
  • سپس هر یک از اشیاء [ArrayList] به یکی از اشیاء [ListBox] در صفحه ارائه با استفاده از دو عبارت متصل می‌شود:
    • [ListBox.DataSource=ArrayList] و [ListBox.DataBind]. دستور دوم عناصر را از [DataSource] به مجموعه [Items] متعلق به شیء [ListBox] منتقل می‌کند

شایان ذکر است که هیچ‌یک از دو شیء [ListBox] به طور صریح توسط عملیاتی از نوع [New] ایجاد نمی‌شوند. بنابراین می‌توان نتیجه گرفت که وقتی تگ <asp:listbox id="xx">...<asp:listbox/> موجود باشد، خود سرور وب شیء [ListBox] را که توسط ویژگی [id] تگ ارجاع شده است، ایجاد می‌کند.

  • تابع [getValeurs] از ابجکتی از نوع [NameValueCollection] که به‌عنوان پارامتر به آن ارسال شده است، استفاده می‌کند تا نتیجه‌ای از نوع [ArrayList] تولید کند.

ما دو فایل قبلی را در پوشه‌ای به نام <application-path> قرار می‌دهیم و سرور Cassini را با پارامترهای (<application-path>,/request2) راه‌اندازی می‌کنیم، سپس URL را درخواست می‌کنیم

[http://localhost/request2/main.aspx?nom=tintin&age=27]. پاسخ زیر را دریافت می‌کنیم:

Image

اکنون ما یک URL را درخواست می‌کنیم که در آن کلید [nom] دو بار ظاهر می‌شود:

Image

ما متوجه می‌شویم که شیء [Request.QueryString("name")) در واقع یک آرایه بود. در اینجا، درخواست‌ها با استفاده از متد GET ارسال شدند. ما از کلاینت [curl] برای ارسال درخواست با استفاده از POST استفاده می‌کنیم:

E:\curl>curl --data nom=milou --data nom=tintin --data age=14 --data age=27 http://localhost/request2/main.aspx

<HTML>
        <HEAD>
                <title>Requête client</title>
        </HEAD>
        <body>
                <P>Informations passées par le client :</P>
                <form name="_ctl0" method="post" action="main.aspx" id="_ctl0">
<input type="hidden" name="__VIEWSTATE" value="dDwtMTI3MjA1MzUzMTs7PtCDC7NG4riDYIB4YjyGFpVAAviD" />

                        <P>QueryString :</P>
                        <P><select name="lstQueryString" size="6" id="lstQueryString">

</select></P>
                        <P>Form :</P>
                        <P><select name="lstForm" size="2" id="lstForm">
        <option value="nom(0)=milou">nom(0)=milou</option>
        <option value="nom(1)=tintin">nom(1)=tintin</option>
        <option value="age(0)=14">age(0)=14</option>
        <option value="age(1)=27">age(1)=27</option>

</select></P>
                </form>
        </body>
</HTML>

می‌توانیم ببینیم که کلاینت در واقع کد استاندارد HTML را برای هر دو لیست در صفحه دریافت می‌کند. اطلاعاتی ظاهر می‌شود که ما خودمان اضافه نکرده‌ایم، مانند فیلد مخفی [_VIEWSTATE]. این اطلاعات توسط تگ‌های <asp:xx runat="server"> تولید شده است. ما باید یاد بگیریم چگونه آن‌ها را مدیریت کنیم.

4.3. پیاده‌سازی معماری MVC

4.3.1. مفهوم

بیایید این فصل طولانی را با پیاده‌سازی یک برنامه‌ای که بر اساس الگوی MVC (مدل-نما-کنترل‌کننده) ساخته شده است، به پایان برسانیم. یک برنامه وب که بر اساس این الگو معماری شده است، به این صورت است:

Image

  • کلاینت درخواست‌های خود را به یک مؤلفهٔ خاص از برنامه به نام کنترل‌کننده ارسال می‌کند
  • کنترل‌کننده درخواست مشتری را تحلیل کرده و آن را اجرا می‌کند. برای این کار، از کلاس‌هایی که شامل منطق کسب‌وکار و کلاس‌های دسترسی به داده‌های برنامه هستند، استفاده می‌کند.
  • بسته به نتیجه اجرای درخواست، کنترل‌کننده انتخاب می‌کند که یک صفحهٔ خاص را به عنوان پاسخ برای کلاینت ارسال کند

در مدل ما، تمام درخواست‌ها از یک کنترل‌کننده واحد عبور می‌کنند که به‌عنوان هماهنگ‌کننده کل برنامه وب عمل می‌کند. مزیت این مدل این است که هر کاری که باید قبل از هر درخواست انجام شود، می‌تواند در خود کنترل‌کننده متمرکز شود. فرض کنید، به‌عنوان مثال، برنامه به احراز هویت نیاز دارد. این کار فقط یک بار انجام می‌شود. پس از موفقیت، برنامه اطلاعات مربوط به کاربری را که به تازگی احراز هویت شده است در جلسه (session) ذخیره خواهد کرد. از آنجایی که یک کلاینت می‌تواند بدون احراز هویت مستقیماً یک صفحه را در برنامه فراخوانی کند، بنابراین هر صفحه باید جلسه را بررسی کند تا اطمینان حاصل شود که احراز هویت واقعاً انجام شده است. اگر همه درخواست‌ها از یک کنترلر واحد عبور کنند، این کنترلر است که می‌تواند این کار را انجام دهد. صفحاتی که درخواست ممکن است بعداً به آن‌ها ارسال شود، نیازی به انجام این کار نخواهند داشت.

4.3.2. کنترل یک برنامه MVC بدون جلسه

از آنچه تاکنون دیده‌ایم، به نظر می‌رسد که فایل [global.asax] می‌تواند به عنوان کنترل‌کننده عمل کند. در واقع، می‌دانیم که تمام درخواست‌ها از آن عبور می‌کنند. بنابراین، این فایل جایگاه مناسبی برای کنترل همه چیز دارد. برنامه کاربردی زیر از آن برای این منظور استفاده می‌کند. مسیر مجازی آن [http://localhost/mvc1/main.aspx] خواهد بود. برای مشخص کردن درخواست خود، کلاینت یک پارامتر action=value را به URL اضافه می‌کند. بسته به مقدار پارامتر [action]، کنترل‌کننده [global.asax] درخواست را به یک صفحهٔ خاص هدایت می‌کند:

  1. [main.aspx] اگر پارامتر 'action' تنظیم نشده باشد یا اگر 'action'='main' باشد
  2. [action1.aspx] اگر action=action1 باشد
  3. [inconnu.aspx] اگر پارامتر `action` در موارد ۱ یا ۲ قرار نگیرد

صفحات [main.aspx, action1.aspx, inconnu.aspx] صرفاً مقدار [action] را که باعث نمایش آن‌ها شده است، نمایش می‌دهند. ما هشت فایل این برنامه را در زیر فهرست کرده و در صورت لزوم توضیحاتی ارائه می‌دهیم:

[global.asax]

<%@ Application src="Global.asax.vb" Inherits="Global" %>

[global.asax.vb]


Imports System
Imports System.Web
Imports System.Web.SessionState

Public Class Global
    Inherits System.Web.HttpApplication

    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        'بازیابی اقدام انجام‌شدنی
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' ما اقدام را در زمینه درخواست قرار می‌دهیم
        Context.Items("action") = action
        ' اجرای اقدام
        Select Case action
            Case "main"
                Server.Transfer("main.aspx", True)
            Case "action1"
                Server.Transfer("action1.aspx", True)
            Case Else
                Server.Transfer("inconnu.aspx", True)
        End Select
    End Sub
End Class

نکات قابل توجه:

  • ما تمام درخواست‌های مشتری را در رویه [Application_BeginRequest] که به طور خودکار در ابتدای هر درخواست جدید به برنامه اجرا می‌شود، رهگیری می‌کنیم.
  • در این رویه، ما به شیء [Request] دسترسی داریم که نمایانگر درخواست مشتری HTTP است. از آنجایی که منتظر یک URL از نوع [http://localhost/mvc1/main.aspx?action=xx] هستیم، در مجموعه [Request.QueryString] به دنبال کلیدی با نام [action] می‌گردیم. اگر این کلید وجود نداشته باشد، پارامتر 'action' را به صورت پیش‌فرض روی 'main' تنظیم می‌کنیم.
  • مقدار پارامتر [action] در شیء [Context] قرار می‌گیرد. مانند اشیاء [Application, Session, Request, Response, Server]، این شیء نیز جهانی است و از هر کدی به آن دسترسی وجود دارد. این شیء از صفحه‌ای به صفحه دیگر منتقل می‌شود اگر درخواست توسط چندین صفحه پردازش شود، که در اینجا نیز همینطور خواهد بود. این شیء به محض ارسال پاسخ به کلاینت، حذف می‌شود. بنابراین، عمر این شیء با مدت زمان پردازش درخواست مطابقت دارد.
  • بسته به مقدار پارامتر [action]، درخواست به صفحه مناسب ارسال می‌شود. برای این کار از شیء سراسری [Server] استفاده می‌شود؛ متد آن امکان انتقال درخواست جاری به صفحه دیگر را فراهم می‌کند. پارامتر اول آن نام صفحه مقصد است؛ پارامتر دوم یک مقدار بولی است که مشخص می‌کند آیا مجموعه‌های [QueryString] و [Form] باید به صفحه مقصد منتقل شوند یا خیر. در این مورد، پاسخ بله است.

فایل‌های [main.aspx] و [main.aspx.vb]:


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <head>
        <title>main</title></head>
    <body>
        <h3>Page [main]</h3>
        Action : <% =action %>
    </body>
</HTML>

Public Class main
    Inherits System.Web.UI.Page

    Protected action As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' بازیابی اقدام فعلی
        action = Me.Context.Items("action").ToString
    End Sub
End Class

کنترل‌کننده [main.aspx.vb] به سادگی مقدار کلید [action] را از زمینه بازیابی می‌کند؛ این مقدار توسط کد ارائه نمایش داده می‌شود. هدف در اینجا نشان دادن نحوهٔ انتقال شیء [Context] بین صفحات مختلف است که یک درخواست مشتری را پردازش می‌کنند. صفحات [action1.aspx] و [inconnu.aspx] به شیوه‌ای مشابه عمل می‌کنند:

[action1.aspx]


<%@ Page src="action1.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="action1" %>
<HTML>
    <head>
        <title>action1</title></head>
    <body>
        <h3>Page [action1]</h3>
        Action : <% =action %>
    </body>
</HTML>

[action1.aspx.vb]

Public Class action1
    Inherits System.Web.UI.Page

    Protected action As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
         'بازیابی اقدام فعلی
        action = Me.Context.Items("action").ToString
    End Sub
End Class

[inconnu.aspx]


<%@ Page src="inconnu.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="inconnu" %>
<HTML>
    <head>
        <title>inconnu</title></head>
    <body>
        <h3>Page [inconnu]</h3>
        Action : <% =action %>
    </body>
</HTML>

[inconnu.aspx.vb]

Public Class inconnu
    Inherits System.Web.UI.Page

    Protected action As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
         ' بازیابی اقدام فعلی
        action = Me.Context.Items("action").ToString
    End Sub
End Class

برای آزمایش این موضوع، اسناد فوق در پوشه‌ای به نام <application-path> قرار داده می‌شوند و Cassini با پارامترهای (<application-path>,/mvc1) راه‌اندازی می‌شود. ما URL [http://localhost/mvc1/main.aspx] را درخواست می‌کنیم:

Image

درخواست هیچ پارامتری ارسال نکرد [action]. کد کنترل‌کنندهٔ برنامه [global.asax.vb] باعث شد صفحه [main.aspx] ارائه شود. اکنون ما در حال درخواست URL [http://localhost/mvc1/main.aspx?action=action1] هستیم:

Image

کد کنترل‌کنندهٔ برنامه [global.asax.vb] صفحهٔ [action1.aspx] را ارائه کرد. اکنون ما در حال درخواست URL [http://localhost/mvc1/main.aspx?action=xx] هستیم:

Image

اقدام شناسایی نشد و کنترلر [global.asax.vb] صفحه [inconnu.aspx] را بازگرداند.

4.3.3. کنترل یک برنامه MVC با یک جلسه

در اکثر موارد، درخواست‌های مختلف یک مشتری برای یک برنامه نیاز دارند که اطلاعات را به اشتراک بگذارند. ما یک راه‌حل ممکن برای این مشکل دیده‌ایم: ذخیره اطلاعاتی که باید به اشتراک گذاشته شوند در شیء [Session] درخواست. این شیء در واقع توسط تمام درخواست‌ها مشترک است و قادر به ذخیره اطلاعات به صورت (کلید، مقدار) است، جایی که کلید از نوع [String] و مقدار از هر نوع مشتق شده از [Object] است.

در مثال قبلی، صفحات مختلف مرتبط با عملیات گوناگون در داخل رویه [Application_BeginRequest] در فایل [global.asax.vb] فراخوانی شدند:


    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        'بازیابی اقدام قابل اجرا
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' عمل را در زمینهٔ درخواست قرار می‌دهد
        Context.Items("action") = action
        ' اجرای اقدام
        Select Case action
            Case "main"
                Server.Transfer("main.aspx", True)
            Case "action1"
                Server.Transfer("action1.aspx", True)
            Case Else
                Server.Transfer("inconnu.aspx", True)
        End Select
    End Sub

مشخص شد که در رویه [Application_BeginRequest]، شیء [Session] در دسترس نیست. همین امر در مورد صفحه‌ای که اجرای برنامه به آن منتقل می‌شود نیز صدق می‌کند. در نتیجه، این قالب را نمی‌توان برای یک برنامه مبتنی بر جلسه (session-based) استفاده کرد. ما می‌توانیم نقش کنترلر را به هر صفحه‌ای، برای مثال [default.aspx]، اختصاص دهیم. سپس فایل‌های [global.asax, global.asax.vb] حذف شده و با فایل‌های [default.aspx, default.aspx.vb] جایگزین می‌شوند:

[default.aspx]

<%@ Page codebehind="default.aspx.vb" Inherits="vs.controleur" %>

[default.aspx.vb]


Imports System
Imports System.Web
Imports System.Web.SessionState

Public Class controleur
    Inherits System.Web.UI.Page

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        ' بازیابی اقدام قابل اجرا
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If

        ' عمل را در زمینهٔ درخواست قرار می‌دهد
        Context.Items("action") = action
        ' بازیابی اقدام قبلی، در صورت وجود
        Context.Items("actionPrec") = Session.Item("actionPrec")
        If Context.Items("actionPrec") Is Nothing Then Context.Items("actionPrec") = ""
        ' عمل فعلی در جلسه ذخیره می‌شود
        Session.Item("actionPrec") = action

        ' اجرای اقدام
        Select Case action
            Case "main"
                Server.Transfer("main.aspx", True)
            Case "action1"
                Server.Transfer("action1.aspx", True)
            Case Else
                Server.Transfer("inconnu.aspx", True)
        End Select
    End Sub
End Class

برای برجسته کردن مکانیزم جلسه، صفحات مختلف نه تنها اقدام فعلی بلکه اقدام قبلی را نیز نمایش می‌دهند. برای دنباله‌ای از اقدامات A1، A2، …، An، وقتی اقدام Ai رخ می‌دهد، کنترل‌کنندهٔ بالا:

  • عمل فعلی Ai را در زمینه قرار می‌دهد
  • عمل قبلی Ai-1 را از جلسه بازیابی می‌کند. اگر چنین عملی وجود نداشته باشد (مانند مورد عمل A1)، رشته مربوط به عمل قبلی خالی باقی می‌ماند.
  • عمل جاری Ai را در جلسه قرار می‌دهد و Ai-1 را جایگزین می‌کند
  • اجرا را به صفحهٔ مناسب منتقل می‌کند

سه صفحهٔ برنامه به شرح زیر است:

[main.aspx]


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <HEAD>
        <title>main</title>
    </HEAD>
    <body>
        <h3>Page [main]</h3>
        Action courante :
        <% =action %>
        <br>
        Action précédente :
        <% =actionPrec %>
    </body>
</HTML>

[action1.aspx]


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <head>
        <title>action1</title></head>
    <body>
        <h3>Page [action1]</h3>
        Action courante :
        <% =action %>
        <br>
        Action précédente :
        <% =actionPrec %>
    </body>
</HTML>

[inconnu.aspx]


<%@ Page src="main.aspx.vb" Language="vb" AutoEventWireup="false" Inherits="main" %>
<HTML>
    <head>
        <title>inconnu</title>
    </head>
    <body>
        <h3>Page [inconnu]</h3>
        Action courante :
        <% =action %>
        <br>
        Action précédente :
        <% =actionPrec %>
    </body>
</HTML>

چون هر سه صفحه اطلاعات یکسانی را نمایش می‌دهند ([action, actionPrec])، همگی می‌توانند از یک کنترل‌کننده صفحه مشترک استفاده کنند. بنابراین ما همگی آن‌ها را از کلاس [main] در فایل [main.aspx.vb] مشتق کرده‌ایم:


Public Class main
    Inherits System.Web.UI.Page

    Protected action As String
    Protected actionPrec As String

    Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'بازیابی اقدام فعلی
        action = Me.Context.Items("action").ToString
        'و اقدام قبلی
        actionPrec = Me.Context.Items("actionPrec").ToString
    End Sub
End Class

کد بالا به سادگی اطلاعاتی را که توسط کنترل‌کنندهٔ برنامه [default.aspx.vb] در زمینه قرار داده شده است، بازیابی می‌کند.

تمام این فایل‌ها در <application-path> قرار می‌گیرند و Cassini با پارامترهای (<application-path>,/mvc2) راه‌اندازی می‌شود. ابتدا URL [http://localhost/mvc2] درخواست می‌شود:

Image

URL [http://localhost/mvc2] به یک پوشه اشاره می‌کند. می‌دانیم که در این مورد، اگر سند [default.aspx] در آن پوشه وجود داشته باشد، توسط سرور بازگردانده می‌شود. در این مورد، هیچ اقدام مشخصی تعیین نشده بود. در نتیجه، اقدام [main] اجرا شد. بیایید به اقدام [action1] بپردازیم:

Image

عمل فعلی و عمل قبلی به درستی شناسایی شده‌اند. بیایید به عمل [xx] برویم:

Image

4.4. Conclusion

اکنون ما بلوک‌های پایه را داریم که هر برنامه ASP.ET از آن‌ها ساخته شده است. با این حال، یک مفهوم مهم باقی مانده است که باید معرفی شود: مفهوم فرم. این موضوع فصل بعدی است.