1. 程式人生 > >Web應用伺服器 相關知識梳理(四)Tomcat的其他元件——Session

Web應用伺服器 相關知識梳理(四)Tomcat的其他元件——Session

         由於HTTP是一種無狀態協議,當用戶的一次訪問請求結束後,後端伺服器就無法知道下一次來訪問的還是不是上次訪問的使用者;提到Session與Cookie,二者的作用都是為了保持訪問使用者與後端伺服器的互動狀態,解決了HTTP是一種無狀態協議的弊端。

  • Cookie

         在Tomcat 8中是如何呼叫addCookie方法將Cookie加到HTTP的Header中的???

                 

        根據上圖可知真正的Cookie構建是在org.apache.catalina.connector.Response中完成的:

/**
 * Add the specified Cookie to those that will be included with
 * this Response.
 *
 * @param cookie Cookie to be added
 */
@Override
public void addCookie(final Cookie cookie) {

    // Ignore any call from an included servlet
    if (included || isCommitted()) {
        return;
    }

    cookies.add(cookie);
    
    // 此時 若未設定版本號Version 0 或 Version 1,Tomcat在最後構建HTTP響應頭時也會自動將其設定為Version 1
    String header = generateCookieString(cookie);
    
    //if we reached here, no exception, cookie is valid
    // the header name is Set-Cookie for both "old" and v.1 ( RFC2109 )
    // RFC2965 is not supported by browsers and the Servlet spec
    // asks for 2109.
    addHeader("Set-Cookie", header, getContext().getCookieProcessor().getCharset());
}

        注意:所建立Cookie的NAME值不能和Set-Cookie或者Set-Cookie2屬性項值相同!!!

        Tomcat中根據response.addCookie建立多個Cookie時,這些Cookie都是建立一個以NAME為Set-Cookie的

MimeHeaders,最終在請求返回時HTTP響應頭將相同Header標識的Set-Cookie值進行合併,瀏覽器在接收HTTP返回的

資料時將分別解析每一個Header項;合併過程在org.apache.coyote.http11.Http11Processor的prepareResponse方法中進行:

int size = headers.size();
for (int i = 0; i < size; i++) {
    outputBuffer.sendHeader(headers.getName(i), headers.getValue(i));
}
  • Session 

       當Cookie很多時,在傳輸的過程中,無形增加了客戶端與服務端的資料傳輸量 以及 直接傳輸Cookie所帶來的資料不

安全性,而Session解決了該問題;同一個客戶端每次和服務端互動時,不需要每次都傳回所有的Cookie值,而只要傳回

一個ID,該ID由第一次訪問服務端時生成,每個客戶端唯一;這個ID通常是NAME為JSESSIONID的一個Cookie:就如同

寶箱 與 鑰匙。按通常的瞭解Session的工作方式是基於Cookie的,實際上Tomcat中Session工作方式分為三種:

(一)基於URL Path Parameter,預設支援:         

          當瀏覽器不支援Cookie功能時,瀏覽器會將使用者的SessionCookieName重寫到使用者請求的URL引數中,格式為:

/cxt/login.do;SessionCookieName=3EF0AEC40F2C6C30A0580844C0E6B2E8?count=8。

          關於SessionCookieName,可在web.xml配置:

<session-config>
    <session-timeout>30</session-timeout>
    <cookie-config>
    		<name>SessionCookieName</name>
    </cookie-config>
</session-config>

          否則預設為" JSESSIONID "。然後Request會根據這個SessionCookieName到Paramters中獲取到SessionID並設定

到org.apache.catalina.connector.Request中的requestedSessionId屬性中。

(二)基於Cookie,如果沒有修改Context容器中的Cookies標識,預設也是支援的。

           如果客戶端也支援Cookie,則Tomcat中仍會解析Cookie中的SessionID並會覆蓋URL中的SessionID.

(三)基於SSL,預設不支援,只有在connector.getAttribute("SSLEnabled")為true時才支援

                          

Session在Tomcat中如何工作:

        HttpSession物件,實際上是StandardSession物件的門面類物件;通過 HttpServletRequest request.getSession

( boolean create ) 建立,並將建立後的HttpSession物件新增到org.apache.catalina.Manager的sessions容器中儲存,

只要HttpSession物件存在,便可根據SessionID來獲取該物件,一個requestedSessionId對應一個訪問的客戶端,一

個客戶端對應一個 StandardSession,時序圖如下:

StandardManager類如何管理StandardSession???

         Manager實現類是org.apache.catalina.session.StandardManager;StandardManager類將管理所有的Session

物件的生命週期,過期被收回,伺服器關閉,被序列化到磁碟等。

         當Servlet容器重啟或關閉時,StandardManager負責持久化沒有過期的StandardSession物件,呼叫doUnload方法

將其持久化到一個以" SESSIONS.ser "為檔名的檔案中;當Servlet容器重啟時,即StandardManager初始化時,呼叫

doLoad方法會重新讀取該檔案,解析所有Session物件,重新儲存在StandardManager的sessions集合中:

/**
 * Path name of the disk file in which active sessions are saved
 * when we stop, and from which these sessions are loaded when we start.
 * A <code>null</code> value indicates that no persistence is desired.
 * If this pathname is relative, it will be resolved against the
 * temporary working directory provided by our context, available via
 * the <code>javax.servlet.context.tempdir</code> context attribute.
 */
protected String pathname = "SESSIONS.ser";


/**
 * Load any currently active sessions that were previously unloaded
 * to the appropriate persistence mechanism, if any.  If persistence is not
 * supported, this method returns without doing anything.
 *
 * @exception ClassNotFoundException if a serialized class cannot be
 *  found during the reload
 * @exception IOException if an input/output error occurs
 */
protected void doLoad() throws ClassNotFoundException, IOException {
    if (log.isDebugEnabled()) {
        log.debug("Start: Loading persisted sessions");
    }

    // Initialize our internal data structures
    sessions.clear();

    // Open an input stream to the specified pathname, if any
    File file = file();
    if (file == null) {
        return;
    }
    if (log.isDebugEnabled()) {
        log.debug(sm.getString("standardManager.loading", pathname));
    }
    Loader loader = null;
    ClassLoader classLoader = null;
    Log logger = null;
    try (FileInputStream fis = new FileInputStream(file.getAbsolutePath());
         BufferedInputStream bis = new BufferedInputStream(fis)) {
        Context c = getContext();
        loader = c.getLoader();
        logger = c.getLogger();
        if (loader != null) {
            classLoader = loader.getClassLoader();
        }
        if (classLoader == null) {
            classLoader = getClass().getClassLoader();
        }

        // Load the previously unloaded active sessions
        synchronized (sessions) {
            try (ObjectInputStream ois = new CustomObjectInputStream(bis, classLoader, logger,
                    getSessionAttributeValueClassNamePattern(),
                    getWarnOnSessionAttributeFilterFailure())) {
                Integer count = (Integer) ois.readObject();
                int n = count.intValue();
                if (log.isDebugEnabled())
                    log.debug("Loading " + n + " persisted sessions");
                for (int i = 0; i < n; i++) {
                    StandardSession session = getNewSession();
                    session.readObjectData(ois);
                    session.setManager(this);
                    sessions.put(session.getIdInternal(), session);
                    session.activate();
                    if (!session.isValidInternal()) {
                        // If session is already invalid,
                        // expire session to prevent memory leak.
                        session.setValid(true);
                        session.expire();
                    }
                    sessionCounter++;
                }
            } finally {
                // Delete the persistent storage file
                if (file.exists()) {
                    file.delete();
                }
            }
        }
    } catch (FileNotFoundException e) {
        if (log.isDebugEnabled()) {
            log.debug("No persisted data file found");
        }
        return;
    }

    if (log.isDebugEnabled()) {
        log.debug("Finish: Loading persisted sessions");
    }
}


/**
 * Save any currently active sessions in the appropriate persistence
 * mechanism, if any.  If persistence is not supported, this method
 * returns without doing anything.
 *
 * @exception IOException if an input/output error occurs
 */
protected void doUnload() throws IOException {

    if (log.isDebugEnabled())
        log.debug(sm.getString("standardManager.unloading.debug"));

    if (sessions.isEmpty()) {
        log.debug(sm.getString("standardManager.unloading.nosessions"));
        return; // nothing to do
    }

    // Open an output stream to the specified pathname, if any
    File file = file();
    if (file == null) {
        return;
    }
    if (log.isDebugEnabled()) {
        log.debug(sm.getString("standardManager.unloading", pathname));
    }

    // Keep a note of sessions that are expired
    ArrayList<StandardSession> list = new ArrayList<>();

    try (FileOutputStream fos = new FileOutputStream(file.getAbsolutePath());
            BufferedOutputStream bos = new BufferedOutputStream(fos);
            ObjectOutputStream oos = new ObjectOutputStream(bos)) {

        synchronized (sessions) {
            if (log.isDebugEnabled()) {
                log.debug("Unloading " + sessions.size() + " sessions");
            }
            // Write the number of active sessions, followed by the details
            oos.writeObject(Integer.valueOf(sessions.size()));
            Iterator<Session> elements = sessions.values().iterator();
            while (elements.hasNext()) {
                StandardSession session =
                    (StandardSession) elements.next();
                list.add(session);
                session.passivate();
                session.writeObjectData(oos);
            }
        }
    }

    // Expire all the sessions we just wrote
    if (log.isDebugEnabled()) {
        log.debug("Expiring " + list.size() + " persisted sessions");
    }
    Iterator<StandardSession> expires = list.iterator();
    while (expires.hasNext()) {
        StandardSession session = expires.next();
        try {
            session.expire(false);
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
        } finally {
            session.recycle();
        }
    }

    if (log.isDebugEnabled()) {
        log.debug("Unloading complete");
    }
}


/**
 * Return a File object representing the pathname to our
 * persistence file, if any.
 * @return the file
 */
protected File file() {
    if (pathname == null || pathname.length() == 0) {
        return null;
    }
    File file = new File(pathname);
    if (!file.isAbsolute()) {
        Context context = getContext();
        ServletContext servletContext = context.getServletContext();
        File tempdir = (File) servletContext.getAttribute(ServletContext.TEMPDIR);
        if (tempdir != null) {
            file = new File(tempdir, pathname);
        }
    }
    return file;
}

StandardSession物件並不是永遠存在的,防止記憶體空間消耗殆盡,會為每個Session物件設定有效時間???

           Tomcat中預設有效時間是60s,由(maxInactiveInterval屬性控制),超過60s會過期,在後臺執行緒

backgroundProcess方法中檢查每個StandardSession是否過期;

           request.getSession( boolean create )該方法呼叫時也會檢查Session物件是否過期,如果過期,會重新建立一個

StandardSession,但是以前設定的Session值會全部消失,所以如果session.getAttribute( )獲取不到以前的值請勿大驚

小怪。

     注意:

           request.getSession(true):若存在會話則返回該會話,否則新建一個會話。

           request.getSession(false):若存在會話則返回該會話,否則返回NULL。

如何自定義封裝HttpSession物件???

     在應用的web.xml中配置一個SessionFilter,用於在請求到達MVC框架之前封裝HttpServletRequest和

HttpServletResponse物件,並建立自定義InnerHttpSession,並將其設定到request及response物件中。此時通過

request.getSession(boolean create)獲取的便是自定義的HttpSession物件,並將個別重要的Session通過攔截response的

addCookies保留到Cookie中留作備份。時序圖如下: