這篇文章挖掘Session的原理和tomcat實現機制。
因爲HTTP是無狀態的協議,客戶程序每次都去web頁面,都打開到web服務器的單獨的鏈接,而且不維護客戶的上下文信息。若是須要維護上下文信息,好比用戶登陸系統後,每次都可以知道操做的是此登陸用戶,而不是其餘用戶。對於這個問題,存在三種解決方案:cookie,url重寫和隱藏表單域。
一、cookie
cookie是一個服務器和客戶端相結合的技術,服務器能夠將會話ID發送到瀏覽器,瀏覽器將此cookie信息保存起來,後面再訪問網頁時,服務器又可以從瀏覽器中讀到此會話ID,經過這種方式判斷是不是同一用戶。
html
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 GMTjava
生成響應的時候,服務器向客戶端發送cookie。cookie的屬性是JSESSIONID,值是267A671BFEAA147A2383B7E083D4G7E。之後每次客戶端請求時,都會附上此cookie,服務器端就能夠讀取到。
web
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 apache
服務器端根據讀取到的JSESSIONID,在一個map裏面查找其對應的session對象,這個map的key是jsessionid的值,value是session對象。
二、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());tomcat
從上面URL的實現原理可知,URL重寫有一個缺點:在你的站點上不能有任何靜態的HTML頁面(至少靜態頁面中不能有任何連接到站點動態頁面的連接)。所以,每一個頁面都必須使用servlet或 JSP動態生成。即便全部的頁面都動態生成,若是用戶離開了會話並經過書籤或連接再次回來,會話的信息都會丟失,由於存儲下來的連接含有錯誤的標識信息- 該URL後面的SESSION ID已通過期了。
三、隱藏表單域
這種方式藉助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 }cookie
requestSessionId從哪裏來呢?這個確定是經過Session實現機制的cookie或URL重寫來設置的。見HttpProcessor類中的parseHeaders(SocketInputStream input):
session
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 }app
或者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); }}