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، يحدث هذا التعيين عند تشغيل الخادم. على سبيل المثال، في نافذة موجه الأوامر، سنقوم بتشغيل Cassini باستخدام:

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

في المجلد <application-path>، وبحسب احتياجاتنا، سنجد:

  • مجلد [bin] لوضع الفئات المُجمَّعة مسبقًا (DLLs)
  • ملف [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 في <application-path>. إذا كان موجودًا، فإنه يقرأه ويخزن معلوماته في كائن [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
        'retrieve configuration information
        nom = ConfigurationSettings.AppSettings("nom")
        age = ConfigurationSettings.AppSettings("age")
    End Sub
 
End Class

يمكننا أن نرى أنه عند تحميل الصفحة، يتم استرداد قيم معلمات التكوين [name] و [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 إلى فئة وحدة تحكم وكود عرض. هذا هو الخيار الافتراضي الذي يحدده Visual Studio، وسنفعل الشيء نفسه هنا. عادةً لا يوجد عرض للتعامل معه، حيث يتم تعيين هذه المهمة لصفحات .aspx. وبالتالي، يتم اختزال محتوى ملف [global.asax] إلى توجيه يشير إلى الملف الذي يحتوي على كود وحدة التحكم:


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

لاحظ أن التوجيه لم يعد [Page] بل [Application]. كود وحدة التحكم المرتبط [global.asax.vb]، الذي تم إنشاؤه بواسطة Visual Studio، هو كما يلي:


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)
        ' Triggered when application is started
    End Sub
 
    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Triggered when the session is started
    End Sub
 
    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Triggered at the start of each request
    End Sub
 
    Sub Application_AuthenticateRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' Triggered when user authentication is attempted
    End Sub
 
    Sub Application_Error(ByVal sender As Object, ByVal e As EventArgs)
        ' Triggers when an error occurs
    End Sub
 
    Sub Session_End(ByVal sender As Object, ByVal e As EventArgs)
        ' Triggered when session ends
    End Sub
 
    Sub Application_End(ByVal sender As Object, ByVal e As EventArgs)
        ' Triggered when application ends
    End Sub
 
End Class

لاحظ أن فئة وحدة التحكم مشتقة من فئة [HttpApplication]. طوال عمر التطبيق، تحدث عدة أحداث مهمة. يتم التعامل مع هذه الأحداث بواسطة إجراءات يظهر هيكلها الأساسي أعلاه.

  • [Application_Start]: تذكر أن تطبيق الويب "موجود" داخل مسار افتراضي. يبدأ التطبيق بمجرد أن يطلب العميل صفحة موجودة في هذا المسار الافتراضي. ثم يتم تنفيذ الإجراء [Application_Start]. وستكون هذه هي المرة الوحيدة. في هذا الإجراء، سنقوم بأي تهيئة ضرورية للتطبيق، مثل إنشاء كائنات تتطابق مدة حياتها مع مدة حياة التطبيق.
  • [Application-End]: يتم تنفيذه عند إنهاء التطبيق. يرتبط بكل تطبيق مهلة خمول، قابلة للتكوين في [web.config]، وبعدها يُعتبر التطبيق منتهياً. وبالتالي، فإن خادم الويب هو الذي يتخذ هذا القرار بناءً على إعدادات التطبيق. تُعرَّف مهلة الخمول للتطبيق بأنها الفترة التي لم يقم خلالها أي عميل بطلب مورد من موارد التطبيق.
  • [Session-Start]/[Session_End]: ترتبط جلسة عمل بكل عميل ما لم يتم تكوين التطبيق بحيث لا يكون له جلسات عمل. العميل ليس مستخدمًا جالسًا أمام شاشة. إذا فتح المستخدم متصفحين للتفاعل مع التطبيق، فإنهما يمثلان عميلين. يتم تحديد العميل بواسطة رمز جلسة يجب أن يرفقه مع كل طلب من طلباته. رمز الجلسة هذا هو سلسلة فريدة من الأحرف يتم إنشاؤها عشوائيًا بواسطة خادم الويب. لا يمكن أن يكون لدى عميلين رمز جلسة واحد. يتبع هذا الرمز العميل على النحو التالي:
    • لا يرسل العميل الذي يقدم طلبه الأول رمز جلسة. يتعرف خادم الويب على ذلك ويخصص له رمزًا. وهذا يمثل بداية الجلسة، ويتم تنفيذ الإجراء [Session_Start]. يحدث هذا مرة واحدة فقط.
    • يقوم العميل بإرسال الطلبات اللاحقة عن طريق إرسال الرمز الذي يحدده. وهذا يسمح لخادم الويب باسترداد المعلومات المرتبطة بهذا الرمز. وهذا يتيح التتبع بين الطلبات المختلفة للعميل.
    • يمكن للتطبيق تزويد العميل بنموذج إنهاء الجلسة. في هذه الحالة، يبادر العميل بنفسه بإنهاء الجلسة. سيتم تنفيذ الإجراء [Session_End]. يحدث هذا مرة واحدة فقط.
    • قد لا يطلب العميل أبدًا إنهاء الجلسة بنفسه. في هذه الحالة، بعد فترة معينة من عدم نشاط الجلسة — والتي يمكن أيضًا تكوينها عبر [web.config] — سيتم إنهاء الجلسة بواسطة خادم الويب. سيتم بعد ذلك تنفيذ الإجراء [Session_End].
  • [Application_BeginRequest]: يتم تنفيذ هذا الإجراء بمجرد وصول طلب جديد. وبالتالي، يتم تنفيذه لكل طلب من أي عميل. يعد هذا مكانًا جيدًا لفحص الطلب قبل إعادة توجيهه إلى الصفحة المطلوبة. يمكنك حتى أن تقرر إعادة توجيهه إلى صفحة أخرى.
  • [Application_Error]: يتم تنفيذه كلما حدث خطأ لم يتم معالجته صراحةً بواسطة الكود الموجود في وحدة التحكم [global.asax.vb]. هنا، يمكنك إعادة توجيه طلب العميل إلى صفحة تشرح سبب الخطأ.

إذا لم تكن هناك حاجة لمعالجة أي من هذه الأحداث، فيمكن تجاهل ملف [global.asax]. وهذا ما تم فعله في الأمثلة الأولى من هذا الفصل.

4.1.3.2. المثال 1

لنقم بتطوير تطبيق لفهم اللحظات الثلاث الرئيسية بشكل أفضل: بدء تشغيل التطبيق، وبدء الجلسة، وطلب العميل. سيبدو ملف [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)
        ' Triggered when application is started
        ' we note the time
        Dim startApplication As String = Date.Now.ToString("T")
        ' we place it in the context of the application
        Application.Item("startApplication") = startApplication
    End Sub
 
    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Triggered when the session is started
        ' we note the time
        Dim startSession As String = Date.Now.ToString("T")
        ' put it in the session
        Session.Item("startSession") = startSession
    End Sub
 
    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' we note the time
        Dim startRequest As String = Date.Now.ToString("T")
        ' put it in the session
        Context.Items("startRequest") = startRequest
    End Sub
End Class

النقاط الرئيسية في الكود هي كما يلي:

  • يتيح خادم الويب عددًا من الكائنات لفئة [HttpApplication] في [global.asax.vb]:
    • Application من النوع [HttpApplicationState]—يمثل تطبيق الويب—يوفر الوصول إلى قاموس كائنات [Application.Item] التي يمكن لجميع عملاء التطبيق الوصول إليها—يمكّن من مشاركة المعلومات بين العملاء المختلفين—يتطلب الوصول المتزامن للقراءة/الكتابة من قبل عملاء متعددين إلى نفس البيانات مزامنة العملاء.
    • جلسة من النوع [HttpSessionState] — تمثل عميلاً معينًا — توفر الوصول إلى قاموس كائنات [Session.Item] التي يمكن لجميع الطلبات من ذلك العميل الوصول إليها — تسمح بتخزين المعلومات المتعلقة بالعميل، والتي يمكن بعد ذلك استردادها عبر طلبات العميل.
    • طلب من النوع [HttpRequest] — يمثل طلب HTTP الحالي للعميل
    • استجابة من النوع [HttpResponse] — تمثل استجابة HTTP التي يقوم الخادم بإنشائها حاليًا للعميل
    • خادم من النوع [HttpServerUtility] — يوفر طرقًا مساعدة، خاصةً لإعادة توجيه الطلب إلى صفحة أخرى غير تلك المقصودة في الأصل.
    • سياق من النوع [HttpContext] — يتم إعادة إنشاء هذا الكائن مع كل طلب جديد ولكنه مشترك بين جميع الصفحات المشاركة في معالجة الطلب — يسمح بتمرير المعلومات من صفحة إلى أخرى أثناء معالجة الطلب عبر قاموس العناصر الخاص به.
  • تسجل الإجراء [Application_Start] بدء تشغيل التطبيق في متغير مخزن في قاموس يمكن الوصول إليه على مستوى التطبيق
  • يسجل الإجراء [Session_Start] بداية الجلسة في متغير مخزن في قاموس يمكن الوصول إليه على مستوى الجلسة
  • يسجل الإجراء [Application_BeginRequest] بداية الطلب في متغير مخزن في قاموس يمكن الوصول إليه على مستوى الطلب (أي متاح طوال معالجته ولكنه يضيع في نهايته)

ستكون الصفحة المستهدفة هي الصفحة [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
        ' retrieve application and session info
        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. يقوم نفس العميل بإجراء طلب جديد (باستخدام خيار إعادة التحميل في المتصفح):

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». وسيتم إنشاء كائن [ApplicationA] من النوع [HttpApplicationState]. وستقوم الأجزاء المختلفة من التطبيق بتخزين البيانات ذات نطاق [Application] في هذا الكائن، أي البيانات المتعلقة بجميع المستخدمين. سيظل كائن [ApplicationA] موجودًا حتى يقوم خادم الويب بإلغاء تحميل التطبيق A.
  • إذا اكتشف خادم الويب أيضًا أنه يتعامل مع عميل جديد للتطبيق A، فسيطلق الحدث [Session_Start] في ملف [global.asax] الخاص بالتطبيق A. سيتم إنشاء كائن [Session-1A] من النوع [HttpSessionState]. سيسمح هذا الكائن للتطبيق A بتخزين كائنات نطاق [Session]، أي الكائنات التي تنتمي إلى عميل معين. سيظل الكائن [Session-1A] موجودًا طالما أن العميل 1A يقدم طلبات. وسيسمح ذلك بتتبع هذا العميل. يكتشف خادم الويب أنه يتعامل مع عميل جديد في حالتين:
    • لم يرسل العميل رمز جلسة في رؤوس HTTP لطلبه
    • أرسل العميل رمز جلسة عمل غير موجود (خلل في العميل أو محاولة اختراق) أو لم يعد موجودًا. تنتهي صلاحية رمز جلسة العمل بعد فترة معينة من عدم نشاط العميل (20 دقيقة بشكل افتراضي مع IIS). يمكن تكوين فترة انتهاء الصلاحية هذه.
  • في جميع الحالات، سيقوم خادم الويب بتشغيل الحدث [Application_BeginRequest] في ملف [global.asax]. يبدأ هذا الحدث معالجة طلب العميل. من الشائع عدم معالجة هذا الحدث وتمرير التحكم إلى الصفحة التي طلبها العميل، والتي ستقوم بعد ذلك بمعالجة الطلب. يمكننا أيضًا استخدام هذا الحدث لتحليل الطلب ومعالجته وتحديد الصفحة التي يجب إرسالها كاستجابة. سنستخدم هذه التقنية لتنفيذ تطبيق يتبع بنية MVC التي ناقشناها.
  • بمجرد تجاوز المرشح في [global.asax]، يتم تمرير طلب العميل إلى صفحة .aspx ستقوم بمعالجة الطلب. سنرى لاحقًا أنه من الممكن تمرير الطلب عبر مرشح يتكون من عدة صفحات. وستكون الصفحة الأخيرة مسؤولة عن إرسال الاستجابة إلى العميل. يمكن للصفحات إضافة المعلومات التي قامت بحسابها إلى الطلب الأولي للعميل. ويمكنها تخزين هذه المعلومات في مجموعة Context.Items. في الواقع، تتمتع جميع الصفحات المشاركة في معالجة طلب العميل بإمكانية الوصول إلى مجموعة البيانات هذه.
  • يتمتع كود الصفحات المختلفة بإمكانية الوصول إلى مستودعات البيانات التي تمثلها الكائنات [ApplicationA] و[Session-1A]... من المهم تذكر أن خادم الويب يعالج في وقت واحد عدة عملاء للتطبيق A. يتمتع جميع هؤلاء العملاء بإمكانية الوصول إلى كائن [Application A]. إذا احتاجوا إلى تعديل البيانات في هذا الكائن، فسيكون من الضروري إجراء مزامنة العملاء. يتمتع كل عميل XA أيضًا بإمكانية الوصول إلى مجموعة البيانات [Session-XA]. ونظرًا لأن هذه المجموعة مخصصة لهم، فلا حاجة إلى المزامنة هنا.
  • يخدم خادم الويب تطبيقات ويب متعددة في وقت واحد. ولا يوجد أي تداخل بين عملاء هذه التطبيقات المختلفة.

من هذه التفسيرات، يمكننا تلخيص النقاط التالية:

  • في أي وقت معين، يخدم خادم الويب عدة عملاء في وقت واحد. وهذا يعني أنه لا ينتظر انتهاء طلب واحد قبل معالجة طلب آخر. في الوقت T، هناك بالتالي عدة طلبات قيد المعالجة تنتمي إلى عملاء مختلفين لتطبيقات مختلفة. يُشار أحيانًا إلى كود المعالجة الذي يعمل في وقت واحد داخل خادم الويب باسم خيوط التنفيذ.
  • لا تتداخل خيوط التنفيذ الخاصة بالعملاء من تطبيقات الويب المختلفة مع بعضها البعض. هناك عزل.
  • قد تحتاج خيوط التنفيذ من عملاء نفس التطبيق إلى مشاركة البيانات:
    • يمكن لخيوط التنفيذ الخاصة بالطلبات الواردة من عميلين مختلفين (ليس نفس رمز الجلسة) مشاركة البيانات عبر كائن [Application].
    • يمكن لخيوط التنفيذ للطلبات المتتالية من نفس العميل مشاركة البيانات عبر كائن [Session].
    • يمكن لخيوط التنفيذ الخاصة بالصفحات المتتالية التي تعالج نفس الطلب من عميل معين مشاركة البيانات عبر كائن [Context].

4.1.3.4. المثال 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)
        ' Triggered when application is started
        ' init customer counter
        Application.Item("nbRequêtes") = 0
    End Sub
 
    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Triggered when the session is started
        ' init query counter
        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. هوية العميل عبر رمز الجلسة الخاص به: [token]
  2. العدد الإجمالي للطلبات الموجهة إلى التطبيق: [nbRequêtesApplication]
  3. العدد الإجمالي للطلبات التي قدمها العميل المحدد برقم 1: [nbClientRequests]

يتم حساب هذه المعلومات الثلاث في [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
        ' one more request for the
        Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1
        ' one more request in the session
        Session.Item("nbRequêtes") = CType(Session.Item("nbRequêtes"), Integer) + 1
        ' init presentation variables
        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] على النحو التالي:

        ' une requête de plus pour l'application
        Application.Item("nbRequêtes") = CType(Application.Item("nbRequêtes"), Integer) + 1

هذه التعليمات، على الرغم من بساطتها، تتطلب عدة تعليمات معالج لتنفيذها. لنفترض أنها تتطلب ثلاث تعليمات:

  1. قراءة العداد
  2. زيادة العداد
  3. كتابة العداد

يعمل خادم الويب على جهاز متعدد المهام، مما يعني أن كل مهمة تحصل على المعالج لبضع ميلي ثوانٍ قبل أن تفقده ثم تستعيده بعد أن تحصل جميع المهام الأخرى على حصتها من الوقت. لنفترض أن عميلين، A و B، يقدمان طلبًا إلى خادم الويب في نفس الوقت. لنفترض أن العميل A يبدأ أولاً، ويدخل الإجراء [Form_Load] في [main.aspx.vb]، ويقرأ العداد (=100)، ثم يتم مقاطعته لأن شريحة الوقت الخاصة به قد انتهت. الآن لنفترض أن الدور قد حان للعميل B وأن العميل 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
        ' one more request for the application and session
        ' meter reading
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' wait 5 s
        Thread.Sleep(5000)
        ' meter incrementation
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' meter registration
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' init presentation variables
        jeton = Session.SessionID
    End Sub
End Class

تم تقسيم عملية زيادة العداد إلى أربع مراحل:

  1. قراءة العداد
  2. تعليق مؤشر التنفيذ
  3. زيادة العداد
  4. إعادة كتابة العداد

لننظر مرة أخرى إلى عميلينا، A و B. بين مرحلة القراءة ومرحلة زيادة عدادات الطلبات، نجبر مؤشر الترابط التنفيذي على التوقف لمدة 5 ثوانٍ. والنتيجة المباشرة لذلك هي أنه سيفقد المعالج، الذي سيتم تخصيصه بعد ذلك لمهمة أخرى. لنفترض أن العميل A يبدأ أولاً. سيقرأ قيمة العداد N وسيتم مقاطعته لمدة 5 ثوانٍ. إذا تمكن العميل B من الوصول إلى وحدة المعالجة المركزية خلال تلك الفترة، فمن المفترض أن يقرأ نفس قيمة العداد N. في النهاية، من المفترض أن يعرض كلا العميلين نفس قيمة العداد، وهو ما يعتبر أمراً غير طبيعي.

نقوم باختبار التطبيق عن طريق وضع الملفات الأربعة السابقة في مجلد نسميه <application-path> وتشغيل خادم Cassini بالمعلمات (<application-path>,/aspnet/webapplib). نقوم بإعداد متصفحين مختلفين بعنوان URL [http://localhost/aspnet/webapplib/main.aspx]. نطلق المتصفح الأول لطلب عنوان URL، ثم، دون انتظار الرد الذي سيصل بعد 5 ثوانٍ، نطلق المتصفح الثاني. بعد ما يزيد قليلاً عن 5 ثوانٍ، نحصل على النتيجة التالية:

Image

نرى:

  • أن لدينا عميلين مختلفين (ليس نفس رمز الجلسة)
  • أن كل عميل قد أرسل طلبًا
  • أن عداد الطلبات الموجهة إلى التطبيق يجب أن يكون عند الرقم 2 في أحد المتصفحين. لكن هذا ليس هو الحال.

الآن، دعونا نجرب تجربة أخرى. باستخدام نفس المتصفح، نرسل خمسة طلبات إلى عنوان URL [http://localhost/aspnet/webapplib/main.aspx]. مرة أخرى، نرسلها واحدًا تلو الآخر دون انتظار النتائج. بمجرد تنفيذ جميع الطلبات، نحصل على النتيجة التالية للطلب الأخير:

Image

يمكننا ملاحظة:

  • أن الطلبات الخمسة اعتُبرت قادمة من نفس العميل لأن عداد طلبات العميل يبلغ 5. على الرغم من عدم ظهور ذلك أعلاه، يمكننا أن نرى أن رمز الجلسة هو نفسه بالفعل لجميع الطلبات الخمسة.
  • أن عداد الطلبات الموجهة إلى التطبيق صحيح.

ما الذي يمكننا استنتاجه؟ لا شيء قاطع. ربما لا يبدأ خادم الويب في تنفيذ طلب من عميل إذا كان لدى هذا العميل طلب قيد التنفيذ بالفعل؟ وبالتالي، لن يكون هناك أبدًا تنفيذ متزامن لطلبات من نفس العميل. سيتم تنفيذها واحدًا تلو الآخر. يجب التحقق من هذه النقطة. قد يعتمد ذلك بالفعل على نوع العميل المستخدم.

4.1.3.6. مزامنة العملاء

المشكلة التي تم تسليط الضوء عليها في التطبيق السابق هي مشكلة كلاسيكية (ولكن ليس من السهل حلها) تتعلق بالوصول الحصري إلى مورد ما. في حالتنا المحددة، يجب أن نتأكد من أن عميلين، A و B، لا يمكن أن يكونا في تسلسل الكود في نفس الوقت:

  1. قراءة العداد
  2. زيادة العداد
  3. الكتابة في العداد

يُطلق على تسلسل التعليمات البرمجية هذا اسم "القسم الحرج". وهو يتطلب مزامنة الخيوط التي تنفذه في وقت واحد. توفر منصة .NET أدوات متنوعة لضمان ذلك. هنا، سنستخدم فئة [Mutex].

Image

هنا، سنستخدم فقط المنشئات والأساليب التالية:

public Mutex()
ينشئ كائن تزامن M
public bool WaitOne()
يطلب الخيط T1، الذي ينفذ عملية M.WaitOne()، ملكية كائن التزامن M. إذا لم يكن أي خيط يمتلك Mutex M (الحالة الأولية)، يتم "منحه" للخيط T1 الذي طلبه. إذا قام الخيط T2، بعد ذلك بقليل، بتنفيذ نفس العملية، فسيتم حظره. وذلك لأن الموتكس يمكن أن ينتمي إلى خيط واحد فقط في كل مرة. وسيتم تحريره عندما يقوم الخيط T1 بتحرير الموتكس M الذي يحتفظ به. وبالتالي، قد يتم حظر عدة خيوط أثناء انتظار الموتكس M.
public void ReleaseMutex()
يتخلى الخيط T1 الذي يقوم بعملية M.ReleaseMutex() عن ملكية الموتكس M. عندما يفقد الخيط T1 المعالج، يمكن للنظام تخصيصه لأحد الخيوط التي تنتظر الموتكس M. سيحصل عليه واحد فقط بالترتيب؛ بينما تظل الخيوط الأخرى التي تنتظر M محجوبة

يدير الموتكس M الوصول إلى المورد المشترك R. يطلب الخيط المورد R عبر M.WaitOne() ويحرره عبر M.ReleaseMutex(). يعد الجزء الحرج من الكود الذي يجب تنفيذه بواسطة خيط واحد فقط في كل مرة موردًا مشتركًا. يمكن تحقيق تزامن تنفيذ الجزء الحرج على النحو التالي:

M.WaitOne()
' le thread est seul à entrer ici
' section critique
....
M.ReleaseMutex()

حيث M هو كائن Mutex. بالطبع، يجب ألا تنسى أبدًا تحرير Mutex الذي لم يعد مطلوبًا، حتى يتمكن مؤشر ترابط آخر من الدخول إلى القسم الحرج بدوره؛ وإلا، فإن مؤشرات الترابط التي تنتظر Mutex الذي لم يتم تحريره أبدًا لن تتمكن أبدًا من الوصول إلى المعالج. علاوة على ذلك، يجب تجنب حالة التعطل التي ينتظر فيها مؤشرا ترابط بعضهما البعض. ضع في اعتبارك الإجراءات التالية التي تحدث بالتسلسل:

  • يستحوذ الخيط T1 على ملكية Mutex M1 للوصول إلى المورد المشترك R1
  • يستحوذ الخيط T2 على Mutex M2 للوصول إلى مورد مشترك R2
  • يطلب الخيط T1 Mutex M2. يتم حظره.
  • يطلب الخيط T2 Mutex M1. يتم حظره.

هنا، ينتظر الخيطان T1 و T2 بعضهما البعض. تحدث هذه الحالة عندما يحتاج الخيطان إلى موردين مشتركين: المورد R1 الذي يتحكم فيه Mutex M1 والمورد R2 الذي يتحكم فيه Mutex M2. أحد الحلول الممكنة هو طلب كلا الموردين في نفس الوقت باستخدام Mutex 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)
        ' Triggered when application is started
        ' init customer counter
        Application.Item("nbRequêtes") = 0
        ' create a synchronization lock
        Application.Item("verrou") = New Mutex
    End Sub
 
    Sub Session_Start(ByVal sender As Object, ByVal e As EventArgs)
        ' Triggered when the session is started
        ' init query counter
        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
        ' one more request for the application and session
        ' enter a critical section - retrieve the synchronization lock
        Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
        ' we ask you to enter the following critical section on your own
        verrou.WaitOne()
        ' meter reading
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' wait 5 s
        Thread.Sleep(5000)
        ' meter incrementation
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' meter registration
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' allows access to the critical section
        verrou.ReleaseMutex()
        ' init presentation variables
        jeton = Session.SessionID
    End Sub
End Class

يمكننا أن نرى أن العميل:

  • يطلب الدخول إلى القسم الحرج بمفرده. وللقيام بذلك، يطلب الملكية الحصرية للموتكس [القفل]
  • يحرر Mutex [lock] في نهاية القسم الحرج حتى يتمكن عميل آخر من الدخول إلى القسم الحرج بدوره.

نختبر التطبيق عن طريق وضع الملفات الأربعة السابقة في مجلد نسميه <application-path> ونقوم بتشغيل خادم Cassini بالمعلمات (<application-path>,/aspnet/webapplic). نفتح متصفحين مختلفين باستخدام عنوان URL [http://localhost/aspnet/webapplic/main.aspx]. نطلق المتصفح الأول لطلب عنوان URL، ثم، دون انتظار الرد الذي سيصل بعد 5 ثوانٍ، نطلق المتصفح الثاني. بعد ما يزيد قليلاً عن 5 ثوانٍ، نحصل على النتيجة التالية:

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
        ' one more request for the application and session
        ' enter a critical section - retrieve the synchronization lock
        Dim verrou As Mutex = CType(Application.Item("verrou"), Mutex)
        ' you ask to enter the next section on your own
        verrou.WaitOne()
        ' meter reading
        nbRequêtesApplication = CType(Application.Item("nbRequêtes"), Integer)
        nbRequêtesClient = CType(Session.Item("nbRequêtes"), Integer)
        ' wait 5 s
        Thread.Sleep(5000)
        ' counter incrementation
        nbRequêtesApplication += 1
        nbRequêtesClient += 1
        ' meter registration
        Application.Item("nbRequêtes") = nbRequêtesApplication
        Session.Item("nbRequêtes") = nbRequêtesClient
        ' allows access to the critical section
        verrou.ReleaseMutex()
        ' init presentation variables
        jeton = Session.SessionID
    End Sub
 
    Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Init
        ' the client request is stored in request.txt of the application folder
        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] هو كائن يمثل الطلب الذي تتم معالجته حاليًا. تم إنشاء هذا الكائن من خلال معالجة الطلب الأولي المرسل من العميل، أي سلسلة من الأسطر النصية بالشكل التالي:

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. سنستخدمه مرة أخرى، في نافذة سطر الأوامر، للاستعلام عن نفس عنوان 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 key=value] في curl، والذي سيُنشئ رأس HTTP [Cookie: key=value]. وقد رأينا أن المتصفح أرسل رأس 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]، حيث نستخدم الرمز الذي تم الحصول عليه خلال طلبنا الأول (الطلب رقم 1):

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>

ماذا يعني هذا المثال؟ لقد أرسلنا رمزًا تم الحصول عليه مسبقًا. عندما ينشئ خادم الويب رمزًا، فإنه يحتفظ به طالما استمر العميل المرتبط بهذا الرمز في إرسال الطلبات إليه. بعد فترة معينة من عدم النشاط (20 دقيقة بشكل افتراضي مع 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 وعنوان URL المرتبط U. وهذا يعني أنه عندما يطلب العميل عنوان URL بالصيغة /U/path من الخادم S في ، قد يعيد الخادم ملف تعريف الارتباط إذا لم يتلقه العميل لفترة أطول من D. لا شيء يمنع العميل من تجاهل قواعد السلوك هذه. ومع ذلك، فإن المتصفحات تلتزم بها. توفر بعض المتصفحات إمكانية الوصول إلى محتويات ملفات تعريف الارتباط التي تتلقاها. هذا هو الحال مع متصفح Mozilla. فيما يلي، على سبيل المثال، المعلومات المتعلقة بملف تعريف الارتباط الذي أرسله الخادم في المثال السابق:

Image

وهي تتضمن:

  • اسم ملف تعريف الارتباط [ASP.NET_SessionId]
  • قيمته [y153...m3]
  • الجهاز المرتبط به [localhost]
  • عنوان URL المرتبط به [/]
  • مدة صلاحيته [في نهاية الجلسة]

وبالتالي، سيرسل المتصفح رمز الجلسة في كل مرة يطلب فيها عنوان URL بالصيغة [http://localhost/...], أي في كل مرة يطلب فيها عنوان URL من خادم الويب على الجهاز [localhost]. مدة صلاحية ملف تعريف الارتباط هي مدة الجلسة. بالنسبة للمتصفح، هذا يعني أن ملف تعريف الارتباط لا تنتهي صلاحيته أبدًا. وسيرسله في كل مرة يطلب فيها عنوان URL من الجهاز [localhost]. وبالتالي، إذا تلقى المتصفح رمز الجلسة في اليوم D، وأغلقه، وأعاد فتحه في اليوم التالي، فسوف يعيد إرسال رمز الجلسة (الذي تم تخزينه في ملف). سيتلقى الخادم هذا الرمز، الذي لم يعد موجودًا لديه، لأن رمز الجلسة له عمر محدود على الخادم (20 دقيقة على IIS). وبالتالي، سيبدأ جلسة جديدة.

من الممكن تعطيل ملفات تعريف الارتباط في المتصفح. في هذه الحالة، يتلقى العميل رمز الجلسة ولكنه لا يرسله مرة أخرى، مما يمنع تتبع الجلسة. لتوضيح ذلك، نقوم بتعطيل ملفات تعريف الارتباط في متصفحنا (Mozilla في هذه الحالة):

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") وأن المدة القصوى لعدم النشاط لرمز الجلسة هي 10 دقائق (timeout="10"). بعد هذه المدة، يتم إنهاء الجلسة المرتبطة بالرمز. تتم عملية تبادل رمز الجلسة بين الخادم والعميل على النحو التالي:

  1. يطلب العميل عنوان URL [http://machine:port/V/chemin]، حيث V هو دليل افتراضي على خادم الويب
  2. يقوم الخادم بإنشاء رمز J ويوجه العميل لإعادة التوجيه إلى عنوان URL [http://machine:port/V/(J)/path]. وبالتالي، فقد وضع الرمز في عنوان URL المطلوب، مباشرة بعد الدليل الافتراضي V
  3. يتبع العميل عملية إعادة التوجيه هذه ويطلب عنوان URL الجديد [http://machine:port/V/(J)/path].
  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 [HTTP/1.1 302 Found] بدلاً من [HTTP/1.1 200 OK]. يوجه هذا الرأس العميل إلى إعادة التوجيه إلى عنوان URL المحدد بواسطة رأس HTTP Location [Location: /session2/(hinadjag3bt0u155g5hqe245)/main.aspx]. يمكننا رؤية رمز الجلسة الذي تم إدراجه في عنوان URL لإعادة التوجيه. يطلب المتصفح الذي يتلقى هذا الرد عنوان URL الجديد بشكل شفاف للمستخدم، الذي لا يرى الطلب الجديد. في حالة عدم قيام المتصفح بمعالجة إعادة التوجيه بنفسه، يتم إرسال مستند HTML مع رمز HTTP أعلاه. يحتوي على رابط إلى عنوان URL لإعادة التوجيه، والذي يمكن للمستخدم النقر عليه.

الآن، لنفعل الشيء نفسه مع متصفح تم تعطيل ملفات تعريف الارتباط فيه. نطلب عنوان URL [http://localhost/session2/main.aspx] مرة أخرى. نتلقى الاستجابة التالية من الخادم:

Image

أولاً، لاحظ أن عنوان URL الذي يعرضه المتصفح ليس هو العنوان الذي طلبناه. وهذا يشير إلى حدوث إعادة توجيه. في الواقع، يعرض المتصفح دائمًا عنوان URL لآخر مستند تم استلامه. لذا، إذا لم يعرض عنوان URL [http://localhost/session2/main.aspx]، فهذا يعني أنه تلقى تعليمات بإعادة التوجيه إلى عنوان URL آخر. قد تكون هناك عمليات إعادة توجيه متعددة. وعنوان 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

إذا نقرنا على رابط [إعادة تحميل التطبيق]، فسيتم استدعاء عنوان URL

[http://localhost/session2/(gu5ee455pkpffn554e3b1a32)/main.aspx] الذي يتم استدعاؤه. وبالتالي، سيتلقى الخادم رمز الجلسة وسيكون قادرًا على استرداد المعلومات المرتبطة به. هذا ما يظهره لنا رد الخادم:

Image

يجب أن نلاحظ أنه إذا كنا بحاجة إلى تتبع جلسة عمل في تطبيق ويب ولم نكن متأكدين مما إذا كانت متصفحات العملاء لهذا التطبيق ستسمح باستخدام ملفات تعريف الارتباط،

  • يجب علينا تكوين التطبيق ليعمل بدون ملفات تعريف الارتباط
  • يجب أن تستخدم صفحات التطبيق روابط نسبية بدلاً من الروابط المطلقة

4.2. استرداد المعلومات من طلب العميل

4.2.1. دورة طلبات واستجابات الويب بين العميل والخادم

دعونا نستعرض سياق العميل-الخادم لتطبيق ويب:

Image

تتم معالجة طلب العميل لتطبيق ويب على النحو التالي:

  1. يفتح العميل اتصال TCP/IP بالمنفذ P لخدمة الويب على الجهاز M الذي يستضيف تطبيق الويب
  2. يرسل سلسلة من الأسطر النصية عبر هذا الاتصال وفقًا لبروتوكول HTTP. تشكل هذه المجموعة من الأسطر ما يُسمى بطلب العميل. وهي تأخذ الشكل التالي:

Image

بمجرد إرسال الطلب، ينتظر العميل الرد.

  1. يحدد السطر الأول من رؤوس HTTP الإجراء المطلوب من خادم الويب. ويمكن أن يتخذ عدة أشكال:
    • GET HTTP URL/<version>، حيث <version> هي حاليًا 1.0 أو 1.1. في هذه الحالة، لا يتضمن الطلب قسم [Document]
    • POST HTTP URL/<version>. في هذه الحالة، يتضمن الطلب قسم [Document]، وغالبًا ما يكون قائمة بالمعلومات المخصصة لتطبيق الويب
    • PUT HTTP URL/<version>. يرسل العميل مستندًا في قسم [Document] ويريد تخزينه على الخادم على عنوان URL

عندما يرغب العميل في إرسال معلومات إلى تطبيق الويب الذي اتصل به، فإن لديه طريقتين رئيسيتين:

  • (تابع)
    • يكون طلبه هو [GET enriched_url HTTP/<version>] حيث يكون enriched_url بالشكل [url?param1=val1&param2=val2&...]. بالإضافة إلى عنوان URL، ينقل العميل سلسلة من المعلومات بالشكل [key=value].
    • طلبهم هو [POST enriched-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) لربط هذا الاتصال الجديد بأي اتصال سابق. ونظام رموز الجلسة هو الذي سيمكّن من إجراء هذا الربط.

4.2.2. استرداد المعلومات المرسلة من العميل

سنقوم الآن بفحص بعض خصائص وأساليب كائن [Request] التي تسمح لرمز التطبيق بالوصول إلى طلب العميل وبالتالي إلى المعلومات التي أرسلها. كائن [Request] هو من النوع [HttpRequest]:

Image

تحتوي هذه الفئة على العديد من الخصائص والأساليب. نحن مهتمون بخصائص HttpMethod و QueryString و Form و Params، والتي ستسمح لنا بالوصول إلى عناصر سلسلة المعلومات [param1=val1&param2=val2&...].

HttpMethod كسلسلة
طريقة طلب العميل: GET، POST، HEAD، ...
QueryString كـ NameValueCollection
مجموعة من العناصر من سلسلة الاستعلام param1=val1&param2=val2&... من السطر الأول HTTP [method]?param1=val1&param2=val2&... حيث يمكن أن تكون [method] هي GET، POST، HEAD.
Form كـ NameValueCollection
مجموعة من العناصر من سلسلة الاستعلام param1=val1&param2=val2&.. الموجودة في جزء [Document] من الطلب (طريقة POST).
Params كـ NameValueCollection
يجمع عدة مجموعات: QueryString و Form و ServerVariables و Cookies في مجموعة واحدة.

4.2.3. مثال 1

دعونا نطبق هذه العناصر في المثال الأول. سيحتوي التطبيق على عنصر واحد فقط [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>

تعرض الصفحة ثلاث معلومات [method، name، 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
        ' the client request is stored in request.txt of the application folder
        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
        ' retrieve query parameters
        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)، يتم استرداد معلومات [name, 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=value
لإرسال معلومات 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 المطلوب و[name] في البيانات المرسلة:

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. المثال 2

يمكن للعميل إرسال قيم متعددة لنفس المفتاح. فماذا يحدث إذا طلبنا، في المثال السابق، عنوان URL [http://localhost/request1/main.aspx?nom=tintin&age=27&nom=milou]، حيث يظهر المفتاح [name] مرتين؟ لنجرب ذلك في متصفح:

Image

نجح تطبيقنا في استرداد القيمتين المرتبطتين بالمفتاح [name]. العرض مضلل بعض الشيء. تم الحصول عليه باستخدام العبارة


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

أنتجت الطريقة [ToString] السلسلة [tintin,milou]، والتي تم عرضها. وهي تخفي حقيقة أن الكائن [Request.QueryString("name")] هو في الواقع مصفوفة من السلاسل {"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
        ' the client request is stored in request.txt of the application folder
        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
        ' we retrieve the entire collection of information from 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
        ' starting with an empty info list
        Dim infos As New ArrayList
        ' we retrieve the keys of the
        Dim clés() As String = data.AllKeys
        ' browse the key table
        Dim valeurs() As String
        For Each clé As String In clés
            ' values associated with the key
            valeurs = data.GetValues(clé)
            ' a single value?
            If valeurs.Length = 1 Then
                infos.Add(clé + "=" + valeurs(0))
            Else
                ' several values
                For ivalue As Integer = 0 To valeurs.Length - 1
                    infos.Add(clé + "(" + ivalue.ToString + ")=" + valeurs(ivalue))
                Next
            End If
        Next
        ' we return the result
        Return infos
    End Function
End Class

النقاط الرئيسية في هذا الكود هي كما يلي:

  • في [Form_Load]، تسترد الصفحة المجموعتين [QueryString] و [Form]. وتستخدم دالة [getValues] لوضع محتويات هاتين المجموعتين في كائنين [ArrayList]، اللذين سيحتويان على سلاسل من النوع [key=value] إذا كان مفتاح المجموعة مرتبطًا بقيمة واحدة، أو [key(i)=value] إذا كان المفتاح مرتبطًا بقيم متعددة.
  • ثم يتم ربط كل كائن من كائنات [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("nom")] كان بالفعل مصفوفة. هنا، تم إجراء الطلبات باستخدام طريقة 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 (Model-View-Controller). يبدو تطبيق الويب المصمم وفقًا لهذا النمط كما يلي:

Image

  • يرسل العميل طلباته إلى مكون معين من التطبيق يسمى وحدة التحكم
  • يقوم وحدة التحكم بتحليل طلب العميل وتنفيذه. للقيام بذلك، يعتمد على فئات تحتوي على منطق الأعمال الخاص بالتطبيق وفئات الوصول إلى البيانات.
  • اعتمادًا على نتيجة تنفيذ الطلب، تختار وحدة التحكم إرسال صفحة معينة ردًا على العميل

في نموذجنا، تمر جميع الطلبات عبر وحدة تحكم واحدة، والتي تعمل كمنسق لتطبيق الويب بأكمله. وتتمثل ميزة هذا النموذج في أنه يمكن تجميع كل ما يجب القيام به قبل كل طلب داخل وحدة التحكم. لنفترض، على سبيل المثال، أن التطبيق يتطلب المصادقة. يتم تنفيذ ذلك مرة واحدة فقط. بمجرد نجاحها، سيقوم التطبيق بتخزين المعلومات المتعلقة بالمستخدم الذي تم مصادقته للتو في الجلسة. نظرًا لأن العميل يمكنه استدعاء صفحة في التطبيق مباشرةً دون مصادقة، فسيتعين على كل صفحة بالتالي التحقق في الجلسة من أن المصادقة قد اكتملت بالفعل. إذا مرت جميع الطلبات عبر وحدة تحكم واحدة، فإن وحدة التحكم هي التي يمكنها تنفيذ هذه المهمة. لن تضطر الصفحات التي يتم تمرير الطلب إليها في النهاية إلى القيام بذلك.

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. [unknown.aspx] إذا لم تندرج action ضمن الحالتين 1 و 2

تعرض الصفحات [main.aspx، action1.aspx، unknown.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)
        ' retrieve the action to be performed
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' put the action in the context of the request
        Context.Items("action") = action
        ' execute the 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]، فإننا نبحث عن مفتاح باسم [action] في مجموعة [Request.QueryString]. إذا لم يكن موجودًا، فإننا نعيّن القيمة الافتراضية لـ [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
        ' retrieve the current action
        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
        ' retrieve the current action
        action = Me.Context.Items("action").ToString
    End Sub
End Class

[unknown.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>

[unknown.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
        ' retrieve the current action
        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] بعرض الصفحة [unknown.aspx].

4.3.3. التحكم في تطبيق MVC باستخدام الجلسات

في معظم الأحيان، تحتاج الطلبات المختلفة التي يرسلها العميل إلى التطبيق إلى مشاركة المعلومات. وقد رأينا حلاً محتملاً لهذه المشكلة: تخزين المعلومات المراد مشاركتها في كائن [Session] الخاص بالطلب. يتم مشاركة هذا الكائن بالفعل بين جميع الطلبات، وهو قادر على تخزين المعلومات في شكل (مفتاح، قيمة)، حيث يكون المفتاح من النوع [String] والقيمة من أي نوع مشتق من [Object].

في المثال السابق، تم استدعاء الصفحات المختلفة المرتبطة بالإجراءات المختلفة في الإجراء [Application_BeginRequest] في ملف [global.asax.vb]:


    Sub Application_BeginRequest(ByVal sender As Object, ByVal e As EventArgs)
        ' retrieve the action to be performed
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If
        ' put the action in the context of the request
        Context.Items("action") = action
        ' execute the 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]. وينطبق الأمر نفسه على الصفحة التي يتم نقل التنفيذ إليها. لذلك، لا يمكن استخدام هذا النموذج لتطبيق يحتوي على جلسة عمل. يمكننا تعيين دور وحدة التحكم لأي صفحة، على سبيل المثال [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
        ' retrieve the action to be performed
        Dim action As String
        If Request.QueryString("action") Is Nothing Then
            action = "main"
        Else
            action = Request.QueryString("action").ToString.ToLower
        End If

        ' put the action in the context of the request
        Context.Items("action") = action
        ' retrieve the previous action if it exists
        Context.Items("actionPrec") = Session.Item("actionPrec")
        If Context.Items("actionPrec") Is Nothing Then Context.Items("actionPrec") = ""
        ' the current action is saved in the session
        Session.Item("actionPrec") = action
 
        ' execute the 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>

[unknown.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
        ' retrieve the current action
        action = Me.Context.Items("action").ToString
        ' and the previous action
        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. الخلاصة

لدينا الآن العناصر الأساسية التي يُبنى عليها كل تطبيق ASP.NET. ومع ذلك، هناك مفهوم مهم واحد متبقي لتقديمه: النموذج. وهذا هو موضوع الفصل التالي.