
3.7 会话管理
3.1节讲过HTTP协议是一种无状态的协议,客户端每次打开一个Web页面,它就会与服务器建立一个新的连接,发送一个新的请求到服务器,服务器处理客户端的请求,返回响应到客户端,并关闭与客户端建立的连接。当客户端发起新的请求,那么它重新与服务器建立连接,因此服务器并不记录关于客户的任何信息。但是对于许多Web应用而言,服务器往往需要记录特定客户端与服务器之间的一系列请求响应之间的特定信息。例如,一个在线网上商店需要记录在线客户的个人信息、添加到购物车中的商品信息等。如果顾客每打开一个新的页面都需要重新输入登录信息确认身份,那么这个网上商店可能只能关门大吉了。从特定客户端到服务器的一系列请求称为会话。在Web服务器看来,一个会话是由在一次浏览过程中所发出的全部HTTP请求组成的。换句话说,一次会话是从客户打开浏览器开始到关闭浏览器结束。记录会话信息的技术称为会话跟踪,对于开发人员而言会话跟踪不是容易解决的问题。会话跟踪的第一个障碍是如何唯一标识每一个客户会话。这只能通过为每一个客户分配一个某种标识,并将这些标识保存在客户端上,以后客户端发给服务器的每一个HTTP请求都提供这些标识来实现。那么为什么不能用客户端的IP地址作为标识呢?这是因为在一台客户端上可能同时发出多个不同的客户请求,而且,如果多个不同客户请求还可能是通过同一个代理服务器发出的,因此IP地址不能作为唯一标识。
常见会话跟踪技术有Cookie和URL重写等。
3.7.1 Cookie
Cookie是一小块可以嵌入到HTTP请求和响应中的数据。典型情况下,Web服务器将Cookie值嵌入到响应的Header,而浏览器则在其以后的请求中都将携带同样的Cookie。Cookie的信息中可以有一部分用来存储会话ID,这个ID被服务器用来将某些HTTP请求绑定在会话中。Cookie由浏览器保存在客户端,通常保存为一个文本文件。Cookie还含有一些其他属性,诸如可选的注释、版本号及最长生命期。
为加深对Cookie的理解,下面创建一个Servlet来显示Cookie的相关信息。代码如程序3-22所示。
程序3-22:CookieServlet.java
package com.servlet; … @WebServlet(name=" CookieServlet ", urlPatterns={"/cookie"}) public class CookieServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Cookie cookie = null; //获取请求相关的cookie Cookie[] cookies = request.getCookies( ); boolean newCookie = false; //判断Cookie ServletStudy是否存在 if (cookies ! = null){ for (int i = 0; i < cookies.length; i++){ if (cookies[i].getName( ).equals("Chapter3")){ cookie= cookies[i]; } }//end for }//end if if (cookie == null){ newCookie=true; int maxAge=10000; //生成cookie对象 cookie= new Cookie("Chapter3", "create by hyl"); cookie.setPath(request.getContextPath( )); cookie.setMaxAge(maxAge); response.addCookie(cookie); }//end if // 显示信息 response.setContentType("text/html"); java.io.PrintWriter out = response.getWriter( ); out.println("<html>"); out.println("<head>"); out.println("<title>Cookie Info</title>"); out.println("</head>"); out.println("<body>"); out.println( "<h2> Information about the cookie named \"Chapter3\"</h2>"); out.println("Cookie value: "+cookie.getValue( )+"<br>"); if (newCookie){ out.println("Cookie Max-Age: "+cookie.getMaxAge( )+"<br>"); out.println("Cookie Path: "+cookie.getPath( )+"<br>"); } out.println("</body>"); out.println("</html>"); } }
程序说明:HttpServletRequest对象有一个getCookies方法,它可以返回当前请求中的Cookie对象的一个数组。程序首先调用getCookies方法获得request对象中的所有Cookie,然后寻找是否有名为Chapter3的Cookie。如果有,则调用Cookie对象的getValue、getName等方法显示其信息;如果没有,则创建一个新的Cookie对象,并调用response.addCookie方法将其加入到response对象并返回到客户端。以后客户端对服务器的任何访问都会在其头部携带此Cookie。可以通过刷新页面来查看Cookie的信息,可以看到显示的Cookie信息是不变的。
重新发布Web应用并启动浏览器,在地址栏中输入http://localhost:8080/Chapter3/cookie,得到如图3-27所示的运行结果页面。

图3-27 显示Cookie信息
由于同一客户端对服务器的请求都会携带Cookie,因此可以通过在Cookie中添加与会话相关的信息以达到会话跟踪的目的。下面通过创建一个Servlet来演示如何通过Cookie实现会话跟踪。代码如程序3-23所示。
程序3-23:CookieTrackServlet.java
… @WebServlet(name=" CookieTrackServlet ", urlPatterns={"/ cookietrack"}) public class CookieTrackServlet extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Cookie cookie=null; //获取请求相关的Cookie Cookie[] cookies=request.getCookies(); //判断Cookie VisitTimes是否存在,如果存在,其值加1 if(cookies! =null){ boolean flag=false; for(int i=0; (i<cookies.length)&&(! flag); i++){ if(cookies[i].getName().equals("VisitTimes")){ String v=cookies[i].getValue(); int value=Integer.parseInt(v)+1; cookies[i].setValue(Integer.toString(value)); //将值更新后的cookie重新写回响应 response.addCookie(cookies[i]); flag=true; cookie=cookies[i]; }//end if }//end for }//end if //不存在,创建cookie if(cookie==null){ int maxAge=-1; //创建cookie对象 cookie=new Cookie("VisitTimes", "1"); cookie.setPath(request.getContextPath()); cookie.setMaxAge(maxAge); response.addCookie(cookie); }//end if //显示信息 response.setContentType("text/html; charset=utf-8"); PrintWriter out= response.getWriter(); out.println("<html>"); out.println("<head>"); out.println("<title>Cookie跟踪会话</title>"); out.println("</head>"); out.println("<body>"); out.println("<h2>您好!</h2>"); out.println("欢迎您第"+cookie.getValue()+"次访问本页面<br>"); out.println("</body>"); out.println("</html>"); } … }
程序说明:程序使用Cookie来实现会话的跟踪,在本示例中跟踪的是会话中页面的访问次数。程序通过将页面访问的次数写入一个名为VisitTimes的Cookie中。由于对页面的请求每次都包含了这个Cookie,因此通过每次将Cookie的值取出来显示页面的访问次数,同时又将更新过的值写回到Cookie来达到会话跟踪的目的。
重新发布Web应用并启动浏览器,在地址栏中输入http://localhost:8080/Chapter3/cookietrack,得到如图3-28所示的运行结果页面,不停地刷新页面,页面中显示的值也不停地刷新,可以看到服务器可以准确地跟踪客户端的访问次数。

图3-28 用Cookie实现会话跟踪
3.7.2 URL重写
关于是否应当使用Cookie有很多的争论,因为一些人认为Cookie可能会造成对隐私权的侵犯。有鉴于此,大部分浏览器允许用户关闭Cookie功能,这使得跟踪会话变得更加困难。如果不能依赖Cookie的支持又该怎么办呢?那将导致不得不使用另外一种会话跟踪方法——URL重写。
URL重写通过在URL地址后面增加一个包含会话信息的字符串来记录会话信息。URL地址与会话信息的字符串之间用“?”隔开。如果请求还包含多个参数,则参数与会话信息以及参数间用“&”隔开。
下面通过编写一个Servlet URLRewrite1来演示如何利用URL重写来向服务器端传递会话信息。这里假设客户端向服务器端传递的会话信息是用户的身份信息:姓名和年龄。代码如程序3-24所示。
程序3-24:URLRewrite1.java
package com.servlet; … @WebServlet(name=" URLRewrite1", urlPatterns={"/ url1 "}) public class URLRewrite1 extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html; charset=UTF-8"); java.io.PrintWriter out = response.getWriter( ); String contextPath = request.getContextPath( ); String encodedUrl = response.encodeURL(contextPath + "/url2? name=张三&age=27"); out.println("<html>"); out.println("<head>"); out.println("<title>URL Rewriter</title>"); out.println("</head>"); out.println("<body>"); out.println( "<h1>URL重写演示:发送参数</h2>"); out.println("转到URL2<a href=\"" + encodedUrl + "\">here</a>."); out.println("</body>"); out.println("</html>"); out.close(); } … }
程序说明:程序首先调用response的encodeURL生成URL字符串。其中request的getContextPath用来获取请求上下文路径。URL字符串包含的会话信息为两个参数:name和age,其值分别为“张三”和27。
下面通过在Web应用Chapter3中创建一个名为URLRewrite2的Servlet来演示服务器端如何获取通过URL重写方式传递来的会话信息。代码如程序3-25所示。
程序3-25:URLRewrite2.java
package com.servlet; … @WebServlet(name=" URLRewrite2", urlPatterns={"/ url2 "}) public class URLRewrite2 extends HttpServlet { protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html; charset=UTF-8"); request.setCharacterEncoding("UTF-8"); java.io.PrintWriter out = response.getWriter( ); String contextPath = request.getContextPath( ); out.println("<html>"); out.println("<head>"); out.println("<title>URL Rewriter</title>"); out.println("</head>"); out.println("<body>"); out.println( "<h1>URL重写演示:接收参数</h2>"); out.println("下面是接收的参数:<br>"); out.println("name="+request.getParameter("name")); out.println("age="+request.getParameter("age")); out.println("</body>"); out.println("</html>"); out.close(); } … }
程序说明:对于利用URL重写技术传递来会话信息,可以调用request.getParameter来获取,就像获取表单提取的参数信息一样。实际上,通过表单向服务器端提交数据就是通过URL重写的方式。注意,如果传递的是汉字编码的信息,在提取参数前,别忘了通过“request.setCharacterEncoding("UTF-8");”来设置请求编码格式,否则得到的将会是乱码。
重新发布Web应用并启动浏览器,在地址栏中输入http://localhost:8080/Chapter3/url1,得到如图3-29所示的运行结果页面。单击页面中的链接here,则浏览器被导向地址http://localhost:8080/Chapter3/url2,得到如图3-30所示的运行结果页面,可以看到,客户端通过URL重写的会话信息已经传递到Servlet组件URLRewrite2并被正确解析。

图3-29 URL重写:发送参数

图3-30 接收URL重写的参数信息
3.7.3 HttpSession
为消除代码中手工管理会话信息的需要(无论使用什么会话跟踪方式),Servlet规范定义了HttpSession接口以方便Servlet容器进行会话跟踪。这个高级接口实际上是建立在Cookie和URL重写这两种会话跟踪技术之上的,只不过由Web容器自动实现了关于会话跟踪的底层机制,不再需要开发人员了解具体细节。HttpSession接口允许Servlet查看和管理关于会话的信息,确保信息持续跨越多个用户连接等。
使用HttpSession接口进行程序开发的基本步骤如下:
(1)获取HttpSession对象。
(2)对HttpSession对象进行读或写。
(3)手工终止HttpSession,或者什么也不做,让它自动终止。每个HttpSession对象都有一定的生命周期,超过这个周期,容器自动将HttpSession对象中止。
程序开发中经常使用的HttpSession接口方法有以下几个:
(1)isNew()。如果客户端还不知道会话,则返回true。如果客户端已经禁用了Cookie,则会话在每个请求上都是新的。
(2)getId()。返回包含分配给这个会话的唯一标识的字符串。在使用URL改写已标识会话时比较有用。
(3)setAttribute()。使用指定的名称将对象绑定到会话。
(4)getAttribute()。返回绑定到此会话的指定名称的对象。
(5)setMaxInactiveInterval()。指定在Servlet使该会话无效之前客户端请求间的时间。负的时间表示会话永远不会超时。
(6)invalidate()。终止当前会话,并解开与它绑定的对象。
下面通过一个示例来演示如何用HttpSession来存储当前会话中用户访问站点的次数。
在项目中创建Servlet HitCounter,代码如程序3-26所示。
程序3-26:HitCounter
package com.Servlet; … @WebServlet(name=" HitCounter ", urlPatterns={"/hitcounter "}) public class HitCounter extends HttpServlet { static final String COUNTER_KEY = "Counter"; protected void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //获取会话对象 HttpSession session = request.getSession(true); response.setContentType("text/html; charset=gb2312"); java.io.PrintWriter out = response.getWriter(); //从会话中获取属性 int count = 1; Integer i = (Integer) session.getAttribute(COUNTER_KEY); if (i ! = null) { count = i.intValue() + 1; } //将属性信息存入会话 session.setAttribute(COUNTER_KEY, new Integer(count)); Date lastAccessed = new Date(session.getLastAccessedTime( )); Date sessionCreated=new Date(session.getCreationTime()); DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM); //输出会话信息 out.println("<html>"); out.println("<head>"); out.println("<title>会话计数器</title>"); out.println("</head>"); out.println("<body>"); out.println("你的会话ID: <b>" +session.getId()+ "<br>"); out.println("会话创建时间:"+formatter.format(sessionCreated) + "<br>"); out.println("会话上次访问时间:"+formatter.format(lastAccessed) + "<br>"); out.println("</b> 会话期间你对页面发起 <b>" + count + "</b> 次请求"); out.println("<form method=GET action=\"" + request.getRequestURI() + "\">"); out.println("<input type=submit " + "value=\"再次单击...\">"); out.println("</form>"); out.println("</body>"); out.println("</html>"); out.flush(); out.close(); } … }
程序说明:Servlet中使用HttpServletRequest对象的getSession方法来取得当前的用户会话。GetSession的参数决定了如果会话不存在,是否创建一个新会话(还有一个版本的getSession没有任何参数,它将默认创建一个新会话)。一旦获得了会话对象,就可以像操作哈希表一样使用一个唯一的键,在会话对象中加入或者获取任何对象。通过调用setAttribute将用户访问次数信息存入会话,通过调用getAttribute来获取会话中存储的信息。
注意:由于会话数据是由Web容器维护存储的,在为这些键赋值时一定要注意维护它的唯一性。一个比较好的方法是为每个会话属性的名称定义一个static final类型的String变量。
重新发布Web应用并启动浏览器,在地址栏中输入http://localhost:8080/Chapter3/hitcounter,得到如图3-31所示的运行结果页面。用户第一次打开HitCounter Servlet的时候,如果会话还不存在,就会创建一个新的会话(一定要注意,其他Servlet可能已经建立了这个用户的会话对象)。通过一个唯一键从会话对象中取得一个整数,如果这个整数不存在,就使用初始值1,否则每次给这个整数加1。最后,新的值被写回会话对象。一个简单的HTML页被返回给浏览器显示,它显示了会话ID及用户通过单击“再次单击”按钮获取这一页的访问次数。

图3-31 利用会话存储页面访问次数