Skip to content

4. Відстеження сеансу

4.1. Проблема

Веб-додаток може складатися з декількох обмінів формами між сервером і клієнтом. У цьому випадку відбувається наступне:

  • крок 1
    • клієнт C1 встановлює з’єднання з сервером і надсилає початковий запит.
    • сервер надсилає форму F1 клієнту C1 і закриває з’єднання, відкрите на кроці 1.
  • Крок 2
    • клієнт C1 заповнює її та надсилає назад на сервер. Для цього браузер відкриває нове з’єднання з сервером.
    • Сервер обробляє дані форми 1, обчислює на їх основі інформацію I1, надсилає форму F2 клієнту C1 і закриває з’єднання, відкрите на кроці 3.
  • Крок 3
    • Цикл етапів 3 і 4 повторюється на етапах 5 і 6. По завершенні кроку 6 сервер отримає дві форми F1 та F2 і на їх основі обчислить дані I1 та I2.

Постає таке питання: як серверу зберегти дані I1 та I2, пов’язані з клієнтом C1? Цю проблему називають відстеженням сеансу клієнта C1. Щоб зрозуміти її суть, розглянемо схему серверного додатка TCP-IP, який одночасно обслуговує декількох клієнтів:

У класичному клієнт-серверному додатку TCP-IP:

  • клієнт встановлює з’єднання із сервером
  • через нього обмінюється даними з сервером
  • з’єднання закривається одним із двох учасників

Два важливі моменти цього механізму:

  1. для кожного клієнта створюється окреме з’єднання
  2. це з'єднання використовується протягом усього діалогу сервера зі своїм клієнтом

Те, що дозволяє серверу в будь-який момент знати, з яким клієнтом він працює, — це з’єднання, або, інакше кажучи, «канал», що з’єднує його з клієнтом. Оскільки цей канал призначений для конкретного клієнта, все, що надходить по ньому, походить від цього клієнта, а все, що надсилається по ньому, доходить до клієнта.

Механізм «клієнт-сервер» HTTP цілком відповідає наведеному вище схемі, проте має таку особливість: взаємодія між клієнтом і сервером обмежується єдиним обміном даними між ними:

  • клієнт відкриває з’єднання з сервером і надсилає запит
  • сервер надає відповідь і закриває з’єднання

Якщо в момент часу T1 клієнт C надсилає запит до сервера, він отримує з’єднання C1, яке буде використовуватися для єдиного обміну запитом і відповіддю. Якщо в момент часу T2 цей самий клієнт надсилає другий запит на сервер, він отримає з’єднання C2, яке відрізняється від з’єднання C1. Для сервера при цьому немає жодної різниці між цим другим запитом користувача C та його початковим запитом: в обох випадках сервер розглядає клієнта як нового клієнта. Щоб встановити зв’язок між різними з’єднаннями клієнта C із сервером, клієнт C повинен бути «впізнаний» сервером як «постійний користувач», а сервер — отримати інформацію, яку він має про цього постійного користувача.

Уявімо собі систему управління, яка працювала б таким чином:

  • Існує єдина черга
  • Є кілька вікон. Отже, одночасно можна обслуговувати кількох клієнтів. Коли одне вікно звільняється, клієнт залишає чергу, щоб отримати обслуговування в цьому вікні
  • Якщо клієнт звертається вперше, працівник за вікном видає йому жетон із номером. Клієнт може поставити лише одне запитання. Отримавши відповідь, він повинен залишити вікно та стати в кінець черги. Працівник за вікном записує дані цього клієнта в папку з відповідним номером.
  • Коли знову настає його черга, клієнта може обслуговувати інший працівник, ніж попереднього разу. Той просить у нього жетон і дістає справу з номером, що відповідає номеру жетона. Клієнт знову звертається із запитом, отримує відповідь, і до його справи додають інформацію.
  • І так далі... Згодом клієнт отримає відповіді на всі свої запити. Відстеження взаємозв’язку між різними запитами здійснюється за допомогою жетона та пов’язаної з ним справи.

Механізм відстеження сеансу у веб-додатку «клієнт-сервер» аналогічний описаному вище:

  • під час першого запиту клієнт отримує токен від веб-сервера
  • він надаватиме цей токен при кожному наступному запиті для ідентифікації

Токен може мати різні форми:

  • це може бути приховане поле у формі
    • клієнт надсилає свій перший запит (сервер розпізнає його за тим, що клієнт не має токена)
    • сервер надсилає відповідь (форму) і розміщує токен у прихованому полі цієї форми. У цей момент з’єднання закривається (клієнт залишає сервер зі своїм токеном). Сервер, за необхідності, пов’язав з цим токеном певну інформацію.
    • клієнт надсилає другий запит, відправляючи форму. Сервер витягує з неї токен. Тоді він може обробити другий запит клієнта, маючи доступ, завдяки токену, до інформації, обчисленої під час першого запиту. До файлу, пов’язаного з токеном, додаються нові дані, клієнту надсилається друга відповідь, і з’єднання закривається вдруге. Токен знову вставляється у форму відповіді, щоб користувач міг надати його під час наступного запиту.
    • І так далі...

Головним недоліком цієї техніки є те, що токен потрібно вставляти у форму. Якщо відповідь сервера не є формою, метод прихованого поля більше не можна використовувати.

  • Метод із використанням файлу cookie
    • клієнт надсилає свій перший запит (сервер розпізнає його за тим, що клієнт не має токена)
    • сервер надсилає відповідь, додавши файл cookie до заголовків HTTP цієї відповіді. Це здійснюється за допомогою команди HTTP Set-Cookie:

Set-Cookie: param1=значення1;param2=значення2;....

де parami — це імена параметрів, а valeursi — їхні значення. Серед параметрів буде токен. Дуже часто у файлі cookie міститься лише токен, а інша інформація зберігається сервером у папці, пов’язаній із цим токеном. Браузер, який отримує файл cookie, зберігає його у файлі на диску. Після відповіді сервера з’єднання закривається (клієнт залишає сервіс зі своїм токеном).

  • (продовження)
    • клієнт надсилає другий запит на сервер. Кожного разу, коли надсилається запит на сервер, браузер перевіряє серед усіх своїх файлів cookie, чи є серед них файл, отриманий від запитуваного сервера. Якщо так, він надсилає його серверу, як і раніше, у вигляді команди HTTP — команди Cookie, синтаксис якої аналогічний синтаксису команди Set-Cookie, що використовується сервером:

Cookie: param1=значення1;param2=значення2;....

Серед параметрів, надісланих браузером, сервер знайде токен, який дозволить йому розпізнати клієнта та знайти пов’язану з ним інформацію.

Це найпоширеніший тип токена. Він має один недолік: користувач може налаштувати свій браузер так, щоб той не приймав файли cookie. У такому разі користувач не матиме доступу до веб-додатків, що використовують файли cookie.

  • переписано з URL
    • клієнт надсилає свій перший запит (сервер розпізнає його за тим, що клієнт не має токена)
    • сервер надсилає відповідь. Вона містить посилання, якими користувач повинен скористатися, щоб продовжити роботу з додатком. У URL кожного з цих посилань сервер додає токен у формі URL;token=значення.
    • коли користувач натискає на одне з посилань, щоб продовжити роботу з додатком, браузер надсилає запит на веб-сервер, вказуючи в заголовках HTTP, URL, URL;token=запитане значення. Після цього сервер може отримати токен.

4.2. Java-код API для відстеження сеансу

Тепер розглянемо основні методи, корисні для відстеження сеансу:

HttpSession [HttpServletRequest].getSession()
отримує об’єкт Session, до якого належить поточний запит. Якщо цей запит ще не був частиною сесії, то сесія створюється.
String [HttpSession].getId()
ідентифікатор поточної сесії
long [HttpSession].getCreationTime()
дата створення поточної сесії (кількість мілісекунд, що минули з 1 січня 1970 року, 0:00).
long [HttpSession].getLastAccessedTime()
дата останнього доступу клієнта до сесії
long [HttpSession].getMaxInactiveInterval()
максимальний час бездіяльності сеансу в секундах. Після закінчення цього часу сеанс вважається недійсним.
[HttpSession].setMaxInactiveInterval(int durée)
визначає у секундах максимальну тривалість бездіяльності сеансу. Після закінчення цього часу сеанс анулюється.
boolean [HttpSession].isNew()
«true», якщо сесія щойно створена
[HttpSession].setAttribute(String paramètre, Object valeur)
присвоює значення параметру в певній сесії. Саме цей механізм дозволяє зберігати інформацію, яка залишатиметься доступною протягом усієї сесії.
[HttpSession].removeAttribute(String paramètre)
видаляє parametre з даних сесії.
Object [HttpSession].getAttribute(String paramètre)
значення, пов'язане з параметром paramètre сеансу. Повертає null, якщо останній не існує.
Enumeration [HttpSession].getAttributeNames()
список у вигляді переліку всіх атрибутів поточної сесії
[HttpSession].invalidate()
завершує поточну сесію. Уся інформація, пов’язана з нею, знищується.

4.3. Приклад 1

Ми наводимо приклад, взятий із чудової книги «Програмування з J2EE», виданої видавництвом Wrox та розповсюджуваної компанією Eyrolles. Ця книга є джерелом високоякісної інформації для розробників веб-рішень на Java. Додаток, представлений у цій книзі у вигляді єдиного Java-сервлета, тут реалізовано у вигляді головного сервлета, який використовує сторінки JSP для відображення різних можливих відповідей клієнту.

Додаток називається sessions і налаштований у файлі <tomcat>\conf\server.xml наступним чином:

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

У вищезазначеній папці docBase містяться такі елементи:

Image

Файли erreur.jsp, invalide.jsp, valide.jsp — усі три пов’язані з додатком sessions. У папці WEB-INF, зазначеній вище, містяться:

Image

Вище показано файл конфігурації додатка sessions — web.xml. У папці classes міститься файл класу сервлета:

Image

Файл web.xml додатка має такий вигляд:

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

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

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

Головний сервлет називається cycledevie (servlet-name) і пов'язаний із файлом класу cycledevie.class (servlet-class). Вона має псевдонім /cycledevie (servlet-mapping), що дозволяє викликати її через URL та http://localhost:8080/sessions/cycledevie. Вона має три параметри ініціалізації:

urlSessionValide
URL-адреса сторінки, що містить характеристики поточної сесії
urlSessionInvalide
URL-адреса сторінки, що відображається після анулювання поточної сесії
urlErreur
URL-адреса сторінки, що відображається у разі помилки ініціалізації головного сервлету cycledevie

Компоненти додатка «сесії» такі:

cycledevie
головний сервлет — аналізує запит клієнта:
  • якщо запит належить до сесії, передає управління сторінці valide.jsp, яка відображає характеристики цієї сесії. З цієї сторінки користувач може:
    • перезавантажити її
    • скасувати її
  • якщо у запиті міститься прохання скасувати поточну сесію, сервлет передає управління сторінці invalide.jsp, яка запропонує користувачеві створити нову сесію
  • якщо під час ініціалізації сервлет стикається з помилками, він передає управління сторінці erreur.jsp, яка відобразить повідомлення про помилку.
valide.jsp
  • відображає характеристики поточної сесії та пропонує два посилання:
    • одне — для перезавантаження сторінки, щоб побачити, як змінюється параметр останнього доступу до поточної сесії
    • інше — для скасування поточної сесії
invalide.jsp
відображається, коли користувач скасував поточну сесію. Після цього пропонується створити нову сесію.
erreur.jsp
відображається, коли головний сервлет стикається з помилками під час ініціалізації.

Головний сервлет 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");

        // параметри в порядку?
        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>

Слід зауважити, що в рядку

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

використовується об’єкт сесії, який з’явився нізвідки. Насправді цей об’єкт є одним із неявних об’єктів, доступних для сторінок 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:

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

. Вона вказує браузеру не використовувати кеш для сторінки, яку він отримує.

Сторінка 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. Тепер розглянемо приклади виконання. Сервлета викликається вперше:

Image

На сторінці вище вказано, що ми перебуваємо в новій сесії. Використовуємо посилання «Оновити сторінку»:

Image

Попередній результат вказує, що ми все ще перебуваємо в тій самій сесії, що й на попередній сторінці (той самий ID). Зверніть увагу, що час останнього доступу до цієї сесії змінився. Тепер скористаємося посиланням «Скасувати сесію»:

Image

Зверніть увагу на URL на цій новій сторінці з параметром action=invalider. Скористаймося посиланням «Створити нову сесію», щоб створити нову сесію:

Image

Помітимо, що розпочалася нова сесія. У попередніх прикладах сесія базувалася на механізмі файлів cookie. Тепер вимкнемо використання файлів cookie у нашому браузері та повторимо тести. Наступні приклади було реалізовано за допомогою Netscape Communicator. З незрозумілих причин тести, проведені з IE6, давали несподівані результати, ніби IE6 продовжував використовувати файли cookie, хоча їх було вимкнено. Сервлет cycledevie запитується вперше:

Image

Тепер ми використовуємо посилання «Оновити сторінку»:

Image

Можна помітити дві речі:

  • ідентифікатор сесії ID змінився
  • сервлет розпізнає сесію як нову

Сервер Tomcat вирішує проблему користувачів, які блокують використання файлів cookie у своїх браузерах. Він використовує два механізми для реалізації токена, про який йшлося на початку цього абзацу: файли cookie та перезапис URL. Якщо сесійний файл cookie недоступний, сервер спробує отримати токен із запиту URL, надісланого клієнтом. Для цього URL-адреса повинна містити цей токен. Загалом, усі посилання, згенеровані в документі HTML на веб-додаток, повинні містити його токен. Це можна зробити за допомогою методу encodeURL:

String [HttpResponse].encodeURL(String URL)
додає токен поточної сесії до параметра URL у формі URL;jsessionid=xxxx

Ми модифікуємо наш додаток наступним чином:

  • у сервлеті 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, а файли cookie вимкнені. Спочатку ми звертаємося до сервлету cycledevie:

Image

і перезавантажуємо сторінку за посиланням «Перезавантажити сторінку»:

Image

Ми бачимо, що:

  • сесія не змінилася (той самий ID)
  • URL сервлета cycledevie дійсно містить токен, як показано у полі Adresse вище
  • отже, сервер Tomcat отримує токен сесії у запитуваному URL (якщо розробник подбав про його кодування).

4.4. Приклад 2

Тепер наведемо приклад, який демонструє, як зберігати інформацію в сесії клієнта. У цьому випадку єдиною інформацією буде лічильник, який збільшуватиметься щоразу, коли користувач викликатиме URL сервлета. Під час першого виклику з’являється така сторінка:

Image

Якщо натиснути на посилання «Оновити сторінку» вище, з’явиться така нова сторінка:

Image

Додаток складається з трьох компонентів:

  • сервлет, що обробляє запит клієнта
  • сторінка 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>
  • сервлет називається «лічильник» (servlet-name) і пов'язаний із файлом класу compteur.class (servlet-class)
  • вона має два параметри ініціалізації:
    • urlAffichageCompteur: URL зі сторінки JSP, на якій відображається лічильник
    • urlErreur: URL зі сторінки JSP, що відображає можливу помилку
  • та псевдонім /лічильник, завдяки якому його можна викликати через 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");

         // параметри в порядку?
        if(urlAffichageCompteur==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Цей сервлет має структуру, подібну до вже розглянутих сервлетів. Звернемо увагу лише на управління лічильником:

  • сесія отримується за допомогою request.getSession()
  • лічильник отримується в цій сесії за допомогою session.getAttribute("лічильник")
  • якщо отримується значення null, це означає, що сесія щойно розпочалася. У цьому випадку лічильник обнуляється.
  • лічильник збільшується, повертається в сесію (session.setAttribute("лічильник",лічильник)) і вставляється в запит, який буде передано сервлету відображення (request.setAttribute("лічильник",лічильник)).

Сторінка відображення 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("лічильник")) від головного сервлету та відображає його.

Сторінка помилки 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. Приклад 3

Ми пропонуємо написати Java-додаток, який буде клієнтом попереднього додатка compteur. Він буде викликати його N разів поспіль, де N передаватиметься як параметр. Наша мета — продемонструвати запрограмований веб-клієнт та спосіб обробки файлів cookie. Нашою відправною точкою стане загальний веб-клієнт, представлений у посібнику з Java того самого автора. Він викликається наступним чином:

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/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


<!-- початок сторінки 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, за допомогою якої сервер надсилає файл cookie своєму клієнту. У цьому випадку файл cookie містить дві інформації:

  • JSESSIONID — це токен сеансу
  • Path, що визначає URL, до якої належить файл cookie. Path=/sessions вказує браузеру, що він повинен повертати файл cookie серверу щоразу, коли надсилає запит на URL, що починається з /sessions. У додатку sessions ми використовували різні сервлети, зокрема сервлети /sessions/cycledevie та /sessions/compteur. Якщо викликати сервлет /sessions/cycledevie, браузер отримає токен J. Якщо за допомогою цього ж браузера потім викликати сервлет /sessions/compteur, браузер надішле серверу токен J, оскільки він стосується всіх сервлетів URL, починаючи з /sessions. У нашому прикладі сервлети cycledevie та compteur не повинні використовувати один і той самий сесійний токен. Тому їх не слід було розміщувати в одному веб-додатку. Це важливо пам’ятати: усі сервлети одного веб-додатку використовують один і той самий сесійний токен.
  • Файл cookie також може мати термін дії. У даному випадку ця інформація відсутня. Отже, файл cookie буде видалено після закриття браузера. Термін дії файлу cookie може становити, наприклад, N днів. Поки він дійсний, браузер надсилатиме його щоразу, коли буде відкрито будь-яку сторінку з домену URL (Path). Візьмемо, наприклад, інтернет-магазин CD. Він може відстежувати пересування клієнта по своєму каталогу та поступово визначати його вподобання: наприклад, класичну музику. Ці вподобання можна зберегти у файлі cookie з терміном дії 3 місяці. Якщо цей самий клієнт повернеться на сайт через місяць, браузер надішле файл cookie серверному додатку. На основі інформації, що міститься у файлі cookie, додаток зможе адаптувати згенеровані сторінки до вподобань клієнта.

Далі наведено код веб-клієнта. Згодом він стане основою для створення іншого клієнта.

// імпортовані пакети
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);
        }//виняток
         // перевірка замовлення
        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);
            }//while
             // процес завершено
            client.close();
        } catch(Exception e){
            // обробка винятку
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // виведення помилок
    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/лічильник 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, які вона отримує
  • значення лічильника після кожного виклику

Бачимо, що під час першого виклику:

  • клієнт не надсилає cookie
  • сервер надсилає його

Під час наступних звернень:

  • клієнт систематично надсилає файл cookie, який він отримав від сервера під час першого звернення. Саме це дозволить серверу впізнати його та збільшити значення лічильника.
  • сервер, у свою чергу, більше не надсилає cookie

Ми запускаємо попередню програму, передаючи вищезазначений токен як третій параметр:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/лічильник 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

Тут видно, що вже під час першого запиту клієнта сервер отримує дійсний сесійний файл cookie. Слід знати, що для Tomcat максимальний час бездіяльності сесії за замовчуванням становить 20 хвилин (насправді це можна налаштувати). Якщо під час другого запиту програма досить швидко надсилає файл cookie, отриманий під час першого запиту, для сервера це буде та сама сесія. Тут ми вказуємо на потенційну уразливість. Якщо мені вдасться перехопити в мережі сесійний токен, я зможу видати себе за того, хто ініціював цю сесію. У нашому прикладі перший виклик представляє користувача, який ініціює сесію (можливо, за допомогою логіна та пароля, що надають йому право на отримання токена), а другий виклик — користувача, який «викрав» сесійний токен першого виклику. Якщо поточна операція є банківською, це може стати дуже неприємним...

Код клієнта виглядає так:

// імпортовані пакети
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);
        }//виняток
         // перевірка кількості викликів 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,"");

                 // зчитується відповідь до кінця заголовків з пошуком можливого файлу cookie
                while((réponse=IN.readLine())!=null){
                     // аналіз відповіді
                    System.out.println(réponse);
                     // порожній рядок?
                    if(réponse.equals("")) break;
                     // рядок HTTP не порожній
                     // якщо немає токена сеансу, його шукають
                    if (JSESSIONID.equals("")){
                        // порівнюємо рядок HTTP із шаблоном файлу cookie
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // файл cookie знайдено
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while

                 // обробка заголовків 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;
                        }
                    }
                }//while
                 // все закінчено
                client.close();
            }//for
        } catch(Exception e){
            // обробляємо виняток
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // виведення помилок
    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 обмінів між клієнтом і сервером. Саме тому вони знаходяться в циклі
            for(int i=0;i<N;i++){
  • під час кожного обміну клієнт відкриває з’єднання 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 доступний, він надсилається у вигляді файлу cookie, інакше — ні.

  • Після відправлення запиту клієнт очікує відповіді від сервера. Спочатку він аналізує заголовки HTTP цієї відповіді, шукаючи можливий файл cookie. Щоб його знайти, він порівнює отримані рядки з регулярним виразом файлу cookie:
         // пошук шаблону в заголовках HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // зчитується відповідь до кінця заголовків, шукаючи можливий файл cookie
                while((réponse=IN.readLine())!=null){
                     // аналіз відповіді
                    System.out.println(réponse);
                     // порожній рядок?
                    if(réponse.equals("")) break;
                     // рядок HTTP не порожній
                     // якщо немає токена сеансу, його шукають
                    if (JSESSIONID.equals("")){
                        // порівнюємо рядок HTTP із шаблоном файлу cookie
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // файл cookie знайдено
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • коли токен буде знайдено вперше, його більше не шукатимуть під час наступних звернень до сервера. Після обробки заголовків 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;
                        }
                    }
                }//while

4.6. Приклад 4

У попередньому прикладі веб-клієнт повертає токен у вигляді файлу cookie. Ми бачили, що він також може повернути його безпосередньо в запитуваному URL у форматі URL;jsessionid=xxx. Перевіримо це. Програма clientCompteur.java перетворюється на clientCompteur2.java і змінюється наступним чином:

....
                 // запитуємо URL  надсилаємо заголовки 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 і більше не надсилає cookie. Це єдина зміна. Ось результати першого запиту:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur2 http://localhost:8080/sessions/лічильник 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.

Тут відбувається три обміни даними між клієнтом і сервером:

  • під час першого обміну клієнт запитує форму page0, яку надсилає сервер
  • під час другого обміну клієнт запитує форму page1, яку надсилає сервер. Клієнт надсилає ім’я на сервер.
  • під час третього обміну клієнт запитує документ page3, який надсилає сервер. Клієнт надсилає вік на сервер. Документ page3 має відображати ім’я та вік. Ім’я було отримано сервером під час другого обміну і з того часу «забуто». Для збереження імені під час обміну 2 використовується сесія, щоб воно було доступним під час обміну 3.

Сторінка page0, отримана під час першого обміну, має такий вигляд:

Image

Заповнюємо поле імені:

Image

Натискаємо кнопку Suite і отримуємо таку сторінку page1:

Image

Заповнюємо поле «Вік»:

Image

Натискаємо кнопку Suite і отримуємо таку сторінку page2:

Image

Коли сторінка page0 надсилається на сервер, він може повернути її з кодом помилки, якщо поле імені порожнє:

Image

Коли сторінка page1 надсилається на сервер, він може повернути її з кодом помилки, якщо вік є недійсним:

Image

Додаток складається з сервлета та чотирьох сторінок JSP:

page0.jsp
відображає сторінку «page0»
page1.jsp
відображає сторінку 1
page2.jsp
відобразити сторінку 2
erreur.jsp
відображає сторінку помилки

Веб-додаток називається suitedepages і налаштований у файлі server.xml сервера Tomcat наступним чином:

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

Файл конфігурації web.xml додатка suitedepages має такий вигляд:

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

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

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

Головний сервлет називається main, і завдяки його псевдоніму (servlet-mapping) доступ до нього здійснюється через URL http://localhost:8080/suitedepages/main. Вона має чотири параметри ініціалізації, які є URL чотирьох сторінок JSP, що використовуються для різних відображень. Код сервлету «main» такий:

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

public class main extends HttpServlet{

    // змінні екземпляра
    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{

        // чи пройшла ініціалізація успішно?
        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");

         // параметри в порядку?
        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);
    }

     //-------- крок 1
    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);
    }

     //-------- крок 2
    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){
            // повернення на сторінку 1
            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);
             // повернення на сторінку 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // вік дійсний  відображається сторінка 2
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • метод init отримує чотири параметри ініціалізації та формує повідомлення про помилку, якщо один із них відсутній
  • ми бачили, що запит складається з трьох етапів. Щоб дізнатися, на якому етапі цих обмінів ми перебуваємо, форми page0 та page1 містять приховану змінну etape, яка має значення 1 (page0) або 2 (page1). Цей номер можна розглядати як номер наступної сторінки, яку слід відобразити. У методі doGet цей параметр отримується із запиту, і залежно від його значення обробка делегується трьом іншим методам:
    • étape0 обробляє початковий запит і передає його page0
    • étape1 обробляє форму з page0 і надсилає page1 або знову page0, якщо сталася помилка
    • Етап 2 обробляє форму page1 і надсилає page2 або знову page1, якщо сталася помилка
  • етап 0
    • виводить page0 із порожнім іменем
  • крок 1
    • отримує параметр nom з форми page0.
    • перевіряє, чи ім'я існує (не null). Якщо це не так, знову відображається page0, ніби це перший виклик.
    • перевіряє, чи ім'я не порожнє. Якщо це не так, знову відображається page0 із повідомленням про помилку.
    • запам'ятовує ім'я в поточній сесії та виводить page1, якщо ім'я є дійсним.
  • крок 2
    • отримує параметр nom у поточній сесії.
    • перевіряє, чи ім’я існує (не нуль). Якщо це не так, знову виводиться 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) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>
  • Сторінка page0.jsp може бути викликана головним сервлетом у двох випадках:
    • під час початкового запиту
    • після обробки форми page0 у разі виникнення помилки
  • Параметр nom, який потрібно відобразити, передається їй головним сервлетом разом із можливим списком помилок. Отже, сервлет page0.jsp спочатку отримує ці дві інформації.
  • форма «відправляється» до головного сервлету разом із прихованим полем (hidden) 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) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </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, тобто сторінку головного сервлету.