Skip to content

4. تتبع الجلسة

4.1. المشكلة

قد يتكون تطبيق الويب من عدة عمليات تبادل للنماذج بين الخادم والعميل. وتعمل العملية على النحو التالي:

  • الخطوة 1
    • يقوم العميل C1 بفتح اتصال مع الخادم وتقديم طلبه الأولي.
    • يرسل الخادم النموذج F1 إلى العميل C1 ويغلق الاتصال الذي تم فتحه في الخطوة 1.
  • الخطوة 2
    • يقوم العميل C1 بملء النموذج وإرساله مرة أخرى إلى الخادم. للقيام بذلك، يفتح المتصفح اتصالاً جديداً بالخادم.
    • يقوم الخادم بمعالجة البيانات الواردة في النموذج 1، ويحسب المعلومات I1 منها، ويرسل النموذج F2 إلى العميل C1، ويغلق الاتصال الذي تم فتحه في الخطوة 3.
  • الخطوة 3
    • تتكرر دورة الخطوتين 3 و 4 في الخطوتين 5 و 6. في نهاية الخطوة 6، سيكون الخادم قد تلقى نموذجين، F1 و F2، وسيكون قد حساب المعلومات I1 و I2 منهما.

المشكلة المطروحة هي: كيف يتتبع الخادم المعلومات I1 و I2 المرتبطة بالعميل C1؟ تسمى هذه المشكلة تتبع جلسة عمل العميل C1. لفهم السبب الجذري لها، دعونا نفحص الرسم التخطيطي لتطبيق خادم TCP-IP يخدم عدة عملاء في وقت واحد:

في تطبيق خادم-عميل TCP-IP كلاسيكي:

  • يقوم العميل بإنشاء اتصال مع الخادم
  • يتبادل البيانات مع الخادم عبر هذا الاتصال
  • يتم إغلاق الاتصال من قبل أحد الطرفين

النقطتان الأساسيتان في هذه الآلية هما:

  1. يتم إنشاء اتصال واحد لكل عميل
  2. يُستخدم هذا الاتصال طوال مدة الحوار بين الخادم وعميله

ما يسمح للخادم بمعرفة العميل الذي يتعامل معه في أي لحظة هو الاتصال — أو بعبارة أخرى، "القناة" — التي تربطه بعميله. وبما أن هذه القناة مخصصة لعميل معين، فإن كل ما يمر عبرها ينشأ من ذلك العميل، وكل ما يُرسل عبرها يصل إلى العميل.

تتبع آلية HTTP للعميل والخادم النموذج السابق عن كثب، باستثناء أن الحوار بين العميل والخادم يقتصر على تبادل واحد بين العميل والخادم:

  • يفتح العميل اتصالاً بالخادم ويقدم طلبه
  • يرسل الخادم استجابته ويغلق الاتصال

إذا قام العميل C في الوقت T1 بتقديم طلب إلى الخادم، فإنه يحصل على اتصال C1 سيُستخدم لتبادل الطلب والاستجابة الواحد. إذا قام هذا العميل نفسه في الوقت T2 بتقديم طلب ثانٍ إلى الخادم، فإنه سيحصل على اتصال C2 يختلف عن الاتصال C1. بالنسبة للخادم، لا يوجد فرق بين هذا الطلب الثاني من المستخدم C وطلبه الأولي: في كلتا الحالتين، يعامل الخادم العميل كعميل جديد. لكي يكون هناك رابط بين الاتصالات المختلفة للعميل C بالخادم، يجب أن "يتعرف" الخادم على العميل C كعميل "دائم" ويجب أن يسترد الخادم المعلومات التي لديه عن هذا العميل الدائم.

لنتخيل نظامًا يعمل على النحو التالي:

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

تعمل آلية تتبع الجلسة في تطبيق ويب خادم-عميل بشكل مشابه:

  • عند تقديم الطلب الأول، يصدر خادم الويب رمزًا للعميل
  • وسيقدم هذا الرمز مع كل طلب لاحق لتعريف نفسه

يمكن أن يتخذ الرمز أشكالاً مختلفة:

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

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

  • طريقة ملفات تعريف الارتباط
    • يقوم العميل بإرسال طلبه الأول (يتعرف الخادم على ذلك لأن العميل لا يمتلك رمزًا مميزًا)
    • يستجيب الخادم بإضافة ملف تعريف ارتباط إلى رؤوس HTTP. ويتم ذلك باستخدام أمر HTTP Set-Cookie:

Set-Cookie: param1=value1;param2=value2;....

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

  • (تابع)
    • يقوم العميل بتقديم طلبه الثاني إلى الخادم. في كل مرة يتم فيها تقديم طلب إلى الخادم، يتحقق المتصفح من بين جميع ملفات تعريف الارتباط الموجودة لديه لمعرفة ما إذا كان لديه ملف تعريف ارتباط من الخادم المطلوب. إذا كان الأمر كذلك، فإنه يرسله إلى الخادم، دائمًا في شكل أمر HTTP — أمر Cookie، الذي له صيغة مشابهة لتلك الخاصة بأمر Set-Cookie الذي يستخدمه الخادم:

Cookie: param1=value1;param2=value2;....

من بين المعلمات التي يرسلها المتصفح، سيجد الخادم الرمز الذي يسمح له بالتعرف على العميل واسترداد المعلومات المرتبطة به.

هذا هو الشكل الأكثر استخدامًا للرمز المميز. له عيب واحد: يمكن للمستخدم تكوين متصفحه لرفض ملفات تعريف الارتباط. عندئذٍ لا يمكن لهؤلاء المستخدمين الوصول إلى تطبيقات الويب التي تستخدم ملفات تعريف الارتباط.

  • إعادة كتابة عنوان URL
    • يقوم العميل بإرسال طلبه الأول (يتعرف الخادم على هذا الطلب لأن العميل لا يمتلك رمزًا مميزًا)
    • يرسل الخادم استجابته. تحتوي هذه الاستجابة على روابط يجب على المستخدم استخدامها لمواصلة استخدام التطبيق. في عنوان URL لكل رابط من هذه الروابط، يضيف الخادم الرمز المميز بالشكل URL;token=value.
    • عندما ينقر المستخدم على أحد الروابط لمواصلة استخدام التطبيق، يرسل المتصفح طلبًا إلى خادم الويب، يتضمن عنوان URL المطلوب (URL;token=value) في رؤوس HTTP. عندئذٍ يتمكن الخادم من استرداد الرمز المميز.

4.2. واجهة برمجة تطبيقات Java لتتبع الجلسة

سنعرض الآن الطرق الرئيسية المفيدة لتتبع الجلسات:

HttpSession [HttpServletRequest].getSession()
يسترد كائن الجلسة الذي ينتمي إليه الطلب الحالي. إذا لم يكن الطلب جزءًا من جلسة بعد، يتم إنشاء واحدة.
String [HttpSession].getId()
معرف الجلسة الحالية
long [HttpSession].getCreationTime()
تاريخ إنشاء الجلسة الحالية (عدد المللي ثانية المنقضية منذ 1 يناير 1970، الساعة 00:00).
long [HttpSession].getLastAccessedTime()
تاريخ آخر وصول للعميل إلى الجلسة
long [HttpSession].getMaxInactiveInterval()
المدة القصوى بالثواني لعدم النشاط في الجلسة. بعد هذه الفترة، تصبح الجلسة غير صالحة.
[HttpSession].setMaxInactiveInterval(int duration)
يحدد المدة القصوى لعدم النشاط لجلسة العمل بالثواني. بعد هذه الفترة، تصبح الجلسة غير صالحة.
boolean [HttpSession].isNew()
true إذا تم إنشاء الجلسة للتو
[HttpSession].setAttribute(String parameter, Object value)
يربط قيمة بمعلمة في جلسة معينة. تسمح هذه الآلية بتخزين المعلومات التي ستظل متاحة طوال الجلسة.
[HttpSession].removeAttribute(String parameter)
يزيل المعلمة من بيانات الجلسة.
Object [HttpSession].getAttribute(String parameter)
تُرجع القيمة المرتبطة بالمعلمة في الجلسة. تُرجع null إذا كانت المعلمة غير موجودة.
التعداد [HttpSession].getAttributeNames()
يسرد جميع سمات الجلسة الحالية كعدد
[HttpSession].invalidate()
يغلق الجلسة الحالية. يتم إتلاف جميع المعلومات المرتبطة بها.

4.3. مثال 1

نقدم مثالاً مأخوذًا من الكتاب الممتاز "Programming with J2EE" الذي نشرته دار Wrox ووزعته Eyrolles. هذا الكتاب هو كنز دفين من المعلومات عالية المستوى لمطوري حلول الويب بلغة Java. تم تكييف التطبيق المقدم في هذا الكتاب كبرنامج Java servlet واحد ليصبح هنا برنامج servlet رئيسي يستدعي صفحات JSP لعرض مختلف الاستجابات الممكنة للعميل.

يُسمى التطبيق "sessions" ويتم تكوينه على النحو التالي في ملف <tomcat>\conf\server.xml:

                <Context path="/sessions" docBase="e:/data/serge/servlets/sessions" />

في مجلد docBase أعلاه، ستجد العناصر التالية:

Image

ترتبط الملفات error.jsp و invalid.jsp و valid.jsp جميعها بتطبيق sessions. في المجلد WEB-INF أعلاه، نجد:

Image

يظهر أعلاه ملف التكوين web.xml لتطبيق sessions. في مجلد classes، ستجد ملف فئة servlet:

Image

فيما يلي ملف web.xml الخاص بالتطبيق:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
      <servlet-name>cycledevie</servlet-name>
    <servlet-class>cycledevie</servlet-class>
    <init-param>
          <param-name>urlSessionValide</param-name>
        <param-value>/valide.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlSessionInvalide</param-name>
        <param-value>/invalide.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreur.jsp</param-value>
    </init-param>
  </servlet>
  <servlet-mapping>
      <servlet-name>cycledevie</servlet-name>
    <url-pattern>/cycledevie</url-pattern>
  </servlet-mapping>
</web-app>

يُسمى السيرفلت الرئيسي cycledevie (servlet-name) وهو مرتبط بملف cycledevie.class (servlet-class). وله اسم مستعار /cycledevie (servlet-mapping) الذي يسمح باستدعائه عبر عنوان URL http://localhost:8080/sessions/cycledevie. وله ثلاث معلمات تهيئة:

urlSessionValide
عنوان URL للصفحة التي تعرض خصائص الجلسة الحالية
urlSessionInvalid
عنوان URL للصفحة التي يتم عرضها بعد إبطال صلاحية الجلسة الحالية
urlErreur
عنوان URL للصفحة التي يتم عرضها في حالة حدوث خطأ في التهيئة للبرنامج الخادم الرئيسي cycledevie

مكونات تطبيق الجلسات هي كما يلي:

دورة الحياة
البرنامج الخادم الرئيسي - يحلل طلب العميل:
  • إذا كانت هذه الصفحة جزءًا من جلسة عمل، فإنها تنقل التحكم إلى صفحة valide.jsp، التي تعرض تفاصيل الجلسة. من هذه الصفحة، يمكن للمستخدم:
    • إعادة تحميلها
    • إبطال صلاحيتها
  • إذا طلب الطلب إبطال صلاحية الجلسة الحالية، فإن السيرفلت ينقل التحكم إلى صفحة invalide.jsp، والتي ستطالب المستخدم بإنشاء جلسة جديدة
  • إذا واجهت الخدمة أخطاء أثناء التهيئة، فإنها تنقل التحكم إلى صفحة error.jsp، التي ستعرض رسالة خطأ.
valide.jsp
  • تعرض تفاصيل الجلسة الحالية وتوفر رابطين:
    • أحدهما لإعادة تحميل الصفحة ومشاهدة كيفية تغير المعلمة الخاصة بآخر وصول إلى الجلسة الحالية
    • والآخر لإبطال صلاحية الجلسة الحالية
invalid.jsp
يتم عرضه عندما يقوم المستخدم بإبطال صلاحية الجلسة الحالية. ثم يعرض إنشاء جلسة جديدة.
error.jsp
يتم عرضها عندما يواجه السيرفلت الرئيسي أخطاء أثناء التهيئة.

دورة حياة السيرفلت الرئيسي هي كما يلي:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class cycledevie extends HttpServlet{

    // instance variables
    String msgErreur=null;
    String urlSessionInvalide=null;
    String urlSessionValide=null;
    String urlErreur=null;

    //-------- GET
    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{

        // was the initialization successful?
        if(msgErreur!=null){
             // we hand over to the error page
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // retrieve the current session
        HttpSession session=request.getSession();

         // analyze the action to be taken
        String action=request.getParameter("action");
        // invalidate current session
        if(action!=null && action.equals("invalider")){
            // the current session is invalidated
            session.invalidate();
             // we hand over to the urlSessionInvalide url
            getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
        }
         // other cases
         // we hand over to the urlSessionInvalide url
        getServletContext().getRequestDispatcher(urlSessionValide).forward(request,response);
    }

     //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

     //-------- INIT
    public void init(){
         // retrieve initialization parameters
        ServletConfig config=getServletConfig();
        urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
        urlSessionValide=config.getInitParameter("urlSessionValide");
        urlErreur=config.getInitParameter("urlErreur");

        // parameters ok?
        if(urlSessionValide==null || urlSessionInvalide==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

يرجى ملاحظة النقاط التالية:

  • في طريقة التهيئة الخاصة به، يسترد السيرفلت معلماته الثلاثة
  • عند معالجة (doGet) طلب، يقوم السيرفلت بما يلي:
    • يتحقق أولاً من عدم حدوث أخطاء أثناء التهيئة. إذا كانت هناك أخطاء، فإنه يعيد التوجيه إلى صفحة error.jsp.
    • يتحقق من قيمة معلمة الإجراء. إذا كانت قيمة هذه المعلمة هي "invalid"، فإن السيرفلت يعيد التوجيه إلى صفحة invalid.jsp؛ وإلا، فإنه يعيد التوجيه إلى صفحة valid.jsp.

تعرض صفحة JSP **valide.jsp** خصائص الجلسة الحالية:

<%@ page import="java.util.*" %>

<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
%>
<!-- top of page HTML -->
  <html>
      <meta http-equiv="pragma" content="no-cache">
    <head>
        <title>Cycle de vie d'une session</title>
    </head>
    <body>
        <h3>Cycle de vie d'une session</h3>
        <hr>
        <br>Etat session : <%= etat %>
      <br>ID session : <%= session.getId() %>
      <br>Heure de création : <%= new Date(session.getCreationTime()) %>
      <br>Heure du dernier accès : <%= new Date(session.getLastAccessedTime()) %>
      <br>Intervalle maximum d'inactivité : <%= session.getMaxInactiveInterval() %>
      <br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
      <br><a href="/sessions/cycledevie">Recharger la page</a>
    <body>
  </html>

لاحظ أنه في السطر

  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";

نستخدم كائن جلسة يظهر من العدم. في الواقع، هذا الكائن هو أحد الكائنات الضمنية المتاحة لصفحات JSP، تمامًا مثل كائنات request و response و out و config (ServletConfig) و context (ServletContext) التي سبق أن تعرفنا عليها. يشير الرابطان الموجودان على الصفحة إلى سيرفلت دورة الحياة الذي تم عرضه سابقًا:

      <br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
      <br><a href="/sessions/cycledevie">Recharger la page</a>

يتضمن الرابط الخاص بإبطال صلاحية الجلسة المعلمة action=invalidate، والتي تسمح لـ lifecycle servlet بالتعرف على أن المستخدم يريد إبطال صلاحية الجلسة الحالية. أما الرابط الآخر فيقوم بإعادة تحميل الصفحة. ولمنع المتصفح من جلب الصفحة من ذاكرة التخزين المؤقت، يتم استخدام توجيه HTML:

      <meta http-equiv="pragma" content="no-cache">

. وهي توجه المتصفح بعدم استخدام ذاكرة التخزين المؤقت للصفحة التي يتلقاها.

صفحة invalidate.jsp هي كما يلي:

<!-- top of page HTML -->
<html>
  <head>
      <title>Cycle de vie d'une session</title>
  </head>
  <body>
      <h3>Cycle de vie d'une session</h3>
      <hr>
    Votre session a été invalidée
    <a href="/sessions/cycledevie">Créer une nouvelle session</a>
  </body>
</html>

يوفر رابطًا إلى خدمة cycledevie بدون معلمة الإجراء. سيؤدي هذا الرابط إلى قيام خدمة cycledevie بإنشاء جلسة جديدة.

صفحة error.jsp هي كما يلي:

<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée)";
%>
<!-- top of page HTML -->
<html>
  <head>
      <title>Cycle de vie d'une session</title>
  </head>
  <body>
      <h3>Cycle de vie d'une session</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

وتتمثل مهمته في عرض رسالة الخطأ التي أرسلها إليه سيرفلت دورة الحياة. لنلقِ نظرة الآن على بعض أمثلة التنفيذ. يتم طلب السيرفلت للمرة الأولى:

Image

تشير الصفحة أعلاه إلى أننا في جلسة جديدة. نستخدم رابط "إعادة تحميل الصفحة":

Image

تشير النتيجة السابقة إلى أننا ما زلنا في نفس الجلسة التي كنا فيها في الصفحة السابقة (نفس المعرف). لاحظ أن وقت آخر وصول إلى هذه الجلسة قد تغير. والآن دعونا نستخدم رابط "إبطال الجلسة":

Image

لاحظ عنوان URL لهذه الصفحة الجديدة مع المعلمة action=invalidate. دعونا نستخدم رابط "إنشاء جلسة جديدة" لإنشاء جلسة جديدة:

Image

يمكننا أن نرى أن جلسة جديدة قد بدأت. في الأمثلة السابقة، تعتمد الجلسة على آلية ملفات تعريف الارتباط. دعونا الآن نعطل ملفات تعريف الارتباط في متصفحنا ونكرر الاختبارات. تم تنفيذ الأمثلة التالية باستخدام Netscape Communicator. لسبب غير مفسر، أسفرت الاختبارات التي أجريت باستخدام IE6 عن نتائج غير متوقعة، كما لو أن IE6 استمر في استخدام ملفات تعريف الارتباط على الرغم من تعطيلها. يتم طلب خدمة cycledevie لأول مرة:

Image

نستخدم الآن رابط "إعادة تحميل الصفحة":

Image

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

  • تغير معرف الجلسة
  • يكتشف السيرفلت الجلسة على أنها جلسة جديدة

يوفر خادم Tomcat حلاً لمشكلة المستخدمين الذين يعطلون ملفات تعريف الارتباط في متصفحهم. ويستخدم آليتين لتنفيذ الرمز المميز المذكور في بداية هذه الفقرة: ملفات تعريف الارتباط وإعادة كتابة عناوين URL. إذا لم يكن ملف تعريف ارتباط الجلسة متاحًا، فسيحاول استرداد الرمز المميز من عنوان URL الذي طلبه العميل. لكي يعمل هذا، يجب أن يحتوي عنوان URL على الرمز المميز. بشكل عام، يجب أن تحتوي جميع الروابط التي يتم إنشاؤها في مستند HTML والتي تشير إلى تطبيق الويب على الرمز المميز للتطبيق. يمكن القيام بذلك باستخدام طريقة encodeURL:

String [HttpResponse].encodeURL(String URL)
تضيف رمز الجلسة الحالي إلى عنوان URL الذي تم تمريره كمعلمة في النموذج URL;jsessionid=xxxx

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

  • في برنامج الخدمة cycledevie.java، يتم ترميز عناوين URL:
             // we hand over to the error page
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
             // we hand over to the urlSessionInvalide url
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
         // we hand over to the urlSessionInvalide url
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • في صفحة valide.jsp، يتم ترميز عناوين URL:
<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
  // encodage URL cycledevie
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");  
%>
............
      <br><a href="<%= URLcycledevie %>?action=invalider">Invalider la session</a>
      <br><a href="<%= URLcycledevie %>">Recharger la page</a>
  • في صفحة invalide.jsp، يتم ترميز عناوين URL:
<%
    // jspservice - on invalide la session en cours
  session.invalidate();
  // encodage URL cycledevie
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>  
..........
    <a href="<%= URLcycledevie %>">Créer une nouvelle session</a>

الآن نحن جاهزون للاختبار. نستخدم Netscape 4.5 وقد تم تعطيل ملفات تعريف الارتباط. نطلب خدمة cycledevie لأول مرة:

Image

ونقوم بإعادة تحميل الصفحة باستخدام رابط "إعادة تحميل الصفحة":

Image

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

  • لم تتغير الجلسة (نفس المعرف)
  • عنوان URL لبرنامج servlet cycledevie يحتوي بالفعل على الرمز المميز، كما هو موضح في حقل العنوان أعلاه
  • وبالتالي، يسترد خادم Tomcat رمز الجلسة من عنوان URL المطلوب (إذا حرص المطور على ترميزه).

4.4. المثال 2

نقدم الآن مثالاً يوضح كيفية تخزين المعلومات في جلسة عمل العميل. هنا، ستكون المعلومة الوحيدة هي عداد يتم زيادته في كل مرة يستدعي فيها المستخدم عنوان URL الخاص بـ servlet. عند استدعائه للمرة الأولى، تظهر الصفحة التالية:

Image

إذا نقرت على رابط "إعادة تحميل الصفحة" أعلاه، فستحصل على الصفحة الجديدة التالية:

Image

يتكون التطبيق من ثلاثة مكونات:

  • برنامج خادم صغير (servlet) يعالج طلب العميل
  • صفحة JSP تعرض قيمة العداد
  • صفحة JSP تعرض أي أخطاء

يتم تثبيت هذه المكونات الثلاثة في تطبيق الويب "sessions" المستخدم بالفعل. وقد تم تعديل ملف web.xml الخاص به لتكوين السيرفلتات الجديدة:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
...
  <servlet>
      <servlet-name>compteur</servlet-name>
    <servlet-class>compteur</servlet-class>
    <init-param>
          <param-name>urlAffichageCompteur</param-name>
        <param-value>/compteur.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreurcompteur.jsp</param-value>
    </init-param>
  </servlet>
...
  <servlet-mapping>
      <servlet-name>compteur</servlet-name>
    <url-pattern>/compteur</url-pattern>
  </servlet-mapping>
</web-app>
  • يُسمى السيرفلت counter (servlet-name) وهو مرتبط بملف الفئة counter.class (servlet-class)
  • ولديه معلمتان للتهيئة:
    • displayCounterURL: عنوان URL لصفحة JSP التي تعرض العداد
    • urlErreur: عنوان URL لصفحة JSP التي تعرض أي أخطاء
  • واسم مستعار /compteur، مما يعني أنه سيتم استدعاؤه عبر عنوان URL http://localhost:8080/sessions/compteur

يكون سيرفلت compteur.java كما يلي:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class compteur extends HttpServlet{

     // instance variables
    String msgErreur=null;
    String urlAffichageCompteur=null;
    String urlErreur=null;

    //-------- GET
    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{

        // was the initialization successful?
        if(msgErreur!=null){
             // we hand over to the error page
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // retrieve the current session
        HttpSession session=request.getSession();
         // and the
        String compteur=(String)session.getAttribute("compteur");
        if(compteur==null) compteur="0";
         // counter incrementation
        try{
            compteur=""+(Integer.parseInt(compteur)+1);
        }catch(Exception ex){}
         // save counter in session
        session.setAttribute("compteur",compteur);
         // and in the query
        request.setAttribute("compteur",compteur);

         // hand over to counter display url
        getServletContext().getRequestDispatcher(urlAffichageCompteur).forward(request,response);
    }

     //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

     //-------- INIT
    public void init(){
         // retrieve initialization parameters
        ServletConfig config=getServletConfig();
        urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
        urlErreur=config.getInitParameter("urlErreur");

         // parameters ok?
        if(urlAffichageCompteur==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

يتمتع هذا السيرفلت بنفس بنية السيرفلتات التي سبق أن تناولناها. لاحظ كيفية التعامل مع العداد:

  • يتم استرداد الجلسة عبر request.getSession()
  • يتم استرداد العداد من هذه الجلسة عبر session.getAttribute("counter")
  • إذا تم استرداد قيمة فارغة، فهذا يعني أن الجلسة قد بدأت للتو. ثم يتم تعيين العداد على 0.
  • يتم زيادة العداد، وتخزينه مرة أخرى في الجلسة (session.setAttribute("counter", counter))، ووضعه في الطلب الذي سيتم تمريره إلى سيرفلت العرض (request.setAttribute("counter", counter)).

صفحة العرض، compteur.jsp، هي كما يلي:

<%
    // jspService
  // on récupère le compteur
  String compteur= (String) request.getAttribute("compteur");
  if(compteur==null) compteur="inconnu";
%>
<!-- top of page HTML -->
<html>
  <head>
      <title>Comptage au fil d'une session</title>
  </head>
  <body>
      <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
      <hr>
    compteur = (<%= compteur %>)
    <br><a href="/sessions/compteur">Recharger la page</a>
  </body>
</html>

تقوم الصفحة أعلاه ببساطة باسترداد سمة العداد (request.getAttribute("counter")) التي تم تمريرها إليها بواسطة السيرفلت الرئيسي وعرضها.

صفحة الخطأ، *erreurcompteur.jsp،* هي كما يلي:

<%
    // jspService
  // une erreur s'est produite
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- top of page HTML -->
<html>
  <head>
      <title>Comptage au fil d'une session</title>
  </head>
  <body>
      <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

4.5. مثال 3

نقترح كتابة تطبيق Java يعمل كعميل لتطبيق العداد السابق. سيقوم باستدعائه N مرات متتالية، حيث يتم تمرير N كمعلمة. هدفنا هو عرض عميل ويب مبرمج وكيفية إدارة ملفات تعريف الارتباط. ستكون نقطة انطلاقنا هي عميل ويب عام مقدم في نشرة Java من قبل المؤلف نفسه. يتم استدعاؤه على النحو التالي:

webclient URL GET/HEAD

  • URL: عنوان URL المطلوب
  • GET/HEAD: GET لطلب كود HTML للصفحة، و HEAD لتقييد الاستجابة برؤوس HTTP فقط

فيما يلي مثال باستخدام عنوان URL http://localhost:8080/sessions/compteur:


E:\data\serge\JAVA\SOCKETS\client web>java clientweb http://localhost:8080/sessions/compteur GET
 
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 14:21:18 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=B8A9076E552945009215C34A97A0EC5D;Path=/sessions
 
 
<!-- top of page HTML -->
<html>
  <head>
        <title>Comptage au fil d'une session</title>
  </head>
  <body>
        <h3>Comptage au fil d'une session (nécessite l'activation des cookies)</h3>
        <hr>
    compteur = (1)
    <br><a href="/sessions/compteur">Recharger la page</a>
  </body>
</html>

يعرض برنامج clientweb كل ما يتلقاه من الخادم. في الأعلى، نرى أمر HTTP Set-cookie، الذي يستخدمه الخادم لإرسال ملف تعريف ارتباط إلى عميله. هنا، يحتوي ملف تعريف الارتباط على معلومتين:

  • JSESSIONID، وهو رمز الجلسة
  • المسار، الذي يحدد عنوان URL الذي ينتمي إليه ملف تعريف الارتباط. المسار=/sessions يُعلم المتصفح بضرورة إعادة إرسال ملف تعريف الارتباط إلى الخادم في كل مرة يطلب فيها عنوان URL يبدأ بـ /sessions. في تطبيق sessions، استخدمنا سيرفلتات مختلفة، بما في ذلك سيرفلتات /sessions/lifecycle و /sessions/counter. إذا استدعينا سيرفلت /sessions/cycledevie، فسيتلقى المتصفح رمز J. إذا استدعينا بعد ذلك، باستخدام نفس المتصفح، سيرفلت /sessions/compteur، فسيقوم المتصفح بإرسال رمز J إلى الخادم لأنه ينطبق على جميع عناوين URL التي تبدأ بـ /sessions. في مثالنا، لا تحتاج سيرفلتات cycledevie و compteur إلى مشاركة رمز الجلسة نفسه. لذلك، ما كان يجب وضعهما في نفس تطبيق الويب. هذه نقطة مهمة يجب تذكرها: جميع السيرفلتات داخل نفس التطبيق تشترك في رمز الجلسة نفسه.
  • يمكن أن يكون لملف تعريف الارتباط أيضًا وقت انتهاء صلاحية. هنا، هذه المعلومات مفقودة. وبالتالي، سيتم حذف ملف تعريف الارتباط عند إغلاق المتصفح. يمكن أن يكون لملف تعريف الارتباط وقت انتهاء صلاحية يبلغ N يومًا، على سبيل المثال. طالما أنه صالح، سيرسله المتصفح مرة أخرى في كل مرة يتم فيها الوصول إلى أحد عناوين URL في نطاقه (المسار). لنفترض وجود متجر أقراص مضغوطة عبر الإنترنت. يمكنه تتبع مسار تصفح العميل عبر الكتالوج الخاص به وتحديد تفضيلاته تدريجيًا — الموسيقى الكلاسيكية، على سبيل المثال. يمكن تخزين هذه التفضيلات في ملف تعريف ارتباط مدته 3 أشهر. إذا عاد هذا العميل نفسه إلى الموقع بعد شهر، فسيرسل المتصفح ملف تعريف الارتباط إلى تطبيق الخادم. استنادًا إلى المعلومات الموجودة في ملف تعريف الارتباط، يمكن لتطبيق الخادم بعد ذلك تخصيص الصفحات التي تم إنشاؤها وفقًا لتفضيلات العميل.

فيما يلي كود عميل الويب. سيكون هذا الكود لاحقًا نقطة انطلاق لعميل آخر.

// imported packages
import java.io.*;
import java.net.*;

public class clientweb{

    // requests a URL
     // displays its contents on the screen

    public static void main(String[] args){
        // syntax
        final String syntaxe="pg URI GET/HEAD";

        // number of arguments
        if(args.length != 2)
            erreur(syntaxe,1);

         // note the URI required
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // URI validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // order verification
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // incorrect order
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // extract useful information from URL
    String path=url.getPath();
    if(path.equals("")) path="/";
    String query=url.getQuery();
    if(query!=null) query="?"+query; else query="";
    String host=url.getHost();
    int port=url.getPort();
    if(port==-1) port=url.getDefaultPort();

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
        try{
             // connect to the server
            client=new Socket(host,port);

            // create customer input/output flows TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // request URL - send HTTP headers
            OUT.println(commande + " " + path + query + " HTTP/1.1");   
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // we read the answer
            while((réponse=IN.readLine())!=null){
                 // the answer is processed
                System.out.println(réponse);
            }//while
             // it's over
            client.close();
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error
}//class

نقوم الآن بإنشاء برنامج clientCounter، الذي يتم استدعاؤه على النحو التالي:

clientCounter URL N [JSESSIONID]

  • URL: عنوان URL لبرنامج servlet الخاص بالعداد
  • N: عدد المكالمات التي سيتم إجراؤها إلى هذا السيرفلت
  • JSESSIONID: معلمة اختيارية — رمز الجلسة

الغرض من البرنامج هو استدعاء سيرفلت العداد N مرات من خلال إدارة ملف تعريف ارتباط الجلسة وعرض قيمة العداد التي يعيدها الخادم في كل مرة. في نهاية N مكالمة، يجب أن تكون قيمة العداد N. فيما يلي مثال أول للتنفيذ:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/counter 3
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->
 
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A;Path=/sessions
cookie trouvÚ : 92DB3808CE8FCB47D47D997C8B52294A
 
compteur : 1
 
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
 
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
 
compteur : 2
 
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
 
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:00 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
 
compteur : 3
 

يعرض البرنامج:

  • رؤوس HTTP التي يرسلها إلى الخادم في النموذج -->
  • رؤوس HTTP التي يتلقاها
  • قيمة العداد بعد كل استدعاء

يمكننا أن نرى أنه خلال المكالمة الأولى:

  • لا يرسل العميل ملف تعريف ارتباط
  • يرسل الخادم ملف تعريف ارتباط واحد

بالنسبة للطلبات اللاحقة:

  • يرسل العميل بشكل منهجي ملف تعريف الارتباط الذي تلقّاه من الخادم خلال الطلب الأول. وهذا ما يسمح للخادم بالتعرف عليه وزيادة عداده.
  • لم يعد الخادم يرسل ملف تعريف الارتباط

نعيد تشغيل البرنامج السابق بتمرير الرمز أعلاه كمعلمة ثالثة:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/compteur 3 92DB3808CE8FCB47D47D997C8B52294A
 
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->

HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
 
compteur : 4
 
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
 
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
 
compteur : 5
 
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Cookie: JSESSIONID=92DB3808CE8FCB47D47D997C8B52294A
--> Connection: close
-->
 
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:25:25 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
 
compteur : 6

نرى هنا أنه بمجرد قيام العميل بإرسال طلبه الأول، يتلقى الخادم ملف تعريف ارتباط جلسة صالح. من المهم ملاحظة أنه بالنسبة لـ Tomcat، فإن الحد الأقصى الافتراضي لوقت عدم النشاط لجلسة ما هو 20 دقيقة (وهذا قابل للتكوين في الواقع). إذا أرسل الطلب الثاني للبرنامج ملف تعريف الارتباط الذي تم استلامه خلال الطلب الأول بسرعة كافية، فسيتعامل الخادم معه على أنه نفس الجلسة. وهذا يسلط الضوء على ثغرة أمنية محتملة. إذا تمكنت من اعتراض رمز الجلسة على الشبكة، يمكنني عندئذ انتحال شخصية المستخدم الذي بدأ الجلسة. في مثالنا، يمثل الاتصال الأول المستخدم الذي يبدأ الجلسة (ربما باستخدام اسم مستخدم وكلمة مرور يمنحانه الحق في تلقي رمز)، ويمثل الاتصال الثاني المستخدم الذي "اخترق" رمز الجلسة من الاتصال الأول. إذا كانت العملية الحالية معاملة مصرفية، فقد يصبح هذا مشكلة كبيرة...

فيما يلي كود العميل:

// imported packages
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientCompteur{

     // requests a URL
     // displays its contents on the screen

    public static void main(String[] args){
        // syntax
        final String syntaxe="pg URL-COMPTEUR N [JSESSIONID]";

         // number of arguments
        if(args.length !=2 && args.length != 3)
            erreur(syntaxe,1);

         // note the URL required
        String URLString=args[0];

        // URL validity check
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI incorrect
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//catch
         // check number of calls N
        int N=0;
        try{
            N=Integer.parseInt(args[1]);
            if(N<=0) throw new Exception();
        }catch(Exception ex){
             // incorrect N argument
            erreur("Le nombre d'appels N doit être un entier >0",3);
        }
         // has the JSESSIONID token been passed as a parameter?
        String JSESSIONID="";
        if (args.length==3) JSESSIONID=args[2];

        // extract useful information from URL
        String path=url.getPath();
        if(path.equals("")) path="/";
        String query=url.getQuery();
        if(query!=null) query="?"+query; else query="";
        String host=url.getHost();
        int port=url.getPort();
        if(port==-1) port=url.getDefaultPort();

         // we can work
        Socket  client=null;                        // the customer
        BufferedReader IN=null;                    // the customer's reading flow
        PrintWriter OUT=null;                        // the customer's writing flow
        String réponse=null;                        // server response
         // the model searched for in HTTP headers
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // the model searched for in the HTML code
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
         // the result of the model comparison
        Matcher résultat=null;
         // a Boolean giving the result of the counter search
        boolean compteurTrouvé;

        try{
             // we make N calls to the server
            for(int i=0;i<N;i++){
                // connect to the server
                client=new Socket(host,port);

                // create customer input/output flows TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // request URL - send HTTP headers
                envoie(OUT,"GET " + path + query + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                if(! JSESSIONID.equals("")){
                    envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
                }
                envoie(OUT,"Connection: close");
                envoie(OUT,"");

                 // we read the response through to the end of the headers, looking for any cookies
                while((réponse=IN.readLine())!=null){
                     // follow-up response
                    System.out.println(réponse);
                     // empty line?
                    if(réponse.equals("")) break;
                     // line HTTP not empty
                     // if you don't have the session token, look for it
                    if (JSESSIONID.equals("")){
                        // compare the HTTP line with the cookie template
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // we found the cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while

                 // that's it for HTTP headers - move on to HTML code
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // does the current line contain the counter?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // counter found - displayed
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while
                 // it's over
                client.close();
            }//for
        } catch(Exception e){
            // we handle the exception
            erreur(e.getMessage(),4);
        }//catch
    }//hand

     // error display
    public static void erreur(String msg, int exitCode){
         // error display
        System.err.println(msg);
         // stop with error
        System.exit(exitCode);
    }//error

     // monitoring client-server exchanges
    public static void envoie(PrintWriter OUT,String msg){
        // sends message to server
        OUT.println(msg);
         // screen tracking
        System.out.println("--> "+msg);
    }//error
}//class

دعونا نحلل النقاط الرئيسية في هذا البرنامج:

  • نحتاج إلى إجراء N عملية تبادل بين العميل والخادم. ولهذا السبب يتم تنفيذها في حلقة
            for(int i=0;i<N;i++){
  • في كل عملية تبادل، يفتح العميل اتصال TCP/IP مع الخادم. وبمجرد إقامة الاتصال، يرسل رؤوس HTTP لطلبه إلى الخادم:
                // on demande l'URL - envoi des entêtes HTTP
                envoie(OUT,"GET " + path + query + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                if(! JSESSIONID.equals("")){
                    envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
                }
                envoie(OUT,"Connection: close");
                envoie(OUT,"");

إذا كان الرمز JSESSIONID متاحًا، يتم إرساله كملف تعريف ارتباط؛ وإلا، فلا يتم إرساله.

  • بمجرد إرسال الطلب، ينتظر العميل استجابة الخادم. يبدأ بفحص رؤوس HTTP لهذه الاستجابة للبحث عن ملف تعريف ارتباط محتمل. للعثور عليه، يقارن الأسطر التي يتلقاها بالتعبير العادي لملف تعريف الارتباط:
         // the model searched in HTTP headers
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // we read the response through to the end of the headers, looking for any cookies
                while((réponse=IN.readLine())!=null){
                     // follow-up response
                    System.out.println(réponse);
                     // empty line?
                    if(réponse.equals("")) break;
                     // line HTTP not empty
                     // if you don't have the session token, look for it
                    if (JSESSIONID.equals("")){
                        // compare the HTTP line with the cookie template
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // we found the cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • بمجرد العثور على الرمز المميز للمرة الأولى، لن يتم البحث عنه مرة أخرى في المكالمات اللاحقة إلى الخادم. وبمجرد معالجة رؤوس HTTP الخاصة بالاستجابة، ننتقل إلى كود HTML الخاص بتلك الاستجابة نفسها. ونبحث فيه عن السطر الذي يقدم قيمة العداد. ويتم إجراء هذا البحث أيضًا باستخدام تعبير عادي:
         // the counter model searched for in the HTML code
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                 // that's it for HTTP headers - move on to HTML code
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // does the current line contain the counter?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // counter found - displayed
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. المثال 4

في المثال السابق، يعيد عميل الويب الرمز المميز كملف تعريف ارتباط. وقد رأينا أنه يمكنه أيضًا إعادته ضمن عنوان URL المطلوب نفسه بالشكل URL;jsessionid=xxx. دعونا نتحقق من ذلك. تم تغيير اسم برنامج clientCompteur.java إلى clientCompteur2.java وتعديله على النحو التالي:

....
                // on demande l'URL - envoi des entêtes HTTP
                if(JSESSIONID.equals(""))
                    envoie(OUT,"GET " + path + query + " HTTP/1.1");
                else envoie(OUT,"GET " + path + query + ";jsessionid=" + JSESSIONID + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                envoie(OUT,"Connection: close");
                envoie(OUT,"");
....

وبالتالي، يطلب العميل عنوان URL الخاص بالعداد عبر GET URL;jsessionid=xx HTTP/1.1 ولم يعد يرسل ملف تعريف ارتباط. هذا هو التغيير الوحيد. فيما يلي نتائج المكالمة الأولية:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur2 http://localhost:8080/sessions/counter 2
 
--> GET /sessions/compteur HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->
 
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:49:30 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
Set-Cookie: JSESSIONID=48A6DBA8357D808EC012AAF3A2AFDA63;Path=/sessions
cookie trouvÚ : 48A6DBA8357D808EC012AAF3A2AFDA63
 
compteur : 1
 
--> GET /sessions/compteur;jsessionid=48A6DBA8357D808EC012AAF3A2AFDA63 HTTP/1.1
--> Host: localhost:8080
--> Connection: close
-->
 
HTTP/1.1 200 OK
Content-Type: text/html;charset=ISO-8859-1
Date: Thu, 08 Aug 2002 18:49:30 GMT
Connection: close
Server: Apache Tomcat/4.0.3 (HTTP/1.1 Connector)
 
compteur : 2

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

4.7. المثال 5

يوضح هذا المثال تطبيقًا يتكون من ثلاث صفحات، سنسميها page0 و page1 و page2. يجب على المستخدم الوصول إليها بهذا الترتيب:

  • page0 هو نموذج يطلب معلومات: الاسم
  • page1 هو نموذج يتم استلامه استجابةً لإرسال النموذج الموجود في page0. ويطلب معلومة ثانية: العمر
  • page2 هي مستند HTML يعرض الاسم الذي تم الحصول عليه من page0 والعمر الذي تم الحصول عليه من page1.

هناك ثلاثة تبادلات بين العميل والخادم هنا:

  • في التبادل الأول، يطلب العميل نموذج الصفحة 0 ويرسله الخادم
  • في التبادل الثاني، يطلب العميل نموذج الصفحة 1 ويقوم الخادم بإرساله. يرسل العميل الاسم إلى الخادم.
  • في التبادل الثالث، يطلب العميل مستند الصفحة 3 ويقوم الخادم بإرساله. يرسل العميل العمر إلى الخادم. يجب أن يعرض مستند الصفحة 3 الاسم والعمر. تم الحصول على الاسم بواسطة الخادم في التبادل الثاني ومنذ ذلك الحين تم "نسيانه". تُستخدم جلسة عمل لتخزين الاسم من التبادل 2 بحيث يكون متاحًا أثناء التبادل 3.

مستند page0 الذي تم الحصول عليه في التبادل الأول هو كما يلي:

Image

نملأ حقل الاسم:

Image

نضغط على زر "التالي" ثم تظهر لنا الصفحة التالية1:

Image

نملأ حقل العمر:

Image

نضغط على زر "التالي" لتظهر لنا الصفحة التالية2:

Image

عند إرسال الصفحة 0 إلى الخادم، قد يعيدها الخادم مع رمز خطأ إذا كان حقل الاسم فارغًا:

Image

عند إرسال الصفحة 1 إلى الخادم، قد يعيدها الخادم مع رمز خطأ إذا كان العمر غير صالح:

Image

يتكون التطبيق من سيرفلت وأربع صفحات JSP:

page0.jsp
تعرض page0
page1.jsp
تعرض الصفحة 1
page2.jsp
يعرض الصفحة 2
error.jsp
تعرض صفحة خطأ

يُسمى تطبيق الويب suitedepages ويتم تكوينه على النحو التالي في ملف server.xml الخاص بـ Tomcat:

                <Context path="/suitedepages" docBase="e:/data/serge/servlets/suitedepages" />

ملف التكوين web.xml لتطبيق suitedepages هو كما يلي:

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
    <servlet>
      <servlet-name>main</servlet-name>
    <servlet-class>main</servlet-class>
    <init-param>
          <param-name>urlPage0</param-name>
        <param-value>/page0.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlPage1</param-name>
        <param-value>/page1.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlPage2</param-name>
        <param-value>/page2.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreur.jsp</param-value>
    </init-param>    
  </servlet>
  <servlet-mapping>
      <servlet-name>main</servlet-name>
    <url-pattern>/main</url-pattern>
  </servlet-mapping>
</web-app>

يُسمى السيرفلت الرئيسي main، وبفضل الاسم المستعار الخاص به (servlet-mapping) يمكن الوصول إليه عبر عنوان URL http://localhost:8080/suitedepages/main*. ويحتوي على أربعة معلمات تهيئة، وهي عناوين URL لأربع صفحات JSP تُستخدم للعروض المختلفة. وفيما يلي كود السيرفلت main*:

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
import java.util.regex.*;

public class main extends HttpServlet{

    // instance variables
    String msgErreur=null;
    String urlPage0=null;
    String urlPage1=null;
    String urlPage2=null;
    String urlErreur=null;

    //-------- GET
    public void doGet(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{

        // was the initialization successful?
        if(msgErreur!=null){
             // we hand over to the error page
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }
         // we retrieve the step parameter
        String étape=request.getParameter("etape");
         // retrieve the current session
        HttpSession session=request.getSession();
         // the current step is processed
        if(étape==null) étape0(request,response,session);
        if(étape.equals("1")) étape1(request,response,session);
        if(étape.equals("2")) étape2(request,response,session);
         // other cases are invalid
        étape0(request,response,session);
    }

     //-------- POST
    public void doPost(HttpServletRequest request, HttpServletResponse response)
    throws IOException, ServletException{
        doGet(request,response);
    }

     //-------- INIT
    public void init(){
         // retrieve initialization parameters
        ServletConfig config=getServletConfig();
        urlPage0=config.getInitParameter("urlPage0");
        urlPage1=config.getInitParameter("urlPage1");
        urlPage2=config.getInitParameter("urlPage2");
        urlErreur=config.getInitParameter("urlErreur");

         // parameters ok?
        if(urlPage0==null || urlPage1==null || urlPage2==null){
            msgErreur="Configuration incorrecte";
        }
    }

     //-------- step0
    public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // we set a few attributes
        request.setAttribute("nom","");
         // we present page 0
        request.getRequestDispatcher(urlPage0).forward(request,response);
    }

     //-------- step1
    public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // retrieve the name from the query
        String nom=request.getParameter("nom");
        // name positioned?
        if(nom==null) étape0(request,response,session);
         // remove any spaces from the name
        nom=nom.trim();
         // we put it in a query attribute
        request.setAttribute("nom",nom);
         // empty name?
        if(nom.equals("")){
             // it's a mistake
            ArrayList erreurs=new ArrayList();
            erreurs.add("Nous n'avez pas indiqué de nom");
             // put the errors in the query
            request.setAttribute("erreurs",erreurs);
             // back to page 0
            étape0(request,response,session);
        }
         // valid name - stored in the current session
        session.setAttribute("nom",nom);
         // set the age attribute in the query
        request.setAttribute("age","");
         // we present page 1
        request.getRequestDispatcher(urlPage1).forward(request,response);
    }

     //-------- step2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // retrieve the name from the session
        String nom=(String)session.getAttribute("nom");
         // name positioned?
        if(nom==null) étape0(request,response,session);
         // we put it in a query attribute
        request.setAttribute("nom",nom);
         // the age is retrieved from the query
        String age=request.getParameter("age");
        // age positioned?
        if(age==null){
            // back to page 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // the age is stored in the query
        age=age.trim();
        request.setAttribute("age",age);
        // valid age?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // this is a mistake
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // put the errors in the query
            request.setAttribute("erreurs",erreurs);
             // back to page 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // age valid - page 2 is presented
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • تسترد طريقة init المعلمات الأربعة للتهيئة وتضع رسالة خطأ في حالة فقدان أي منها
  • لقد رأينا أن الطلب يتكون من ثلاث عمليات تبادل. لتتبع المرحلة الحالية من هذه العمليات، تحتوي نماذج page0 و page1 على متغير مخفي "etape" بقيمة 1 (page0) أو 2 (page1). يمكن تفسير هذا الرقم على أنه رقم الصفحة التالية التي سيتم عرضها. في طريقة doGet، يتم استرداد هذه المعلمة من الطلب، واعتمادًا على قيمتها، يتم تفويض المعالجة إلى ثلاث طرق أخرى:
    • تقوم étape0 بمعالجة الطلب الأولي وإرسال page0
    • تقوم étape1 بمعالجة النموذج الموجود في page0 وإرسال page1 أو page0 مرة أخرى في حالة حدوث خطأ
    • تقوم étape2 بمعالجة النموذج الموجود في الصفحة 1 وإرسال الصفحة 2 أو الصفحة 1 مرة أخرى في حالة حدوث خطأ
  • الخطوة 0
    • تعرض الصفحة 0 مع اسم فارغ
  • الخطوة 1
    • يسترد المعلمة "name" من نموذج الصفحة 0.
    • يتحقق من وجود الاسم (ليس فارغًا). إذا لم يكن موجودًا، يتم عرض الصفحة 0 مرة أخرى كما لو كانت المرة الأولى.
    • يتحقق من أن الاسم ليس فارغًا. إذا كان كذلك، يتم عرض الصفحة 0 مرة أخرى مع رسالة خطأ.
    • يخزن الاسم في الجلسة الحالية ويعرض الصفحة 1 إذا كان الاسم صالحًا.
  • الخطوة 2
    • يسترد المعلمة name من الجلسة الحالية.
    • يتحقق من وجود الاسم (ليس فارغًا). إذا لم يكن موجودًا، يتم عرض الصفحة 0 مرة أخرى كما لو كانت المرة الأولى.
    • يسترد معلمة العمر من الطلب الحالي المرسل بواسطة الصفحة 1.
    • يتحقق من صحة العمر. إذا لم يكن كذلك، يعرض الصفحة 1 مرة أخرى مع رسالة خطأ.
    • يخزن الاسم والعمر كسمات للطلب ويعرض الصفحة 2 إذا كان الاسم والعمر صالحين.

فيما يلي نص الصفحة page0.jsp:

<%@ page import="java.util.*" %>

<% // page0.jsp
    // on récupère les attributs de la requête
  String nom=(String)request.getAttribute("nom");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // attributs valides ?
  if(nom==null){
      // retour à la servlet principale
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  

<html>
  <head>
    <title>page 0</title>
  </head>
  <body>
    <h3>Page 0/2</h3>
    <form name="frmNom" method="POST" action="/suitedepages/main">
        <input type="hidden" name="etape" value="1">
      <table>
        <tr>
          <td>Votre nom</td>
          <td><input type="text" name="nom" value="<%= nom %>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <% // erreurs ?
      if (erreurs!=null){
    %>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <% for(int i=0;i<erreurs.size();i++){ %>
            <li><%= erreurs.get(i) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>
  • يمكن استدعاء الصفحة page0.jsp بواسطة السيرفلت الرئيسي في حالتين:
    • أثناء الطلب الأولي
    • بعد معالجة نموذج page0 عند حدوث خطأ
  • يتم توفير المعلمة `nameToDisplay` بواسطة السيرفلت الرئيسي، إلى جانب أي قائمة أخطاء. لذلك يبدأ السيرفلت page0.jsp باسترداد هاتين المعلومتين.
  • يتم "إرسال" النموذج إلى السيرفلت الرئيسي مع الحقل المخفي "etape"، الذي يشير إلى المرحلة التي يمر بها المستخدم في التطبيق.

صفحة page1.jsp هي كما يلي:

<%@ page import="java.util.*" %>

<% // page1.jsp
    // on récupère les attributs de la requête
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // attributs valides ?
  if(nom==null || age==null){
      // retour à la servlet principale
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  

<html>
  <head>
    <title>page 1</title>
  </head>
  <body>
    <h3>Page 1/2</h3>
    <form name="frmAge" method="POST" action="/suitedepages/main">
        <input type="hidden" name="etape" value="2">    
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><%= nom %></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><input type="text" name="age" size="3" value="<%= age %>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <% // erreurs ?
      if (erreurs!=null){
    %>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <% for(int i=0;i<erreurs.size();i++){ %>
            <li><%= erreurs.get(i) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>

تتميز صفحة page1.jsp بهيكل مشابه لصفحة page0.jsp، باستثناء أنها تتلقى الآن سمتين من السيرفلت الرئيسي: name و age. وأخيرًا، تبدو صفحة page2.jsp كما يلي:

<% 
    // page2.jsp
    // on récupère les attributs de la requête
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  // attributs valides ?
  if(nom==null || age==null){
      // retour à la servlet principale
    request.getRequestDispatcher("/main").forward(request,response);
  }
%>  


<html>
  <head>
    <title>page 2</title>
  </head>
  <body>
    <h3>Page 2/2</h3>
      <table>
        <tr>
          <td>Nom</td>
          <td><font color="green"><%= nom %></font></td>
        </tr>
        <tr>
          <td>Votre âge</td>
          <td><font color="green"><%= age %></font></td>
        </tr>
      </table>
  </body>
</html>

تستقبل صفحة page2.jsp أيضًا سمات الاسم والعمر من السيرفلت الرئيسي. وهي تعرضها ببساطة. وأخيرًا، فإن صفحة error.jsp، المسؤولة عن عرض خطأ في حالة التهيئة غير الصحيحة للسيرفلت، هي كما يلي:

<%
    // jspService
  // une erreur s'est produite
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- top of page HTML -->
<html>
  <head>
      <title>Suite de pages</title>
  </head>
  <body>
      <h3>Suite de pages</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

يعرض السمة msgError التي تم تمريرها إليه من قبل السيرفلت الرئيسي.

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