Skip to content

4. Sitzungsverfolgung

4.1. Das Problem

Eine Webanwendung kann aus mehreren Formularübermittlungen zwischen Server und Client bestehen. Der Ablauf ist wie folgt:

  • Schritt 1
    • Client C1 baut eine Verbindung zum Server auf und sendet seine erste Anfrage.
    • Der Server sendet das Formular F1 an den Client C1 und schließt die in Schritt 1 geöffnete Verbindung.
  • Schritt 2
    • Client C1 füllt das Formular aus und sendet es an den Server zurück. Dazu öffnet der Browser eine neue Verbindung zum Server.
    • Der Server verarbeitet die Daten aus Formular 1, berechnet daraus die Information I1, sendet das Formular F2 an den Client C1 und schließt die in Schritt 3 geöffnete Verbindung.
  • Schritt 3
    • Der Zyklus aus den Schritten 3 und 4 wiederholt sich in den Schritten 5 und 6. Am Ende von Schritt 6 hat der Server zwei Formulare, F1 und F2, erhalten und daraus die Informationen I1 und I2 berechnet.

Das vorliegende Problem lautet: Wie behält der Server den Überblick über die Informationen I1 und I2, die mit Client C1 verbunden sind? Dieses Problem wird als Verfolgung der Sitzung von Client C1 bezeichnet. Um die Ursache zu verstehen, betrachten wir das Diagramm einer TCP-IP-Serveranwendung, die mehrere Clients gleichzeitig bedient:

In einer klassischen TCP-IP-Client-Server-Anwendung:

  • stellt der Client eine Verbindung zum Server her
  • tauscht über diese Verbindung Daten mit dem Server aus
  • wird die Verbindung von einer der beiden Parteien geschlossen

Die beiden Kernpunkte dieses Mechanismus sind:

  1. Für jeden Client wird eine einzige Verbindung hergestellt
  2. Diese Verbindung wird für die gesamte Dauer des Dialogs zwischen dem Server und seinem Client verwendet

Was es dem Server ermöglicht, zu jedem Zeitpunkt zu wissen, mit welchem Client er gerade arbeitet, ist die Verbindung – oder, mit anderen Worten, der „Kanal“ –, die ihn mit seinem Client verbindet. Da dieser Kanal einem bestimmten Client zugeordnet ist, stammt alles, was über diesen Kanal kommt, von diesem Client, und alles, was über diesen Kanal gesendet wird, erreicht den Client.

Der HTTP-Client-Server-Mechanismus folgt weitgehend dem vorherigen Modell, mit der Ausnahme, dass der Client-Server-Dialog auf einen einzigen Austausch zwischen dem Client und dem Server beschränkt ist:

  • Der Client öffnet eine Verbindung zum Server und stellt seine Anfrage
  • der Server sendet seine Antwort und schließt die Verbindung

Wenn zu Zeitpunkt T1 ein Client C eine Anfrage an den Server stellt, erhält er eine Verbindung C1, die für den einmaligen Anfrage-Antwort-Austausch verwendet wird. Wenn derselbe Client zu Zeitpunkt T2 eine zweite Anfrage an den Server stellt, erhält er eine Verbindung C2, die sich von der Verbindung C1 unterscheidet. Für den Server besteht dann kein Unterschied zwischen dieser zweiten Anfrage von Benutzer C und seiner ursprünglichen Anfrage: In beiden Fällen behandelt der Server den Client als neuen Client. Damit eine Verbindung zwischen den verschiedenen Verbindungen von Client C zum Server besteht, muss Client C vom Server als „Stammkunde“ „erkannt“ werden und der Server muss die Informationen abrufen, die er über diesen Stammkunden besitzt.

Stellen wir uns ein System vor, das wie folgt funktioniert:

  • Es gibt eine einzige Warteschlange
  • Es gibt mehrere Serviceschalter. Das bedeutet, dass mehrere Kunden gleichzeitig bedient werden können. Wenn ein Schalter frei wird, verlässt ein Kunde die Schlange, um an diesem Schalter bedient zu werden
  • Wenn dies der erste Besuch des Kunden ist, händigt der Schalterbeamte ihm eine Wartemarke mit einer Nummer aus. Der Kunde darf nur eine Frage stellen. Sobald er seine Antwort erhalten hat, muss er den Schalter verlassen und sich wieder hinten in die Schlange stellen. Der Schalterbeamte trägt die Daten des Kunden in eine Datei mit der entsprechenden Nummer ein.
  • Wenn der Kunde wieder an der Reihe ist, wird er möglicherweise von einem anderen Schalterbeamten bedient als beim letzten Mal. Der Schalterbeamte bittet um den Token und holt die Akte mit der Token-Nummer hervor. Erneut stellt der Kunde eine Anfrage, erhält eine Antwort, und die Informationen werden seiner Akte hinzugefügt.
  • Und so weiter... Im Laufe der Zeit erhält der Kunde Antworten auf alle seine Anfragen. Die Verknüpfung zwischen den verschiedenen Anfragen erfolgt über den Token und die damit verbundene Akte.

Der Mechanismus zur Sitzungsverfolgung in einer Client-Server-Webanwendung funktioniert ähnlich:

  • Bei der ersten Anfrage erhält ein Client vom Webserver ein Token
  • Er legt dieses Token bei jeder nachfolgenden Anfrage vor, um sich zu identifizieren

Das Token kann verschiedene Formen annehmen:

  • ein verstecktes Feld in einem Formular
    • Der Client stellt seine erste Anfrage (der Server erkennt dies, da der Client kein Token hat)
    • Der Server sendet seine Antwort (ein Formular) und platziert das Token in einem versteckten Feld darin. Zu diesem Zeitpunkt wird die Verbindung geschlossen (der Client verlässt die Sitzung mit seinem Token). Der Server hat möglicherweise Informationen mit diesem Token verknüpft.
    • Der Client stellt eine zweite Anfrage, indem er das Formular erneut absendet. Der Server ruft das Token aus dem Formular ab. Er kann dann die zweite Anfrage des Clients bearbeiten, indem er über das Token auf die Informationen zugreift, die während der ersten Anfrage berechnet wurden. Neue Informationen werden der mit dem Token verknüpften Datei hinzugefügt, eine zweite Antwort wird an den Client gesendet, und die Verbindung wird ein zweites Mal geschlossen. Das Token wurde wieder in das Antwortformular eingefügt, damit der Benutzer es bei seiner nächsten Anfrage vorlegen kann.
    • und so weiter...

Der größte Nachteil dieser Technik besteht darin, dass das Token in ein Formular eingefügt werden muss. Wenn die Antwort des Servers kein Formular ist, kann die Methode mit dem versteckten Feld nicht mehr verwendet werden.

  • Die Cookie-Methode
    • Der Client stellt seine erste Anfrage (der Server erkennt dies, da der Client kein Token hat)
    • Der Server antwortet, indem er ein Cookie zu den HTTP-Headern hinzufügt. Dies geschieht mithilfe des HTTP-Befehls „Set-Cookie“:

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

wobei param1, param2 usw. Parameternamen und die entsprechenden Werte sind. Unter den Parametern befindet sich auch das Token. Sehr oft ist nur das Token im Cookie enthalten, während der Server die anderen Informationen in dem mit dem Token verknüpften Ordner speichert. Der Browser, der das Cookie empfängt, speichert es in einer Datei auf der Festplatte. Nach der Antwort des Servers wird die Verbindung geschlossen (der Client verlässt die Sitzung mit seinem Token).

  • (Fortsetzung)
    • Der Client sendet seine zweite Anfrage an den Server. Bei jeder Anfrage an einen Server überprüft der Browser alle seine Cookies, um festzustellen, ob er eines vom angefragten Server hat. Ist dies der Fall, sendet er es an den Server, immer in Form eines HTTP-Befehls – des Cookie-Befehls, dessen Syntax der des vom Server verwendeten Set-Cookie-Befehls ähnelt:

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

Unter den vom Browser gesendeten Parametern findet der Server das Token, mit dem er den Client erkennen und die damit verbundenen Informationen abrufen kann.

Dies ist die am häufigsten verwendete Form von Token. Sie hat einen Nachteil: Ein Benutzer kann seinen Browser so konfigurieren, dass er Cookies ablehnt. Solche Benutzer können dann nicht auf Webanwendungen zugreifen, die Cookies verwenden.

  • URL-Umschreibung
    • Der Client stellt seine erste Anfrage (der Server erkennt dies, da der Client kein Token hat)
    • Der Server sendet seine Antwort. Diese Antwort enthält Links, die der Benutzer verwenden muss, um die Anwendung weiter nutzen zu können. In die URL jedes dieser Links fügt der Server das Token in der Form URL;token=value ein.
    • Wenn der Benutzer auf einen der Links klickt, um die Anwendung weiter zu nutzen, sendet der Browser eine Anfrage an den Webserver, wobei die angeforderte URL (URL;token=value) in den HTTP-Headern enthalten ist. Der Server kann dann das Token abrufen.

4.2. Die Java-API für die Sitzungsverfolgung

Im Folgenden stellen wir die wichtigsten Methoden zur Sitzungsverfolgung vor:

HttpSession [HttpServletRequest].getSession()
Ruft das Session-Objekt ab, zu dem die aktuelle Anfrage gehört. Falls die Anfrage noch nicht Teil einer Sitzung war, wird eine erstellt.
String [HttpSession].getId()
Kennung der aktuellen Sitzung
long [HttpSession].getCreationTime()
Erstellungsdatum der aktuellen Sitzung (Anzahl der seit dem 1. Januar 1970, 00:00 Uhr, verstrichenen Millisekunden).
long [HttpSession].getLastAccessedTime()
Datum des letzten Zugriffs des Clients auf die Sitzung
long [HttpSession].getMaxInactiveInterval()
maximale Dauer der Inaktivität einer Sitzung in Sekunden. Nach Ablauf dieser Zeit wird die Sitzung ungültig.
[HttpSession].setMaxInactiveInterval(int duration)
Legt die maximale Dauer der Inaktivität für eine Sitzung in Sekunden fest. Nach Ablauf dieser Zeit wird die Sitzung ungültig.
boolean [HttpSession].isNew()
true, wenn die Sitzung gerade erstellt wurde
[HttpSession].setAttribute(String parameter, Object value)
Ordnet einem Parameter in einer bestimmten Sitzung einen Wert zu. Dieser Mechanismus ermöglicht es, Informationen zu speichern, die während der gesamten Sitzung verfügbar bleiben.
[HttpSession].removeAttribute(String parameter)
Entfernt den Parameter aus den Sitzungsdaten.
Object [HttpSession].getAttribute(String parameter)
gibt den Wert zurück, der dem Parameter in der Sitzung zugeordnet ist. Gibt null zurück, wenn der Parameter nicht existiert.
Enumeration [HttpSession].getAttributeNames()
listet alle Attribute der aktuellen Sitzung als Aufzählung auf
[HttpSession].invalidate()
schließt die aktuelle Sitzung. Alle damit verbundenen Informationen werden gelöscht.

4.3. Beispiel 1

Wir präsentieren ein Beispiel aus dem ausgezeichneten Buch „Programming with J2EE“, das bei Wrox erschienen ist und von Eyrolles vertrieben wird. Dieses Buch ist eine Fundgrube an hochwertigen Informationen für Entwickler von Java-Weblösungen. Die in diesem Buch als einzelnes Java-Servlet vorgestellte Anwendung wurde hier als Hauptservlet angepasst, das JSP-Seiten aufruft, um dem Client die verschiedenen möglichen Antworten anzuzeigen.

Die Anwendung heißt „sessions“ und ist in der Datei <tomcat>\conf\server.xml wie folgt konfiguriert:

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

Im oben genannten docBase-Ordner finden Sie die folgenden Elemente:

Image

Die Dateien error.jsp, invalid.jsp und valid.jsp gehören alle zur Anwendung „sessions“. Im oben genannten Ordner WEB-INF finden wir:

Image

Oben sehen Sie die Konfigurationsdatei web.xml für die Anwendung „sessions“. Im Ordner „classes“ finden Sie die Servlet-Klassendatei:

Image

Die Datei web.xml der Anwendung sieht wie folgt aus:

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

Das Haupt-Servlet heißt cycledevie (servlet-name) und ist mit der Datei cycledevie.class (servlet-class) verknüpft. Es hat einen Alias /cycledevie (servlet-mapping), über den es über die URL http://localhost:8080/sessions/cycledevie aufgerufen werden kann. Es verfügt über drei Initialisierungsparameter:

urlSessionValide
URL der Seite, auf der die Eigenschaften der aktuellen Sitzung angezeigt werden
urlSessionInvalid
URL der Seite, die angezeigt wird, nachdem die aktuelle Sitzung ungültig gemacht wurde
urlErreur
URL der Seite, die im Falle eines Initialisierungsfehlers für das Haupt-Servlet angezeigt wird cycledevie

Die Komponenten der Sessions-Anwendung sind wie folgt:

Lebenszyklus
Haupt-Servlet – analysiert die Client-Anfrage:
  • Wenn diese Seite Teil einer Sitzung ist, übergibt sie die Kontrolle an die Seite „valide.jsp“, die die Sitzungsdetails anzeigt. Von dieser Seite aus kann der Benutzer:
    • sie neu laden
    • sie ungültig machen
  • Wenn die Anfrage die Ungültigmachung der aktuellen Sitzung verlangt, übergibt das Servlet die Kontrolle an die Seite „invalide.jsp“, die den Benutzer auffordert, eine neue Sitzung zu erstellen
  • Wenn das Servlet während der Initialisierung auf Fehler stößt, übergibt es die Kontrolle an die Seite error.jsp, die eine Fehlermeldung anzeigt.
valide.jsp
  • zeigt die Details der aktuellen Sitzung an und bietet zwei Links:
    • einen, um die Seite neu zu laden und zu sehen, wie sich der Parameter für den letzten Zugriff auf die aktuelle Sitzung ändert
    • den anderen, um die aktuelle Sitzung zu ungültig zu machen
invalid.jsp
wird angezeigt, wenn der Benutzer die aktuelle Sitzung ungültig gemacht hat. Es wird dann angeboten, eine neue Sitzung zu erstellen.
error.jsp
wird angezeigt, wenn das Haupt-Servlet während der Initialisierung auf Fehler stößt.

Der Lebenszyklus des Haupt-Servlets verläuft wie folgt:

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

public class cycledevie extends HttpServlet{

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

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

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

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

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

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

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

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

Beachten Sie folgende Punkte:

  • In seiner Initialisierungsmethode ruft das Servlet seine drei Parameter ab
  • Bei der Verarbeitung (doGet) einer Anfrage führt das Servlet folgende Schritte aus:
    • zuerst, ob bei der Initialisierung keine Fehler aufgetreten sind. Falls doch, leitet es auf die Seite error.jsp weiter.
    • überprüft den Wert des Parameters „action“. Wenn dieser Parameter den Wert „invalid“ hat, leitet das Servlet zur Seite „invalid.jsp“ weiter; andernfalls zur Seite „valid.jsp“.

Die JSP-Seite **valide.jsp** zeigt die Eigenschaften der aktuellen Sitzung an:

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

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

Beachten Sie, dass in der Zeile

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

verwenden wir ein Session-Objekt, das wie aus dem Nichts auftaucht. Tatsächlich handelt es sich bei diesem Objekt um eines der impliziten Objekte, die JSP-Seiten zur Verfügung stehen, genau wie die Objekte request, response, out, config (ServletConfig) und context (ServletContext), die wir bereits kennengelernt haben. Die beiden Links auf der Seite verweisen auf das zuvor vorgestellte Lifecycle-Servlet:

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

Der Link zum Ungültigmachen der Sitzung enthält den Parameter action=invalidate, der es dem Lifecycle-Servlet ermöglicht zu erkennen, dass der Benutzer die aktuelle Sitzung ungültig machen möchte. Der andere Link lädt die Seite neu. Um zu verhindern, dass der Browser die Seite aus dem Cache abruft, wird die HTML-Anweisung:

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

verwendet. Sie weist den Browser an, den Cache für die empfangene Seite nicht zu verwenden.

Die Seite invalidate.jsp sieht wie folgt aus:

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

Sie enthält einen Link zum cycledevie-Servlet ohne den Parameter „action“. Dieser Link veranlasst das cycledevie-Servlet, eine neue Sitzung zu erstellen.

Die Seite error.jsp sieht wie folgt aus:

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

Seine Aufgabe ist es, die Fehlermeldung anzuzeigen, die ihm vom Lifecycle-Servlet gesendet wurde. Sehen wir uns nun einige Ausführungsbeispiele an. Das Servlet wird zum ersten Mal aufgerufen:

Image

Die obige Seite zeigt an, dass wir uns in einer neuen Sitzung befinden. Wir verwenden den Link „Seite neu laden“:

Image

Das vorherige Ergebnis zeigt an, dass wir uns noch in derselben Sitzung wie auf der vorherigen Seite befinden (gleiche ID). Beachten Sie, dass sich der Zeitpunkt des letzten Zugriffs auf diese Sitzung geändert hat. Verwenden wir nun den Link „Sitzung ungültig machen“:

Image

Beachten Sie die URL dieser neuen Seite mit dem Parameter action=invalidate. Verwenden wir den Link „Neue Sitzung erstellen“, um eine neue Sitzung zu erstellen:

Image

Wir können sehen, dass eine neue Sitzung gestartet wurde. In den vorherigen Beispielen basiert die Sitzung auf dem Cookie-Mechanismus. Deaktivieren wir nun die Cookies in unserem Browser und wiederholen wir die Tests. Die folgenden Beispiele wurden mit Netscape Communicator durchgeführt. Aus unerklärlichen Gründen lieferten Tests mit IE6 unerwartete Ergebnisse, als ob IE6 weiterhin Cookies verwendet hätte, obwohl diese deaktiviert waren. Das cycledevie-Servlet wird zum ersten Mal angefordert:

Image

Wir verwenden nun den Link „Seite neu laden“:

Image

Wir können zwei Dinge feststellen:

  • Die Sitzungs-ID hat sich geändert
  • das Servlet erkennt die Sitzung als neue Sitzung

Der Tomcat-Server bietet eine Lösung für das Problem von Benutzern, die Cookies in ihrem Browser deaktivieren. Er nutzt zwei Mechanismen, um das am Anfang dieses Absatzes erwähnte Token zu implementieren: Cookies und URL-Umschreibung. Wenn das Sitzungs-Cookie nicht verfügbar ist, versucht er, das Token aus der vom Client angeforderten URL abzurufen. Damit dies funktioniert, muss die URL das Token enthalten. Generell müssen alle in einem HTML-Dokument generierten Links, die auf die Webanwendung verweisen, das Token der Anwendung enthalten. Dies kann mithilfe der Methode encodeURL erfolgen:

String [HttpResponse].encodeURL(String URL)
fügt das aktuelle Sitzungstoken der als Parameter übergebenen URL in der Form URL;jsessionid=xxxx hinzu

Wir ändern unsere Anwendung wie folgt:

  • Im Servlet „cycledevie.java“ werden die URLs verschlüsselt:
             // we hand over to the error page
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
             // we hand over to the urlSessionInvalide url
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
         // we hand over to the urlSessionInvalide url
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • Auf der Seite valide.jsp werden die URLs kodiert:
<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
  // encodage URL cycledevie
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");  
%>
............
      <br><a href="<%= URLcycledevie %>?action=invalider">Invalider la session</a>
      <br><a href="<%= URLcycledevie %>">Recharger la page</a>
  • Auf der Seite „invalide.jsp“ sind die URLs verschlüsselt:
<%
    // jspservice - on invalide la session en cours
  session.invalidate();
  // encodage URL cycledevie
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>  
..........
    <a href="<%= URLcycledevie %>">Créer une nouvelle session</a>

Nun sind wir bereit für den Test. Wir verwenden Netscape 4.5 und Cookies sind deaktiviert. Wir rufen das cycledevie-Servlet zum ersten Mal auf:

Image

und laden die Seite über den Link „Seite neu laden“ neu:

Image

Wir sehen, dass:

  • sich die Sitzung nicht geändert hat (gleiche ID)
  • die URL des cycledevie-Servlets tatsächlich das Token enthält, wie im Feld „Adresse“ oben zu sehen ist
  • der Tomcat-Server daher das Session-Token aus der angeforderten URL abruft (sofern der Entwickler darauf geachtet hat, es zu kodieren).

4.4. Beispiel 2

Wir zeigen nun ein Beispiel, wie Informationen in der Sitzung eines Clients gespeichert werden. Hier handelt es sich bei der einzigen Information um einen Zähler, der jedes Mal erhöht wird, wenn der Benutzer die URL des Servlets aufruft. Beim ersten Aufruf erscheint die folgende Seite:

Image

Wenn Sie oben auf den Link „Seite neu laden“ klicken, erhalten Sie die folgende neue Seite:

Image

Die Anwendung besteht aus drei Komponenten:

  • ein Servlet, das die Anfrage des Clients verarbeitet
  • eine JSP-Seite, die den Zählerstand anzeigt
  • eine JSP-Seite, die eventuelle Fehler anzeigt

Diese drei Komponenten sind in der bereits verwendeten Webanwendung „sessions“ installiert. Die Datei web.xml wurde geändert, um die neuen Servlets zu konfigurieren:

<?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>
  • Das Servlet heißt „counter“ (servlet-name) und ist mit der Klassendatei „counter.class“ (servlet-class) verknüpft
  • Es verfügt über zwei Initialisierungsparameter:
    • displayCounterURL: URL der JSP-Seite, auf der der Zähler angezeigt wird
    • urlErreur: URL der JSP-Seite, auf der etwaige Fehler angezeigt werden
  • sowie einen Alias /compteur, was bedeutet, dass es über die URL http://localhost:8080/sessions/compteur aufgerufen wird

Das Servlet compteur.java sieht wie folgt aus:

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

public class compteur extends HttpServlet{

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

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

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

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

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

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

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

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

Dieses Servlet hat dieselbe Struktur wie die Servlets, die wir bereits kennengelernt haben. Beachten Sie die Handhabung des Zählers:

  • Die Sitzung wird über request.getSession() abgerufen
  • Der Zähler wird aus dieser Sitzung über session.getAttribute("counter") abgerufen
  • Wird ein Nullwert abgerufen, bedeutet dies, dass die Sitzung gerade erst begonnen hat. Der Zähler wird dann auf 0 gesetzt.
  • Der Zähler wird erhöht, wieder in der Sitzung gespeichert (session.setAttribute("counter", counter)) und in die Anfrage eingefügt, die an das Anzeige-Servlet übergeben wird (request.setAttribute("counter", counter)).

Die Anzeigeseite compteur.jsp sieht wie folgt aus:

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

Die obige Seite ruft einfach das Attribut „counter“ (request.getAttribute("counter")) ab, das ihr vom Haupt-Servlet übergeben wurde, und zeigt es an.

Die Fehlerseite, *erreurcompteur.jsp,* sieht wie folgt aus:

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

4.5. Beispiel 3

Wir schlagen vor, eine Java-Anwendung zu schreiben, die als Client für die vorherige Zähleranwendung fungiert. Sie würde diese N-mal hintereinander aufrufen, wobei N als Parameter übergeben wird. Unser Ziel ist es, einen programmierten Web-Client zu demonstrieren und zu zeigen, wie Cookies verwaltet werden. Unser Ausgangspunkt wird ein generischer Web-Client sein, der im Java-Handout desselben Autors vorgestellt wird. Er wird wie folgt aufgerufen:

webclient URL GET/HEAD

  • URL: angeforderte URL
  • GET/HEAD: GET, um den HTML-Code der Seite anzufordern, HEAD, um die Antwort auf HTTP-Header zu beschränken

Hier ist ein Beispiel mit der URL http://localhost:8080/sessions/compteur:


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

Das Programm „clientweb“ zeigt alles an, was es vom Server empfängt. Oben sehen wir den HTTP-Befehl „Set-cookie“, mit dem der Server ein Cookie an seinen Client sendet. In diesem Fall enthält das Cookie zwei Informationen:

  • JSESSIONID, das ist das Sitzungstoken
  • „Path“, der die URL definiert, zu der das Cookie gehört. „Path=/sessions“ teilt dem Browser mit, dass er das Cookie jedes Mal an den Server zurücksenden muss, wenn er eine URL anfordert, die mit „/sessions“ beginnt. In der Sessions-Anwendung haben wir verschiedene Servlets verwendet, darunter die Servlets /sessions/lifecycle und /sessions/counter. Wenn wir das Servlet /sessions/cycledevie aufrufen, erhält der Browser ein J-Token. Wenn wir dann mit demselben Browser das Servlet /sessions/compteur aufrufen, sendet der Browser das J-Token an den Server zurück, da es für alle URLs gilt, die mit /sessions beginnen. In unserem Beispiel müssen die Servlets „cycledevie“ und „compteur“ nicht dasselbe Session-Token teilen. Sie hätten daher nicht in derselben Webanwendung platziert werden dürfen. Dies ist ein wichtiger Punkt, den man beachten sollte: Alle Servlets innerhalb derselben Anwendung teilen sich dasselbe Session-Token.
  • Ein Cookie kann auch eine Ablaufzeit haben. Hier fehlt diese Angabe. Das Cookie wird daher gelöscht, wenn der Browser geschlossen wird. Ein Cookie kann beispielsweise eine Ablaufzeit von N Tagen haben. Solange es gültig ist, sendet der Browser es jedes Mal zurück, wenn auf eine der URLs in seiner Domäne (Path) zugegriffen wird. Nehmen wir einen Online-CD-Shop als Beispiel. Er kann den Browsing-Pfad eines Kunden durch seinen Katalog verfolgen und nach und nach dessen Vorlieben ermitteln – beispielsweise klassische Musik. Diese Vorlieben können in einem Cookie mit einer Lebensdauer von 3 Monaten gespeichert werden. Kehrt derselbe Kunde nach einem Monat auf die Website zurück, sendet der Browser das Cookie an die Serveranwendung zurück. Anhand der im Cookie enthaltenen Informationen kann die Serveranwendung die generierten Seiten dann auf die Vorlieben des Kunden zuschneiden.

Es folgt der Web-Client-Code. Er dient später als Ausgangspunkt für einen weiteren Client.

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

public class clientweb{

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

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

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

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

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

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

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

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

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

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

Wir erstellen nun das Programm clientCounter, das wie folgt aufgerufen wird:

clientCounter URL N [JSESSIONID]

  • URL: URL des Zähler-Servlets
  • N: Anzahl der Aufrufe dieses Servlets
  • JSESSIONID: optionaler Parameter – Sitzungstoken

Der Zweck des Programms besteht darin, das Zähler-Servlet N-mal aufzurufen, indem das Sitzungs-Cookie verwaltet und jedes Mal der vom Server zurückgegebene Zählerwert angezeigt wird. Am Ende der N Aufrufe muss der Zählerwert N betragen. Hier ist ein erstes Ausführungsbeispiel:


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

Das Programm zeigt an:

  • die HTTP-Header, die es an den Server sendet, in der Form -->
  • die HTTP-Header, die es empfängt
  • den Zählerstand nach jedem Aufruf

Wir können sehen, dass beim ersten Aufruf:

  • der Client kein Cookie sendet
  • der Server eines sendet

Bei nachfolgenden Anfragen:

  • sendet der Client systematisch das Cookie zurück, das er bei der ersten Anfrage vom Server erhalten hat. Dadurch kann der Server ihn erkennen und seinen Zähler erhöhen.
  • Der Server sendet kein Cookie mehr

Wir führen das vorherige Programm erneut aus und übergeben dabei das oben genannte Token als dritten Parameter:


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

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

Hier sehen wir, dass der Server, sobald der Client seine erste Anfrage stellt, ein gültiges Sitzungscookie erhält. Es ist wichtig zu beachten, dass bei Tomcat die standardmäßige maximale Inaktivitätszeit für eine Sitzung 20 Minuten beträgt (dies ist tatsächlich konfigurierbar). Wenn die zweite Anfrage des Programms das bei der ersten Anfrage empfangene Cookie schnell genug übermittelt, behandelt der Server dies als dieselbe Sitzung. Dies weist auf eine potenzielle Sicherheitslücke hin. Wenn ich in der Lage bin, ein Session-Token im Netzwerk abzufangen, kann ich mich als der Benutzer ausgeben, der die Sitzung initiiert hat. In unserem Beispiel stellt der erste Aufruf den Benutzer dar, der die Sitzung initiiert (möglicherweise mit einem Benutzernamen und einem Passwort, die ihm das Recht zum Erhalt eines Tokens gewähren), und der zweite Aufruf stellt den Benutzer dar, der das Session-Token aus dem ersten Aufruf „gehackt“ hat. Wenn es sich bei der aktuellen Operation um eine Banktransaktion handelt, kann dies sehr problematisch werden...

Der Client-Code lautet wie folgt:

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

public class clientCompteur{

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Schauen wir uns die wichtigsten Punkte dieses Programms einmal genauer an:

  • Wir müssen N Client-Server-Austausche durchführen. Deshalb befinden sie sich in einer Schleife
            for(int i=0;i<N;i++){
  • Bei jedem Austausch baut der Client eine TCP/IP-Verbindung zum Server auf. Sobald die Verbindung hergestellt ist, sendet er die HTTP-Header seiner Anfrage an den Server:
                // on demande l'URL - envoi des entêtes HTTP
                envoie(OUT,"GET " + path + query + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                if(! JSESSIONID.equals("")){
                    envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
                }
                envoie(OUT,"Connection: close");
                envoie(OUT,"");

Wenn das JSESSIONID-Token verfügbar ist, wird es als Cookie gesendet; andernfalls nicht.

  • Sobald die Anfrage gesendet wurde, wartet der Client auf die Antwort des Servers. Zunächst überprüft er die HTTP-Header dieser Antwort, um nach einem möglichen Cookie zu suchen. Um es zu finden, vergleicht er die empfangenen Zeilen mit dem regulären Ausdruck des Cookies:
         // the model searched in HTTP headers
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // we read the response through to the end of the headers, looking for any cookies
                while((réponse=IN.readLine())!=null){
                     // follow-up response
                    System.out.println(réponse);
                     // empty line?
                    if(réponse.equals("")) break;
                     // line HTTP not empty
                     // if you don't have the session token, look for it
                    if (JSESSIONID.equals("")){
                        // compare the HTTP line with the cookie template
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // we found the cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • Sobald das Token beim ersten Mal gefunden wurde, wird bei nachfolgenden Aufrufen des Servers nicht mehr danach gesucht. Nachdem die HTTP-Header der Antwort verarbeitet wurden, gehen wir zum HTML-Code derselben Antwort über. Darin suchen wir nach der Zeile, die den Zählerwert angibt. Diese Suche wird ebenfalls mithilfe eines regulären Ausdrucks durchgeführt:
         // the counter model searched for in the HTML code
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                 // that's it for HTTP headers - move on to HTML code
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // does the current line contain the counter?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // counter found - displayed
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. Beispiel 4

Im vorherigen Beispiel gibt der Web-Client das Token als Cookie zurück. Wir haben gesehen, dass er es auch innerhalb der angeforderten URL selbst in der Form URL;jsessionid=xxx zurückgeben könnte. Lassen Sie uns dies überprüfen. Das Programm clientCompteur.java wird in clientCompteur2.java umbenannt und wie folgt geändert:

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

Der Client fordert daher die Zähler-URL über GET URL;jsessionid=xx HTTP/1.1 an und sendet kein Cookie mehr. Dies ist die einzige Änderung. Hier sind die Ergebnisse eines ersten Aufrufs:


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

Bei der ersten Anfrage fordert der Client die URL ohne Sitzungstoken an. Der Server antwortet, indem er das Token sendet. Der Client fordert dann dieselbe URL erneut an und hängt das empfangene Token an. Wir können sehen, dass der Zähler erhöht wird, was beweist, dass der Server korrekt erkannt hat, dass es sich um dieselbe Sitzung handelt.

4.7. Beispiel 5

Dieses Beispiel zeigt eine Anwendung, die aus drei Seiten besteht, die wir page0, page1 und page2 nennen. Der Benutzer muss sie in dieser Reihenfolge aufrufen:

  • Seite0 ist ein Formular, das Informationen abfragt: einen Namen
  • page1 ist ein Formular, das als Antwort auf die Übermittlung des Formulars auf page0 empfangen wird. Es fordert eine zweite Information an: ein Alter
  • Seite2 ist ein HTML-Dokument, das den von Seite0 erhaltenen Namen und das von Seite1 erhaltene Alter anzeigt.

Hier finden drei Client-Server-Austausche statt:

  • Beim ersten Austausch wird das Formular auf Seite 0 vom Client angefordert und vom Server gesendet
  • Im zweiten Austausch wird das Formular von Seite 1 vom Client angefordert und vom Server gesendet. Der Client sendet den Namen an den Server.
  • Im dritten Austausch wird das Dokument „page3“ vom Client angefordert und vom Server gesendet. Der Client sendet das Alter an den Server. Das Dokument „page3“ muss den Namen und das Alter anzeigen. Der Name wurde vom Server im zweiten Austausch abgerufen und ist seitdem „vergessen“ worden. Eine Sitzung wird verwendet, um den Namen aus Austausch 2 zu speichern, damit er während Austausch 3 verfügbar ist.

Das im ersten Austausch erhaltene Dokument „page0“ lautet wie folgt:

Image

Wir füllen das Namensfeld aus:

Image

Wir klicken auf die Schaltfläche „Weiter“ und gelangen dann zur folgenden Seite1:

Image

Wir füllen das Feld „Alter“ aus:

Image

Wir klicken auf die Schaltfläche „Weiter“ und gelangen dann zur folgenden Seite 2:

Image

Wenn Seite 0 an den Server gesendet wird, gibt der Server möglicherweise einen Fehlercode zurück, wenn das Namensfeld leer ist:

Image

Wenn Sie Seite 1 an den Server senden, gibt dieser möglicherweise einen Fehlercode zurück, wenn das Alter ungültig ist:

Image

Die Anwendung besteht aus einem Servlet und vier JSP-Seiten:

page0.jsp
zeigt page0 an
page1.jsp
zeigt Seite1 an
page2.jsp
zeigt Seite2 an
error.jsp
zeigt eine Fehlerseite an

Die Webanwendung heißt „suitedepages“ und ist in der Datei „server.xml“ von Tomcat wie folgt konfiguriert:

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

Die Konfigurationsdatei web.xml für die Anwendung „suitedepages“ sieht wie folgt aus:

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

Das Haupt-Servlet heißt main und ist dank seines Alias (servlet-mapping) über die URL http://localhost:8080/suitedepages/main* erreichbar. Es verfügt über vier Initialisierungsparameter, bei denen es sich um die URLs der vier JSP-Seiten handelt, die für die verschiedenen Ansichten verwendet werden. Der Code für das main*-Servlet lautet wie folgt:

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

public class main extends HttpServlet{

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

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

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

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

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

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

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

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

     //-------- step2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // retrieve the name from the session
        String nom=(String)session.getAttribute("nom");
         // name positioned?
        if(nom==null) étape0(request,response,session);
         // we put it in a query attribute
        request.setAttribute("nom",nom);
         // the age is retrieved from the query
        String age=request.getParameter("age");
        // age positioned?
        if(age==null){
            // back to page 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // the age is stored in the query
        age=age.trim();
        request.setAttribute("age",age);
        // valid age?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // this is a mistake
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // put the errors in the query
            request.setAttribute("erreurs",erreurs);
             // back to page 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // age valid - page 2 is presented
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • Die Methode init ruft die vier Initialisierungsparameter ab und gibt eine Fehlermeldung aus, falls einer davon fehlt
  • Wir haben gesehen, dass die Anfrage aus drei Schritten besteht. Um den aktuellen Stand dieser Schritte zu verfolgen, enthalten die Formulare page0 und page1 eine versteckte Variable „etape“ mit dem Wert 1 (page0) oder 2 (page1). Diese Zahl kann als Nummer der als Nächstes anzuzeigenden Seite interpretiert werden. In der doGet-Methode wird dieser Parameter aus der Anfrage abgerufen, und je nach seinem Wert wird die Verarbeitung an drei andere Methoden delegiert:
    • étape0 verarbeitet die ursprüngliche Anfrage und sendet page0
    • étape1 verarbeitet das Formular auf Seite 0 und sendet Seite 1 oder erneut Seite 0, falls ein Fehler aufgetreten ist
    • étape2 verarbeitet das Formular auf Seite1 und sendet Seite2 oder erneut Seite1, falls ein Fehler aufgetreten ist
  • étape0
    • zeigt Seite0 mit einem leeren Namen an
  • Schritt 1
    • Ruft den Parameter „name“ aus dem Formular auf Seite 0 ab.
    • Prüft, ob der Name existiert (nicht null ist). Falls nicht, wird Seite 0 erneut angezeigt, als wäre es der erste Aufruf.
    • prüft, ob der Name nicht leer ist. Ist dies der Fall, wird Seite0 erneut zusammen mit einer Fehlermeldung angezeigt.
    • Speichert den Namen in der aktuellen Sitzung und zeigt Seite1 an, wenn der Name gültig ist.
  • Schritt 2
    • Ruft den Parameter „name“ aus der aktuellen Sitzung ab.
    • Prüft, ob der Name existiert (nicht null ist). Falls nicht, wird Seite0 erneut angezeigt, als wäre es der erste Aufruf.
    • Ruft den Parameter „age“ aus der aktuellen Anfrage ab, die von Seite 1 gesendet wurde.
    • Prüft, ob das Alter gültig ist. Falls nicht, zeigt Seite 1 erneut mit einer Fehlermeldung an.
    • Speichert den Namen und das Alter als Anfrageattribute und zeigt Seite 2 an, wenn der Name und das Alter gültig sind.

Die Seite „page0.jsp“ sieht wie folgt aus:

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

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

<html>
  <head>
    <title>page 0</title>
  </head>
  <body>
    <h3>Page 0/2</h3>
    <form name="frmNom" method="POST" action="/suitedepages/main">
        <input type="hidden" name="etape" value="1">
      <table>
        <tr>
          <td>Votre nom</td>
          <td><input type="text" name="nom" value="<%= nom %>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <% // erreurs ?
      if (erreurs!=null){
    %>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <% for(int i=0;i<erreurs.size();i++){ %>
            <li><%= erreurs.get(i) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>
  • Die Seite „page0.jsp“ kann vom Haupt-Servlet in zwei Fällen aufgerufen werden:
    • während der ersten Anfrage
    • nach der Verarbeitung des Formulars „page0“, wenn ein Fehler auftritt
  • Der Parameter `nameToDisplay` wird vom Haupt-Servlet zusammen mit einer eventuellen Fehlerliste bereitgestellt. Das Servlet „page0.jsp“ beginnt daher damit, diese beiden Informationen abzurufen.
  • Das Formular wird mit dem versteckten Feld „etape“, das angibt, in welcher Phase der Anwendung sich der Benutzer befindet, an das Haupt-Servlet „gesendet“.

Die Seite page1.jsp sieht wie folgt aus:

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

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

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

Die Seite „page1.jsp“ hat eine ähnliche Struktur wie die Seite „page0.jsp“, mit der Ausnahme, dass sie nun zwei Attribute vom Haupt-Servlet erhält: name und age. Schließlich sieht die Seite „page2.jsp“ wie folgt aus:

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


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

Die Seite page2.jsp erhält ebenfalls die Attribute „name“ und „age“ vom Haupt-Servlet. Sie zeigt diese lediglich an. Schließlich sieht die Seite error.jsp, die für die Anzeige einer Fehlermeldung im Falle einer fehlerhaften Servlet-Initialisierung zuständig ist, wie folgt aus:

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

Es zeigt das Attribut „msgError“ an, das ihm vom Haupt-Servlet übergeben wurde.

Zusammenfassend lässt sich feststellen, dass in allen drei Phasen der Anwendung immer zuerst das Haupt-Servlet vom Browser abgefragt wird. Es ist jedoch nicht das Haupt-Servlet, das die anzuzeigende Antwort generiert, sondern eine der vier JSP-Seiten. Der Benutzer bemerkt dies nicht, da der Browser weiterhin die ursprünglich angeforderte URL – die des Haupt-Servlets – im Adressfeld anzeigt.