Skip to content

4. 会话跟踪

4.1. 问题

一个 Web 应用程序可能包含服务器与客户端之间的多次表单交互。该过程的工作原理如下:

  • 步骤 1
    • 客户端 C1 与服务器建立连接并发出初始请求。
    • 服务器向客户端 C1 发送表单 F1,并关闭在步骤 1 中建立的连接。
  • 步骤 2
    • 客户端 C1 填写表单并将其发回服务器。为此,浏览器会与服务器建立新的连接。
    • 服务器处理表单 1 中的数据,据此计算出信息 I1,将表单 F2 发送给客户端 C1,并关闭在步骤 3 中建立的连接。
  • 步骤 3
    • 步骤 3 和 4 的循环在步骤 5 和 6 中重复。在步骤 6 结束时,服务器将收到两个表单 F1 和 F2,并据此计算出信息 I1 和 I2。

当前的问题是:服务器如何跟踪与客户端 C1 相关的信息 I1 和 I2?这个问题被称为跟踪客户端 C1 的会话。为了理解其根本原因,让我们来分析一个同时为多个客户端提供服务的 TCP-IP 服务器应用程序的示意图:

在典型的 TCP-IP 客户端-服务器应用程序中:

  • 客户端与服务器建立连接
  • 通过此连接与服务器交换数据
  • 连接由双方中的一方关闭

该机制的两个关键点是:

  1. 为每个客户端建立一个单独的连接
  2. 该连接将贯穿服务器与其客户端对话的整个过程

服务器之所以能在任何时刻识别正在交互的客户端,正是因为连接——或者说“通道”——将服务器与客户端紧密相连。由于该通道专用于特定客户端,因此通过该通道接收的所有数据均来自该客户端,而通过该通道发送的所有数据也都能送达该客户端。

HTTP 客户端-服务器机制与上述模型非常相似,唯一的区别在于客户端与服务器的交互仅限于一次往返通信

  • 客户端向服务器建立连接并发出请求
  • 服务器发送响应并关闭连接

如果在时间 T1,客户端 C 向服务器发出请求,它将获得连接 C1,该连接将用于单次请求-响应交互。如果在时间 T2,同一客户端向服务器发出第二次请求,它将获得连接 C2,该连接与连接 C1 不同。 对于服务器而言,用户 C 的第二次请求与其初始请求并无区别:在两种情况下,服务器都将该客户端视为新客户端。若要建立客户端 C 与服务器之间不同连接的关联,客户端 C 必须被服务器“识别”为“常客”,且服务器必须检索其关于该常客的信息。

让我们设想一个按以下方式运行的系统:

  • 存在一个单一队列
  • 设有多个服务窗口。这意味着可以同时为多名顾客提供服务。当某个窗口有空位时,顾客会离开队伍前往该窗口接受服务
  • 如果这是客户首次到访,柜员会给他们一张印有编号的号牌。客户只能提出一个问题。一旦得到答复,他们必须离开柜台窗口并回到队尾。柜员会将客户的信息记录在标有该编号的文件中。
  • 当轮到该客户再次办理业务时,可能由与上次不同的柜员接待。柜员会索要其代币,并调出与该代币号对应的档案。客户再次提出请求,获得答复,相关信息会被添加到其档案中。
  • 如此循环往复……随着时间推移,客户将获得所有请求的答复。通过代币及其关联的档案,实现了不同请求之间的关联追踪。

客户端-服务器Web应用程序中的会话跟踪机制工作原理与此类似:

  • 客户端发出首次请求时,Web 服务器会向其发放一个令牌
  • 客户端将在后续每次请求中提交该令牌以进行身份验证

令牌可以采用多种形式:

  • 表单中的隐藏字段
    • 客户端发起首次请求(服务器能识别该请求,因为客户端此时没有令牌)
    • 服务器发送响应(一个表单),并将令牌放置在表单内的隐藏字段中。此时,连接被关闭(客户端带着令牌退出会话)。服务器可能已将相关信息与该令牌关联起来。
    • 客户端通过重新提交表单发起第二次请求。服务器从表单中提取令牌。随后,服务器可通过该令牌访问首次请求期间计算出的信息,从而处理客户端的第二次请求。 新信息被添加到与该令牌关联的文件中,第二份响应被发送给客户端,连接第二次关闭。令牌已被放回响应表单中,以便用户在下次请求时能够提供它。
    • 以此类推……

此技术的主要缺点在于令牌必须放置在表单中。如果服务器的响应不是表单,则无法再使用隐藏字段方法。

  • Cookie 方法
    • 客户端发出首次请求(服务器通过客户端未持有令牌来识别此请求)
    • 服务器通过在 HTTP 头部添加一个 Cookie 进行响应。这通过 HTTP Set-Cookie 命令实现:

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

其中 param1、param2 等为参数名称及其对应值。这些参数中将包含令牌。通常情况下,Cookie 中仅包含令牌,服务器会将其他信息存储在与该令牌关联的文件夹中。接收该 Cookie 的浏览器会将其存储在磁盘上的文件中。服务器响应后,连接即被关闭(客户端携带着其令牌退出会话)。

  • (待续)
    • 客户端向服务器发出第二次请求。每次向服务器发送请求时,浏览器都会检查其拥有的所有 Cookie,以确认是否包含来自该服务器的 Cookie。如果存在,则将其发送给服务器,形式始终为 HTTP 命令——即 Cookie 命令,其语法与服务器使用的 Set-Cookie 命令类似:

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

在浏览器发送的参数中,服务器将找到一个令牌,该令牌使其能够识别客户端并检索与其关联的信息。

这是最常用的令牌形式。它有一个缺点:用户可以配置浏览器拒绝Cookie。这样,这些用户就无法访问使用Cookie的Web应用程序。

  • URL 重写
    • 客户端发出首次请求(服务器能识别出这一点,因为客户端没有令牌)
    • 服务器发送响应。该响应包含用户必须使用的链接,以便继续使用应用程序。在每个链接的 URL 中,服务器都会以 URL;token=value 的形式添加令牌。
    • 当用户点击其中一个链接以继续使用应用程序时,浏览器会向 Web 服务器发送请求,并在 HTTP 头中包含所请求的 URL(URL;token=value)。随后,服务器便能检索到该令牌。

4.2. 用于会话跟踪的 Java API

下面我们将介绍几种用于会话跟踪的主要方法:

HttpSession [HttpServletRequest].getSession()
获取当前请求所属的 Session 对象。如果该请求尚未属于任何会话,则会创建一个会话。
String [HttpSession].getId()
当前会话的标识符
long [HttpSession].getCreationTime()
当前会话的创建时间(自 1970 年 1 月 1 日 00:00 起经过的毫秒数)。
long [HttpSession].getLastAccessedTime()
客户端上次访问该会话的日期
long [HttpSession].getMaxInactiveInterval()
会话最长闲置时间(以秒为单位)。超过此时间后,会话将失效。
[HttpSession].setMaxInactiveInterval(int duration)
设置会话的最大闲置时长(以秒为单位)。超过此时间后,会话将失效。
boolean [HttpSession].isNew()
如果会话刚刚创建,则返回 true
[HttpSession].setAttribute(String parameter, Object value)
将一个值与指定会话中的参数关联。此机制允许存储在整个会话期间始终可用的信息。
[HttpSession].removeAttribute(String parameter)
从会话数据中移除该参数
Object [HttpSession].getAttribute(String parameter)
返回会话中与参数关联的值。若参数不存在,则返回 null
Enumeration [HttpSession].getAttributeNames()
将当前会话的所有属性作为枚举列表返回
[HttpSession].invalidate()
关闭当前会话。与之关联的所有信息将被销毁。

4.3. 示例 1

我们提供一个摘自Wrox出版、Eyrolles发行的优秀著作《J2EE编程》的示例。该书是Java Web解决方案开发人员获取高级知识的宝库。书中作为单个Java Servlet介绍的应用程序,在此已被改编为一个主Servlet,该Servlet通过调用JSP页面向客户端显示各种可能的响应。

该应用程序名为“sessions”,在 <tomcat>\conf\server.xml 文件中的配置如下:

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

在上述 docBase 文件夹中,您将找到以下元素:

Image

error.jspinvalid.jspvalid.jsp 这三个文件均与 sessions 应用程序相关。在上述 WEB-INF 文件夹中,我们发现:

Image

上图所示为 sessions 应用程序的 web.xml 配置文件。在 classes 文件夹中,您将找到 Servlet 类文件:

Image

该应用程序的 web.xml 文件如下:

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

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

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

主 Servlet 名为 cycledevieservlet-name),并与 cycledevie.class 文件(servlet-class)相关联。它有一个别名 /cycledevieservlet-mapping),允许通过 URL http://localhost:8080/sessions/cycledevie 调用它。它有三个初始化参数:

urlSessionValide
显示当前会话属性的页面 URL
urlSessionInvalid
当前会话失效后显示的页面 URL
urlErreur
主 Servlet 初始化错误时显示的页面 URLcycledevie

会话应用程序的组件如下:

生命周期
主Servlet - 分析客户端请求:
  • 如果该页面属于某个会话,则将控制权传递给 valide.jsp 页面,该页面会显示会话详细信息。在此页面上,用户可以:
    • 重新加载
    • 使其失效
  • 如果请求要求作废当前会话,Servlet 将控制权传递给 invalide.jsp 页面,该页面将提示用户创建新会话
  • 如果 Servlet 在初始化过程中遇到错误,它将控制权传递给 error.jsp 页面,该页面将显示一条错误消息。
valide.jsp
  • 显示当前会话的详细信息,并提供两个链接:
    • 一个用于刷新页面,查看当前会话上次访问时的参数如何变化
    • 另一个用于使当前会话失效
invalid.jsp
当用户使当前会话失效时显示。随后会提示创建一个新的会话。
error.jsp
当主 Servlet 在初始化过程中遇到错误时显示。

主Servlet的生命周期如下:

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";
        }
    }
}

请注意以下几点:

  • 在初始化方法中,Servlet 会获取其三个参数
  • 在处理(doGet)请求时,Servlet:
    • 首先检查初始化过程中是否发生错误。如果发生任何错误,则重定向至 error.jsp 页面。
    • 检查 action 参数的值。如果该参数的值为“invalid”,Servlet 将重定向至 invalid.jsp 页面;否则,将重定向至 valid.jsp 页面。

JSP 页面 **valide.jsp** 显示当前会话的特征:

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

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

请注意在该行中

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

中,我们使用了一个不知从何而来的会话对象。实际上,该对象是 JSP 页面可用的隐式对象之一,就像我们之前遇到的 requestresponseoutconfig(ServletConfig)和 context(ServletContext)对象一样。页面上的两个链接指向了前面介绍的生命周期 Servlet:

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

用于注销会话的链接包含 action=invalidate 参数,这使生命周期 Servlet 能够识别用户希望注销当前会话。另一个链接用于刷新页面。为防止浏览器从缓存中获取页面,需使用以下 HTML 指令:

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

。它指示浏览器不要对接收到的页面使用缓存。

invalidate.jsp 页面内容如下:

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

它提供了一个不带 action 参数的 cycledevie servlet 链接。此链接将导致 cycledevie servlet 创建一个新会话。

error.jsp 页面如下所示:

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

其作用是显示生命周期 Servlet 发送给它的错误消息。现在让我们来看一些执行示例。首次请求该 Servlet:

Image

上图表明我们处于一个新的会话中。我们点击“重新加载页面”链接:

Image

上一个结果表明,我们仍处于与上一页相同的会话中(ID相同)。请注意,该会话的最后访问时间已发生变化。现在,让我们点击“失效会话”链接:

Image

请注意此新页面中带有 action=invalidate 参数的 URL。现在,让我们使用“创建新会话”链接来创建一个新会话:

Image

我们可以看到一个新的会话已经开始。在之前的示例中,会话依赖于 Cookie 机制。现在让我们在浏览器中禁用 Cookie 并重复测试。以下示例是在 Netscape Communicator 上进行的。出于某些无法解释的原因,使用 IE6 进行的测试产生了意想不到的结果,仿佛 IE6 即使在禁用 Cookie 后仍继续使用它们。首次请求 cycledevie Servlet:

Image

现在我们点击“重新加载页面”链接:

Image

我们可以观察到两点:

  • 会话 ID 已发生变化
  • Servlet 将该会话识别为新会话

Tomcat 服务器为用户在浏览器中禁用 Cookie 的问题提供了解决方案。它使用两种机制来实现本段开头提到的令牌:Cookie 和 URL 重写。如果会话 Cookie 不可用,它将尝试从客户端请求的 URL 中检索令牌。 要使此机制生效,URL 必须包含该令牌。一般而言,HTML 文档中所有指向 Web 应用程序的链接都必须包含该应用程序的令牌。这可以通过调用 encodeURL 方法来实现:

String [HttpResponse].encodeURL(String URL)
该方法会将当前会话令牌添加到作为参数传递的 URL 中,格式为 URL;jsessionid=xxxx

我们将应用程序修改如下:

  • cycledevie.java Servlet 中,URL 经过了编码:
             // we hand over to the error page
            getServletContext().getRequestDispatcher(response.encodeURL(urlErreur)).forward(request,response);
....
             // we hand over to the urlSessionInvalide url
            getServletContext().getRequestDispatcher(response.encodeURL(urlSessionInvalide)).forward(request,response);
....
         // we hand over to the urlSessionInvalide url
        getServletContext().getRequestDispatcher(response.encodeURL(urlSessionValide)).forward(request,response);
  • valide.jsp 页面中,URL 经过编码:
<%
    // jspService
  // ici on est dans le cas où on doit décrire la session en cours
  String etat= session.isNew() ? "Nouvelle session" : "Ancienne session";
  // encodage URL cycledevie
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");  
%>
............
      <br><a href="<%= URLcycledevie %>?action=invalider">Invalider la session</a>
      <br><a href="<%= URLcycledevie %>">Recharger la page</a>
  • invalid.jsp 页面中,URL 经过了编码:
<%
    // jspservice - on invalide la session en cours
  session.invalidate();
  // encodage URL cycledevie
  String URLcycledevie=response.encodeURL("/sessions/cycledevie");
%>  
..........
    <a href="<%= URLcycledevie %>">Créer une nouvelle session</a>

现在我们可以进行测试了。我们使用的是 Netscape 4.5,并且已禁用 Cookie。我们首次请求 cycledevie Servlet:

Image

然后通过“重新加载页面”链接刷新页面

Image

我们可以看到:

  • 会话状态未发生变化(ID相同)
  • 如上方的“地址”字段所示,cycledevie servlet 的 URL 确实包含该令牌
  • 因此,Tomcat 服务器会从请求的 URL 中检索会话令牌(前提是开发人员已将其进行编码)。

4.4. 示例 2

下面我们通过一个示例展示如何在客户端会话中存储信息。在此示例中,唯一的信息是一个计数器,该计数器会在用户每次调用 Servlet 的 URL 时递增。首次调用时,将显示以下页面:

Image

如果您点击上方的“重新加载页面”链接,将看到以下新页面:

Image

该应用程序包含三个组件:

  • 一个处理客户端请求的 Servlet
  • 一个显示计数器值的 JSP 页面
  • 一个用于显示错误信息的 JSP 页面

这三个组件已安装在正在使用的“sessions”Web应用程序中。其 web.xml 文件已进行修改以配置新的 Servlet:

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

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

<web-app>
...
  <servlet>
      <servlet-name>compteur</servlet-name>
    <servlet-class>compteur</servlet-class>
    <init-param>
          <param-name>urlAffichageCompteur</param-name>
        <param-value>/compteur.jsp</param-value>
    </init-param>
    <init-param>
          <param-name>urlErreur</param-name>
        <param-value>/erreurcompteur.jsp</param-value>
    </init-param>
  </servlet>
...
  <servlet-mapping>
      <servlet-name>compteur</servlet-name>
    <url-pattern>/compteur</url-pattern>
  </servlet-mapping>
</web-app>
  • 该 Servlet 名为 counterservlet-name),并关联到类文件 counter.classservlet-class
  • 它有两个初始化参数:
    • displayCounterURL:显示计数器的 JSP 页面的 URL
    • urlErreur:显示错误信息的 JSP 页面的 URL
  • 并有一个别名 /compteur,这意味着它将通过 URL http://localhost:8080/sessions/compteur 被调用

compteur.java Servlet 代码如下:

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";
        }
    }
}

该 Servlet 的结构与我们之前遇到的 Servlet 相同。请注意计数器的处理方式:

  • 通过 request.getSession() 获取会话
  • 通过 session.getAttribute("counter") 从该会话中获取计数器
  • 如果获取到的值为 null,则表示会话刚刚开始。此时将计数器设置为 0。
  • 计数器被递增,存储回会话中(session.setAttribute("counter", counter)),并放入将传递给显示 Servlet 的请求中(request.setAttribute("counter", counter)

显示页面 compteur.jsp 如下所示:

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

上方的页面仅需获取主Servlet传递给它的计数器属性(request.getAttribute("counter")并将其显示出来。

错误页面 *erreurcompteur.jsp* 如下所示:

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

4.5. 示例 3

我们建议编写一个 Java 应用程序,使其作为前一个计数器应用程序的客户端。该程序将连续调用该应用程序 N 次,其中 N 作为参数传递。我们的目标是演示一个编程实现的 Web 客户端以及如何管理 Cookie。我们将以同一位作者在 Java 讲义中介绍的通用 Web 客户端作为起点。其调用方式如下:

webclient URL GET/HEAD

  • URL:请求的 URL
  • GET/HEAD:GET 用于请求页面的 HTML 代码,HEAD 用于将响应限制为仅包含 HTTP 头部

以下是一个使用 URL http://localhost:8080/sessions/compteur 的示例:


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

clientweb 程序会显示从服务器接收到的所有内容。在上文中,我们可以看到 HTTP Set-cookie 命令,服务器使用该命令向客户端发送一个 Cookie。此处的 Cookie 包含两部分信息:

  • JSESSIONID,即会话令牌
  • Path 字段定义了 Cookie 所属的 URL。Path=/sessions 表示每次浏览器请求以 /sessions 开头的 URL 时,都必须将该 Cookie 发回给服务器。 在 sessions 应用程序中,我们使用了多种 Servlet,包括 /sessions/lifecycle /sessions/counter Servlet。如果调用 /sessions/cycledevie Servlet,浏览器将收到一个 J 令牌。如果随后使用同一浏览器调用 /sessions/compteur Servlet,浏览器会将 J 令牌发回给服务器,因为该令牌适用于所有以 /sessions 开头的 URL。 在本例中,cycledevie compteur Servlet 无需共享同一会话令牌。因此,它们本不应被放置在同一个 Web 应用程序中。这一点需要牢记:同一应用程序内的所有 Servlet 共享同一会话令牌。
  • Cookie 还可以设置过期时间。此处缺少该信息,因此当浏览器关闭时,该 Cookie 将被删除。例如,Cookie 的过期时间可以设为 N 天。只要它在有效期内,每次访问其所属域名(Path)下的任一 URL 时,浏览器都会将其发回。 以一家在线CD商店为例。它可以通过追踪客户在其目录中的浏览路径,逐步确定客户的偏好——例如古典音乐。这些偏好可以存储在一个有效期为3个月的Cookie中。如果同一位客户在一个月后重返该网站,浏览器会将该Cookie发回给服务器应用程序。基于Cookie中包含的信息,服务器应用程序随后可以根据客户的偏好定制生成的页面。

以下是 Web 客户端代码。它将作为另一个客户端的起点。

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

public class clientweb{

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

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

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

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

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

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

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

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

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

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

现在我们编写 clientCounter 程序,其调用方式如下:

clientCounter URL N [JSESSIONID]

  • URL:计数器 Servlet 的 URL
  • N:调用此 Servlet 的次数
  • JSESSIONID:可选参数——会话令牌

该程序的目的是通过管理会话 Cookie 向计数器 Servlet 发起 N 次调用,并每次显示服务器返回的计数器值。在完成 N 次调用后,计数器值必须为 N。以下是一个初始的执行示例:


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

程序显示:

  • 以以下形式显示其发送给服务器的 HTTP 头部 -->
  • 接收到的 HTTP 头部
  • 每次调用后的计数器值

我们可以看到在第一次调用期间:

  • 客户端未发送 Cookie
  • 服务器发送了一个 Cookie

对于后续请求:

  • 客户端会系统性地回传在首次请求中从服务器接收到的 Cookie。正是这一点使得服务器能够识别该客户端并递增其计数器。
  • 服务器不再发送 Cookie

我们将上述令牌作为第三个参数传入,重新运行之前的程序:


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

这里我们可以看到,一旦客户端发出第一个请求,服务器就会收到一个有效的会话 Cookie。需要注意的是,对于 Tomcat 而言,会话的默认最大闲置时间为 20 分钟(实际上这是可配置的)。如果程序的第二个请求能够及时发送在第一个请求中收到的 Cookie,服务器就会将其视为同一会话。这凸显了一个潜在的安全漏洞。 如果我能在网络上截获一个会话令牌,我就能冒充发起该会话的用户。在我们的示例中,第一次调用代表发起会话的用户(可能使用具有获取令牌权限的用户名和密码),而第二次调用代表从第一次调用中“窃取”了会话令牌的用户。如果当前操作是一笔银行交易,这可能会引发严重问题……

客户端代码如下:

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

public class clientCompteur{

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

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

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

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

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

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

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

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

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

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

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

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

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

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

让我们来分析一下这个程序的关键点:

  • 我们需要进行 N 次客户端-服务器交互。这就是为什么它们被放在循环中的原因
            for(int i=0;i<N;i++){
  • 每次交互时,客户端都会与服务器建立一个 TCP/IP 连接。连接建立后,它会将请求的 HTTP 头部发送给服务器:
                // on demande l'URL - envoi des entêtes HTTP
                envoie(OUT,"GET " + path + query + " HTTP/1.1");
                envoie(OUT,"Host: " + host + ":" + port);
                if(! JSESSIONID.equals("")){
                    envoie(OUT,"Cookie: JSESSIONID="+JSESSIONID);
                }
                envoie(OUT,"Connection: close");
                envoie(OUT,"");

如果 JSESSIONID 令牌可用,则将其作为 Cookie 发送;否则,则不发送。

  • 请求发送后,客户端将等待服务器的响应。它首先检查该响应的 HTTP 头部,以查找可能存在的 Cookie。为了找到它,它会将收到的行与 Cookie 的正则表达式进行比对:
         // the model searched in HTTP headers
        Pattern modèleCookie=Pattern.compile("^Set-Cookie: JSESSIONID=(.*?);");
...........................
                 // we read the response through to the end of the headers, looking for any cookies
                while((réponse=IN.readLine())!=null){
                     // follow-up response
                    System.out.println(réponse);
                     // empty line?
                    if(réponse.equals("")) break;
                     // line HTTP not empty
                     // if you don't have the session token, look for it
                    if (JSESSIONID.equals("")){
                        // compare the HTTP line with the cookie template
                        résultat=modèleCookie.matcher(réponse);
                        if(résultat.find()){
                            // we found the cookie
                            JSESSIONID=résultat.group(1);
                        }
                    }
                }//while
  • 一旦首次找到该令牌,后续对服务器的调用将不再搜索该令牌。处理完响应的 HTTP 头部后,我们将转而分析该响应的 HTML 代码。在其中,我们寻找提供计数器值的行。此搜索同样使用正则表达式进行:
         // the counter model searched for in the HTML code
        Pattern modèleCompteur=Pattern.compile("compteur = .*?(\\d+)");
..................................
                 // that's it for HTTP headers - move on to HTML code
                compteurTrouvé=false;
                while((réponse=IN.readLine())!=null){
                     // does the current line contain the counter?
                    if (! compteurTrouvé){
                        résultat=modèleCompteur.matcher(réponse);
                        if(résultat.find()){
                            // counter found - displayed
                            System.out.println("compteur : " + résultat.group(1));
                            compteurTrouvé=true;
                        }
                    }
                }//while

4.6. 示例 4

在上一个示例中,Web 客户端将令牌作为 Cookie 返回。我们看到它也可以将令牌包含在请求的 URL 本身中,形式为 URL;jsessionid=xxx。让我们验证一下这一点。将 clientCompteur.java 程序重命名为 clientCompteur2.java,并按以下方式进行修改:

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

因此,客户端通过 GET URL;jsessionid=xx HTTP/1.1 请求计数器 URL,且不再发送 Cookie。这是唯一的更改。以下是首次调用的结果:


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

在第一次请求中,客户端未携带会话令牌请求该 URL。服务器响应时发送了令牌。随后,客户端重新请求同一 URL,并在 URL 后附加了收到的令牌。我们可以看到计数器已递增,这证明服务器正确识别出这是同一会话。

4.7. 示例 5

本例展示了一个由三个页面组成的应用程序,我们将它们分别命名为 page0page1page2。用户必须按以下顺序访问这些页面:

  • page0 是一个用于收集信息的表单:姓名
  • page1 是作为对 page0 表单提交的响应而收到的表单。它请求第二项信息:年龄
  • page2 是一个 HTML 文档,用于显示从 page0 获取的姓名以及从 page1 获取的年龄。

此处包含三次客户端与服务端之间的交互:

  • 在第一次交互中,客户端请求 page0 表单,服务器将其发送
  • 在第二次交互中,客户端请求 page1 表单,服务器将其发送。客户端将姓名发送给服务器。
  • 在第三次交互中,客户端请求 page3 文档,服务器将其发送。客户端将年龄发送给服务器。page3 文档必须显示姓名和年龄。姓名是在第二次交互中由服务器获取的,此后已被“遗忘”。使用会话来存储第二次交互中的姓名,以便在第三次交互中可用。

在第一次交互中获取的 page0 文档如下:

Image

我们填写姓名字段:

Image

我们点击“下一步”按钮,随后进入以下页面1

Image

我们填写年龄字段:

Image

我们点击“下一步”按钮,随后会看到以下页面2

Image

页面0提交至服务器时,如果“姓名”字段为空,服务器可能会返回错误代码:

Image

当您将 page1 提交至服务器时,如果年龄字段无效,服务器可能会返回错误代码:

Image

该应用程序由一个Servlet和四个JSP页面组成:

page0.jsp
显示 page0
page1.jsp
显示 page1
page2.jsp
显示 page2
error.jsp
显示错误页面

该 Web 应用程序名为 suitedepages,在 Tomcat 的 server.xml 文件中配置如下:

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

suitedepages 应用程序的 web.xml 配置文件如下:

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

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

<web-app>
    <servlet>
      <servlet-name>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>

主 Servlet 命名为 main,并通过其别名(servlet-mapping)可通过 URL http://localhost:8080/suitedepages/main* 访问。它有四个初始化参数,即用于不同视图的四个 JSP 页面的 URL。main* Servlet 的代码如下:

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

public class main extends HttpServlet{

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

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

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

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

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

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

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

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

     //-------- step2
    public void étape2(HttpServletRequest request, HttpServletResponse response, HttpSession session)
            throws IOException, ServletException{
         // retrieve the name from the session
        String nom=(String)session.getAttribute("nom");
         // name positioned?
        if(nom==null) étape0(request,response,session);
         // we put it in a query attribute
        request.setAttribute("nom",nom);
         // the age is retrieved from the query
        String age=request.getParameter("age");
        // age positioned?
        if(age==null){
            // back to page 1
            request.setAttribute("age","");
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // the age is stored in the query
        age=age.trim();
        request.setAttribute("age",age);
        // valid age?
        if(! Pattern.matches("^\\s*\\d+\\s*$",age)){
            // this is a mistake
            ArrayList erreurs=new ArrayList();
            erreurs.add("Age invalide");
            // put the errors in the query
            request.setAttribute("erreurs",erreurs);
             // back to page 1
            request.getRequestDispatcher(urlPage1).forward(request,response);
        }
         // age valid - page 2 is presented
        request.getRequestDispatcher(urlPage2).forward(request,response);
    }
}
  • init 方法会获取四个初始化参数,如果其中任何一个缺失,则设置一条错误信息
  • 我们已经看到,请求由三个交互步骤组成。为了跟踪这些交互的当前阶段,page0 page1 表单中包含一个名为 "etape" 的隐藏变量,其值为 1(page0)或 2(page1)。该数字可解释为待显示的下一页的编号。 在 doGet 方法中,该参数从请求中获取,并根据其值将处理委托给其他三个方法:
    • étape0 处理初始请求并发送 page0
    • étape1 处理 page0 上的表单,若发生错误则发送 page1 或再次发送 page0
    • étape0 处理 page1 上的表单,并发送 page2;若发生错误,则再次发送 page1
  • étape0
    • 显示一个名称为空的 page0
  • 步骤 1
    • page0 表单中检索“name”参数。
    • 检查名称是否存在(不为空)。如果不存在,则再次显示 page0,如同首次调用一样。
    • 检查名称是否为空。如果是,则再次显示 page0 并附带一条错误消息。
    • 将名称存储在当前会话中,如果名称有效,则显示 page1
  • 步骤2
    • 从当前会话中检索 name 参数。
    • 检查名称是否存在(不为空)。如果不存在,则再次显示 page0,如同首次调用一样。
    • page1 发送的当前请求中检索 age 参数。
    • 检查 age 是否有效。如果无效,则再次显示 page1 并显示一条错误消息。
    • 将 name 和 age 作为请求属性存储,如果 name 和 age 有效,则显示 page2

page0.jsp 页面内容如下:

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

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

<html>
  <head>
    <title>page 0</title>
  </head>
  <body>
    <h3>Page 0/2</h3>
    <form name="frmNom" method="POST" action="/suitedepages/main">
        <input type="hidden" name="etape" value="1">
      <table>
        <tr>
          <td>Votre nom</td>
          <td><input type="text" name="nom" value="<%= nom %>"></td>
        </tr>
      </table>
      <input type="submit" value="Suite">
    </form>
    <% // erreurs ?
      if (erreurs!=null){
    %>
      <hr>
      <font color="red">
        Les erreurs suivantes se sont produites
        <ul>
        <% for(int i=0;i<erreurs.size();i++){ %>
            <li><%= erreurs.get(i) %>
        <% }//for %>
        </ul>
     <% }//if %>
  </body>
</html>
  • 在以下两种情况下,主Servlet可以调用page0.jsp页面:
    • 在初始请求期间
    • 在处理 page0 表单后发生错误时
  • 主 Servlet 会提供 `nameToDisplay` 参数以及任何错误列表。因此,page0.jsp Servlet 首先会检索这两项信息。
  • 表单通过隐藏字段“etape”提交至主 Servlet,该字段用于标识用户当前处于应用程序的哪个阶段。

page1.jsp 页面内容如下:

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

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

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

page1.jsp 页面的结构与 page0.jsp 页面类似,不同之处在于它现在从主 Servlet 接收两个属性:nameage。最后,page2.jsp 页面如下所示:

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


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

page2.jsp 页面也会从主 Servlet 接收 nameage 属性,并简单地显示它们。最后,error.jsp 页面负责在 Servlet 初始化错误时显示错误信息,其代码如下:

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

它显示由主Servlet传递给它的msgError属性。

综上所述,我们可以看到,在应用程序的三个阶段中,浏览器总是首先查询主Servlet。然而,生成待显示响应的并非主Servlet,而是四个JSP页面中的一个。用户不会察觉这一点,因为浏览器会在其“地址”字段中继续显示最初请求的URL——即主Servlet的URL。