这篇文章挖掘Session的原理和tomcat实现机制。     
由于HTTP是无状态的协议,客户程序每次都去web页面,都打开到web服务器的单独的连接,并且不维护客户的上下文信息。如果需要维护上下文信息,比如用户登录系统后,每次都能够知道操作的是此登录用户,而不是其他用户。对于这个问题,存在三种解决方案:cookie,url重写和隐藏表单域。

1、cookie
   cookie是一个服务器和客户端相结合的技术,服务器可以将会话ID发送到浏览器,浏览器将此cookie信息保存起来,后面再访问网页时,服务器又能够从浏览器中读到此会话ID,通过这种方式判断是否是同一用户。
 1  请求:  
 2   POST /ibsm/LoginAction.do HTTP/1.1  
 3   Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*  
 4   Referer: http://192.168.1.20:8080/crm/  
 5   Accept-Language: zh-cn  
 6   Content-Type: application/x-www-form-urlencoded  
 7   UA-CPU: x86  
 8   Accept-Encoding: gzip, deflate  
 9   User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2)  
10   Host: 192.168.1.20:8080  
11   Content-Length: 13  
12   Connection: Keep-Alive  
13   Cache-Control: no-cache  
14     
15   username=jack   
16     
17   响应:  
18   HTTP/1.1 200 OK  
19   Server: Apache-Coyote/1.1  
20   Set-Cookie: JSESSIONID=3267A671BFEAA147A2383B7E083D4G7E; Path=/crm  
21   Content-Type: text/html;charset=GBK  
22   Content-Length: 436  
23   Date: Sat, 10 June 2009 12:43:26 GMT

生成响应的时候,服务器向客户端发送cookie。cookie的属性是 JSESSIONID,值是 267A671BFEAA147A2383B7E083D4G7E。以后每次客户端请求时,都会附上此cookie,服务器端就可以读取到。

1     1. GET /ibsm/ApplicationFrame.frame HTTP/1.1  
2     2. Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*  
3     3. Accept-Language: zh-cn  
4     4. UA-CPU: x86  
5     5. Accept-Encoding: gzip, deflate  
6     6. User-Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.2)  
7     7. Host: 192.168.1.20:8080  
8     8. Connection: Keep-Alive  
9     9. Cookie: JSESSIONID= 267A671BFEAA147A2383B7E083D4G7E  

服务器端根据读取到的JSESSIONID,在一个map里面查找其对应的session对象,这个map的key是jsessionid的值,value是session对象。


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());

从上面URL的实现原理可知,URL重写有一个缺点: 在你的站点上不能有任何静态的HTML页面(至少静态页面中不能有任何链接到站点动态页面的链接)。因此,每个页面都必须使用servlet或 JSP动态生成。即使所有的页面都动态生成,如果用户离开了会话并通过书签或链接再次回来,会话的信息都会丢失,因为存储下来的链接含有错误的标识信息- 该URL后面的SESSION ID已经过期了。

3、隐藏表单域
   这种方式借助html表单中的hidden来实现,适用特定的一个流程,但是不适用于通常意义的会话跟踪。

综上所述,session实现会话跟踪通常是cookie和url重写,如果浏览器不禁止cookie的话,tomcat优先使用cookie实现。

服务器端实现原理

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);      

}

}