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 كلاسيكي:
- يقوم العميل بإنشاء اتصال مع الخادم
- يتبادل البيانات مع الخادم عبر هذا الاتصال
- يتم إغلاق الاتصال من قبل أحد الطرفين
النقطتان الأساسيتان في هذه الآلية هما:
- يتم إنشاء اتصال واحد لكل عميل
- يُستخدم هذا الاتصال طوال مدة الحوار بين الخادم وعميله
ما يسمح للخادم بمعرفة العميل الذي يتعامل معه في أي لحظة هو الاتصال — أو بعبارة أخرى، "القناة" — التي تربطه بعميله. وبما أن هذه القناة مخصصة لعميل معين، فإن كل ما يمر عبرها ينشأ من ذلك العميل، وكل ما يُرسل عبرها يصل إلى العميل.
تتبع آلية 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 لتتبع الجلسة
سنعرض الآن الطرق الرئيسية المفيدة لتتبع الجلسات:
يسترد كائن الجلسة الذي ينتمي إليه الطلب الحالي. إذا لم يكن الطلب جزءًا من جلسة بعد، يتم إنشاء واحدة. | |
معرف الجلسة الحالية | |
تاريخ إنشاء الجلسة الحالية (عدد المللي ثانية المنقضية منذ 1 يناير 1970، الساعة 00:00). | |
تاريخ آخر وصول للعميل إلى الجلسة | |
المدة القصوى بالثواني لعدم النشاط في الجلسة. بعد هذه الفترة، تصبح الجلسة غير صالحة. | |
يحدد المدة القصوى لعدم النشاط لجلسة العمل بالثواني. بعد هذه الفترة، تصبح الجلسة غير صالحة. | |
true إذا تم إنشاء الجلسة للتو | |
يربط قيمة بمعلمة في جلسة معينة. تسمح هذه الآلية بتخزين المعلومات التي ستظل متاحة طوال الجلسة. | |
يزيل المعلمة من بيانات الجلسة. | |
تُرجع القيمة المرتبطة بالمعلمة في الجلسة. تُرجع null إذا كانت المعلمة غير موجودة. | |
يسرد جميع سمات الجلسة الحالية كعدد | |
يغلق الجلسة الحالية. يتم إتلاف جميع المعلومات المرتبطة بها. |
4.3. مثال 1
نقدم مثالاً مأخوذًا من الكتاب الممتاز "Programming with J2EE" الذي نشرته دار Wrox ووزعته Eyrolles. هذا الكتاب هو كنز دفين من المعلومات عالية المستوى لمطوري حلول الويب بلغة Java. تم تكييف التطبيق المقدم في هذا الكتاب كبرنامج Java servlet واحد ليصبح هنا برنامج servlet رئيسي يستدعي صفحات JSP لعرض مختلف الاستجابات الممكنة للعميل.
يُسمى التطبيق "sessions" ويتم تكوينه على النحو التالي في ملف <tomcat>\conf\server.xml:
في مجلد docBase أعلاه، ستجد العناصر التالية:

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

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

فيما يلي ملف 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. وله ثلاث معلمات تهيئة:
عنوان URL للصفحة التي تعرض خصائص الجلسة الحالية | |
عنوان URL للصفحة التي يتم عرضها بعد إبطال صلاحية الجلسة الحالية | |
عنوان URL للصفحة التي يتم عرضها في حالة حدوث خطأ في التهيئة للبرنامج الخادم الرئيسي cycledevie |
مكونات تطبيق الجلسات هي كما يلي:
البرنامج الخادم الرئيسي - يحلل طلب العميل:
| |
| |
يتم عرضه عندما يقوم المستخدم بإبطال صلاحية الجلسة الحالية. ثم يعرض إنشاء جلسة جديدة. | |
يتم عرضها عندما يواجه السيرفلت الرئيسي أخطاء أثناء التهيئة. |
دورة حياة السيرفلت الرئيسي هي كما يلي:
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>
لاحظ أنه في السطر
نستخدم كائن جلسة يظهر من العدم. في الواقع، هذا الكائن هو أحد الكائنات الضمنية المتاحة لصفحات 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:
. وهي توجه المتصفح بعدم استخدام ذاكرة التخزين المؤقت للصفحة التي يتلقاها.
صفحة 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>
وتتمثل مهمته في عرض رسالة الخطأ التي أرسلها إليه سيرفلت دورة الحياة. لنلقِ نظرة الآن على بعض أمثلة التنفيذ. يتم طلب السيرفلت للمرة الأولى:

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

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

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

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

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

يمكننا ملاحظة أمرين:
- تغير معرف الجلسة
- يكتشف السيرفلت الجلسة على أنها جلسة جديدة
يوفر خادم Tomcat حلاً لمشكلة المستخدمين الذين يعطلون ملفات تعريف الارتباط في متصفحهم. ويستخدم آليتين لتنفيذ الرمز المميز المذكور في بداية هذه الفقرة: ملفات تعريف الارتباط وإعادة كتابة عناوين URL. إذا لم يكن ملف تعريف ارتباط الجلسة متاحًا، فسيحاول استرداد الرمز المميز من عنوان URL الذي طلبه العميل. لكي يعمل هذا، يجب أن يحتوي عنوان URL على الرمز المميز. بشكل عام، يجب أن تحتوي جميع الروابط التي يتم إنشاؤها في مستند HTML والتي تشير إلى تطبيق الويب على الرمز المميز للتطبيق. يمكن القيام بذلك باستخدام طريقة encodeURL:
تضيف رمز الجلسة الحالي إلى عنوان 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 لأول مرة:

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

يمكننا أن نرى أن:
- لم تتغير الجلسة (نفس المعرف)
- عنوان URL لبرنامج servlet cycledevie يحتوي بالفعل على الرمز المميز، كما هو موضح في حقل العنوان أعلاه
- وبالتالي، يسترد خادم Tomcat رمز الجلسة من عنوان URL المطلوب (إذا حرص المطور على ترميزه).
4.4. المثال 2
نقدم الآن مثالاً يوضح كيفية تخزين المعلومات في جلسة عمل العميل. هنا، ستكون المعلومة الوحيدة هي عداد يتم زيادته في كل مرة يستدعي فيها المستخدم عنوان URL الخاص بـ servlet. عند استدعائه للمرة الأولى، تظهر الصفحة التالية:

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

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

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

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

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

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

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

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

يتكون التطبيق من سيرفلت وأربع صفحات JSP:
تعرض page0 | |
تعرض الصفحة 1 | |
يعرض الصفحة 2 | |
تعرض صفحة خطأ |
يُسمى تطبيق الويب suitedepages ويتم تكوينه على النحو التالي في ملف server.xml الخاص بـ Tomcat:
ملف التكوين 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 المطلوب في البداية — وهو عنوان السيرفلت الرئيسي — في حقل "العنوان" الخاص به.
