Session的创建和管理机制如下图所示:
由于HTTP是无状态的协议,客户程序每次都去web页面,都打开到web服务器的单独的连接,并且不维护客户的上下文信息。如果需要维护上下文信息,比如用户登录系统后,每次都能够知道操作的是此登录用户,而不是其他用户。对于这个问题,存在三种解决方案:cookie,url重写和隐藏表单域。
1、cookie
cookie是一个服务器和客户端相结合的技术,服务器可以将会话ID发送到浏览器,浏览器将此cookie信息保存起来,后面再访问网页时,服务器又能够从浏览器中读到此会话ID,通过这种方式判断是否是同一用户。
GET /bbs/admin/top.jsp HTTP/1.1
Accept: image/jpeg, application/x-ms-application, image/gif, application/xaml+xml, image/pjpeg, application/x-ms-xbap, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*
Referer: http://localhost:8080/bbs/admin/backGround.jsp
Accept-Language: zh-CN
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; InfoPath.2; .NET4.0C; Tablet PC 2.0)
Host: localhost:8080
Connection: Keep-Alive
Cookie: JSESSIONID=425436B1E13327552467F065B8357B37
2、URL重写
重写这种方式,客户端程序在每个URL的尾部自动添加一些额外数据,这些数据以表示这个会话,比如
http://192.168.1.20:8080/crm/getuserprofile.html;jsessionid=abc123。URL重写的额外数据是服务器自动添加的,那么服务器是怎么添加的呢?Tomcat在返回Response的时候,检查JSP页面中所有的URL,包括所有的链接,和 Form的Action属性,在这些URL后面加上“;jsessionid=xxxxxx”。 添加url后缀的代码片段如下:
org.apache.coyote.tomcat5.CoyoteResponse类的toEncoded()方法支持URL重写。
1
StringBuffer sb
=
new
StringBuffer(path);
2
if
( sb.length()
>
0
) {
//
jsessionid can't be first.
3
sb.append(
"
;jsessionid=
"
);
4
sb.append(sessionId);
5
}
6
sb.append(anchor);
7
sb.append(query);
8
return
(sb.toString());
1
StringBuffer sb
=
new
StringBuffer(path);
2
if
( sb.length()
>
0
) {
//
jsessionid can't be first.
3
sb.append(
"
;jsessionid=
"
);
4
sb.append(sessionId);
5
}
6
sb.append(anchor);
7
sb.append(query);
8
return
(sb.toString());
从上面URL的实现原理可知,URL重写有一个缺点:在你的站点上不能有任何静态的HTML页面(至少静态页面中不能有任何链接到站点动态页面的链接)。因此,每个页面都必须使用servlet或 JSP动态生成。即使所有的页面都动态生成,如果用户离开了会话并通过书签或链接再次回来,会话的信息都会丢失,因为存储下来的链接含有错误的标识信息- 该URL后面的SESSION ID已经过期了。其实经过本人的实践,只要在在第一次通过jsessionId验证登录之后通过下面的代码:
Cookie cookie=new Cookie("JSESSIONID", req.getSession().getId());
resp.addCookie(cookie);
的方式把jsessionid写到客户端,那么当第二次登录之后客户端的cookie里面就已经拥有了jsessionid,此时就算不用加上jsessionid也可以直接访问后台页面了。
3、隐藏表单域
这种方式借助html表单中的hidden来实现,适用特定的一个流程,但是不适用于通常意义的会话跟踪。
综上所述,session实现会话跟踪通常是cookie和url重写,如果浏览器不禁止cookie的话,tomcat优先使用cookie实现,这种方式适合在服务端通过Session监听器和HashMap来管理Session时适用。
服务器端实现原理
当服务器接收到客户端同时发过来的Cookie:Jsessionid和UL重写的Jsessionid时,会优先获取URL重写的Jsessionid去获取Session。
Session在服务器端具体是怎么实现的呢?我们使用session的时候一般都是这么使用的:
request.getSession()或者request.getSession(true)。
这个时候,服务器就检查是不是已经存在对应的Session对象,见HttpRequestBase类
doGetSession(boolean create)方法:
1
if
((session
!=
null
)
&&
!
session.isValid())
2
session
=
null
;
3
if
(session
!=
null
)
4
return
(session.getSession());
5
6
7
//
Return the requested session if it exists and is valid
8
Manager manager
=
null
;
9
if
(context
!=
null
)
10
manager
=
context.getManager();
11
if
(manager
==
null
)
12
return
(
null
);
//
Sessions are not supported
13
if
(requestedSessionId
!=
null
) {
14
try
{
15
session
=
manager.findSession(requestedSessionId);
16
}
catch
(IOException e) {
17
session
=
null
;
18
}
19
if
((session
!=
null
)
&&
!
session.isValid())
20
session
=
null
;
21
if
(session
!=
null
) {
22
return
(session.getSession());
23
}
24
}
requestSessionId从哪里来呢?这个肯定是通过Session实现机制的cookie或URL重写来设置的。见HttpProcessor类中的parseHeaders(SocketInputStream input):
1
for
(
int
i
=
0
; i
<
cookies.length; i
++
) {
2
if
(cookies[i].getName().equals
3
(Globals.SESSION_COOKIE_NAME)) {
4
//
Override anything requested in the URL
5
if
(
!
request.isRequestedSessionIdFromCookie()) {
6
//
Accept only the first session id cookie
7
request.setRequestedSessionId
8
(cookies[i].getValue());
9
request.setRequestedSessionCookie(
true
);
10
request.setRequestedSessionURL(
false
);
11
12
}
13
}
14
}
或者HttpOrocessor类中的parseRequest(SocketInputStream input, OutputStream output)
1
//
Parse any requested session ID out of the request URI
2
int
semicolon
=
uri.indexOf(match); //match 是";jsessionid="字符串
3
if
(semicolon
>=
0
) {
4
String rest
=
uri.substring(semicolon
+
match.length());
5
int
semicolon2
=
rest.indexOf(
'
;
'
);
6
if
(semicolon2
>=
0
) {
7
request.setRequestedSessionId(rest.substring(
0
, semicolon2));
8
rest
=
rest.substring(semicolon2);
9
}
else
{
10
request.setRequestedSessionId(rest);
11
rest
=
""
;
12
}
13
request.setRequestedSessionURL(
true
);
14
uri
=
uri.substring(
0
, semicolon)
+
rest;
15
if
(debug
>=
1
)
16
log(
"
Requested URL session id is
"
+
17
((HttpServletRequest) request.getRequest())
18
.getRequestedSessionId());
19
}
else
{
20
request.setRequestedSessionId(
null
);
21
request.setRequestedSessionURL(
false
);
22
}
23
里面的manager.findSession(requestSessionId)用于查找此会话ID对应的session对象。Tomcat实现
是通过一个HashMap实现,见ManagerBase.java的findSession(String id):
1
if
(id
==
null
)
2
return
(
null
);
3
synchronized
(sessions) {
4
Session session
=
(Session) sessions.get(id);
5
return
(session);
6
}
Session本身也是实现为一个HashMap,因为Session设计为存放key-value键值对,Tomcat里面Session实现类是StandardSession,里面一个attributes属性:
1
/**
2
* The collection of user data attributes associated with this Session.
3
*/
4
private
HashMap attributes
=
new
HashMap();
所有会话信息的存取都是通过这个属性来实现的。Session会话信息不会一直在服务器端保存,超过一定的时间期限就会被删除,这个时间期限可以在web.xml中进行设置,不设置的话会有一个默认值,Tomcat的默认值是60。那么服务器端是怎么判断会话过期的呢?原理服务器会启动一个线程,一直查询所有的Session对象,检查不活动的时间是否超过设定值,如果超过就将其删除。见StandardManager类,它实现了Runnable接口,里面的run方法如下:
1
/**
2
* The background thread that checks for session timeouts and shutdown.
3
*/
4
public
void
run() {
5
6
//
Loop until the termination semaphore is set
7
while
(
!
threadDone) {
8
threadSleep();
9
processExpires();
10
}
11
12
}
13
14
/**
15
* Invalidate all sessions that have expired.
16
*/
17
private
void
processExpires() {
18
19
long
timeNow
=
System.currentTimeMillis();
20
Session sessions[]
=
findSessions();
21
22
for
(
int
i
=
0
; i
<
sessions.length; i
++
) {
23
StandardSession session
=
(StandardSession) sessions[i];
24
if
(
!
session.isValid())
25
continue
;
26
int
maxInactiveInterval
=
session.getMaxInactiveInterval();
27
if
(maxInactiveInterval
<
0
)
28
continue
;
29
int
timeIdle
=
//
Truncate, do not round up
30
(
int
) ((timeNow
-
session.getLastUsedTime())
/
1000L
);
31
if
(timeIdle
>=
maxInactiveInterval) {
32
try
{
33
expiredSessions
++
;
34
session.expire();
35
}
catch
(Throwable t) {
36
log(sm.getString(
"
standardManager.expireException
"
), t);
37
}
38
}
39
}
40
41
}
Session信息在create,expire等事情的时候都会触发相应的Listener事件,从而可以对session信息进行监控,这些Listener只需要继承HttpSessionListener,并配置在web.xml文件中。如下是一个监控在线会话数的Listerner:
import
java.util.HashSet;
import
javax.servlet.ServletContext;
import
javax.servlet.http.HttpSession;
import
javax.servlet.http.HttpSessionEvent;
import
javax.servlet.http.HttpSessionListener;
public
class
MySessionListener
implements
HttpSessionListener {
public
void
sessionCreated(HttpSessionEvent event) {
HttpSession session
=
event.getSession();
ServletContext application
=
session.getServletContext();
//
在application范围由一个HashSet集保存所有的session
HashSet sessions
=
(HashSet) application.getAttribute(
"
sessions
"
);
if
(sessions
==
null
) {
sessions
=
new
HashSet();
application.setAttribute(
"
sessions
"
, sessions);
}
//
新创建的session均添加到HashSet集中
sessions.add(session);
//
可以在别处从application范围中取出sessions集合
//
然后使用sessions.size()获取当前活动的session数,即为“在线人数”
}
public
void
sessionDestroyed(HttpSessionEvent event) {
HttpSession session
=
event.getSession();
ServletContext application
=
session.getServletContext();
HashSet sessions
=
(HashSet) application.getAttribute(
"
sessions
"
);
//
销毁的session均从HashSet集中移除
sessions.remove(session);
}
}