4. ردیابی جلسه
4.1. مشکل
یک برنامه وب ممکن است شامل چندین تبادل فرم بین سرور و کلاینت باشد. روند کار به شرح زیر است:
- مرحله ۱
- کلاینت C1 یک اتصال با سرور برقرار میکند و درخواست اولیه خود را ارسال میکند.
- سرور فرم F1 را برای کلاینت C1 ارسال میکند و اتصال بازشده در مرحلهٔ ۱ را میبندد.
- مرحله ۲
- کلاینت C1 فرم را پر میکند و آن را به سرور بازمیگرداند. برای این کار، مرورگر یک اتصال جدید به سرور باز میکند.
- سرور دادههای فرم ۱ را پردازش میکند، اطلاعات I1 را از آن محاسبه میکند، یک فرم F2 را به کلاینت C1 ارسال میکند و اتصال باز شده در مرحله ۳ را میبندد.
- مرحله ۳
- چرخه مراحل ۳ و ۴ در مراحل ۵ و ۶ تکرار میشود. در پایان مرحله ۶، سرور دو فرم، F1 و F2 را دریافت کرده و اطلاعات I1 و I2 را از آنها محاسبه خواهد کرد.
مسئله این است: سرور چگونه اطلاعات I1 و I2 مرتبط با کلاینت C1 را حفظ میکند؟ این مشکل به عنوان ردیابی جلسهٔ مشتری C1 شناخته میشود. برای درک علت اصلی، بیایید نمودار یک برنامهٔ سرور TCP-IP را که همزمان به چندین مشتری خدمترسانی میکند، بررسی کنیم:
![]() |
در یک برنامهٔ معمول TCP-IP کلاینت-سرور:
- کلاینت یک اتصال با سرور برقرار میکند
- از طریق این اتصال با سرور تبادل داده میکند
- ارتباط توسط یکی از دو طرف بسته میشود
دو نکته کلیدی این مکانیزم عبارتند از:
- برای هر کلاینت یک اتصال واحد ایجاد میشود
- این اتصال در تمام مدت مکالمه بین سرور و مشتریاش استفاده میشود
آنچه به سرور امکان میدهد در هر لحظه بداند با کدام کلاینت در حال کار است، اتصال است – یا به عبارت دیگر، «لوله»ای که آن را به کلاینتش متصل میکند. از آنجا که این لوله به یک کلاینت خاص اختصاص دارد، هر چیزی که از طریق این لوله دریافت میشود از آن کلاینت میآید و هر چیزی که از طریق این لوله ارسال میشود به کلاینت میرسد.
مکانیزم کلاینت-سرور HTTP به دقت از نمودار بالا پیروی میکند، با این تفاوت که دیالوگ کلاینت-سرور به یک تبادل واحد بین کلاینت و سرور محدود میشود:
- کلاینت یک اتصال به سرور باز میکند و درخواست خود را ارسال میکند
- سرور پاسخ خود را ارسال میکند و اتصال را قطع میکند
اگر در زمان T1، یک کلاینت C درخواستی به سرور ارسال کند، یک اتصال C1 دریافت میکند که برای یک تبادل درخواست-پاسخ واحد استفاده خواهد شد. اگر در زمان T2، همین کلاینت درخواست دومی به سرور ارسال کند، یک اتصال C2 به آن اختصاص داده میشود که با اتصال C1 متفاوت است. از دیدگاه سرور، بنابراین تفاوتی بین این درخواست دوم از کاربر C و درخواست اولیه او وجود ندارد: در هر دو مورد، سرور با مشتری مانند یک مشتری جدید رفتار میکند. برای اینکه بین اتصالات مختلف مشتری C به سرور پیوندی برقرار شود، مشتری C باید توسط سرور به عنوان یک «مشتری دائمی» «شناخته» شود و سرور باید اطلاعاتی را که در مورد آن مشتری دائمی در اختیار دارد، بازیابی کند.
بیایید سیستمی را تصور کنیم که به شرح زیر عمل میکند:
- یک صف واحد وجود دارد
- چندین صندوق وجود دارد. بنابراین، چندین مشتری میتوانند بهطور همزمان خدمترسانی شوند. وقتی یک صندوق خالی میشود، مشتری صف را ترک میکند تا در آن صندوق خدمترسانی شود.
- اگر این اولین مراجعه مشتری باشد، کارمند باجه یک شماره به او میدهد. مشتری فقط میتواند یک سؤال بپرسد. پس از دریافت پاسخ، باید باجه را ترک کرده و به انتهای صف برود. کارمند باجه مشخصات مشتری را در پروندهای با همان شماره ثبت میکند.
- وقتی دوباره نوبتشان میشود، ممکن است مشتری توسط کارمندی متفاوت از کسی که قبلاً به او خدمترسانی کرده بود، مورد خدمترسانی قرار گیرد. کارمند، توکن او را درخواست میکند و پروندهای را که شماره توکن روی آن است، بیرون میآورد. بار دیگر، مشتری درخواستی ارائه میدهد، پاسخی دریافت میکند و اطلاعات بیشتری به پروندهاش اضافه میشود.
- و به همین ترتیب... با گذشت زمان، مشتری پاسخ تمام پرسشهای خود را دریافت خواهد کرد. ارتباط بین پرسشهای مختلف از طریق توکن و پرونده مرتبط با آن حفظ میشود.
مکانیزم ردیابی جلسه در یک برنامه وب مشتری-سرور به روشی مشابه عمل میکند:
- هنگام ارسال اولین درخواست، یک توکن توسط وب سرور به کلاینت صادر میشود
- آنها این توکن را در هر درخواست بعدی برای شناسایی خود ارائه خواهند داد
توکن میتواند اشکال مختلفی داشته باشد:
- یک فیلد مخفی در یک فرم
- کلاینت اولین درخواست خود را ارسال میکند (سرور این را تشخیص میدهد زیرا کلاینت توکنی ندارد)
- سرور پاسخ خود (یک فرم) را ارسال میکند و توکن را در یک فیلد مخفی درون آن قرار میدهد. در این نقطه، اتصال بسته میشود (کلاینت با توکن خود سرور را ترک میکند). ممکن است سرور اطلاعاتی را با این توکن مرتبط کرده باشد.
- کلاینت با ارسال مجدد فرم، درخواست دوم را انجام میدهد. سرور توکن را از فرم بازیابی میکند. سپس میتواند با دسترسی به اطلاعاتی که در طول درخواست اول محاسبه شده است، از طریق توکن، درخواست دوم کلاینت را پردازش کند. اطلاعات جدید به رکورد مرتبط با توکن اضافه میشود، یک پاسخ دوم برای کلاینت ارسال میشود و اتصال برای بار دوم بسته میشود. توکن بار دیگر در فرم پاسخ گنجانده شده است تا کاربر بتواند آن را در درخواست بعدی خود ارائه دهد.
- و غیره...
نقطه ضعف اصلی این تکنیک این است که توکن باید در یک فرم گنجانده شود. اگر پاسخ سرور یک فرم نباشد، دیگر نمیتوان از روش فیلد مخفی استفاده کرد.
- روش کوکی
- کلاینت اولین درخواست خود را ارسال میکند (سرور این را تشخیص میدهد زیرا کلاینت توکنی ندارد)
- سرور پاسخ خود را ارسال میکند و یک کوکی به هدرهای آن اضافه میکند (HTTP). این کار با استفاده از دستور Set-Cookie انجام میشود:
Set-Cookie: param1=value1;param2=value2;....
که در آن parami نام پارامترها و valeursi مقادیر آنها هستند. در میان پارامترها، توکن نیز وجود خواهد داشت. اغلب اوقات، تنها توکن در کوکی قرار میگیرد و سایر اطلاعات توسط سرور در پوشهای که به توکن مرتبط است، ثبت میشود. مرورگری که کوکی را دریافت میکند، آن را در فایلی روی هارد دیسک ذخیره خواهد کرد. پس از پاسخ سرور، اتصال بسته میشود (کلاینت جلسه را به همراه توکن خود ترک میکند).
- (ادامه)
- کلاینت دومین درخواست خود را به سرور ارسال میکند. هر بار که درخواستی به سرور ارسال میشود، مرورگر تمام کوکیهای موجود در خود را بررسی میکند تا ببیند آیا کوکیای از سرور مورد نظر دارد یا خیر. اگر چنین باشد، آن را در قالب یک دستور HTTP – دستور «Cookie» – که نحوی مشابه دستور Set-Cookie مورد استفاده توسط سرور دارد، به سرور ارسال میکند:
Cookie: param1=value1;param2=value2;....
در میان پارامترهای ارسالشده توسط مرورگر، سرور توکنی را پیدا میکند که به آن امکان میدهد کلاینت را شناسایی کرده و اطلاعات مرتبط با آن را بازیابی کند.
این رایجترین نوع توکن است. یک نقطهضعف دارد: کاربر میتواند مرورگر خود را طوری تنظیم کند که کوکیها را نپذیرد. چنین کاربرانی در این صورت قادر به دسترسی به برنامههای وب که از کوکیها استفاده میکنند، نخواهند بود.
- بازنویسی URL
- کلاینت اولین درخواست خود را ارسال میکند (سرور این را تشخیص میدهد زیرا کلاینت توکنی ندارد)
- سرور پاسخ خود را ارسال میکند. این پاسخ شامل لینکهایی است که کاربر باید برای ادامه کار با برنامه از آنها استفاده کند. در URL هر یک از این لینکها، سرور توکن را به شکل URL;token=value اضافه میکند.
- هنگامی که کاربر برای ادامه استفاده از برنامه روی یکی از لینکها کلیک میکند، مرورگر درخواستی را به وبسرور ارسال میکند که در آن توکن مورد نظر در هدرها به شکل HTTP URL URL;token=value درج شده است. سپس سرور قادر است توکن را بازیابی کند.
4.2. جاوا برای ردیابی جلسه
اکنون روشهای اصلی مفید برای ردیابی جلسه را تشریح میکنیم:
شیء Session را که درخواست فعلی به آن تعلق دارد، بازیابی میکند. اگر درخواست هنوز بخشی از یک جلسه نبود، یک جلسه ایجاد میشود. | |
شناسهی جلسهی جاری | |
تاریخ ایجاد جلسهٔ جاری (تعداد میلیثانیههای گذشته از ۱ ژانویهٔ ۱۹۷۰، ۰۰:۰۰). | |
تاریخ آخرین دسترسی مشتری به جلسه | |
حداکثر مدت زمان عدم فعالیت یک جلسه به ثانیه. پس از این مدت، جلسه باطل میشود. | |
حداکثر مدت زمان عدم فعالیت برای یک جلسه را به ثانیه تعیین میکند. پس از این دوره، جلسه باطل میشود. | |
در صورتی که جلسه به تازگی ایجاد شده باشد، true | |
یک مقدار را به یک پارامتر در یک جلسهٔ مشخص اختصاص میدهد. این مکانیزم امکان ذخیرهسازی اطلاعاتی را فراهم میکند که در طول جلسه در دسترس باقی میماند. | |
parametre را از دادههای جلسه حذف میکند. | |
مقدار مرتبط با پارامتر paramètre در جلسه. در صورتی که پارامتر مذکور وجود نداشته باشد، مقدار null را برمیگرداند. | |
فهرست، به شکل یک شمارششده، از تمام ویژگیهای جلسهٔ جاری | |
جلسهٔ جاری را میبندد. تمام اطلاعات مرتبط با آن حذف میشود. |
4.3. مثال ۱
ما مثالی را از کتاب عالی «برنامهنویسی با J2EE» که توسط Wrox منتشر شده و توسط Eyrolles توزیع شده است، ارائه میدهیم. این کتاب گنجینهای از اطلاعات سطح بالا برای توسعهدهندگان راهحلهای وب جاوا است. برنامهای که در این کتاب به صورت یک سروِلِت واحد جاوا ارائه شده است، در اینجا به عنوان یک سروِلِت اصلی بازتولید شده است که برای نمایش پاسخهای مختلف ممکن به کلاینت، صفحات JSP را فراخوانی میکند.
این برنامه «sessions» نام دارد و در فایل <tomcat>\conf\server.xml به شرح زیر پیکربندی شده است:
در پوشه docBase که در بالا ذکر شد، عناصر زیر یافت میشوند:

فایلهای erreur.jsp، invalide.jsp و valide.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' نمایش داده میشود |
اجزای برنامه «sessions» به شرح زیر است:
سرولیت اصلی – درخواست مشتری را تحلیل میکند:
| |
| |
هنگامی که کاربر جلسهٔ جاری را باطل کرده باشد نمایش داده میشود. سپس پیشنهاد ایجاد یک جلسهٔ جدید را میدهد. | |
هنگامی که سروِلت اصلی در حین راهاندازی با خطاهایی مواجه میشود، نمایش داده میشود. |
سرولیت اصلی cycledevie به شرح زیر است:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class cycledevie extends HttpServlet{
//متغیرهای نمونه
String msgErreur=null;
String urlSessionInvalide=null;
String urlSessionValide=null;
String urlErreur=null;
//-------- GET
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
// آیا فرایند اولیهسازی با موفقیت انجام شد؟
if(msgErreur!=null){
//کنترل به صفحهٔ خطا واگذار میشود
getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
}
// بازیابی جلسهٔ جاری
HttpSession session=request.getSession();
// در حال تجزیه و تحلیل اقدامی است که باید انجام شود
String action=request.getParameter("action");
// اعتبار جلسهٔ جاری را لغو میکند
if(action!=null && action.equals("invalider")){
// اعتبار جلسهٔ جاری را باطل میکند
session.invalidate();
//کنترل به URL urlSessionInvalide واگذار میشود
getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
}
// سایر موارد
//کنترل به URL urlSessionInvalide منتقل میشود
getServletContext().getRequestDispatcher(urlSessionValide).forward(request,response);
}
//-------- POST
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
doGet(request,response);
}
//-------- INIT
public void init(){
//در حال بازیابی پارامترهای راهاندازی
ServletConfig config=getServletConfig();
urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
urlSessionValide=config.getInitParameter("urlSessionValide");
urlErreur=config.getInitParameter("urlErreur");
//پارامترها OK؟
if(urlSessionValide==null || urlSessionInvalide==null){
msgErreur="Configuration incorrecte";
}
}
}
نکات زیر باید مورد توجه قرار گیرند:
- در متد инициализация، سرولت سه پارامتر خود را بازیابی میکند
- در طول پردازش (doGet) یک درخواست، سرولت:
- ابتدا بررسی میکند که در حین راهاندازی هیچ خطایی رخ نداده باشد. اگر خطایی وجود داشته باشد، کنترل را به صفحه erreur.jsp میسپارد.
- مقدار پارامتر action را بررسی میکند. اگر این پارامتر مقدار «invalider» را داشته باشد، سرولت کنترل را به صفحه invalide.jsp میسپارد؛ در غیر این صورت، کنترل را به صفحه valide.jsp میسپارد.
صفحه JSP valide.jsp، که ویژگیهای جلسهٔ جاری را نمایش میدهد:
<%@ page import="java.util.*" %>
<%
//jspService
// در اینجا ما در سناریویی هستیم که باید جلسهٔ جاری را توصیف کنیم
String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
%>
<!-- آغاز صفحه 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)، که قبلاً با آنها مواجه شدهایم. دو لینک موجود در صفحه به سرولت cycledevie که قبلاً توضیح داده شد، اشاره میکنند:
<br><a href="/sessions/cycledevie?action=invalider">Invalider la session</a>
<br><a href="/sessions/cycledevie">Recharger la page</a>
لینک باطلسازی جلسه شامل پارامتر action=invalider است که به سروِلِت cycledevie امکان میدهد تشخیص دهد کاربر مایل به باطلسازی جلسهٔ جاری است. لینک دیگر صفحه را دوباره بارگذاری میکند. برای جلوگیری از بارگیری صفحه از کش توسط مرورگر، دستور HTML:
استفاده شده است. این دستور به مرورگر میگوید که برای صفحهای که دریافت میکند از کش استفاده نکند.
صفحه invalide.jsp به شرح زیر است:
<!-- آغاز صفحه 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 بدون پارامتر action اشاره میکند. این لینک باعث میشود سروِلِت cycledevie یک جلسهٔ جدید ایجاد کند.
صفحه erreur.jsp به شرح زیر است:
<%
//jspService
// در اینجا ما در حالتی هستیم که باید جلسهٔ جاری را توصیف کنیم
String msgErreur= request.getAttribute("msgErreur");
if(msgErreur==null) msgErreur="Erreur non identifiée)";
%>
<!-- آغاز صفحه 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>
نقش آن نمایش پیام خطایی است که توسط سرولت cycledevie برایش ارسال شده است. اکنون به چند مثال از نحوه عملکرد آن میپردازیم. سرولت برای اولین بار فراخوانی میشود:

صفحه بالا نشان میدهد که ما در یک جلسه جدید هستیم. ما از لینک «بارگذاری مجدد صفحه» استفاده میکنیم:

نتیجه قبلی نشان میدهد که ما همچنان در همان جلسه قبلی (همان ID) هستیم. توجه کنید که زمان آخرین دسترسی به این جلسه تغییر کرده است. اکنون بیایید از لینک «لغو جلسه» استفاده کنیم:

به URL در این صفحهٔ جدید توجه کنید که پارامتر action=invalider را دارد. بیایید از لینک «ایجاد یک جلسهٔ جدید» برای ایجاد یک جلسهٔ جدید استفاده کنیم:

میتوانیم ببینیم که یک جلسه جدید آغاز شده است. در مثالهای قبلی، جلسه به استفاده از کوکیها متکی بود. اکنون بیایید کوکیها را در مرورگر خود غیرفعال کرده و آزمایشها را تکرار کنیم. مثالهای زیر با استفاده از Netscape Communicator انجام شدهاند. به دلیلی نامشخص، تستهایی که با IE6 انجام شدند، نتایج غیرمنتظرهای داشتند، گویی IE6 هنوز از کوکیها استفاده میکرد، حتی با وجود اینکه کوکیها غیرفعال شده بودند. سرولت cycledevie برای اولین بار درخواست میشود:

اکنون از لینک «بارگذاری مجدد صفحه» استفاده میکنیم:

دو چیز قابل مشاهده است:
- ID مربوط به جلسه تغییر کرده است
- سرولت جلسه را بهعنوان یک جلسهٔ جدید تشخیص میدهد
سرور Tomcat برای کاربرانی که کوکیها را در مرورگر خود غیرفعال کردهاند، راهحلی ارائه میدهد. این سرور برای پیادهسازی توکنی که در ابتدای این پاراگراف به آن اشاره شد، از دو مکانیزم استفاده میکند: کوکیها و بازنویسی URL. اگر کوکی جلسه در دسترس نباشد، تلاش میکند تا توکن را از URL درخواستشده توسط کلاینت بازیابی کند. برای اینکه این کار انجام شود، درخواست باید حاوی توکن باشد. به طور کلی، تمام لینکهای ایجاد شده در یک سند HTML که به وباپلیکیشن اشاره میکنند، باید حاوی توکن اپلیکیشن باشند. این کار را میتوان با استفاده از متد encodeURL انجام داد:
توکن جلسهٔ جاری را به URL که بهعنوان پارامتر در فرم URL;jsessionid=xxxx ارسال میشود، اضافه میکند. |
ما برنامهٔ خود را به شرح زیر اصلاح میکنیم:
- در servlet cycledevie.java، مقادیر URL به صورت زیر رمزگذاری میشوند:
// ما کنترل را به صفحهٔ خطا واگذار میکنیم
getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
// ما کنترل را به URL urlSessionInvalide واگذار میکنیم
getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
// به URL urlSessionInvalide هدایت میکند
getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
- در صفحه valide.jsp، کدهای URL به شرح زیر رمزگذاری شدهاند:
<%
//jspService
// در اینجا با موردی سروکار داریم که نیاز است جلسهٔ جاری را توصیف کنیم
String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
//رمزگذاری URL چرخهٔ توسعه
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 – جلسهٔ جاری باطل شده است
session.invalidate();
//رمزگذاری URL چرخهٔ توسعه
String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>
..........
<a href="<%= URLcycledevie %>">Créer une nouvelle session</a>
ما اکنون آمادهایم تا آزمایشها را انجام دهیم. ما از Netscape 4.5 استفاده میکنیم و کوکیها غیرفعال شدهاند. ما یک درخواست اولیه به servlet cycledevie ارسال میکنیم:

و صفحه را با استفاده از لینک «بارگذاری مجدد صفحه» دوباره بارگذاری میکنیم:

میتوانیم ببینیم که:
- سشن تغییر نکرده است (هنوز ID)
- URL از servlet cycledevie در واقع حاوی توکن است، همانطور که در فیلد Adresse در بالا نشان داده شده است
- بنابراین سرور Tomcat توکن جلسه را از URL درخواستی بازیابی میکند (به شرطی که توسعهدهنده به رمزگذاری آن توجه کرده باشد).
4.4. مثال ۲
اکنون مثالی را ارائه میدهیم که نشان میدهد چگونه میتوان اطلاعات را در جلسهٔ مشتری ذخیره کرد. در اینجا تنها اطلاعات یک شمارنده خواهد بود که هر بار کاربر متد URL را از سروِلت فراخوانی میکند، مقدار آن افزایش مییابد. وقتی این متد برای اولین بار فراخوانی شود، صفحهٔ زیر نمایش داده میشود:

اگر روی لینک «بارگذاری مجدد صفحه» در بالا کلیک کنید، صفحه جدید زیر را مشاهده خواهید کرد:

این برنامه سه مؤلفه دارد:
- یک سرولت که درخواست مشتری را پردازش میکند
- یک صفحه 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>
- سرولت «compteur» (servlet-name) نام دارد و به فایل کلاس compteur.class (servlet-class) متصل است
- این سرولت دارای دو پارامتر راهاندازی است:
- urlAffichageCompteur: URL از صفحه JSP که شمارنده را نمایش میدهد
- urlErreur: URL از صفحه JSP که هرگونه خطا را نمایش میدهد
- و یک نام مستعار /counter، به این معنی که از طریق URL http://localhost:8080/sessions/compteur فراخوانی خواهد شد
سرولت compteur.java به شرح زیر است:
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class compteur extends HttpServlet{
// متغیرهای نمونه
String msgErreur=null;
String urlAffichageCompteur=null;
String urlErreur=null;
//-------- GET
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
// آیا فرایند راهاندازی بهخوبی انجام شد؟
if(msgErreur!=null){
//کنترل به صفحهٔ خطا منتقل میشود
getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
}
// بازیابی جلسهٔ جاری
HttpSession session=request.getSession();
// و شمارنده
String compteur=(String)session.getAttribute("compteur");
if(compteur==null) compteur="0";
//افزایش شمارنده
try{
compteur=""+(Integer.parseInt(compteur)+1);
}catch(Exception ex){}
// ذخیرهٔ شمارنده در جلسه
session.setAttribute("compteur",compteur);
// و در درخواست
request.setAttribute("compteur",compteur);
//کنترل به 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(){
//پارامترهای راهاندازی را بازیابی میکند
ServletConfig config=getServletConfig();
urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
urlErreur=config.getInitParameter("urlErreur");
//پارامترها OK؟
if(urlAffichageCompteur==null){
msgErreur="Configuration incorrecte";
}
}
}
این سرولت ساختار مشابهی با سرولتهایی دارد که قبلاً با آنها مواجه شدهایم. ما صرفاً به نحوهٔ مدیریت شمارنده اشاره میکنیم:
- سشن از طریق request.getSession() بازیابی میشود
- شمارنده از این جلسه از طریق session.getAttribute("counter") بازیابی میشود
- اگر مقداری از null بازیابی شود، به این معنی است که جلسه به تازگی شروع شده است. سپس شمارنده روی 0 تنظیم میشود.
- شمارنده افزایش مییابد، به جلسه بازگردانده میشود (session.setAttribute("counter", counter)) و در درخواستِ ارسالشده به سرولت نمایش (request.setAttribute("counter", counter)) گنجانده میشود.
صفحه نمایش compteur.jsp به شرح زیر است:
<%
// jspService
// بازیابی شمارنده
String compteur= (String) request.getAttribute("compteur");
if(compteur==null) compteur="inconnu";
%>
<!-- شروع صفحه 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>
صفحهٔ بالا صرفاً ویژگی compteur (request.getAttribute("counter")) را که توسط سرولت اصلی به آن ارسال شده است، بازیابی کرده و نمایش میدهد.
صفحه خطای erreurcompteur.jsp به شرح زیر است:
<%
// jspService
// یک خطا رخ داده است
String msgErreur= request.getAttribute("msgErreur");
if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- بالای صفحه 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. مثال ۳
ما پیشنهاد میکنیم یک برنامهٔ جاوا بنویسیم که بهعنوان کلاینت برای برنامهٔ قبلی compteur عمل کند. این برنامه آن را N بار متوالی فراخوانی میکند، که در آن N بهعنوان پارامتر ارسال میشود. هدف ما نمایش یک کلاینت وب برنامهنویسیشده و نحوهٔ کار با کوکیها است. نقطهٔ شروع ما یک کلاینت وب عمومی است که در جزوهٔ جاوا توسط همان نویسنده ارائه شده است. این کلاینت به صورت زیر فراخوانی میشود:
clientweb 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/counter 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
<!-- بالای صفحه 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 هر چیزی را که از سرور دریافت میکند نمایش میدهد. در بالا دستور Set-cookie HTTP نشان داده شده است که سرور برای ارسال یک کوکی به کلاینت خود از آن استفاده میکند. در اینجا کوکی شامل دو بخش اطلاعات است:
- JSESSIONID که توکن جلسه است
- مسیر (Path)، که دامنهای را تعریف میکند که کوکی به آن تعلق دارد. Path=/sessions به مرورگر دستور میدهد که هر بار یک URL را که با /sessions شروع میشود درخواست میکند، کوکی را به سمت سرور ارسال کند. در برنامه sessions، از سرولتهای مختلفی از جمله سرولتهای /sessions/cycledevie و /sessions/compteur استفاده کردهایم. اگر سرولت /sessions/cycledevie فراخوانی شود، مرورگر یک توکن J دریافت خواهد کرد. اگر با استفاده از همان مرورگر، سپس اگر servlet /sessions/compteur را فراخوانی کنیم، مرورگر توکن J را به سرور بازمیفرستد، زیرا این توکن برای تمام servletهای URL که با /sessions شروع میشوند، اعمال میشود. در مثال ما، سرولتهای cycledevie و compteur نیازی به اشتراکگذاری یک توکن جلسه (session token) یکسان ندارند. بنابراین، آنها نباید در یک برنامه وب یکسان قرار میگرفتند. این یک نکته مهم است که باید به خاطر بسپارید: تمام سرولتها در یک برنامه یکسان، توکن جلسه یکسانی را به اشتراک میگذارند.
- همچنین میتوان یک کوکی را با زمان انقضا تنظیم کرد. در این مورد، این اطلاعات وجود ندارد. بنابراین کوکی هنگام بستن مرورگر حذف خواهد شد. برای مثال، یک کوکی میتواند زمان انقضای N روزه داشته باشد. تا زمانی که معتبر است، مرورگر هر بار که به یکی از صفحات URL در دامنه آن (Path) دسترسی پیدا میکند، آن را بازمیفرستد. بیایید یک فروشگاه آنلاین CD را در نظر بگیریم. این فروشگاه میتواند تاریخچه مرور مشتری را در کاتالوگ خود ردیابی کرده و به تدریج ترجیحات او را شناسایی کند: برای مثال، موسیقی کلاسیک. این ترجیحات میتوانند در یک کوکی با ماندگاری سه ماهه ذخیره شوند. اگر همان مشتری پس از یک ماه به سایت بازگردد، مرورگر کوکی را به سمت برنامه سرور ارسال میکند. برنامه سرور بر اساس اطلاعات موجود در کوکی، میتواند صفحات تولید شده را متناسب با ترجیحات مشتری تنظیم کند.
کد کلاینت وب در ادامه آمده است. این کد بعداً بهعنوان نقطهٔ شروع برای کلاینت دیگری عمل خواهد کرد.
//بستههای وارداتی
import java.io.*;
import java.net.*;
public class clientweb{
//درخواست یک URL میکند
//محتویات خود را روی صفحه نمایش میدهد
public static void main(String[] args){
// دستور زبان
final String syntaxe="pg URI GET/HEAD";
// تعداد آرگومانها
if(args.length != 2)
erreur(syntaxe,1);
// URI درخواستی ثبت شد
String URLString=args[0];
String commande=args[1].toUpperCase();
//اعتبار URI را بررسی میکند
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI نادرست است
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//catch
// تأیید سفارش
if(! commande.equals("GET") && ! commande.equals("HEAD")){
// سفارش نادرست
erreur("Le second paramètre doit être GET ou HEAD",3);
}
//استخراج اطلاعات مرتبط از 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();
// میتوانیم ادامه دهیم
Socket client=null; //مشتری
BufferedReader IN=null; //جریان خواندن مشتری
PrintWriter OUT=null; //جریان نوشتن مشتری
String réponse=null; //پاسخ سرور
try{
// اتصال به سرور
client=new Socket(host,port);
// ایجاد جریانهای ورودی-خروجی کلاینت TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// درخواست URL – ارسال سربرگها HTTP
OUT.println(commande + " " + path + query + " HTTP/1.1");
OUT.println("Host: " + host + ":" + port);
OUT.println("Connection: close");
OUT.println();
//پاسخ را بخوانید
while((réponse=IN.readLine())!=null){
// پردازش پاسخ
System.out.println(réponse);
}//در حالی که
// انجام شد
client.close();
} catch(Exception e){
// استثنا را مدیریت کنید
erreur(e.getMessage(),4);
}//catch
}//اصلی
// نمایش خطاها
public static void erreur(String msg, int exitCode){
// نمایش خطا
System.err.println(msg);
//خاتمه با خطا
System.exit(exitCode);
}//
}//کلاس
اکنون برنامه clientCompteur را ایجاد میکنیم که به شرح زیر فراخوانی میشود:
clientCompteur URL N [JSESSIONID]
- URL: URL سرویسلت شمارنده
- 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/counter 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
در اینجا میبینیم که به محض اینکه کلاینت اولین درخواست خود را ارسال میکند، سرور یک کوکی جلسه (session cookie) معتبر دریافت میکند. توجه به این نکته مهم است که برای Tomcat، حداکثر زمان عدم فعالیت جلسه به طور پیشفرض ۲۰ دقیقه است (که در واقع قابل پیکربندی است). اگر درخواست دوم برنامه کوکی دریافتشده در طول درخواست اول را به سرعت کافی ارسال کند، سرور این را به عنوان همان جلسه در نظر میگیرد. این موضوع یک آسیبپذیری امنیتی بالقوه را برجسته میکند. اگر بتوانم یک توکن جلسه را در شبکه رهگیری کنم، میتوانم خود را به جای کاربری که جلسه را آغاز کرده است، جا بزنم. در مثال ما، فراخوانی اول نماینده کاربری است که جلسه را آغاز میکند (شاید با نام کاربری و رمز عبوری که به او حق دریافت توکن را میدهد)، و فراخوانی دوم نماینده کاربری است که توکن جلسه را از فراخوانی اول «هک» کرده است. اگر تراکنش در حال انجام یک تراکنش بانکی باشد، این موضوع میتواند بسیار مشکلساز شود...
کد کلاینت به شرح زیر است:
//بستههای وارداتی
import java.io.*;
import java.net.*;
import java.util.regex.*;
public class clientCompteur{
// درخواست URL
//محتویات خود را روی صفحه نمایش میدهد
public static void main(String[] args){
// سینتکس
final String syntaxe="pg URL-COMPTEUR N [JSESSIONID]";
// تعداد آرگومانها
if(args.length !=2 && args.length != 3)
erreur(syntaxe,1);
// URL درخواستی ثبت شد
String URLString=args[0];
//اعتبار URL را بررسی میکند
URL url=null;
try{
url=new URL(URLString);
}catch (Exception ex){
// URI نادرست است
erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
}//catch
//بررسی تعداد فراخوانیها N
int N=0;
try{
N=Integer.parseInt(args[1]);
if(N<=0) throw new Exception();
}catch(Exception ex){
// پارامتر N نادرست
erreur("Le nombre d'appels N doit être un entier >0",3);
}
//آیا توکن JSESSIONID بهعنوان پارامتر ارسال شده است؟
String JSESSIONID="";
if (args.length==3) JSESSIONID=args[2];
//استخراج اطلاعات مرتبط از 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();
// میتوانیم ادامه دهیم
Socket client=null; // کلاینت
BufferedReader IN=null; //جریان خواندن مشتری
PrintWriter OUT=null; //جریان نوشتن مشتری
String réponse=null; // پاسخ سرور
// قالبی که در سربرگها جستجو میشود HTTP
Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
//الگوی جستوجو شده در کد HTML
Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
// نتیجهٔ مقایسه با الگو
Matcher résultat=null;
//یک مقدار بولی که نتیجه جستجوی شمارنده را نشان میدهد
boolean compteurTrouvé;
try{
// N تماس با سرور برقرار میشود
for(int i=0;i<N;i++){
//اتصال به سرور
client=new Socket(host,port);
// جریانهای ورودی و خروجی مشتری ایجاد میشوند TCP
IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
OUT=new PrintWriter(client.getOutputStream(),true);
// درخواست برای URL – ارسال سربرگها 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,"");
// پاسخ را تا انتهای هدرها میخواند و به دنبال هرگونه کوکی میگردد
while((réponse=IN.readLine())!=null){
// پاسخ دنبال شد
System.out.println(réponse);
// خط خالی؟
if(réponse.equals("")) break;
// خط غیرخالی HTTP
// اگر توکن جلسه موجود نباشد، آن را جستجو کن
if (JSESSIONID.equals("")){
// خط را با قالب کوکی HTTP مقایسه کنید
résultat=modèleCookie.matcher(réponse);
if(résultat.find()){
//کوکی پیدا شده است
JSESSIONID=résultat.group(1);
}
}
}//در حالی که
// سربرگهای HTTP پایان یافتهاند – به کد HTML بروید
compteurTrouvé=false;
while((réponse=IN.readLine())!=null){
//آیا خط فعلی حاوی شمارنده است؟
if (! compteurTrouvé){
résultat=modèleCompteur.matcher(réponse);
if(résultat.find()){
//شمارشگر پیدا شد – آن را نمایش دهید
System.out.println("compteur : " + résultat.group(1));
compteurTrouvé=true;
}
}
}//در حالی که
//کار تمام شد
client.close();
}//برای
} catch(Exception e){
// پردازش استثنا
erreur(e.getMessage(),4);
}//catch
}//اصلی
// نمایش خطاها
public static void erreur(String msg, int exitCode){
// نمایش خطا
System.err.println(msg);
//خاتمه با خطا
System.exit(exitCode);
}//خطا
// نظارت بر تبادلهای کلاینت-سرور
public static void envoie(PrintWriter OUT,String msg){
// ارسال پیام به سرور
OUT.println(msg);
// صفحهٔ مانیتور
System.out.println("--> "+msg);
}//
}//
بیایید نکات کلیدی این برنامه را بررسی کنیم:
- ما باید N تبادل کلاینت-سرور را انجام دهیم. به همین دلیل اینها در یک حلقه قرار دارند.
- در هر تبادل، کلاینت یک اتصال TCP-IP با سرور برقرار میکند. پس از برقراری اتصال، سرآیندهای HTTP درخواست خود را به سرور ارسال میکند:
//درخواست URL – ارسال سربرگها 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 در این پاسخ را برای یافتن یک کوکی احتمالی بررسی میکند. برای یافتن آن، خطوط دریافتی را با عبارت منظم کوکی مقایسه میکند:
// جستجو برای الگو در هدرها HTTP
Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
//پاسخ را تا انتهای هدرها میخواند و به دنبال هرگونه کوکی میگردد
while((réponse=IN.readLine())!=null){
// پاسخ دنبال میشود
System.out.println(réponse);
// خط خالی؟
if(réponse.equals("")) break;
// خط غیرخالی HTTP
// اگر توکن جلسه موجود نباشد، آن را جستجو کن
if (JSESSIONID.equals("")){
// خط را با قالب کوکی HTTP مقایسه کنید
résultat=modèleCookie.matcher(réponse);
if(résultat.find()){
//کوکی پیدا شده است
JSESSIONID=résultat.group(1);
}
}
}//در حالی که
- پس از اینکه توکن برای اولین بار یافت شد، در فراخوانیهای بعدی به سرور دیگر جستجو نخواهد شد. پس از پردازش سربرگهای HTTP در پاسخ، به کد HTML در همان پاسخ میرویم. در این کد، ما به دنبال خطی میگردیم که مقدار شمارنده را میدهد. این جستجو نیز با استفاده از یک عبارت منظم انجام میشود:
//الگوی شمارنده در کد HTML جستجو میشود
Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
// برای سربرگهای HTTP همین بود – حالا به کد HTML میرویم
compteurTrouvé=false;
while((réponse=IN.readLine())!=null){
//آیا خط فعلی حاوی شمارنده است؟
if (! compteurTrouvé){
résultat=modèleCompteur.matcher(réponse);
if(résultat.find()){
// شمارنده پیدا شد – آن را نمایش دهید
System.out.println("compteur : " + résultat.group(1));
compteurTrouvé=true;
}
}
}//در حالی که
4.6. مثال ۴
در مثال قبلی، کلاینت وب توکن را به شکل یک کوکی بازمیگرداند. ما دیدیم که میتواند آن را در خود URL درخواستشده نیز بازگرداند، به شکل URL;jsessionid=xxx. بیایید این را بررسی کنیم. برنامه clientCompteur.java به clientCompteur2.java تبدیل شده و به شکل زیر اصلاح میشود:
....
//در حال درخواست UR – ارسال هدرها 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,"");
....
بنابراین کلاینت از طریق GET URL;jsessionid=xx HTTP/1.1، URL را از کانتر درخواست میکند و دیگر کوکی ارسال نمیکند. این تنها تغییر است. در اینجا نتایج یک درخواست اولیه آمده است:
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. مثال ۵
این مثال یک برنامه کاربردی را نشان میدهد که شامل سه صفحه است که ما آنها را page0، page1 و page2 مینامیم. کاربر باید به این ترتیب به آنها دسترسی پیدا کند:
- page0 یک فرم است که اطلاعاتی را درخواست میکند: یک نام
- page1 فرمى است که در پاسخ به ارسال فرم در صفحه 0 نمایش داده مى شود. این فرم یک اطلاعات دوم را درخواست مى کند: یک سن
- page2 یک سند، HTML، است که نام دریافتشده از page0 و سن دریافتشده از page1 را نمایش میدهد.
در اینجا سه تبادل کلاینت-سرور وجود دارد:
- در اولین تبادل، فرم page0 توسط کلاینت درخواست شده و توسط سرور ارسال میشود
- در تبادل دوم، فرم page0 توسط کلاینت درخواست شده و توسط سرور ارسال میشود. کلاینت نام را برای سرور ارسال میکند.
- در سومین تبادل، سند صفحه ۳ توسط کلاینت درخواست شده و توسط سرور ارسال میشود. کلاینت سن را برای سرور ارسال میکند. سند page3 باید نام و سن را نمایش دهد. نام در طول تبادل دوم توسط سرور دریافت شده و از آن زمان «فراموش» شده است. یک جلسه (session) برای ذخیره نام در طول تبادل ۲ استفاده میشود تا در طول تبادل ۳ در دسترس باشد.
صفحه 0 که در تبادل اول به دست آمده به شرح زیر است:

ما فیلد نام را پر میکنیم:

ما دکمه Suite را کلیک میکنیم و سپس صفحه 1 زیر به ما نمایش داده میشود:

میدان سن را پر کنید:

ما روی دکمه Suite کلیک میکنیم و سپس به صفحه 2 زیر هدایت میشویم:

وقتی صفحه0 به سرور ارسال میشود، اگر فیلد نام خالی باشد، سرور ممکن است کد خطا بازگرداند:

وقتی صفحه page1 به سرور ارسال میشود، اگر age نامعتبر باشد، سرور ممکن است آن را با یک کد خطا بازگرداند:

برنامه از یک سرولت و چهار صفحه تشکیل شده است JSP:
صفحه 0 را نمایش میدهد | |
صفحهٔ 1 را نمایش میدهد | |
صفحه ۲ را نمایش میدهد | |
صفحه خطا را نمایش میدهد |
برنامهٔ وب با نام suitedepages پیکربندی شده و در فایل Tomcat به نام server.xml به شرح زیر تنظیم شده است:
فایل پیکربندی 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' نام دارد و به لطف نام مستعارش (نقشهبرداری سرولت)، میتوان از طریق 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{
//متغیرهای نمونه
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{
// آیا inicialization موفق بود؟
if(msgErreur!=null){
//کنترل به صفحهٔ خطا واگذار میشود
getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
}
// بازیابی پارامتر مرحله
String étape=request.getParameter("etape");
// بازیابی جلسهٔ جاری
HttpSession session=request.getSession();
// پردازش گام فعلی
if(étape==null) étape0(request,response,session);
if(étape.equals("1")) étape1(request,response,session);
if(étape.equals("2")) étape2(request,response,session);
// سایر موارد نامعتبر هستند
étape0(request,response,session);
}
//-------- POST
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException{
doGet(request,response);
}
//-------- INIT
public void init(){
//در حال بازیابی پارامترهای راهاندازی
ServletConfig config=getServletConfig();
urlPage0=config.getInitParameter("urlPage0");
urlPage1=config.getInitParameter("urlPage1");
urlPage2=config.getInitParameter("urlPage2");
urlErreur=config.getInitParameter("urlErreur");
//پارامترها OK؟
if(urlPage0==null || urlPage1==null || urlPage2==null){
msgErreur="Configuration incorrecte";
}
}
//-------- مرحله 0
public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
throws IOException, ServletException{
// تنظیم چند ویژگی
request.setAttribute("nom","");
//نمایش صفحه 0
request.getRequestDispatcher(urlPage0).forward(request,response);
}
//-------- مرحله ۱
public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
throws IOException, ServletException{
//بازیابی نام از پرسوجو
String nom=request.getParameter("nom");
// نام تنظیم شد؟
if(nom==null) étape0(request,response,session);
// حذف هرگونه فاصله از نام
nom=nom.trim();
// آن را در یک ویژگی پرسوجو قرار دهید
request.setAttribute("nom",nom);
//آیا نام خالی است؟
if(nom.equals("")){
// این یک خطا است
ArrayList erreurs=new ArrayList();
erreurs.add("Nous n'avez pas indiqué de nom");
//خطاها در پرسوجو گنجانده میشوند
request.setAttribute("erreurs",erreurs);
//بازگشت به صفحه 0
étape0(request,response,session);
}
// نام معتبر – آن را در جلسهٔ جاری ذخیره کنید
session.setAttribute("nom",nom);
// ویژگی 'age' را در درخواست تنظیم کنید
request.setAttribute("age","");
// نمایش صفحهٔ 1
request.getRequestDispatcher(urlPage1).forward(request,response);
}
//-------- مرحله ۲
public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
throws IOException, ServletException{
// استخراج نام از جلسه
String nom=(String)session.getAttribute("nom");
//آیا نام تنظیم شده است؟
if(nom==null) étape0(request,response,session);
// ما آن را در یک ویژگی پرسوجو قرار میدهیم
request.setAttribute("nom",nom);
// استخراج سن از پرسوجو
String age=request.getParameter("age");
// سن تنظیم شد؟
if(age==null){
//بازگشت به صفحهٔ ۱
request.setAttribute("age","");
request.getRequestDispatcher(urlPage1).forward(request,response);
}
// سن در پرسوجو ذخیره میشود
age=age.trim();
request.setAttribute("age",age);
//آیا سن معتبر است؟
if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
// این یک خطا است
ArrayList erreurs=new ArrayList();
erreurs.add("Age invalide");
//خطاها در پرسوجو گنجانده شدهاند
request.setAttribute("erreurs",erreurs);
//بازگشت به صفحهٔ ۱
request.getRequestDispatcher(urlPage1).forward(request,response);
}
// سن معتبر – نمایش صفحه ۲
request.getRequestDispatcher(urlPage2).forward(request,response);
}
}
- روش init چهار پارامتر راهاندازی را بازیابی میکند و در صورت وجود هر یک از آنها پیغام خطا را تنظیم میکند
- ما دیدهایم که درخواست شامل سه تبادل است. برای ردیابی پیشرفت در این مراحل، فرمهای page0 و page1 شامل یک متغیر پنهان etape هستند که روی 1 (page0) یا 2 (page1). این عدد میتواند در اینجا بهعنوان شماره صفحه بعدی که باید نمایش داده شود در نظر گرفته شود. در روش doGet، این پارامتر از درخواست بازیابی میشود و بسته به مقدار آن، پردازش به سه روش دیگر واگذار میشود:
- étape0 درخواست اولیه را پردازش میکند و page0 را فراخوانی میکند
- étape1 فرم را از page0 پردازش میکند و page1 را فراخوانی میکند یا، در صورت رخ دادن خطا، دوباره page0 را فراخوانی میکند
- مرحلهٔ ۲ فرم page1 را پردازش میکند و page2 را ارسال میکند یا در صورت وقوع خطا، دوباره page1 را ارسال میکند.
- مرحله ۰
- page0 را با نام خالی نمایش میدهد
- مرحله ۱
- پارامتر nom را از فرم page0 بازیابی میکند.
- بررسی میکند که نام وجود دارد (null نیست). اگر اینطور نباشد، page0 دوباره نمایش داده میشود، گویی اولین فراخوانی است.
- بررسی میکند که نام خالی نباشد. اگر نباشد، page0 دوباره همراه با یک پیام خطا نمایش داده میشود.
- نام را در جلسهٔ جاری ذخیره میکند و در صورتی که نام معتبر باشد، صفحهٔ page1 را نمایش میدهد.
- مرحله ۲
- پارامتر nom را از جلسهٔ جاری بازیابی میکند.
- بررسی میکند که نام وجود دارد (null نیست). اگر اینطور نباشد، page0 دوباره نمایش داده میشود، گویی اولین فراخوانی است.
- پارامتر age را از درخواست جاری ارسالشده توسط page1 بازیابی میکند.
- بررسی میکند که سن معتبر باشد. اگر نباشد، page1 همراه با یک پیام خطا دوباره نمایش داده میشود.
- نام و سن را بهعنوان ویژگیهای پرسوجو ذخیره میکند و در صورتی که نام و سن معتبر باشند، صفحه page2 را نمایش میدهد.
صفحه page0.jsp به شرح زیر است:
<%@ page import="java.util.*" %>
<% //page0.jsp
// ویژگیهای درخواست را بازیابی کنید
String nom=(String)request.getAttribute("nom");
ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
//آیا ویژگیها معتبر هستند؟
if(nom==null){
//بازگشت به سرویسلِت اصلی
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>
<% // آیا خطایی وجود دارد؟
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) %>
<% }//برای %>
</ul>
<% }//اگر %>
</body>
</html>
- صفحه page0.jsp میتواند توسط سرولت اصلی در دو حالت فراخوانی شود:
- در طول درخواست اولیه
- پس از پردازش فرم page0، در صورتی که خطایی رخ دهد
- پارامتر nom که باید نمایش داده شود، به همراه هر فهرست خطا توسط سروِلت اصلی در اختیار آن قرار میگیرد. بنابراین سروِلت page0.jsp کار خود را با بازیابی این دو مورد اطلاعات آغاز میکند.
- فرم با فیلد مخفی etape که نشاندهنده مرحلهٔ فعلی کاربر در اپلیکیشن است، به سرولت اصلی ارسال میشود.
صفحه page1.jsp به شرح زیر است:
<%@ page import="java.util.*" %>
<% //page1.jsp
//بازیابی ویژگیهای درخواست
String nom=(String)request.getAttribute("nom");
String age=(String)request.getAttribute("age");
ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
//آیا ویژگیها معتبر هستند؟
if(nom==null || age==null){
//بازگشت به سرولت اصلی
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>
<% // آیا خطایی وجود دارد؟
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) %>
<% }//برای %>
</ul>
<% }//اگر %>
</body>
</html>
صفحه page1.jsp ساختاری مشابه با صفحه page0.jsp دارد، با این تفاوت که اکنون دو ویژگی از سرولت اصلی دریافت میکند: nom و age. در نهایت، صفحه page2.jsp به شرح زیر است:
<%
//page2.jsp
//بازیابی ویژگیهای درخواست
String nom=(String)request.getAttribute("nom");
String age=(String)request.getAttribute("age");
//آیا ویژگیها معتبر هستند؟
if(nom==null || age==null){
//بازگشت به سرولت اصلی
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 همچنین ویژگیهای nom و age را از سرولت اصلی دریافت میکند. این صفحه صرفاً آنها را نمایش میدهد. در نهایت، صفحه erreur.jsp که مسئول نمایش خطا در صورت راهاندازی نادرست سرولت است، به شرح زیر است:
<%
//jspService
// یک خطا رخ داده است
String msgErreur= request.getAttribute("msgErreur");
if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- بالای صفحه HTML -->
<html>
<head>
<title>Suite de pages</title>
</head>
<body>
<h3>Suite de pages</h3>
<hr>
Application indisponible(<%= msgErreur %>)
</body>
</html>
این ویژگی msgErreur را که توسط سرولت اصلی به آن ارسال شده است، نمایش میدهد.
در نتیجه، میتوان مشاهده کرد که در تمام مراحل سه گانهٔ برنامه، همیشه سروِلت اصلی است که ابتدا توسط مرورگر استعلام میشود. با این حال، سروِلت اصلی پاسخ نمایشی را تولید نمیکند، بلکه یکی از چهار صفحه است: JSP. کاربر از این موضوع بیخبر است، زیرا مرورگر همچنان URL – یعنی URL سرویسلت اصلی – را در نوار «آدرس» خود نمایش میدهد.
