在《TOMCAT源碼分析——session管理分析(上)》一文中我介紹了Session、Session管理器,還以StandardManager為例介紹了Session管理器的初始化與啟動,本文將接著介紹Session管理的其它內容。
在《TOMCAT源碼分析——請求原理分析(下)》一文的最后我們介紹了Filter的職責鏈,Tomcat接收到的請求會經過Filter職責鏈,最后交給具體的Servlet處理。以訪問http://localhost:8080/host-manager這個路徑為例,可以清楚的看到整個調用棧(如圖1所示)中的Filter的職責鏈及之后的jspServlet,最后到達org.apache.catalina.connector.Request的getSession方法。
圖1 請求調用棧
Request的getSession方法(見代碼清單1)用于獲取當前請求對應的會話信息,如果沒有則創建一個新的Session。
代碼清單1
public HttpSession getSession(boolean create) { Session session = doGetSession(create); if (session == null) { return null; } return session.getSession(); }
doGetSession方法的實現見代碼清單2。
代碼清單2
PRotected Session doGetSession(boolean create) { // There cannot be a session if no context has been assigned yet if (context == null) return (null); // Return the current session if it exists and is valid if ((session != null) && !session.isValid()) session = null; if (session != null) return (session); // Return the requested session if it exists and is valid Manager manager = null; if (context != null) manager = context.getManager(); if (manager == null) return (null); // Sessions are not supported if (requestedSessionId != null) { try { session = manager.findSession(requestedSessionId); } catch (IOException e) { session = null; } if ((session != null) && !session.isValid()) session = null; if (session != null) { session.access(); return (session); } } // Create a new session if requested and the response is not committed if (!create) return (null); if ((context != null) && (response != null) && context.getServletContext().getEffectiveSessionTrackingModes(). contains(SessionTrackingMode.COOKIE) && response.getResponse().isCommitted()) { throw new IllegalStateException (sm.getString("coyoteRequest.sessionCreateCommitted")); } // Attempt to reuse session id if one was submitted in a cookie // Do not reuse the session id if it is from a URL, to prevent possible // phishing attacks // Use the SSL session ID if one is present. if (("/".equals(context.getSessionCookiePath()) && isRequestedSessionIdFromCookie()) || requestedSessionSSL ) { session = manager.createSession(getRequestedSessionId()); } else { session = manager.createSession(null); } // Creating a new session cookie based on that session if ((session != null) && (getContext() != null) && getContext().getServletContext(). getEffectiveSessionTrackingModes().contains( SessionTrackingMode.COOKIE)) { Cookie cookie = applicationSessionCookieConfig.createSessionCookie( context, session.getIdInternal(), isSecure()); response.addSessionCookieInternal(cookie); } if (session == null) { return null; } session.access(); return session; }
依據代碼清單2,整個獲取Session的步驟如下:
我們來著重閱讀ManagerBase實現的createSession方法,見代碼清單3。
代碼清單3
public Session createSession(String sessionId) { // Recycle or create a Session instance Session session = createEmptySession(); // Initialize the properties of the new session and return it session.setNew(true); session.setValid(true); session.setCreationTime(System.currentTimeMillis()); session.setMaxInactiveInterval(this.maxInactiveInterval); if (sessionId == null) { sessionId = generateSessionId(); } session.setId(sessionId); sessionCounter++; return (session); }
至此,Session的創建與分配就介紹這些。
HTTP是一種無連接的協議,如果一個客戶端只是單純地請求一個文件,服務器端并不需要知道一連串的請求是否來自于相同的客戶端,而且也不需要擔心客戶端是否處在連接狀態。但是這樣的通信協議使得服務器端難以判斷所連接的客戶端是否是同一個人。當進行Web程序開發時,我們必須想辦法將相關的請求結合一起,并且努力維持用戶的狀態在服務器上,這就引出了會話追蹤(session tracking)。
Tomcat追蹤Session主要借助其ID,因此在接收到請求后應該需要拿到此請求對應的會話ID,這樣才能夠和StandardManager的緩存中維護的Session相匹配,達到Session追蹤的效果。還記得《TOMCAT源碼分析——請求原理分析(中)》一文中介紹CoyoteAdapter的service方法時調用的postParseRequest方法嗎?其中有這么一段代碼,見代碼清單4。
代碼清單4
if (request.getServletContext().getEffectiveSessionTrackingModes() .contains(SessionTrackingMode.URL)) { // Get the session ID if there was one String sessionID = request.getPathParameter( ApplicationSessionCookieConfig.getSessionUriparamName( request.getContext())); if (sessionID != null) { request.setRequestedSessionId(sessionID); request.setRequestedSessionURL(true); } } // 省去中間無關代碼// Finally look for session ID in cookies and SSL session parseSessionCookiesId(req, request); parseSessionSslId(request); return true; }
根據代碼清單4可以看出postParseRequest方法的執行步驟如下:
代碼清單4中首先調用getSessionUriParamName方法(見代碼清單5)獲取Session的參數名稱。
代碼清單5
public static String getSessionUriParamName(Context context) { String result = getConfiguredSessionCookieName(context); if (result == null) { result = DEFAULT_SESSION_PARAMETER_NAME; } return result; }
從代碼清單2看出,getSessionUriParamName方法首先調用getConfiguredSessionCookieName方法獲取Session的Cookie名稱,如果沒有則默認為jsessionid(常量DEFAULT_SESSION_PARAMETER_NAME的值)。回頭看代碼清單1中會以getSessionUriParamName方法返回的值作為request.getPathParameter(見代碼清單6)的參數查詢Session ID。
代碼清單6
protected String getPathParameter(String name) { return pathParameters.get(name); }
代碼清單4中調用的parseSessionCookiesId方法(見代碼清單7)用來從Cookie中獲取Session ID。
代碼清單7
protected void parseSessionCookiesId(org.apache.coyote.Request req, Request request) { // If session tracking via cookies has been disabled for the current // context, don't go looking for a session ID in a cookie as a cookie // from a parent context with a session ID may be present which would // overwrite the valid session ID encoded in the URL Context context = (Context) request.getMappingData().context; if (context != null && !context.getServletContext() .getEffectiveSessionTrackingModes().contains( SessionTrackingMode.COOKIE)) return; // Parse session id from cookies Cookies serverCookies = req.getCookies(); int count = serverCookies.getCookieCount(); if (count <= 0) return; String sessionCookieName = ApplicationSessionCookieConfig.getSessionCookieName(context); for (int i = 0; i < count; i++) { ServerCookie scookie = serverCookies.getCookie(i); if (scookie.getName().equals(sessionCookieName)) { // Override anything requested in the URL if (!request.isRequestedSessionIdFromCookie()) { // Accept only the first session id cookie convertMB(scookie.getValue()); request.setRequestedSessionId (scookie.getValue().toString()); request.setRequestedSessionCookie(true); request.setRequestedSessionURL(false); if (log.isDebugEnabled()) log.debug(" Requested cookie session id is " + request.getRequestedSessionId()); } else { if (!request.isRequestedSessionIdValid()) { // Replace the session id until one is valid convertMB(scookie.getValue()); request.setRequestedSessionId (scookie.getValue().toString()); } } } } }
代碼清單4中調用的parseSessionSslId方法(見代碼清單8)用來從SSL中獲取Session ID。
代碼清單8
protected void parseSessionSslId(Request request) { if (request.getRequestedSessionId() == null && SSL_ONLY.equals(request.getServletContext() .getEffectiveSessionTrackingModes()) && request.connector.secure) { // TODO Is there a better way to map SSL sessions to our sesison ID? // TODO The request.getAttribute() will cause a number of other SSL // attribute to be populated. Is this a performance concern? request.setRequestedSessionId( request.getAttribute(SSLSupport.SESSION_ID_KEY).toString()); request.setRequestedSessionSSL(true); } }
在《TOMCAT源碼分析——生命周期管理》一文中我們介紹了容器的生命周期管理相關的內容,StandardEngine作為容器,其啟動過程中也會調用startInternal方法(見代碼清單9)。
代碼清單9
@Override protected synchronized void startInternal() throws LifecycleException { // Log our server identification information if(log.isInfoEnabled()) log.info( "Starting Servlet Engine: " + ServerInfo.getServerInfo()); // Standard container startup super.startInternal(); }
StandardEngine的startInternal方法實際代理了父類ContainerBase的startInternal方法(見代碼清單10)。
代碼清單10
@Override protected synchronized void startInternal() throws LifecycleException { // Start our subordinate components, if any if ((loader != null) && (loader instanceof Lifecycle)) ((Lifecycle) loader).start(); logger = null; getLogger(); if ((logger != null) && (logger instanceof Lifecycle)) ((Lifecycle) logger).start(); if ((manager != null) && (manager instanceof Lifecycle)) ((Lifecycle) manager).start(); if ((cluster != null) && (cluster instanceof Lifecycle)) ((Lifecycle) cluster).start(); if ((realm != null) && (realm instanceof Lifecycle)) ((Lifecycle) realm).start(); if ((resources != null) && (resources instanceof Lifecycle)) ((Lifecycle) resources).start(); // Start our child containers, if any Container children[] = findChildren(); for (int i = 0; i < children.length; i++) { children[i].start(); } // Start the Valves in our pipeline (including the basic), if any if (pipeline instanceof Lifecycle) ((Lifecycle) pipeline).start(); setState(LifecycleState.STARTING); // Start our thread threadStart(); }
代碼清單10一開始對各種子容器進行了啟動(由于與本文內容關系不大,所以不多作介紹),最后會調用threadStart方法。threadStart的實現見代碼清單11。
代碼清單11
protected void threadStart() { if (thread != null) return; if (backgroundProcessorDelay <= 0) return; threadDone = false; String threadName = "ContainerBackgroundProcessor[" + toString() + "]"; thread = new Thread(new ContainerBackgroundProcessor(), threadName); thread.setDaemon(true); thread.start(); }
threadStart方法啟動了一個后臺線程,任務為ContainerBackgroundProcessor。ContainerBackgroundProcessor的run方法中主要調用了processChildren方法,見代碼清單12。
代碼清單12
protected class ContainerBackgroundProcessor implements Runnable { public void run() { while (!threadDone) { try { Thread.sleep(backgroundProcessorDelay * 1000L); } catch (InterruptedException e) { // Ignore } if (!threadDone) { Container parent = (Container) getMappingObject(); ClassLoader cl = Thread.currentThread().getContextClassLoader(); if (parent.getLoader() != null) { cl = parent.getLoader().getClassLoader(); } processChildren(parent, cl); } } } protected void processChildren(Container container, ClassLoader cl) { try { if (container.getLoader() != null) { Thread.currentThread().setContextClassLoader (container.getLoader().getClassLoader()); } container.backgroundProcess(); } catch (Throwable t) { log.error("Exception invoking periodic Operation: ", t); } finally { Thread.currentThread().setContextClassLoader(cl); } Container[] children = container.findChildren(); for (int i = 0; i < children.length; i++) { if (children[i].getBackgroundProcessorDelay() <= 0) { processChildren(children[i], cl); } } } }
processChildren方法會不斷迭代StandardEngine的子容器并調用這些子容器的backgroundProcess方法。這里我們直接來看StandardEngine的孫子容器StandardManager的backgroundProcess實現,即ManagerBase的backgroundProcess方法,見代碼清單13。
代碼清單13
public void backgroundProcess() { count = (count + 1) % processExpiresFrequency; if (count == 0) processExpires(); }
backgroundProcess里實現了一個簡單的算法:
count:計數器,起始為0;
processExpiresFrequency:執行processExpires方法的頻率,默認為6。
每執行一次backgroundProcess方法,count會增加1,每當count+1與processExpiresFrequency求模等于0,則調用processExpires。簡而言之,每執行processExpiresFrequency指定次數的backgroundProcess方法,執行一次processExpires方法。processExpires的實現見代碼清單14所示。
代碼清單14
public void processExpires() { long timeNow = System.currentTimeMillis(); Session sessions[] = findSessions(); int expireHere = 0 ; if(log.isDebugEnabled()) log.debug("Start expire sessions " + getName() + " at " + timeNow + " sessioncount " + sessions.length); for (int i = 0; i < sessions.length; i++) { if (sessions[i]!=null && !sessions[i].isValid()) { expireHere++; } } long timeEnd = System.currentTimeMillis(); if(log.isDebugEnabled()) log.debug("End expire sessions " + getName() + " processingTime " + (timeEnd - timeNow) + " expired sessions: " + expireHere); processingTime += ( timeEnd - timeNow ); }
代碼清單14中processExpires方法的執行步驟如下:
Session的標準實現是StandardSession,其isValid方法(見代碼清單15)的主要功能是判斷Session是否過期,對于過期的,則將其expiring狀態改為true。判斷過期的公式為:
( (當前時間 - Session的最后訪問時間)/1000) >= 最大訪問間隔
代碼清單15
public boolean isValid() { if (this.expiring) { return true; } if (!this.isValid) { return false; } if (ACTIVITY_CHECK && accessCount.get() > 0) { return true; } if (maxInactiveInterval >= 0) { long timeNow = System.currentTimeMillis(); int timeIdle; if (LAST_ACCESS_AT_START) { timeIdle = (int) ((timeNow - lastAccessedTime) / 1000L); } else { timeIdle = (int) ((timeNow - thisAccessedTime) / 1000L); } if (timeIdle >= maxInactiveInterval) { expire(true); } } return (this.isValid); }
Tomcat對于Session的管理過程包括創建、分配、維護和跟蹤、銷毀等。
如需轉載,請標明本文作者及出處——作者:jiaan.gja,本文原創首發:博客園,原文鏈接:http://www.companysz.com/jiaan-geng/p/4920036.html
新聞熱點
疑難解答