Skip to content

4. Śledzenie sesji

4.1. Problem

Aplikacja internetowa może składać się z kilku wymian formularzy między serwerem a klientem. W takim przypadku przebieg procesu wygląda następująco:

  • krok 1
    • klient C1 nawiązuje połączenie z serwerem i wysyła swoje pierwsze żądanie.
    • serwer wysyła formularz F1 do klienta C1 i zamyka połączenie nawiązane w kroku 1.
  • Krok 2
    • Klient C1 wypełnia formularz i odsyła go do serwera. W tym celu przeglądarka nawiązuje nowe połączenie z serwerem.
    • Serwer przetwarza dane z formularza 1, oblicza na ich podstawie informacje I1, wysyła formularz F2 do klienta C1 i zamyka połączenie otwarte w kroku 3.
  • Krok 3
    • Cykl etapów 3 i 4 powtarza się w etapach 5 i 6. Po zakończeniu etapu 6 serwer otrzyma dwa formularze F1 i F2 i na ich podstawie obliczy informacje I1 oraz I2.

Pojawia się następujące pytanie: w jaki sposób serwer zachowuje informacje I1 i I2 powiązane z klientem C1? Problem ten nazywamy śledzeniem sesji klienta C1. Aby zrozumieć jego źródło, przyjrzyjmy się schematowi aplikacji serwerowej TCP-IP obsługującej jednocześnie wielu klientów:

W klasycznej aplikacji klient-serwer TCP-IP:

  • klient nawiązuje połączenie z serwerem
  • przez to połączenie wymienia dane z serwerem
  • połączenie zostaje zamknięte przez jedną ze stron

Dwa istotne elementy tego mechanizmu to:

  1. dla każdego klienta tworzone jest jedno połączenie
  2. połączenie to jest wykorzystywane przez cały czas trwania komunikacji serwera z klientem

To, co pozwala serwerowi w danym momencie rozpoznać, z którym klientem współpracuje, to połączenie, czyli inaczej mówiąc „kanał”, który łączy go z klientem. Ponieważ kanał ten jest przypisany do konkretnego klienta, wszystko, co do niego dociera, pochodzi od tego klienta, a wszystko, co jest wysyłane tym kanałem, dociera do klienta.

Mechanizm klient-serwer HTTP jest zgodny z powyższym schematem, z tą jednak różnicą, że komunikacja klient-serwer ogranicza się do pojedynczej wymiany danych między klientem a serwerem:

  • klient nawiązuje połączenie z serwerem i wysyła żądanie
  • serwer udziela odpowiedzi i zamyka połączenie

Jeśli w momencie T1 klient C wysyła żądanie do serwera, otrzymuje połączenie C1, które posłuży do jednorazowej wymiany żądania i odpowiedzi. Jeśli w momencie T2 ten sam klient wyśle drugie żądanie do serwera, otrzyma połączenie C2, które różni się od połączenia C1. Z punktu widzenia serwera nie ma zatem żadnej różnicy między tym drugim żądaniem użytkownika C a jego żądaniem początkowym: w obu przypadkach serwer traktuje klienta jako nowego klienta. Aby istniał związek między różnymi połączeniami klienta C z serwerem, klient C musi zostać „rozpoznany” przez serwer jako „stały użytkownik”, a serwer musi pobrać informacje, które posiada na temat tego stałego użytkownika.

Wyobraźmy sobie system zarządzania, który działałby w następujący sposób:

  • Istnieje jedna kolejka
  • Istnieje kilka okienek. Dzięki temu kilku klientów może być obsługiwanych jednocześnie. Gdy okienko się zwalnia, klient opuszcza kolejkę, aby zostać obsłużonym przy tym okienku
  • Jeśli klient pojawia się po raz pierwszy, osoba przy okienku wręcza mu żeton z numerem. Klient może zadać tylko jedno pytanie. Po uzyskaniu odpowiedzi musi opuścić okienko i przejść na koniec kolejki. Pracownik okienka zapisuje informacje o tym kliencie w teczce oznaczonej jego numerem.
  • Kiedy znów nadejdzie jego kolej, klient może zostać obsłużony przez innego pracownika niż poprzednio. Ten prosi go o żeton i pobiera kartotekę o numerze odpowiadającym żetonowi. Klient ponownie zgłasza prośbę, otrzymuje odpowiedź, a informacje są dodawane do jego kartoteki.
  • i tak dalej... Z biegiem czasu klient otrzyma odpowiedzi na wszystkie swoje zapytania. Śledzenie kolejnych zapytań odbywa się za pomocą żetonu i powiązanej z nim teczki.

Mechanizm śledzenia sesji w aplikacji internetowej typu klient-serwer działa analogicznie do powyższego przykładu:

  • przy pierwszym żądaniu klient otrzymuje token od serwera internetowego
  • będzie on przedstawiał ten token przy każdym kolejnym żądaniu w celu identyfikacji

Token może przybierać różne formy:

  • pola ukrytego w formularzu
    • klient wysyła pierwsze żądanie (serwer rozpoznaje go po tym, że klient nie posiada tokenu)
    • serwer wysyła odpowiedź (formularz) i umieszcza token w ukrytym polu tego formularza. W tym momencie połączenie zostaje zamknięte (klient opuszcza stronę z tokenem). Serwer ewentualnie zadbał o powiązanie informacji z tym tokenem.
    • klient wysyła drugie żądanie, odsyłając formularz. Serwer pobiera z niego token. Może wówczas przetworzyć drugie żądanie klienta, mając dostęp – dzięki tokenowi – do informacji obliczonych podczas pierwszego żądania. Do pliku powiązanego z tokenem dodawane są nowe informacje, do klienta wysyłana jest druga odpowiedź, a połączenie zostaje zamknięte po raz drugi. Token został ponownie umieszczony w formularzu odpowiedzi, aby użytkownik mógł go przedstawić podczas kolejnego żądania.
    • i tak dalej...

Główną wadą tej techniki jest to, że token musi zostać umieszczony w formularzu. Jeśli odpowiedź serwera nie jest formularzem, metoda ukrytego pola nie ma już zastosowania.

  • metoda pliku cookie
    • klient wysyła pierwsze żądanie (serwer rozpoznaje go po tym, że klient nie posiada tokenu)
    • serwer wysyła odpowiedź, dodając plik cookie do nagłówków HTTP tej odpowiedzi. Odbywa się to za pomocą polecenia HTTP Set-Cookie:

Set-Cookie: param1=wartość1;param2=wartość2;....

gdzie parami to nazwy parametrów, a valeursi to ich wartości. Wśród parametrów znajdzie się token. Bardzo często w pliku cookie znajduje się wyłącznie token, a pozostałe informacje są zapisywane przez serwer w folderze powiązanym z tokenem. Przeglądarka, która otrzymuje plik cookie, zapisuje go w pliku na dysku. Po otrzymaniu odpowiedzi od serwera połączenie zostaje zamknięte (klient opuszcza okno z tokenem).

  • (ciąg dalszy)
    • klient wysyła drugie żądanie do serwera. Za każdym razem, gdy wysyłane jest żądanie do serwera, przeglądarka sprawdza wśród wszystkich posiadanych plików cookie, czy znajduje się wśród nich plik pochodzący z serwera, do którego skierowano żądanie. Jeśli tak, wysyła je do serwera zawsze w postaci polecenia HTTP, czyli polecenia „Cookie”, którego składnia jest analogiczna do składni polecenia Set-Cookie używanego przez serwer:

Cookie: param1=wartość1;param2=wartość2;....

Wśród parametrów wysyłanych przez przeglądarkę serwer odnajdzie token, który pozwoli mu rozpoznać klienta i uzyskać powiązane z nim informacje.

Jest to najczęściej stosowana forma tokenu. Ma ona jednak jedną wadę: użytkownik może skonfigurować swoją przeglądarkę tak, aby nie akceptowała plików cookie. Taki użytkownik nie ma wówczas dostępu do aplikacji internetowych wykorzystujących pliki cookie.

  • przeróbka URL
    • klient wysyła pierwsze żądanie (serwer rozpoznaje go po tym, że klient nie posiada tokenu)
    • serwer wysyła odpowiedź. Zawiera ona linki, z których użytkownik musi skorzystać, aby kontynuować korzystanie z aplikacji. W adresie URL każdego z tych linków serwer dodaje token w postaci URL;token=wartość.
    • gdy użytkownik kliknie jeden z linków, aby kontynuować korzystanie z aplikacji, przeglądarka wysyła żądanie do serwera WWW, przekazując w nagłówkach HTTP żądany token w postaci URL URL;token=wartość. Serwer jest wówczas w stanie odzyskać token.

4.2. API w Javie do śledzenia sesji

Poniżej przedstawiamy główne metody przydatne do śledzenia sesji:

HttpSession [HttpServletRequest].getSession()
pobiera obiekt Session, do którego należy bieżące żądanie. Jeśli żądanie to nie było jeszcze częścią sesji, sesja ta zostaje utworzona.
String [HttpSession].getId()
identyfikator bieżącej sesji
long [HttpSession].getCreationTime()
data utworzenia bieżącej sesji (liczba milisekund, które upłynęły od 1 stycznia 1970 r., godz. 0:00).
long [HttpSession].getLastAccessedTime()
data ostatniego dostępu klienta do sesji
long [HttpSession].getMaxInactiveInterval()
maksymalny czas bezczynności sesji w sekundach. Po upływie tego czasu sesja zostaje unieważniona.
[HttpSession].setMaxInactiveInterval(int durée)
określa w sekundach maksymalny czas bezczynności sesji. Po upływie tego czasu sesja zostaje unieważniona.
boolean [HttpSession].isNew()
wartość „true”, jeśli sesja została właśnie utworzona
[HttpSession].setAttribute(String paramètre, Object valeur)
przypisuje wartość do parametru w danej sesji. To właśnie ten mechanizm pozwala na zapamiętanie informacji, które pozostaną dostępne przez cały czas trwania sesji.
[HttpSession].removeAttribute(String paramètre)
usuwa parametre z danych sesji.
Object [HttpSession].getAttribute(String paramètre)
wartość przypisana do parametru paramètre sesji. Zwraca null, jeśli ten ostatni nie istnieje.
Enumeration [HttpSession].getAttributeNames()
lista w postaci wyliczenia wszystkich atrybutów bieżącej sesji
[HttpSession].invalidate()
zamyka bieżącą sesję. Wszystkie informacje z nią związane są usuwane.

4.3. Przykład 1

Przedstawiamy przykład zaczerpnięty z doskonałej książki „Programowanie z wykorzystaniem J2EE”, wydanej przez wydawnictwo Wrox i dystrybuowanej przez Eyrolles. Książka ta stanowi bogate źródło informacji na wysokim poziomie dla programistów tworzących rozwiązania internetowe w języku Java. Aplikacja przedstawiona w tej książce w postaci pojedynczego serwletu Java została tutaj zaadaptowana jako główny serwlet wykorzystujący strony JSP do wyświetlania klientowi różnych możliwych odpowiedzi.

Aplikacja nosi nazwę „sessions” i jest skonfigurowana w następujący sposób w pliku <tomcat>\conf\server.xml:

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

W powyższym folderze docBase znajdują się następujące elementy:

Image

Pliki erreur.jsp, invalide.jsp, valide.jsp są powiązane z aplikacją sessions. W powyższym folderze WEB-INF znajdują się:

Image

Powyżej widoczny jest plik konfiguracyjny aplikacji „sessions” o nazwie web.xml. W folderze classes znajduje się plik klasy serwletu:

Image

Plik aplikacji o nazwie web.xml wygląda następująco:

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

Główna serwlet nosi nazwę cycledevie (servlet-name) i jest powiązana z plikiem klasy cycledevie.class (servlet-class). Posiada alias /cycledevie (servlet-mapping), który umożliwia wywołanie jej za pośrednictwem URL http://localhost:8080/sessions/cycledevie. Posiada trzy parametry inicjalizacyjne:

urlSessionValide
adres URL strony zawierającej informacje o bieżącej sesji
urlSessionInvalide
adres URL strony wyświetlanej po unieważnieniu bieżącej sesji
urlErreur
adres URL strony wyświetlanej w przypadku błędu podczas inicjalizacji głównego serwletu „cycledevie”

Komponenty aplikacji „sessions” są następujące:

cycledevie
główny serwlet – analizuje żądanie klienta:
  • jeśli żądanie to jest częścią sesji, przekazuje kontrolę do strony valide.jsp, która wyświetli charakterystykę tej sesji. Z tej strony użytkownik może:
    • odświeżyć stronę
    • unieważnić ją
  • jeśli żądanie dotyczy unieważnienia bieżącej sesji, serwlet przekazuje kontrolę do strony invalide.jsp, która zaproponuje użytkownikowi utworzenie nowej sesji
  • jeśli podczas inicjalizacji serwlet napotka błędy, przekazuje kontrolę do strony erreur.jsp, która wyświetli komunikat o błędzie.
valide.jsp
  • wyświetla charakterystykę bieżącej sesji i udostępnia dwa linki:
    • jeden do ponownego załadowania strony, co pozwala obserwować zmiany parametru ostatniego dostępu do bieżącej sesji
    • drugi – w celu unieważnienia bieżącej sesji
invalide.jsp
wyświetlana po unieważnieniu bieżącej sesji przez użytkownika. Proponuje wówczas utworzenie nowej sesji.
erreur.jsp
wyświetlany, gdy główny serwlet napotyka błędy podczas inicjalizacji.

Główny serwlet cycledevie wygląda następująco:

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

public class cycledevie extends HttpServlet{

    // zmienne instancji
    String msgErreur=null;
    String urlSessionInvalide=null;
    String urlSessionValide=null;
    String urlErreur=null;

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

        // czy inicjalizacja przebiegła pomyślnie?
        if(msgErreur!=null){
             // przekazujemy kontrolę do strony błędu
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // pobieramy bieżącą sesję
        HttpSession session=request.getSession();

         // analizujemy, jakie działanie należy wykonać
        String action=request.getParameter("action");
        // unieważnienie bieżącej sesji
        if(action!=null && action.equals("invalider")){
            // unieważnia się bieżącą sesję
            session.invalidate();
             // przekazujemy kontrolę do adresu URL urlSessionInvalide
            getServletContext().getRequestDispatcher(urlSessionInvalide).forward(request,response);
        }
         // inne przypadki
         // przekazywanie kontroli do adresu 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(){
         // pobieramy parametry inicjalizacyjne
        ServletConfig config=getServletConfig();
        urlSessionInvalide=config.getInitParameter("urlSessionInvalide");
        urlSessionValide=config.getInitParameter("urlSessionValide");
        urlErreur=config.getInitParameter("urlErreur");

        // parametry w porządku?
        if(urlSessionValide==null || urlSessionInvalide==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Należy zwrócić uwagę na następujące kwestie:

  • w swojej metodzie inicjalizacyjnej serwlet pobiera swoje trzy parametry
  • podczas przetwarzania (doGet) żądania serwlet:
    • najpierw sprawdza, czy podczas inicjalizacji nie wystąpił błąd. Jeśli tak, przekazuje kontrolę do strony erreur.jsp.
    • sprawdza wartość parametru action. Jeśli ma on wartość „invalider”, serwlet przekazuje kontrolę do strony invalide.jsp, w przeciwnym razie do strony valide.jsp.

Strona JSP valide.jsp wyświetlająca charakterystykę bieżącej sesji:

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

<%
     // jspService
   // tutaj mamy do czynienia z sytuacją, w której należy opisać bieżącą sesję
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
%>
<!-- początek strony 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>

Należy zauważyć, że w wierszu

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

użyto obiektu sesji, który pojawił się znikąd. W rzeczywistości obiekt ten należy do obiektów domyślnych udostępnianych stronom JSP, podobnie jak obiekty request, response, out, config (ServletConfig), context (ServletContext), z którymi już mieliśmy do czynienia. Oba linki na tej stronie odsyłają do serwletu cycledevie przedstawionego wcześniej:

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

Link służący do unieważnienia sesji zawiera parametr action=invalider, który pozwoli serwletowi cycledevie rozpoznać, że użytkownik chce unieważnić bieżącą sesję. Drugi link służy do ponownego załadowania strony. Aby przeglądarka nie pobierała tej strony z pamięci podręcznej, stosuje się dyrektywę HTML:

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

. Nakazuje ona przeglądarce, aby nie korzystała z pamięci podręcznej w przypadku otrzymanej strony.

Strona invalide.jsp wygląda następująco:

<!-- początek strony 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>

Zawiera link kierujący do serwletu cycledevie bez parametru action. Link ten spowoduje, że serwlet cycledevie utworzy nową sesję.

Strona erreur.jsp wygląda następująco:

<%
     // jspService
   // tutaj mamy do czynienia z sytuacją, w której należy opisać bieżącą sesję
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée)";
%>
<!-- początek strony 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>

Jej zadaniem jest wyświetlenie komunikatu o błędzie przekazanego przez serwlet cycledevie. Przyjrzyjmy się teraz przykładom działania. Serwlet jest wywoływany po raz pierwszy:

Image

Powyższa strona wskazuje, że jesteśmy w nowej sesji. Korzystamy z linku „Odśwież stronę”:

Image

Poprzedni wynik wskazuje, że nadal znajdujemy się w tej samej sesji, co na poprzedniej stronie (ten sam identyfikator ID). Należy zauważyć, że zmieniła się godzina ostatniego dostępu do tej sesji. Teraz skorzystajmy z linku „Unieważnij sesję”:

Image

Zwróćmy uwagę na identyfikator URL tej nowej strony z parametrem action=invalider. Skorzystajmy z linku „Utwórz nową sesję”, aby utworzyć nową sesję:

Image

Widać, że rozpoczęła się nowa sesja. W poprzednich przykładach sesja opierała się na mechanizmie plików cookie. Teraz wyłączmy obsługę plików cookie w naszej przeglądarce i powtórzmy testy. Poniższe przykłady zostały przeprowadzone w przeglądarce Netscape Communicator. Z niewyjaśnionych przyczyn testy przeprowadzone z użyciem IE6 dawały nieoczekiwane wyniki, tak jakby IE6 nadal korzystał z plików cookie, mimo że zostały one wyłączone. Serwlet cycledevie jest wywoływany po raz pierwszy:

Image

Teraz korzystamy z linku „Odśwież stronę”:

Image

Można zauważyć dwie rzeczy:

  • identyfikator sesji ID uległ zmianie
  • serwlet rozpoznaje tę sesję jako nową

Serwer Tomcat rozwiązuje problem użytkowników, którzy blokują pliki cookie w swoich przeglądarkach. Wykorzystuje dwa mechanizmy do wdrożenia tokenu, o którym mowa na początku tego akapitu: pliki cookie oraz przepisywanie URL. Jeśli plik cookie sesji jest niedostępny, serwer spróbuje uzyskać token na podstawie adresu URL URL, o który poprosił klient. W tym celu adres ten musi zawierać token. Ogólnie rzecz biorąc, wszystkie linki generowane w dokumencie HTML prowadzące do aplikacji internetowej muszą zawierać jej token. Można to osiągnąć za pomocą metody encodeURL:

String [HttpResponse].encodeURL(String URL)
dodaje token bieżącej sesji do parametru URL w postaci URL;jsessionid=xxxx

Modyfikujemy naszą aplikację w następujący sposób:

  • w serwlecie cycledevie.java kody URL są zakodowane:
             // przekazujemy kontrolę do strony błędu
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
             // przekazujemy kontrolę do adresu URL urlSessionInvalide
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
         // przekierowuje do adresu URL urlSessionInvalide
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • na stronie valide.jsp zakodowano pliki URL:
<%
     // jspService
   // tutaj mamy do czynienia z sytuacją, w której należy opisać bieżącą sesję
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
   // kodowanie URL cycledevie
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");  
%>
............
      <br><a href="<%= URLcycledevie %>?action=invalider">Invalider la session</a>
      <br><a href="<%= URLcycledevie %>">Recharger la page</a>
  • na stronie invalide.jsp zakodowano pliki URL:
<%
     // jspservice – unieważniamy bieżącą sesję
  session.invalidate();
   // kodowanie URL cykl życia
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>  
..........
    <a href="<%= URLcycledevie %>">Créer une nouvelle session</a>

Teraz jesteśmy gotowi do przeprowadzenia testów. Korzystamy z przeglądarki Netscape 4.5, a pliki cookie zostały wyłączone. Po raz pierwszy wywołujemy serwlet cycledevie:

Image

i odświeżamy stronę za pomocą linku „Odśwież stronę”:

Image

Widzimy, że:

  • sesja nie uległa zmianie (ten sam identyfikator ID)
  • URL z serwletu cycledevie rzeczywiście zawiera token, jak pokazuje pole Adresse powyżej
  • serwer Tomcat pobiera zatem token sesji z żądanego URL (o ile programista zadbał o jego zakodowanie).

4.4. Przykład 2

Przedstawiamy teraz przykład pokazujący, jak przechowywać informacje w sesji klienta. W tym przypadku jedyną informacją będzie licznik, który będzie zwiększany za każdym razem, gdy użytkownik wywoła URL z serwletu. Gdy zostanie on wywołany po raz pierwszy, wyświetla się następująca strona:

Image

Jeśli klikniemy powyższy link „Odśwież stronę”, wyświetli się nowa strona:

Image

Aplikacja składa się z trzech elementów:

  • serwlet, który przetwarza żądanie klienta
  • stronę JSP, która wyświetla wartość licznika
  • strona JSP wyświetlająca ewentualny błąd

Te trzy komponenty są zainstalowane w już używanej aplikacji internetowej „sessions”. Plik web.xml tej aplikacji został zmodyfikowany w celu skonfigurowania nowych serwletów:

<?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>
  • serwlet nosi nazwę „compteur” (servlet-name) i jest powiązany z plikiem klasy compteur.class (servlet-class)
  • posiada dwa parametry inicjalizacyjne:
    • urlAffichageCompteur: URL ze strony wyświetlającej licznik JSP
    • urlErreur: URL ze strony JSP wyświetlającej ewentualny błąd
  • oraz alias /licznik, dzięki czemu będzie on wywoływany poprzez URL http://localhost:8080/sessions/compteur

Serwlet compteur.java ma następującą postać:

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

public class compteur extends HttpServlet{

     // zmienne instancji
    String msgErreur=null;
    String urlAffichageCompteur=null;
    String urlErreur=null;

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

        // czy inicjalizacja przebiegła pomyślnie?
        if(msgErreur!=null){
             // przekazujemy kontrolę do strony błędu
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }

         // pobieramy bieżącą sesję
        HttpSession session=request.getSession();
         // oraz licznik
        String compteur=(String)session.getAttribute("compteur");
        if(compteur==null) compteur="0";
         // inkrementacja licznika
        try{
            compteur=""+(Integer.parseInt(compteur)+1);
        }catch(Exception ex){}
         // zapisanie licznika w sesji
        session.setAttribute("compteur",compteur);
         // oraz w zapytaniu
        request.setAttribute("compteur",compteur);

         // przekazujemy kontrolę do adresu URL wyświetlającego licznik
        getServletContext().getRequestDispatcher(urlAffichageCompteur).forward(request,response);
    }

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

     //-------- INIT
    public void init(){
         // pobieramy parametry inicjalizacyjne
        ServletConfig config=getServletConfig();
        urlAffichageCompteur=config.getInitParameter("urlAffichageCompteur");
        urlErreur=config.getInitParameter("urlErreur");

         // parametry w porządku?
        if(urlAffichageCompteur==null){
            msgErreur="Configuration incorrecte";
        }
    }
}

Ten serwlet ma strukturę podobną do serwletów, z którymi mieliśmy już do czynienia. Zwróćmy jedynie uwagę na sposób obsługi licznika:

  • sesja jest pobierana za pomocą request.getSession()
  • licznik jest pobierany z tej sesji za pomocą session.getAttribute("licznik")
  • jeśli pobierana jest wartość null, oznacza to, że sesja właśnie się rozpoczęła. Licznik jest wówczas ustawiany na 0.
  • licznik jest zwiększany, ponownie umieszczany w sesji (session.setAttribute("licznik",licznik)) i umieszczany w żądaniu, które zostanie przekazane do serwletu wyświetlającego (request.setAttribute("licznik",licznik)).

Strona wyświetlająca compteur.jsp wygląda następująco:

<%
     // jspService
   // odczyt licznika
  String compteur= (String) request.getAttribute("compteur");
  if(compteur==null) compteur="inconnu";
%>
<!-- początek strony 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>

Powyższa strona po prostu pobiera atrybut compteur (request.getAttribute („licznik”)) przekazany jej przez główny serwlet i wyświetla go.

Strona błędu erreurcompteur.jsp wygląda następująco:

<%
     // jspService
   // wystąpił błąd
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- początek strony 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. Przykład 3

Zamierzamy napisać aplikację Java, która będzie klientem poprzedniej aplikacji compteur. Będzie ona wywoływać ją N razy z rzędu, gdzie N będzie przekazywane jako parametr. Naszym celem jest zaprezentowanie zaprogramowanego klienta internetowego oraz sposobu obsługi plików cookie. Punktem wyjścia będzie ogólny klient internetowy przedstawiony w materiałach szkoleniowych dotyczących języka Java tego samego autora. Wywołuje się go w następujący sposób:

clientweb URL GET/HEAD

  • URL: żądany adres URL
  • GET/HEAD: GET, aby uzyskać kod HTML strony, HEAD, aby ograniczyć się wyłącznie do nagłówków HTTP

Oto przykład z kodami URL i http://localhost:8080/sessions/compteur:


E:\data\serge\JAVA\SOCKETS\client web>java clientweb http://localhost:8080/sessions/licznik 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


<!-- początek strony 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>

Program clientweb wyświetla wszystko, co otrzymuje z serwera. Powyżej widoczne jest polecenie HTTP Set-cookie, za pomocą którego serwer wysyła plik cookie do klienta. W tym przypadku plik cookie zawiera dwie informacje:

  • JSESSIONID, czyli token sesji
  • Path, który określa adres URL, do którego należy plik cookie. Path=/sessions informuje przeglądarkę, że musi odesłać plik cookie do serwera za każdym razem, gdy zażąda strony URL zaczynającej się od /sessions. W aplikacji sessions wykorzystaliśmy różne serwlety, w tym serwlety /sessions/cycledevie i /sessions/compteur. Jeśli wywołamy serwlet /sessions/cycledevie, przeglądarka otrzyma token J. Jeśli za pomocą tej samej przeglądarki następnie wywołamy serwlet /sessions/compteur, przeglądarka odeśle do serwera token J, ponieważ dotyczy on wszystkich serwletów URL, począwszy od /sessions. W naszym przykładzie serwlety cycledevie i compteur nie muszą dzielić tego samego tokenu sesji. Nie powinny więc zostać umieszczone w tej samej aplikacji internetowej. Należy o tym pamiętać: wszystkie serwlety tej samej aplikacji dzielą ten sam token sesji.
  • Plik cookie może również określać okres ważności. W tym przypadku informacja ta nie została podana. Plik cookie zostanie zatem usunięty po zamknięciu przeglądarki. Plik cookie może mieć na przykład okres ważności wynoszący N dni. Dopóki plik cookie jest ważny, przeglądarka będzie go odsyłać za każdym razem, gdy zostanie wywołana jedna ze stron o adresie URL w jej domenie (Path). Weźmy na przykład stronę internetowego sklepu CD. Może ona śledzić ścieżkę nawigacji klienta w swoim katalogu i stopniowo określać jego preferencje: na przykład muzykę klasyczną. Preferencje te mogą zostać zapisane w pliku cookie o okresie ważności wynoszącym 3 miesiące. Jeśli ten sam klient powróci na stronę po upływie miesiąca, przeglądarka prześle plik cookie do aplikacji serwerowej. Aplikacja ta, na podstawie informacji zawartych w pliku cookie, będzie mogła dostosować generowane strony do preferencji klienta.

Poniżej znajduje się kod klienta internetowego. Będzie on później punktem wyjścia dla innego klienta.

// zaimportowane pakiety
import java.io.*;
import java.net.*;

public class clientweb{

    // żądanie URL
     // wyświetla jej zawartość na ekranie

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

        // liczba argumentów
        if(args.length != 2)
            erreur(syntaxe,1);

         // odnotowuje się żądany URI
        String URLString=args[0];
        String commande=args[1].toUpperCase();

        // weryfikacja poprawności kodu URI
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI jest nieprawidłowy
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//przechwycenie
         // weryfikacja zamówienia
        if(! commande.equals("GET") && ! commande.equals("HEAD")){
            // nieprawidłowe zamówienie
            erreur("Le second paramètre doit être GET ou HEAD",3);
        }

         // wyodrębniamy przydatne informacje z 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();

         // można kontynuować
        Socket  client=null;                        // klient
        BufferedReader IN=null;                    // strumień odczytu klienta
        PrintWriter OUT=null;                        // strumień zapisu klienta
        String réponse=null;                        // odpowiedź serwera
        try{
             // nawiązywanie połączenia z serwerem
            client=new Socket(host,port);

            // tworzenie strumieni wejścia i wyjścia klienta TCP
            IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
            OUT=new PrintWriter(client.getOutputStream(),true);

            // żądanie URL – wysyłanie nagłówków HTTP
            OUT.println(commande + " " + path + query + " HTTP/1.1");   
            OUT.println("Host: " + host + ":" + port);
            OUT.println("Connection: close");
            OUT.println();
             // odczytuje się odpowiedź
            while((réponse=IN.readLine())!=null){
                 // przetwarzamy odpowiedź
                System.out.println(réponse);
            }//while
             // zakończono
            client.close();
        } catch(Exception e){
            // obsługujemy wyjątek
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // wyświetlanie błędów
    public static void erreur(String msg, int exitCode){
         // wyświetlanie błędu
        System.err.println(msg);
         // zakończenie z błędem
        System.exit(exitCode);
    }//błąd
}//klasa

Teraz tworzymy program clientCompteur, wywoływany w następujący sposób:

clientCompteur URL N [JSESSIONID]

  • URL: adres URL serwletu licznika
  • N: liczba wywołań tej serwletu
  • JSESSIONID: parametr opcjonalny – token sesji

Celem programu jest wywołanie serwletu licznika N razy poprzez zarządzanie plikiem cookie sesji i wyświetlanie za każdym razem wartości licznika zwróconej przez serwer. Po zakończeniu N wywołań wartość licznika musi wynosić N. Oto pierwszy przykład działania:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/licznik 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

Program wyświetla:

  • nagłówki HTTP, które wysyła do serwera w postaci -->
  • nagłówki HTTP, które otrzymuje
  • wartość licznika po każdym wywołaniu

Widać, że podczas pierwszego wywołania:

  • klient nie wysyła pliku cookie
  • serwer wysyła plik cookie

W przypadku kolejnych wywołań:

  • klient zawsze odsyła plik cookie, który otrzymał od serwera podczas pierwszego wywołania. To właśnie pozwala serwerowi rozpoznać go i zwiększyć wartość licznika.
  • serwer nie wysyła już pliku cookie

Uruchamiamy ponownie poprzedni program, przekazując powyższy token jako trzeci parametr:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur http://localhost:8080/sessions/licznik 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

Widać tutaj, że już przy pierwszym wywołaniu klienta serwer otrzymuje ważny plik cookie sesji. Należy pamiętać, że w przypadku Tomcata maksymalny czas bezczynności sesji wynosi domyślnie 20 minut (można to jednak skonfigurować). Jeśli drugie wywołanie programu dość szybko wyśle plik cookie otrzymany podczas pierwszego wywołania, dla serwera będzie to ta sama sesja. Wskazujemy tu na potencjalną lukę w zabezpieczeniach. Jeśli uda mi się przechwycić w sieci token sesji, będę w stanie podszyć się pod osobę, która ją zainicjowała. W naszym przykładzie pierwsze wywołanie reprezentuje użytkownika inicjującego sesję (być może za pomocą loginu i hasła, które uprawniają go do otrzymania tokenu), a drugie wywołanie reprezentuje osobę, która „przejęła” token sesji z pierwszego wywołania. Jeśli bieżąca operacja dotyczy bankowości, może to stać się bardzo kłopotliwe...

Kod klienta wygląda następująco:

// zaimportowane pakiety
import java.io.*;
import java.net.*;
import java.util.regex.*;

public class clientCompteur{

     // żądanie URL
     // wyświetla jej zawartość na ekranie

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

         // liczba argumentów
        if(args.length !=2 && args.length != 3)
            erreur(syntaxe,1);

         // odnotowuje się żądany URL
        String URLString=args[0];

        // weryfikacja poprawności kodu URL
        URL url=null;
        try{
            url=new URL(URLString);
        }catch (Exception ex){
             // URI jest nieprawidłowy
            erreur("L'erreur suivante s'est produite : " + ex.getMessage(),2);
        }//przechwycenie
         // weryfikacja liczby wywołań N
        int N=0;
        try{
            N=Integer.parseInt(args[1]);
            if(N<=0) throw new Exception();
        }catch(Exception ex){
             // nieprawidłowy argument N
            erreur("Le nombre d'appels N doit être un entier >0",3);
        }
         // czy token JSESSIONID został przekazany jako parametr?
        String JSESSIONID="";
        if (args.length==3) JSESSIONID=args[2];

        // wyodrębniane są przydatne informacje z 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();

         // można kontynuować pracę
        Socket  client=null;                        // klient
        BufferedReader IN=null;                    // strumień odczytu klienta
        PrintWriter OUT=null;                        // strumień zapisu klienta
        String réponse=null;                        // odpowiedź serwera
         // szukany wzorzec w nagłówkach HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
        // szukany wzorzec w kodzie HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
         // wynik porównania z wzorcem
        Matcher résultat=null;
         // wartość logiczna określająca wynik wyszukiwania licznika
        boolean compteurTrouvé;

        try{
             // wykonywanych jest N wywołań do serwera
            for(int i=0;i<N;i++){
                // nawiązywane jest połączenie z serwerem
                client=new Socket(host,port);

                // tworzy się przepływy wejścia-wyjścia klienta TCP
                IN=new BufferedReader(new InputStreamReader(client.getInputStream()));
                OUT=new PrintWriter(client.getOutputStream(),true);

                // wysyłamy żądanie do URL – wysyłamy nagłówki 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,"");

                 // odczytuje się odpowiedź aż do końca nagłówków, szukając ewentualnego pliku cookie
                while((réponse=IN.readLine())!=null){
                     // śledzenie odpowiedzi
                    System.out.println(réponse);
                     // pusta linia?
                    if(réponse.equals("")) break;
                     // wiersz HTTP nie jest pusty
                     // jeśli nie ma tokenu sesji, należy go wyszukać
                    if (JSESSIONID.equals("")){
                        // porównujemy wiersz HTTP z wzorcem pliku cookie
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // znaleziono plik cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while

                 // przetwarzanie nagłówków HTTP zostało zakończone – przechodzimy do kodu HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // czy bieżąca linia zawiera licznik?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // znaleziono licznik – wyświetlamy go
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while
                 // skończono
                client.close();
            }//for
        } catch(Exception e){
            // obsługujemy wyjątek
            erreur(e.getMessage(),4);
        }//catch
    }//main

     // wyświetlanie błędów
    public static void erreur(String msg, int exitCode){
         // wyświetlanie błędu
        System.err.println(msg);
         // zakończenie z błędem
        System.exit(exitCode);
    }//błąd

     // monitorowanie komunikacji klient-serwer
    public static void envoie(PrintWriter OUT,String msg){
        // wysyła komunikat do serwera
        OUT.println(msg);
         // monitorowanie ekranu
        System.out.println("--> "+msg);
    }//błąd
}//klasa

Przeanalizujmy najważniejsze elementy tego programu:

  • musimy wykonać N wymian danych między klientem a serwerem. Dlatego też są one umieszczone w pętli
            for(int i=0;i<N;i++){
  • przy każdej wymianie klient nawiązuje połączenie TCP-IP z serwerem. Po nawiązaniu połączenia wysyła do serwera nagłówki HTTP swojego żądania:
                 // żądanie URL  wysyłanie nagłówków 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,"");

Jeśli token JSESSIONID jest dostępny, jest wysyłany w postaci pliku cookie, w przeciwnym razie nie jest wysyłany.

  • Po wysłaniu żądania klient oczekuje na odpowiedź serwera. Najpierw analizuje nagłówki HTTP tej odpowiedzi w poszukiwaniu ewentualnego pliku cookie. Aby go znaleźć, porównuje otrzymane wiersze z wyrażeniem regularnym pliku cookie:
         // wyszukiwanie wzorca w nagłówkach HTTP
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // odczytuje się odpowiedź aż do końca nagłówków, szukając ewentualnego pliku cookie
                while((réponse=IN.readLine())!=null){
                     // dalsza część odpowiedzi
                    System.out.println(réponse);
                     // pusta linia?
                    if(réponse.equals("")) break;
                     // wiersz HTTP nie jest pusty
                     // jeśli nie ma tokenu sesji, należy go wyszukać
                    if (JSESSIONID.equals("")){
                        // porównujemy wiersz HTTP z wzorcem pliku cookie
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // znaleziono plik cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • gdy token zostanie znaleziony po raz pierwszy, nie będzie już wyszukiwany podczas kolejnych wywołań serwera. Po przetworzeniu nagłówków HTTP z odpowiedzi przechodzi się do kodu HTML tej samej odpowiedzi. W tym kodzie szuka się wiersza podającego wartość licznika. Wyszukiwanie to odbywa się również za pomocą wyrażenia regularnego:
         // szukany wzorzec licznika w kodzie HTML
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                 // koniec z nagłówkami HTTP – przechodzimy do kodu HTML
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // czy bieżąca linia zawiera licznik?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // znaleziono licznik – wyświetlamy go
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. Przykład 4

W poprzednim przykładzie klient sieciowy zwraca token w postaci pliku cookie. Widzieliśmy, że może go również zwrócić w treści samego żądania URL w postaci URL;jsessionid=xxx. Sprawdźmy to. Program clientCompteur.java zostaje przekształcony w clientCompteur2.java i zmodyfikowany w następujący sposób:

....
                 // żądamy URL  wysyłamy nagłówki 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,"");
....

Klient żąda zatem kodu licznika URL za pomocą GET URL;jsessionid=xx HTTP/1.1 i nie wysyła już pliku cookie. To jedyna zmiana. Oto wyniki pierwszego wywołania:


E:\data\serge\Servlets\sessions\jb7>java.bat clientCompteur2 http://localhost:8080/sessions/licznik 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

Podczas pierwszego wywołania klient wysyła żądanie dotyczące URL bez tokenu sesji. Serwer odpowiada, wysyłając mu token. Następnie klient ponownie wysyła żądanie dotyczące tego samego URL, dołączając do niego otrzymany token. Widać, że licznik został prawidłowo zwiększony, co świadczy o tym, że serwer prawidłowo rozpoznał, iż chodzi o tę samą sesję.

4.7. Przykład 5

Ten przykład przedstawia aplikację składającą się z trzech stron, które nazwiemy page0, page1 i page2. Użytkownik musi je wyświetlić w następującej kolejności:

  • strona0 to formularz wymagający podania informacji: imienia
  • strona1 to formularz wyświetlany w odpowiedzi na wysłanie formularza ze strony0. Wymaga on podania drugiej informacji: wieku
  • strona2 to dokument o nazwie HTML, który wyświetla imię uzyskane na stronie0 oraz wiek uzyskany na stronie1.

Mamy tu do czynienia z trzema wymianami danych między klientem a serwerem:

  • podczas pierwszej wymiany klient żąda formularza page0, a serwer go wysyła
  • podczas drugiej wymiany klient żąda formularza page1, a serwer go wysyła. Klient wysyła imię do serwera.
  • podczas trzeciej wymiany klient żąda dokumentu page3, a serwer go wysyła. Klient wysyła wiek do serwera. Dokument page3 musi wyświetlać imię i wiek. Imię zostało uzyskane przez serwer podczas drugiej wymiany danych i od tego momentu zostało „zapomniane”. Wykorzystuje się sesję do zapisania imienia podczas wymiany nr 2, aby było ono dostępne podczas wymiany nr 3.

Strona „page0” uzyskana podczas pierwszej wymiany wygląda następująco:

Image

Wypełniamy pole imienia:

Image

Klikamy przycisk Suite i otrzymujemy następującą stronę page1:

Image

Wypełniamy pole wieku:

Image

Klikamy przycisk Suite, po czym wyświetla się następująca strona page2:

Image

Po przesłaniu strony page0 do serwera, serwer może zwrócić ją z kodem błędu, jeśli pole „imię” jest puste:

Image

Po przesłaniu strony page1 do serwera może on zwrócić ją z kodem błędu, jeśli wiek jest nieprawidłowy:

Image

Aplikacja składa się z serwletu i czterech stron JSP:

page0.jsp
wyświetla stronę0
page1.jsp
wyświetla stronę 1
page2.jsp
wyświetla stronę 2
erreur.jsp
wyświetla stronę błędu

Aplikacja internetowa nosi nazwę „suitedepages” i jest skonfigurowana w następujący sposób w pliku server.xml serwera Tomcat:

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

Plik konfiguracyjny web.xml aplikacji suitedepages ma następującą treść:

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

Główny serwlet nosi nazwę „main” i dzięki aliasowi (mapowaniu serwletów) jest dostępny pod adresem URL http://localhost:8080/suitedepages/main. Posiada cztery parametry inicjalizacyjne, którymi są URL czterech stron JSP wykorzystywanych do różnych wyświetleń. Kod serwletu „main” wygląda następująco:

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

public class main extends HttpServlet{

    // zmienne instancji
    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{

        // czy inicjalizacja przebiegła pomyślnie?
        if(msgErreur!=null){
             // przekazujemy kontrolę do strony błędu
            getServletContext().getRequestDispatcher(urlErreur).forward(request,response);
        }
         // pobieramy parametr etapu
        String étape=request.getParameter("etape");
         // pobieramy bieżącą sesję
        HttpSession session=request.getSession();
         // przetwarzamy bieżący etap
        if(étape==null) étape0(request,response,session);
        if(étape.equals("1")) étape1(request,response,session);
        if(étape.equals("2")) étape2(request,response,session);
         // inne przypadki  nieprawidłowe
        étape0(request,response,session);
    }

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

     //-------- INIT
    public void init(){
         // pobierane  parametry inicjalizacyjne
        ServletConfig config=getServletConfig();
        urlPage0=config.getInitParameter("urlPage0");
        urlPage1=config.getInitParameter("urlPage1");
        urlPage2=config.getInitParameter("urlPage2");
        urlErreur=config.getInitParameter("urlErreur");

         //  czy parametry  w porządku?
        if(urlPage0==null || urlPage1==null || urlPage2==null){
            msgErreur="Configuration incorrecte";
        }
    }

     //-------- etap 0
    public void étape0(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
        // ustalamy kilka atrybutów
        request.setAttribute("nom","");
         // wyświetlamy stronę 0
        request.getRequestDispatcher(urlPage0).forward(request,response);
    }

     //-------- etap 1
    public void étape1(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // pobieramy nazwę z zapytania
        String nom=request.getParameter("nom");
        // czy nazwa została ustawiona?
        if(nom==null) étape0(request,response,session);
         // usuwamy ewentualne spacje z nazwy
        nom=nom.trim();
         // umieszczamy go w atrybucie zapytania
        request.setAttribute("nom",nom);
         // czy nazwa jest pusta?
        if(nom.equals("")){
             // to błąd
            ArrayList erreurs=new ArrayList();
            erreurs.add("Nous n'avez pas indiqué de nom");
             // błędy umieszczamy w zapytaniu
            request.setAttribute("erreurs",erreurs);
             // powrót do strony 0
            étape0(request,response,session);
        }
         // prawidłowa nazwa  zapisujemy  w bieżącej sesji
        session.setAttribute("nom",nom);
         // ustawiamy atrybut wiek w zapytaniu
        request.setAttribute("age","");
         // wyświetla się strona 1
        request.getRequestDispatcher(urlPage1).forward(request,response);
    }

     //-------- etap 2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // pobieramy nazwę z sesji
        String nom=(String)session.getAttribute("nom");
         // czy nazwa została ustawiona?
        if(nom==null) étape0(request,response,session);
         // umieszczamy go w atrybucie zapytania
        request.setAttribute("nom",nom);
         // pobieramy wiek z zapytania
        String age=request.getParameter("age");
        // czy wiek został ustawiony?
        if(age==null){
            // powrót do strony 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // zapisujemy wiek w zapytaniu
        age=age.trim();
        request.setAttribute("age",age);
        // czy wiek jest prawidłowy?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // to błąd
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // błędy umieszczamy w zapytaniu
            request.setAttribute("erreurs",erreurs);
             // powrót do strony 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // wiek prawidłowy  wyświetla się strona 2
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • metoda init pobiera cztery parametry inicjalizacyjne i generuje komunikat o błędzie, jeśli którykolwiek z nich jest brakujący
  • widzieliśmy, że zapytanie obejmowało trzy wymiany danych. Aby zorientować się, na którym etapie tych etapów się znajdujemy, formularze page0 i page1 zawierają ukrytą zmienną etape, która przyjmuje wartość 1 (page0) lub 2 (page1). Numer ten można tutaj traktować jako numer kolejnej strony do wyświetlenia. W metodzie doGet parametr ten jest pobierany z żądania i w zależności od jego wartości przetwarzanie jest przekazywane do trzech innych metod:
    • étape0 przetwarza żądanie początkowe i wywołuje metodę page0
    • étape1 przetwarza formularz z metody page0 i wysyła metodę page1 lub ponownie metodę page0, jeśli wystąpił błąd
    • Krok 2 przetwarza formularz page1 i wysyła page2 lub ponownie page1, jeśli wystąpił błąd
  • etap 0
    • wyświetla plik page0 z pustą nazwą
  • krok 1
    • pobiera parametr nom z formularza page0.
    • sprawdza, czy nazwa istnieje (nie jest null). Jeśli tak nie jest, wyświetla ponownie page0 tak, jakby było to pierwsze wywołanie.
    • sprawdza, czy nazwa nie jest pusta. Jeśli tak nie jest, ponownie wyświetla page0 wraz z komunikatem o błędzie.
    • zapisuje nazwę w bieżącej sesji i wyświetla page1, jeśli nazwa jest prawidłowa.
  • krok 2
    • pobiera parametr nom z bieżącej sesji.
    • Sprawdza, czy nazwa istnieje (nie jest null). Jeśli tak nie jest, ponownie wyświetla page0, tak jakby było to pierwsze wywołanie.
    • pobiera parametr age z bieżącego żądania wysłanego przez page1.
    • sprawdza, czy wiek jest prawidłowy. Jeśli nie, wyświetla ponownie page1 wraz z komunikatem o błędzie.
    • zapisuje imię i wiek jako atrybuty zapytania i wyświetla stronę page2, jeśli imię i wiek są prawidłowe.

Strona page0.jsp wygląda następująco:

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

<% // page0.jsp
     // pobieramy atrybuty zapytania
  String nom=(String)request.getAttribute("nom");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
   // czy atrybuty są prawidłowe?
  if(nom==null){
       // powrót do głównego serwletu
    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>
    <% // czy wystąpiły błędy?
      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>
  • Strona page0.jsp może zostać wywołana przez główny serwlet w dwóch przypadkach:
    • podczas pierwszego żądania
    • po przetworzeniu formularza page0, gdy wystąpi błąd
  • parametr nom, który ma zostać wyświetlony, jest przekazywany przez główny serwlet wraz z ewentualną listą błędów. Serwlet page0.jsp rozpoczyna zatem od pobrania tych dwóch informacji.
  • Formularz jest „wysyłany” do głównego serwletu wraz z ukrytym polem etape, które wskazuje, na jakim etapie aplikacji się znajdujemy.

Strona page1.jsp wygląda następująco:

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

<% // page1.jsp
     // pobieramy atrybuty żądania
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  ArrayList erreurs=(ArrayList)request.getAttribute("erreurs");
  // czy atrybuty są prawidłowe?
  if(nom==null || age==null){
      // powrót do głównego serwletu
    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>
    <% // czy wystąpiły błędy?
      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>

Strona page1.jsp ma strukturę analogiczną do strony page0.jsp, z tą różnicą, że otrzymuje teraz dwa atrybuty od głównego serwletu: nom i age. Wreszcie strona page2.jsp wygląda następująco:

<% 
     // page2.jsp
     // pobieramy atrybuty żądania
  String nom=(String)request.getAttribute("nom");
  String age=(String)request.getAttribute("age");
  // czy atrybuty są prawidłowe?
  if(nom==null || age==null){
      // powrót do głównego serwletu
    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>

Strona page2.jsp również otrzymuje atrybuty nom i age od głównego serwletu. Jej zadaniem jest jedynie wyświetlenie tych atrybutów. Na koniec strona erreur.jsp, której zadaniem jest wyświetlenie komunikatu o błędzie w przypadku nieprawidłowej inicjalizacji serwletu, wygląda następująco:

<%
     // jspService
   // wystąpił błąd
  String msgErreur= request.getAttribute("msgErreur");
  if(msgErreur==null) msgErreur="Erreur non identifiée";
%>
<!-- początek strony HTML -->
<html>
  <head>
      <title>Suite de pages</title>
  </head>
  <body>
      <h3>Suite de pages</h3>
      <hr>
    Application indisponible(<%= msgErreur %>)
  </body>
</html>

Wyświetla atrybut msgErreur, który przekazała jej główna serwlet.

Podsumowując, można zauważyć, że na wszystkich trzech etapach działania aplikacji przeglądarka zawsze najpierw wysyła zapytanie do głównego serwletu. Jednak to nie on generuje odpowiedź, która ma zostać wyświetlona, lecz jedna z czterech stron o nazwie JSP. Użytkownik nie dostrzega tego faktu, ponieważ przeglądarka nadal wyświetla w polu „Adres” początkowo żądaną stronę URL, czyli stronę głównego serwletu.