文章目录
第九章 HTTP 会话的使用与管理
Web服务器跟踪客户的状态的 4 种方法:
- 在 HTML 表单中加入隐藏字段,它包含用于跟踪客户状态的数据。
- 重写 URL,使它包含跟踪客户状态的数据。
- 用 Cookie 来传送用于跟踪客户状态的数据。
- 使用会话(Session) 机制。
9.1 会话简介
HTTP 是无状态协议。无状态,是指当一个个浏览器客户程序与服务器之间多次进行基于 HTTP 请求/响应模式的通信时,HTTP 协议本身没有提供服务器连续跟踪特定浏览器端状态的规范。
例如,有多个客户同时访问bookstore应用时,需要把同一本书加入到他们自己的购物车中,假如,请求数据是完全相同的,当bookstore 应用接收到请求后,怎么判断每个请求是哪个客户发出的,从而把书加入到对应的购物车中呢?需要在 HTTP 请求中加入一些额外的跟踪客户状态的数据。
在 Web 开发中,会话机制是用于跟踪客户状态的普遍解决方案。
会话 指的是在一段时间内,单个客户与 Web 应用的一连串相关交互过程。在一个会话中,客户可能会多次请求访问 Web 应用的同一个网页或多个网页。
Servlet 规范制定了基于 Java 的会话的具体运作机制。在Servlet API 中定义了会话的 javax.servlet.http.HttpSession 接口,Serlvet容器必须实现这一街口。当一个会话开始时,Servlet 容器将创建一个 HttpSession 对象,在 HttpSession 对象总可以存放表示客户状态的信息。Servlet 容器为每个 HttpSession 对象分配一个唯一标识符,称为 SessionID。
会话的运作流程:
- 一个浏览器第一次访问支持会话的网页时,Servlet 容器会查看 HTTP 请求中表示 SessionID 的 Cookie,这时还不存在,因此就认为一个新的会话开始了,于是容器会创建一个 HttpSession 对象,为它分配一个唯一的 SessionID,然后把SessionID作为Cookie添加到 HTTP 响应结果中。浏览器接收到 HTTP 响应结果后,会把其中表示 SessionID 的Cookie保存在客户端。
- 浏览器进程继续请求访问用用中任意一个支持会话的网页,在本次 HTTP 请求中会包含表示 SesisonID 的 Cookie。Servlet 容器会在 HTTP 请求中查找表示 SessionID 的 Cookie,这时能够获得 Cookie。因此本次请求处于一个会话中,
Servlet 容器不会创建一个新的 HttpSession 对象,而是从 Cookie 中获取 SessionID,然后根据SessionID 找到内存中的 HttpSsession 对象。 - 浏览器进程重复步骤2,直到当前会话被销毁,HttpSession 对象就会结束生命周期。
9.2 HttpSession 的生命周期及会话范围
会话范围: 指浏览器与一个 Web 应用进行一次会话的过程。
在具体实现上,会话范围与 HttpSession 对象的生命周期对应。因此,Web 组件只要共享同一个 HttpSession
对象,也能共享会话范围的共享数据。
HttpSession 接口的以下方法用于向会话范围内存取或删除共享数据:
- setAttribute(String name, Object object):存数据。
- getAttribute(String name):返回会话范围内与参数 name 匹配的数据。
- getAttributeNames():返回共享数据范围内的所有共享数据名。
- removeAttribute(String name):移除一个共享数据。
其他方法:
- invalidate():销毁当前的会话,Servlet 容器会释放 HttpSession 对象占用的资源。
- isNew():判断是否是新建的会话,是,true。
- setMaxInactiveInterval(int interval):设定一个会话处于不活动状态的最长时间,以 s 为单位。如果超过这个时间,Servlet 容器会自动销毁这个会话。如果参数 interval 设置为负数,表示不限制,即会话永远不会过期。
- getMaxInactiveInterval():读取当前会话处于不活动状态的最长时间。
- getServletContext():返回当前 Web 应用的 ServletContext 对象。
Tomcat 为会话设定的默认保持不活动状态的最长时间为 1800 秒。
当一个会话开始后,如果浏览器突然关闭,Servlet 容器无法立即知道浏览器进程已经被关闭,因此 Servlet 容器端的 HttpSession 对象不会立即结束生命周期。但是,当浏览器关闭后,这个会话就进入了不活动状态,等超过了setMaxInactiveInterval(int interval) 设置的时间后,会话就会因为过期而被 Servlet容器销毁。
会话过期具有以下意义:
- 销毁长时间处于不活动状态的会话,可以及时释放无效 HttpSession 对象占用的内存空间。
- 防止未授权的用户访问会话,提高 Web 应用的安全性。
9.3 使用会话的 JSP 范例
maillogin.jsp
<%@ page contentType="text/html; charset=GB2312" %>
<html><head><title>maillogin</title></head>
<body bgcolor="#FFFFFF" onLoad="document.loginForm.username.focus()">
<%
String name = "";
if (!session.isNew()){
name = (String) session.getAttribute("name");
if (name == null){
name = "";
}
}
%>
<p>欢迎光临邮件系统</p>
<p>Session ID:<%=session.getId()%></p>
<table width="500" border="0">
<tr>
<td>
<table width="500" border="0" >
<form name="loginForm" method="post" action="mailcheck.jsp">
<tr>
<td width="401"><div align="right">User Name: </div></td>
<td width="399"><input type="text" name="username" value="<%=name%>" ></td>
</tr>
<tr>
<td width="401"><div align="right">Password: </div></td>
<td width="399"><input type="password" name="password"></td>
</tr>
<tr>
<td width="401"> </td>
<td width="399"><br><input type="Submit" name="Submit" value="提交"></td>
</tr>
</form>
</table>
</td>
</tr>
</table>
</body></html>
mailcheck.jsp
<%@ page contentType="text/html; charset=GB2312" %>
<html><head><title>mailcheck</title></head>
<body>
<%
String name=null;
name=request.getParameter("username");
if(name!=null)
session.setAttribute("username",name);
else{
name=(String)session.getAttribute("username");
if(name==null){
response.sendRedirect("maillogin.jsp");
}
}
%>
<a href="maillogin.jsp">登录</a>
<a href="maillogout.jsp">注销</a>
<p>当前用户为:<%=name%> </P>
<P>你的信箱中有100封邮件</P>
</body></html>
mailloginout.jsp
<%@ page contentType="text/html; charset=GB2312" %>
<html><head><title>maillogout</title></head>
<body>
<%
String name=(String)session.getAttribute("username");
session.invalidate();
%>
<%=name%>,再见!
<p>
<p>
<a href="maillogin.jsp">重新登录邮件系统</a>
</body></html>
9.4 使用会话的 Servlet 范例
JSP文件默认情况下是支持会话的,而 HttpServlet 类默认情况下是不支持会话的。这是 JSP 与 HttpServlet 的一个小区别。
Servlet 容器调用 HttpServlet 类的服务方法时,会传递一个 HttpServletRequest 类型的参数,HttpServlet 可以通过 HttpServletRequest 对象来获取 HttpSession 对象。
- getSession():使得当前 HttpServlet 支持会话。假如会话存在,就返回相应的 HttpSession 对象,否则就创建一个新会话,并将新建的 HttpSession 对象返回。该方法等价于调用 HttpServletReuqest 的 getSession(ture)方法。
- getSession(boolean create):如果为 true,等价于 getSession() 方法。为 false,那么假如会话存在,返回 HttpSession对象,否则返回 null。
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String[] itemNames = {"糖果","收音机","练习簿"};
//获取 HttpSession 对象
HttpSession session = req.getSession(true);
//获取会话范围内的 ShoppingCart 对象
ShoppingCart cart = (ShoppingCart) session.getAttribute("cart");
if (cart == null) {
cart = new ShoppingCart();
session.setAttribute("cart", cart);
}
}
9.5 通过重写 URL 来跟踪会话
当浏览器禁用 Cookie,Servlet 容器无法向客户端存放 SessionID,Servlet 容器就无法跟踪会话。因此,每次客户请求访问支持会话的 JSP 页面时,Servlet 容器就会创建一个新的会话,这样就无法把多个业务逻辑上相关的客户请求放在同一个会话中。
Servlet 规范提供了另一种跟踪会话的方案,如果浏览器不支持Cookie,Servlet 容器可以重写 Web 组件的URL,把 SessionID 添加到 URL 信息中。 HttpServletResponse 接口提供了重写 URL 的方法:
public java.lang.String encodeURL(String url);
对于9.4节中的 maillogin.jsp 的 from 表单提交地址修改
<--!修改前-->
<form name="loginForm" method="post" action="mailcheck.jsp">
......
</form>
<--!修改后-->
<form name="loginForm" method="post" action="<%=response.encodeURL('mailcheck.jsp')%>">
.....
<form>
response.encodeURL(‘mailcheck.jsp’) 运行流程如下:
(1)判断mailcheck.jsp 是否支持会话,如果不支持,那么直接返回参数指定的URL mailcheck.jsp。
(2)判断浏览器是否支持Cookie。支持,直接返回URL。不支持Cookie,就在参数指定的 URL中加入当前 SessionID 的信息,然后返回修改后的 URL。
<form name="loginForm" method="post" action="mailcheck.jsp?jsessionid=954166...">
......
</form>
由此可见,只有当当前 Web 组件支持会话,浏览器不支持 Cookie的情况下,encodeURL(String url) 方法才会重写 URL,否则,直接返回参数指定的原始 URL。
9.6 会话的持久化
把内存中的 HttpSession 对象保存到文件系统或数据库中,这一过程称为会话的持久化。
会话的持久化有以下两个好处:
- 节约内存空间。假定有一万个客户同时访问某个 Web 应用,Servlet 容器中会生成一万个 HttpSession 对象。如果把这些对象都放在内存中,将消耗大量的内存资源,显然是不可取的。因此,把处于不活动状态的 HttpSession 对象转移到文件系统或数据库中,这样可以提高对内存资源的利用率。
- 确保服务器重启或单个Web应用重启后,能恢复重启前的会话。
在持久化会话时,Servlet 容器不仅会持久化 HttpSession 对象,还会对其所有可序列化的属性进行持久化,从而确保存放在会话范围内的共享数据不会丢失。可序列化属性就是指属性所属的类实现了 java.io.Serializable 接口。
会话在其生命周期中,可能会在运行时状态和持久化状态之间转换:
- 运行时状态: 主要特征就是 HttpSession 对象位于内存中。运行时状态还包含两个子状态:不活动状态和活动状态 。
不活动状态: 指在一段时间内,处于会话中的客户端一直没有向 Web 应用发送任何请求。
活动状态: 指在一段时间内,处于会话中的客户端频繁的向 Web 应用发送各种 HTTP请求。 - 持久化状态: 主要特征就是 HttpSession 对象位于永久性存储设备中。
会话从运行时状态变为持久化状态的过程称为搁置(或持久化)。在一下情况下会话会被搁置:
- 服务器终止或单个 Web 应用终止, Web 应用中的会话会被搁置。
- 会话处于不活动状态时间太长,达到了特定的限制值。
- Web 应用中处于运行时状态的会话数目太多,达到了特定的限制值,部分会话会被搁置。
会话从持久化状态变为运行时状态的过程称为激活(或加载)。在一下情况下会话会被激活:
- 服务器或单个 Web 应用重启。
- 处于会话中的客户端向 Web 应用发出 HTTP请求,相应的会话会被激活。
Java Servlet API 没有为会话的持久化提供标准的接口。会话的持久化完全依赖于 Servlet 容器的具体实现。
Tomcat 的会话管理器有两种:
- org.apache.catalina.session.StandarManager 类:标准会话管理器。
- org.apache.catalina.session.PersistentManager 类:提供了更多的管理会话的功能。
9.6.1 标准会话管理器 StandarManager
默认的会话管理器。
它的实现机制为:
当 Tomcat 服务器或打个 Web 应用终止时,会对被终止的Web应用的 HttpSession 对象持久化。把它们保存到文件系统中。
默认的文件为:
<CATALINA_HOME>/work/Catalina/[hostname]/[application]/SESSIONS.ser
当 Tomcat 或单个 Web 应用重启时,会激活已经被持久化的 HttpSession 对象。
9.6.2 持久化会话管理器 PersistentManager
PersistentManager 把存放 HttpSession 对象的永久性存储设备成为会话Store。 PersistentManager具有以下功能:
- 当 Tomcat 服务器或单个 Web 应用关闭或重启,会对 Web 应用的 HttpSession 对象进行持久化,把他们保存到会话 Store中。
- 具有容错功能,及时把 HttpSession 对象备份到会话 Store 中。
- 可以灵活控制在内存中的 Httpsession 对象的数目,将部分 HttpSession 转移到会话 Store 中。
Tomcat 中会话 Store 的接口为 org.apache.Catalina.Store,提供了两个实现这一接口的类:
- org.apache.Catalina.FileStore:把 HttpSession 对象保存到一个文件中。
- org.apache.Catalina.JDBCStore:把 HttpSession 对对象保存到数据库的一张表中。
1、配置 FileStore
FileStore 将 HttpSession 对象保存到文件中,默认的目录是 /work/Catalina/[hostname]/[applicationname]。每个 HttpSession 对象都会对应一个文件,它以 SessionID 作为文件名,扩展名为 .session。
配置会话管理器:
<Context reloadable="true" >
<Manager className="org.apache.catalina.session.PersistentManager"
saveOnRestart="true"
maxActiveSession="10"
minIdleSwap="60"
maxIdleInterval="300"
maxIdleBackup="10"
maxInactiveInterval="300">
<!--directory 指定会话的存放目录-->
<Store className="org.apache.catalina.session.FileStore"
directory="mydir"/>
</Manager>
</Context>
<Manager>
元素的属性
方法 | 描述 |
---|---|
className | 指定会话管理的类名 |
saveOnRestart | true,表示当Web应用终止时,会把内存中所有的 HttpSession 对象都保存到会话 Store 中。当 Web 应用重启时,会重新加载这些会话对象 |
maxActiveSessions | 设定处于运行时状态的会话最大数目,如果超过这一数目,Tomcat 将把一些 HttpSession 对象转移到会话 Store 中。为 -1 表示不限制处于运行时状态的会话数目 |
maxIdleSwap | 指定会话处于不活动状态的最短时间(秒)超过这一时间,Tocmat 有可能把这个 HttpSession 转移到会话 Store 中。为 -1 表示不限制 |
maxIdleSwap | 指定会话处于不活动状态的最长时间(秒),超过这一时间,Tocmat 必须把这个 HttpSession 对象保存到会话 Store中。为 -1 表示不限制 |
maxxIdleBackup | 指定会话处于不活动状态的最长时间(秒),超过这时间,Tomcat 将为这个 HttpSession 对象在会话 Store 中备份。与 maxIdleSwap不同,这个 HttpSession 对象仍然存在于内存中 |
maxInactiveInterval | 指定会话处于不活动状态的最长时间(秒),超过这一时间,Tocmat会使这个会话过期 |
2、配置 JDBCStore
CREATE TABLE `tomcat_sessions` (
`session_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '表示 SessionID',
`valid_session` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '表示会话是否有效',
`max_inactive` int NOT NULL COMMENT '表示会话可以处于不活动状态的最长时间',
`last_access` bigint NOT NULL COMMENT '表示最近一次访问时间',
`app_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '表示会话所属的 Web 应用名称',
`session_data` mediumblob COMMENT '表示 HttpSession 对象的序列化数据',
PRIMARY KEY (`session_id`),
KEY `kapp_name` (`app_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
配置
<Context reloadable="true">
<Manager className="org.apache.catalina.session.PersistentManager"
saveOnRestart="true"
maxActiveSessions="10"
minIdleSwap="60"
maxIdleSwap="120"
maxIdleBackup="180"
maxInactiveInterval="300">
<--! com.mysql.jdbc.Driver 驱动的jar包我是放在tomcat 的 lib 下才识别到的
不然一直报 ClassNotFoundException 找不到驱动 -->
<Store className="org.apache.catalina.session.JDBCStore"
driverName="com.mysql.jdbc.Driver"
connectionURL="jdbc:mysql://localhost/tomcatsessionDB?user=dbuser&password=1234&useSSL=false"
sessionTable="tomcat_sessions"
sessionIdCol="session_id"
sessionDataCol="session_data"
sessionValidCol="valid_session"
sessionMaxInactiveCol="max_inactive"
sessionLastAccessedCol="last_access"
sessionAppCol="app_name"
checkInterval="60" />
</Manager>
</Context>
<Store>
子元素的属性
方法 | 描述 |
---|---|
className | 设定会话 Store 的类名 |
driverName | 设定数据库驱动程序的类名 |
connectionURL | 设定数据库访问的URL,在URL中应该包含访问数据库的用户名和密码 |
sessionTable | 存放HttpSession 对象的表名 |
sessinIdCol | 表示 SessionID 字段 |
sessionDataCol | 表示 HttpSession 对象序列化数据的字段名 |
sessionAppCol | 表示 Web应用的名字的字段 |
sessionVaild | 表示会话是否有效的字段 |
sessionMaxInactiveCol | 表示会话可以处于不活动状态的最长时间 |
sessionLastAcdessCol | 表示最近一次访问会话的时间 |
checkInterval | 表示Tomcat 定期检查会话状态的字段的名字 |
9.7 会话的监听
Servlet API 定义了四个用于监听会话中各种事件的接口。
(1)HttpSessionListener 接口:监听会话以及销毁会话的事件。有两个方法:
- sessionCreated(HttpSessionEvent event):当 Servlet 容器创建了一个会话后调用此方法。
- sessionDestory(HttpSessionEvent event):当 Servlet 容器将要销毁一个会话之前调用此方法。
(2)HttpSessionAttributeListener 接口: 监听会话中加入属性、替换属性和删除属性的事件,有三个方法:
- attributeAdded(HttpSessionBindingEvent event):当Web 应用向一个会话中加入了一个新属性,Servlet 容器会调用此方法。
- attributeRemoved(HttpSessionBindingEvent event):删除一个会话,容器调用此方法。
- attributeReplaced(HttpSessionBindingEvent event):替换了会话中已存在的属性值,容器调用此方法。
(3)HttpSessionBindingListener 接口: 监听会话与一个属性绑定或解除绑定的时间。
- valueBound(HttpSessionBindingEvent event):当 Web 应用把一个属性与会话绑定后,容器调用。
- valueUnBound(HttpSessionBindingEvent event):当 Web 应用把一个属性与会话解除绑定前,容器调用。
**(4)HttpSessionActivationListener(HttpSessionEvent event) 接口:**监听会话被激活或被搁置的事件。
- sessionDidActiveate(HttpSessionEvent event):当 Servlet 容器把一个会话激活后,容器调用。
- sessionWiillPasssivate(HttpSessionEvent event):当 Servlet 容器把一个会话搁置之前,容器调用。
HttpSessionLitener 和 HttpSessionAtrributeLitener 需要在 web.xml 文件中通过 <litener>
配置,向 Servlet 容器中注册。
public class MySessionLifeListener implements HttpSessionListener,HttpSessionAttributeListener{
public void sessionCreated(HttpSessionEvent event) {
System.out.println("A new session is created. id = " + event.getSession().getId());
}
public void sessionDestroyed(HttpSessionEvent event) {
System.out.println("A new session is to be destroyed. id = " + event.getSession().getId());
}
public void attributeAdded(HttpSessionBindingEvent event){
System.out.println("Attribute("+event.getName()+"/"+event.getValue()+") is added into a session.");
}
public void attributeRemoved(HttpSessionBindingEvent event){
System.out.println("Attribute("+event.getName()+"/"+event.getValue()+") is removed from a session.");
}
public void attributeReplaced(HttpSessionBindingEvent event){
System.out.println("Attribute("+event.getName()+"/"+event.getValue()+") is replaced in a session.");
}
}
web.xml
<listener>
<listener-class>com.mypack.MySessionLifeListener</listener-class>
</listener>
HttpSessionBindingLitener 和 HttpSessionActivationListener 则是通过会话的属性来实现。
如下:
public class MyData implements HttpSessionBindingListener,HttpSessionActivationListener,Serializable{
private int data;
public MyData(){}
public MyData(int data){
this.data=data;
}
public int getData(){
return data;
}
public void setData(){
this.data=data;
}
public void valueBound(HttpSessionBindingEvent event){
System.out.println("MyData is bound with a session.");
}
public void valueUnbound(HttpSessionBindingEvent event){
System.out.println("MyData is unbound with a session.");
}
public void sessionDidActivate(HttpSessionEvent se){
System.out.println("A session is activate.");
}
public void sessionWillPassivate(HttpSessionEvent se){
System.out.println("A session will be passivate.");
}
}
使用会话属性(需要添加到会话中的数据)的类来实现 HttpSessionBindingLitener 和 HttpSessionActivationListener 接口,进行相关操作时,就会调用响应的方法。
9.7.1 用 HttpSessionLitener 统计在线用户人数
一个用户登入 Web 应用就会创建一个会话,当这个会话被销毁,就意味着用户离开的 web 应用。
import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
public class OnlineCounterListener implements HttpSessionListener {
@Override
public void sessionCreated(HttpSessionEvent se) {
//创建一个会话后调用
HttpSession session = se.getSession();
ServletContext context = session.getServletContext();
Integer count = (Integer) context.getAttribute("count");
// 把 count 放入应用范围内
if (count == null){
session.setAttribute("count", 1);
} else {
session.setAttribute("count", count + 1);
}
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
// 会话被销毁前调用
HttpSession session = se.getSession();
ServletContext context = session.getServletContext();
Integer count = (Integer) context.getAttribute("count");
if (count == null){
return;
} else {
context.setAttribute("count", count - 1);
}
}
}
将其注册到容器中即可。
9.7.2 用 HttpSessionBindingListener 统计用户在线人数
OnlineUsers 表示在线用户的名单
public class OnlineUsers{
private static final OnlineUsers onlineUsers=new OnlineUsers();
private List<String> users=new ArrayList<String>();
public void add(String name){
users.add(name);
}
public void remove(String name){
users.remove(name);
}
public List getUsers(){
return users;
}
public int getCount(){
return users.size();
}
public static OnlineUsers getInstance(){
return onlineUsers;
}
}
User 表示用户,实现了 HttpSessionBindingListener 监听器
public class User implements HttpSessionBindingListener,Serializable{
private OnlineUsers onlineUsers=OnlineUsers.getInstance();
private String name=null;
public User(String name){
this.name=name;
}
public void setName(String name){
this.name=name;
}
public String getName(){
return name;
}
public void valueBound(HttpSessionBindingEvent event){
onlineUsers.add(name);
System.out.println(name+" is bound with a session");
}
public void valueUnbound(HttpSessionBindingEvent event){
onlineUsers.remove(name);
System.out.println(name+" is unbound with a session");
}
}
通过上面两个类,当用户登入 Web 应用时,在 Web 应用中将用户的信息(User)和 HttpSession 进行绑定,就会调用 valueBound() 方法,从而记录下用户信息。当用户退出应用时,应用将 用户和会话进行解绑,则会调用 valueUnbound()方法。从而实现记录用户在线人数以及用户具体信息的功能。