這篇文章挖掘Session的原理和tomcat實現(xiàn)機制。 由于HTTP是無狀態(tài)的協(xié)議,客戶程序每次都去web頁面,都打開到web服務(wù)器的單獨的連接,并且不維護客戶的上下文信息。如果需要維護上下文信息,比如用戶登錄系統(tǒng)后,每次都能夠知道操作的是此登錄用戶,而不是其他用戶。對于這個問題,存在三種解決方案:cookie,url重寫和隱藏表單域。 1、cookie cookie是一個服務(wù)器和客戶端相結(jié)合的技術(shù),服務(wù)器可以將會話ID發(fā)送到瀏覽器,瀏覽器將此cookie信息保存起來,后面再訪問網(wǎng)頁時,服務(wù)器又能夠從瀏覽器中讀到此會話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 響應(yīng): 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 生成響應(yīng)的時候,服務(wù)器向客戶端發(fā)送cookie。cookie的屬性是JSESSIONID,值是267A671BFEAA147A2383B7E083D4G7E。以后每次客戶端請求時,都會附上此cookie,服務(wù)器端就可以讀取到。 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 服務(wù)器端根據(jù)讀取到的JSESSIONID,在一個map里面查找其對應(yīng)的session對象,這個map的key是jsessionid的值,value是session對象。 2、URL重寫 重寫這種方式,客戶端程序在每個URL的尾部自動添加一些額外數(shù)據(jù),這些數(shù)據(jù)以表示這個會話,比如 http://192.168.1.20:8080/crm/getuserprofile.html;jsessionid=abc123。URL重寫的額外數(shù)據(jù)是服務(wù)器自動添加的,那么服務(wù)器是怎么添加的呢?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的實現(xiàn)原理可知,URL重寫有一個缺點:在你的站點上不能有任何靜態(tài)的HTML頁面(至少靜態(tài)頁面中不能有任何鏈接到站點動態(tài)頁面的鏈接)。因此,每個頁面都必須使用servlet或 JSP動態(tài)生成。即使所有的頁面都動態(tài)生成,如果用戶離開了會話并通過書簽或鏈接再次回來,會話的信息都會丟失,因為存儲下來的鏈接含有錯誤的標識信息- 該URL后面的SESSION ID已經(jīng)過期了。 3、隱藏表單域 這種方式借助html表單中的hidden來實現(xiàn),適用特定的一個流程,但是不適用于通常意義的會話跟蹤。 綜上所述,session實現(xiàn)會話跟蹤通常是cookie和url重寫,如果瀏覽器不禁止cookie的話,tomcat優(yōu)先使用cookie實現(xiàn)。 服務(wù)器端實現(xiàn)原理 Session在服務(wù)器端具體是怎么實現(xiàn)的呢?我們使用session的時候一般都是這么使用的: request.getSession()或者request.getSession(true)。 這個時候,服務(wù)器就檢查是不是已經(jīng)存在對應(yīng)的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實現(xiàn)機制的cookie或URL重寫來設(shè)置的。見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對應(yīng)的session對象。Tomcat實現(xiàn) 是通過一個HashMap實現(xiàn),見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本身也是實現(xiàn)為一個HashMap,因為Session設(shè)計為存放key-value鍵值對,Tomcat里面Session實現(xiàn)類是StandardSession,里面一個attributes屬性: 1 /**
2 * The collection of user data attributes associated with this Session. 3 */ 4 private HashMap attributes = new HashMap(); 所有會話信息的存取都是通過這個屬性來實現(xiàn)的。Session會話信息不會一直在服務(wù)器端保存,超過一定的時間期限就會被刪除,這個時間期限可以在web.xml中進行設(shè)置,不設(shè)置的話會有一個默認值,Tomcat的默認值是60。那么服務(wù)器端是怎么判斷會話過期的呢?原理服務(wù)器會啟動一個線程,一直查詢所有的Session對象,檢查不活動的時間是否超過設(shè)定值,如果超過就將其刪除。見StandardManager類,它實現(xiàn)了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等事情的時候都會觸發(fā)相應(yīng)的Listener事件,從而可以對session信息進行監(jiān)控,這些Listener只需要繼承HttpSessionListener,并配置在web.xml文件中。如下是一個監(jiān)控在線會話數(shù)的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); } // 新創(chuàng)建的session均添加到HashSet集中 sessions.add(session); // 可以在別處從application范圍中取出sessions集合 // 然后使用sessions.size()獲取當前活動的session數(shù),即為“在線人數(shù)” } public void sessionDestroyed(HttpSessionEvent event) { HttpSession session = event.getSession(); ServletContext application = session.getServletContext(); HashSet sessions = (HashSet) application.getAttribute("sessions"); // 銷毀的session均從HashSet集中移除 sessions.remove(session); } } 以上就是我對session的研究,后面還會對session擴展和跨應(yīng)用session共享等進行進一步的研究。本文系原創(chuàng),轉(zhuǎn)載請標明來源。 |
|