新聞中心

EEPW首頁(yè) > 手機(jī)與無(wú)線通信 > 設(shè)計(jì)應(yīng)用 > JSP/Servlet:Servlet/JSP會(huì)話跟蹤機(jī)制 (1)

JSP/Servlet:Servlet/JSP會(huì)話跟蹤機(jī)制 (1)

——
作者: 時(shí)間:2006-07-25 來(lái)源:賽迪網(wǎng)論壇 收藏

一、Servlet的會(huì)話管理機(jī)制 

根據(jù)設(shè)計(jì),HTTP是一種無(wú)狀態(tài)的協(xié)議。它意味著Web應(yīng)用并不了解有關(guān)同一用戶以前請(qǐng)求的信息。維持會(huì)話狀態(tài)信息的方法之一是使用Servlet或者JSP容器提供的會(huì)話跟蹤功能。Servlet API規(guī)范定義了一個(gè)簡(jiǎn)單的HttpSession接口,通過(guò)它我們可以方便地實(shí)現(xiàn)會(huì)話跟蹤。 

HttpSession接口提供了存儲(chǔ)和返回標(biāo)準(zhǔn)會(huì)話屬性的方法。標(biāo)準(zhǔn)會(huì)話屬性如會(huì)話標(biāo)識(shí)符、應(yīng)用數(shù)據(jù)等,都以“名字-值”對(duì)的形式保存。簡(jiǎn)而言之,HttpSession接口提供了一種把對(duì)象保存到內(nèi)存、在同一用戶的后繼請(qǐng)求中提取這些對(duì)象的標(biāo)準(zhǔn)辦法。在會(huì)話中保存數(shù)據(jù)的方法是setAttribute(String s, Object o),從會(huì)話提取原來(lái)所保存對(duì)象的方法是getAttribute(String s)。 

在HTTP協(xié)議中,當(dāng)用戶不再活動(dòng)時(shí)不存在顯式的終止信號(hào)。由于這個(gè)原因,我們不知道用戶是否還要再次返回,如果不采取某種方法解決這個(gè)問題,內(nèi)存中會(huì)積累起大量的HttpSession對(duì)象。 

為此,Servlet采用“超時(shí)限制”的辦法來(lái)判斷用戶是否還在訪問:如果某個(gè)用戶在一定的時(shí)間之內(nèi)沒有發(fā)出后繼請(qǐng)求,則該用戶的會(huì)話被作廢,他的HttpSession對(duì)象被釋放。會(huì)話的默認(rèn)超時(shí)間隔由Servlet容器定義。這個(gè)值可以通過(guò)getMaxInactiveInterval方法獲得,通過(guò)setMaxInactiveInterval方法修改,這些方法中的超時(shí)時(shí)間以秒計(jì)。如果會(huì)話的超時(shí)時(shí)間值設(shè)置成-1,則會(huì)話永不超時(shí)。Servlet可以通過(guò)getLastAccessedTime方法獲得當(dāng)前請(qǐng)求之前的最后一次訪問時(shí)間。 

要獲得HttpSession對(duì)象,我們可以調(diào)用HttpServletRequest對(duì)象的getSession方法。為了正確地維持會(huì)話狀態(tài),我們必須在發(fā)送任何應(yīng)答內(nèi)容之前調(diào)用getSession方法。 
用戶會(huì)話既可以用手工方法作廢,也可以自動(dòng)作廢。作廢會(huì)話意味著從內(nèi)存中刪除HttpSession對(duì)象以及它的數(shù)據(jù)。例如,如果一定時(shí)間之內(nèi)(默認(rèn)30分鐘)用戶不再發(fā)送請(qǐng)求,Java Web Server自動(dòng)地作廢他的會(huì)話。 

Servlet/JSP會(huì)話跟蹤機(jī)制有著一定的局限,比如: 

? 會(huì)話對(duì)象保存在內(nèi)存之中,占用了可觀的資源。 

? 會(huì)話跟蹤依賴于Cookie。由于各種原因,特別是安全上的原因,一些用戶關(guān)閉了Cookie。 

? 會(huì)話跟蹤要用到服務(wù)器創(chuàng)建的會(huì)話標(biāo)識(shí)符。在多個(gè)Web服務(wù)器以及多個(gè)JVM的環(huán)境中,Web服務(wù)器不能識(shí)別其他服務(wù)器創(chuàng)建的會(huì)話標(biāo)識(shí)符,會(huì)話跟蹤機(jī)制無(wú)法發(fā)揮作用。 
要深入理解會(huì)話跟蹤機(jī)制,首先我們必須理解在Servlet/JSP容器中會(huì)話如何運(yùn)作。 


二、會(huì)話標(biāo)識(shí)符 

每當(dāng)新用戶請(qǐng)求一個(gè)使用了HttpSession對(duì)象的JSP頁(yè)面,JSP容器除了發(fā)回應(yīng)答頁(yè)面之外,它還要向?yàn)g覽器發(fā)送一個(gè)特殊的數(shù)字。這個(gè)特殊的數(shù)字稱為“會(huì)話標(biāo)識(shí)符”,它是一個(gè)唯一的用戶標(biāo)識(shí)符。此后,HttpSession對(duì)象就駐留在內(nèi)存之中,等待同一用戶返回時(shí)再次調(diào)用它的方法。 

在客戶端,瀏覽器保存會(huì)話標(biāo)識(shí)符,并在每一個(gè)后繼請(qǐng)求中把這個(gè)會(huì)話標(biāo)識(shí)符發(fā)送給服務(wù)器。會(huì)話標(biāo)識(shí)符告訴JSP容器當(dāng)前請(qǐng)求不是用戶發(fā)出的第一個(gè)請(qǐng)求,服務(wù)器以前已經(jīng)為該用戶創(chuàng)建了HttpSession對(duì)象。此時(shí),JSP容器不再為用戶創(chuàng)建新的HttpSession對(duì)象,而是尋找具有相同會(huì)話標(biāo)識(shí)符的HttpSession對(duì)象,然后建立該HttpSession對(duì)象和當(dāng)前請(qǐng)求的關(guān)聯(lián)。 

會(huì)話標(biāo)識(shí)符以Cookie的形式在服務(wù)器和瀏覽器之間傳送。如果瀏覽器不支持Cookie又如何呢?此時(shí),對(duì)服務(wù)器的后繼請(qǐng)求將不會(huì)帶有會(huì)話標(biāo)識(shí)符。結(jié)果,JSP容器認(rèn)為該請(qǐng)求來(lái)自一個(gè)新用戶,它會(huì)再創(chuàng)建一個(gè)HttpSession對(duì)象,而以前創(chuàng)建的HttpSession對(duì)象仍舊駐留在內(nèi)存中,但該用戶以前的會(huì)話信息卻丟失了。 

另外,Servlet/JSP容器只認(rèn)可它自己創(chuàng)建的會(huì)話標(biāo)識(shí)符。如果同一Web應(yīng)用在“Web農(nóng)場(chǎng)”(Web farm)的多臺(tái)服務(wù)器上運(yùn)行,則必須存在這樣一種機(jī)制:保證來(lái)自同一用戶的請(qǐng)求總是被定向到處理該用戶第一次請(qǐng)求的服務(wù)器。 

三、偽會(huì)話管理機(jī)制 

如前所述,基于Cookie的會(huì)話管理技術(shù)面臨著種種問題。下面我們要設(shè)計(jì)一種新的會(huì)話管理機(jī)制來(lái)解決這些問題。這種會(huì)話管理機(jī)制稱為“偽會(huì)話”(Pseudo Session)機(jī)制,它具有如下特點(diǎn): 

? 對(duì)象和數(shù)據(jù)不是保存在內(nèi)存中,而是以文本文件形式保存。每一個(gè)文本文件與一個(gè)特定的用戶關(guān)聯(lián),文件的名字就是會(huì)話的標(biāo)識(shí)符。因此,文件名字必須是唯一的。 
? 文本文件保存在一個(gè)專用的目錄中,所有Web服務(wù)器都可以訪問這個(gè)目錄。因此,偽會(huì)話可以用于Web農(nóng)場(chǎng)。 
? 會(huì)話標(biāo)識(shí)符不作為Cookie發(fā)送,而是直接編碼到URL里面。因此,采用偽會(huì)話技術(shù)要求修改所有的超級(jí)鏈接,包括HTML表單的ACTION屬性。 
此外,實(shí)現(xiàn)偽會(huì)話管理機(jī)制時(shí)我們還要考慮到以下幾點(diǎn): 
? 它應(yīng)該與應(yīng)用無(wú)關(guān),其他想要實(shí)現(xiàn)同樣功能的開發(fā)者應(yīng)該能夠方便地重用它。 
? 考慮到安全原因,應(yīng)該有一種為會(huì)話標(biāo)識(shí)符生成隨機(jī)數(shù)字的辦法。 
? 為了作廢過(guò)期的會(huì)話,應(yīng)該設(shè)定一個(gè)超時(shí)值。同一個(gè)用戶,如果他超過(guò)一定的時(shí)間之后再次返回,他將獲得一個(gè)新的會(huì)話標(biāo)識(shí)符。此舉能夠防止未經(jīng)授權(quán)的用戶冒用其他人的會(huì)話。 
? 應(yīng)該有一種收集過(guò)期會(huì)話并刪除相應(yīng)文本文件的機(jī)制。 
? 如果用戶使用已經(jīng)過(guò)期的會(huì)話標(biāo)識(shí)符再次訪問服務(wù)器,即使這個(gè)會(huì)話標(biāo)識(shí)符的文本文件還沒有刪除,系統(tǒng)也不應(yīng)該允許用戶使用原來(lái)的會(huì)話。 
? 同時(shí),應(yīng)該存在一種更新會(huì)話文本文件最后改動(dòng)時(shí)間的機(jī)制,使得用戶在會(huì)話過(guò)期時(shí)限之前返回時(shí)會(huì)話總是保持最新且合法的狀態(tài)數(shù)據(jù)。 
 

四、實(shí)現(xiàn)偽會(huì)話管理機(jī)制 

下面所介紹的工程稱為PseudoSession,它是偽會(huì)話機(jī)制一個(gè)很簡(jiǎn)單的實(shí)現(xiàn)??紤]到移植性,我們以JavaBean的形式實(shí)現(xiàn)它。PseudoSessionBean的完整代碼可以從本文后面下載。 

PseudoSessionBean擁有如下域(Field): 
public String path;public long timeOut; 
path是保存所有會(huì)話文本文件的目錄。如果Web服務(wù)器的數(shù)量在一個(gè)以上,這個(gè)目錄必須允許所有服務(wù)器訪問。然而,為了防止用戶直接訪問這些文本文件,這個(gè)路徑應(yīng)該不允許用戶直接訪問。解決這個(gè)問題的一種方法是使用Web網(wǎng)站根之外的目錄。 

timeOut是用戶的最后一個(gè)請(qǐng)求到會(huì)話過(guò)期作廢之間的時(shí)間。在PseudoSessionBean的代碼清單中,timeOut設(shè)置成了以毫秒表示的20分鐘,這是一個(gè)比較合理的超時(shí)時(shí)間值。對(duì)于任何用戶,如果他在這個(gè)超時(shí)時(shí)間之后才繼續(xù)發(fā)出請(qǐng)求,他將得到一個(gè)新的會(huì)話標(biāo)識(shí)符。 
PseudoSessionBean有4個(gè)方法:getSessionID,setValue,getValue,deleteAllInvalidSessions。 

4.1 getSessionID方法 
getSessionID方法的聲明如下: 
public String getSessionID(HttpServletRequest request) 
這個(gè)方法應(yīng)該在每一個(gè)JSP頁(yè)面的開頭調(diào)用。它完成如下任務(wù): 
? 如果用戶是第一次訪問,則為該用戶設(shè)定一個(gè)新的會(huì)話標(biāo)識(shí)符。 
? 檢查URL所帶會(huì)話標(biāo)識(shí)符的合法性。如果會(huì)話標(biāo)識(shí)符已經(jīng)過(guò)期,則getSessionID方法返回一個(gè)新的會(huì)話標(biāo)識(shí)符。 
下面我們來(lái)看看getSessionID方法的工作過(guò)程。 
String sessionId = request.getParameter("sessionId"); 
validSessionIdFound是一個(gè)標(biāo)記,用于指示會(huì)話標(biāo)識(shí)符是否合法。validSessionIdFound的初始值是false。 
boolean validSessionIdFound = false; 
long類型的now變量包含請(qǐng)求出現(xiàn)時(shí)的服務(wù)器時(shí)間。該變量用于確定用戶會(huì)話的合法性。 
long now = System.currentTimeMillis(); 
如果找到了會(huì)話標(biāo)識(shí)符,則getSessionID方法檢查它的合法性。檢查過(guò)程如下: 
? 一個(gè)合法的會(huì)話標(biāo)識(shí)符必須有對(duì)應(yīng)的同名文本文件。 
? 文件的最后修改時(shí)間加上timeOut應(yīng)該大于當(dāng)前時(shí)間。 
? 如果存在與會(huì)話對(duì)應(yīng)的文本文件,但文件已經(jīng)過(guò)期,則原來(lái)的文件被刪除。 
? 把合法會(huì)話標(biāo)識(shí)符所對(duì)應(yīng)文本文件的最后修改日期改為now。 
這些任務(wù)主要借助File對(duì)象完成,創(chuàng)建File對(duì)象的參數(shù)就是會(huì)話文本文件的路徑: 
if (sessionId!=null) {
File f = new File(path + sessionId);
if (f.exists()) {
 if (f.lastModified() + timeOut > now) { // 會(huì)話合法// 使用setLastModified時(shí),如果文件已經(jīng)被其他程序鎖定,// 程序不會(huì)產(chǎn)生任何異常,但文件數(shù)據(jù)不會(huì)改變f.setLastModified(now);validSessionIdFound = true; } else { // 會(huì)話已經(jīng)過(guò)期 // 刪除文件f.delete(); }} // end if (f.exists) } // end if (sessionId!=null) 
如果不存在合法的會(huì)話標(biāo)識(shí)符,則getSessionID方法生成一個(gè)會(huì)話標(biāo)識(shí)符以及相應(yīng)的文本文件: 
if (!validSessionIdFound) { sessionId = Long.toString(now); // 創(chuàng)建文件 File f = new File(path + sessionId); try {f.createNewFile(); } catch (IOException ioe) {}} // end of if !validSessionIdFound 
程序保證文件名字隨機(jī)性的方法非常簡(jiǎn)單:把當(dāng)前的系統(tǒng)時(shí)間直接轉(zhuǎn)換成會(huì)話標(biāo)識(shí)符。對(duì)于那些涉及敏感數(shù)據(jù)的應(yīng)用,我們應(yīng)該考慮運(yùn)用更安全的隨機(jī)數(shù)生成器來(lái)生成會(huì)話標(biāo)識(shí)符。 
綜上所述,getSessionID并不總是返回新的合法會(huì)話標(biāo)識(shí)符:它返回的標(biāo)識(shí)符可能與傳遞給它的標(biāo)識(shí)符相同,也可能是新創(chuàng)建的會(huì)話標(biāo)識(shí)符。 
為了保證JSP頁(yè)面擁有合法的會(huì)話標(biāo)識(shí)符以便調(diào)用setValue、getValue方法,每個(gè)JSP頁(yè)面都必須在開頭位置調(diào)用getSesstionID方法。 


4.2 setValue方法 
setValue方法保存value字符串以及與它關(guān)聯(lián)的字符串名字。這種“名字-值”對(duì)很容易使人想起Dictionary對(duì)象。setValue方法要求在第一個(gè)參數(shù)中提供合法的會(huì)話標(biāo)識(shí)符,它假定在自己被調(diào)用之前getSessionID方法已經(jīng)執(zhí)行,經(jīng)過(guò)檢驗(yàn)的合法會(huì)話標(biāo)識(shí)符必然存在,因此它不再對(duì)傳入的會(huì)話標(biāo)識(shí)符進(jìn)行合法性檢驗(yàn)。 
setValue方法按如下規(guī)則保存名字-值對(duì): 
? 如果與value值關(guān)聯(lián)的name以前還沒有保存過(guò),則新的名字-值對(duì)加入到文本文件的末尾。 
? 如果value字符串關(guān)聯(lián)的name值以前已經(jīng)保存過(guò),則原來(lái)保存的值被新的value值替換。 
setValue方法按照如下格式保存名字-值對(duì),注意“名字”是大小寫敏感的: 
name-1 value-1name-2 value-2name-3 value-3...name-n value-n 
setValue方法的聲明如下: 
public void setValue(String sessionId, String name, String value) 
setValue方法首先尋找與當(dāng)前會(huì)話對(duì)應(yīng)的文本文件。如果不能找到文本文件,則setValue方法不做任何事情直接返回。如果找到了會(huì)話文本文件,setValue方法讀取文本文件的各個(gè)行,然后比較讀入的行與name:如果讀入的文本行開頭與name一樣,則說(shuō)明該名字已經(jīng)保存,setValue方法將替換該行后面的值;如果name不能與讀入的文本行匹配,則這行文本被直接復(fù)制到一個(gè)臨時(shí)文件。 
這部分功能的實(shí)現(xiàn)代碼如下: 
try { FileReader fr = new FileReader(path + sessionId); BufferedReader br = new BufferedReader(fr); FileWriter fw = new FileWriter(path + sessionId + ".tmp"); BufferedWriter bw = new BufferedWriter(fw); String s; while ((s = br.readLine()) != null)if (!s.startsWith(name + " ")) { bw.write(s); bw.newLine();} bw.write(name + " " + value); bw.newLine(); bw.close(); br.close(); fw.close(); bw.close(); . . .}catch (FileNotFoundException e) {}catch (IOException e) { System.out.println(e.toString());} 
原來(lái)文本文件中的所有行復(fù)制到臨時(shí)文件之后,setValue方法刪除原來(lái)的文本文件,然后把臨時(shí)文件改成會(huì)話文本文件的名字: 
File f = new File(path + sessionId + ".tmp");File dest = new File(path + sessionId);dest.delete();f.renameTo(dest); 


4.3 getValue方法 
getValue方法用于提取原來(lái)保存在偽會(huì)話中的數(shù)據(jù)。正如setValue方法,getValue方法也要求傳入一個(gè)合法的會(huì)話標(biāo)識(shí)符,而且getValue方法不再對(duì)傳入的會(huì)話標(biāo)識(shí)符進(jìn)行合法性檢查。getValue方法的第二個(gè)參數(shù)是待提取數(shù)據(jù)的name,返回值是與指定name關(guān)聯(lián)的value。 
getValue方法的聲明如下: 
public String getValue(String sessionId, String name) 
getValue方法的基本執(zhí)行過(guò)程如下:首先找到會(huì)話文本文件,然后按行讀入直至找到與name匹配的文本行;找到匹配的文本行之后,getValue方法返回該行保存的value;如果不能找到,getValue方法返回null。 


4.4 deleteAllInvalidSessions方法 
deleteAllInvalidSessions方法刪除那些與已經(jīng)過(guò)期的會(huì)話關(guān)聯(lián)的文本文件。由于調(diào)用getSessionID方法時(shí)過(guò)期的會(huì)話文本文件會(huì)被刪除,deleteAllInvalidSessions方法并不是關(guān)鍵的方法。什么時(shí)候調(diào)用這個(gè)方法由應(yīng)用自己決定。例如,我們可以編寫一個(gè)專用的后臺(tái)程序,由這個(gè)程序每天一次清除所有過(guò)期的文本文件。最簡(jiǎn)單的辦法是在JSP文件末尾調(diào)用deleteAllInvalidSessions方法,但如果網(wǎng)站比較繁忙,重復(fù)地調(diào)用deleteAllInvalidSessions方法將降低整個(gè)網(wǎng)站的響應(yīng)能力。一種明智的做法是:編寫一個(gè)在訪問量較少的時(shí)候自動(dòng)進(jìn)行清理的后臺(tái)程序。 
deleteAllInvalidSessions方法的聲明如下: 
public void deleteAllInvalidSessions() 
它首先把所有會(huì)話文本文件的名字讀入files字符串?dāng)?shù)組: 
File dir = new File(path); String[] files = dir.list(); 
deleteAllInvalidSessions方法比較文本文件的最后修改時(shí)間(加上超時(shí)時(shí)間)和系統(tǒng)當(dāng)前時(shí)間,確定會(huì)話是否過(guò)期。long類型的變量now用于保存系統(tǒng)的當(dāng)前時(shí)間。 
long now = System.currentTimeMillis(); 
接下來(lái),deleteAllInvalidSessions方法通過(guò)循環(huán)訪問files數(shù)組,依次檢查每個(gè)文件的lastModified屬性。所有與過(guò)期會(huì)話關(guān)聯(lián)的文件都將被刪除: 
for (int i=0; i now) f.delete(); // 刪除過(guò)期的會(huì)話文本文件} 


五、應(yīng)用實(shí)例 


編譯好PseudoSessionBean這個(gè)JavaBean之后,我們就可以利用偽會(huì)話管理機(jī)制來(lái)管理Web應(yīng)用的會(huì)話狀態(tài)信息了。由于不必再使用服務(wù)器的會(huì)話管理機(jī)制,我們可以在page指令中把session屬性設(shè)置為false關(guān)閉默認(rèn)的JSP/Servlet會(huì)話管理功能。 
< %@ page session="false" %> 

然后,我們用JSP的標(biāo)記告訴JSP容器程序要使用PseudoSessionBean: 
< jsp:useBean id="PseudoSessionId" scope="application" class="pseudosession.PseudoSessionBean" /> 

在上面這個(gè)標(biāo)記中,class屬性值是“包.類名字”形式。當(dāng)然,對(duì)于不同的包名字,class屬性的值應(yīng)該作相應(yīng)的修改。注意Bean的scope屬性是“application”,這是因?yàn)槲覀円趹?yīng)用的所有頁(yè)面中使用這個(gè)Bean。在這個(gè)應(yīng)用中,把Bean的scope屬性設(shè)置為“application”具有最好的效率,因?yàn)槲覀冎恍鑴?chuàng)建Bean對(duì)象一次就可以了。另外,正如前面所提到的,getSessionID方法必須在所有其他代碼之前調(diào)用。 
< % String sessionId = PseudoSessionId.getSessionID(request);%> 
為了說(shuō)明PseudoSessionBean的應(yīng)用,下面我們來(lái)看兩個(gè)JSP頁(yè)面,它們是index.jsp和secondPage.jsp。index.jsp頁(yè)面在偽會(huì)話變量中保存用戶的名字,而secondPage.jsp則提取這個(gè)用戶名字。 

index.jsp頁(yè)面的代碼如下: 
< %@ page session="false" contentType="text/html;charset=gb2312" %>
< jsp:useBean id="PseudoSessionId" scope="application" class="pseudosession.PseudoSessionBean" />
< % String sessionId = PseudoSessionId.getSessionID(request);%>
< html>
< head>
< title>偽會(huì)話< /title>
< /head>
< body>
< h1>偽會(huì)話管理機(jī)制< /h1>
< % String userName = "bulbul"; PseudoSessionId.setValue(sessionId, "userName", userName);%>
< a href="/secondPage.jsp?sessionId=<";%=sessionId%>>點(diǎn)擊此處
< form method="post" action=anotherPage.jsp?sessionId=< %=sessionId%>>
輸入數(shù)據(jù):< input type="text" name="sample">
< input type="submit" name="Submit" value="Submit">
< /form>
< /body>
< /html>
< % PseudoSessionId.deleteAllInvalidSessions();%> 

注意,包括

標(biāo)記的action屬性在內(nèi),所有的超級(jí)鏈接都已經(jīng)改寫,現(xiàn)在都包含了會(huì)話標(biāo)識(shí)符。另外也請(qǐng)注意頁(yè)面的最后調(diào)用了deleteAllInvalidSessions方法。 
secondPage.jsp頁(yè)面只簡(jiǎn)單地返回以前保存的用戶名字。 
< %@ contentType="text/html;charset=gb2312" page session="false" %>
< jsp:useBean id="PseudoSessionId" scope="application" class="pseudosession.PseudoSessionBean" />
< % String sessionId = PseudoSessionId.getSessionID(request);%>
< html>
< head>
< title>第2個(gè)頁(yè)面
< /head>
< body>
< % String userName = PseudoSessionId.getValue(sessionId, "userName"); out.println("用戶名字是 " + userName);%>
< /body>
< /html>  




評(píng)論


相關(guān)推薦

技術(shù)專區(qū)

關(guān)閉