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中留作備份。時序圖如下: