這篇討論會話管理。我們一旦發送了響應,web服務立馬就會忘了你是誰,下一次你再做請求時,web伺服器不會認識你,它不記得你做過什麼請求,也不記得給過你什麼回應,記憶力比魚還短。但是對於購物車這類應用,如果要求客戶在一個請求中既做出選擇又要結賬,是不合理的。對此,servlet中該如何解決?
如何跟蹤使用者的回答?
我們想完成一個這樣的功能,在對話中,使用者回答一個問題後,web應用能根據上一個回答提出一個新的問題。我們都可以採用哪些做法呢?
做法一:使用一個有狀態會話的企業JavaBean
當然了,可以讓servlet成為一個有狀態會話的bean的用戶端,每次請求到來時,就可以找到使用者的有狀態bean。如果供應商沒有一個帶EJB容器的完整J2EE伺服器怎麼辦?
做法二:使用一個資料庫
這樣也行。每次把客戶的資料寫進資料庫,但是這樣會導致運行時效能很差。所以,不是一個好的選擇。
做法三:使用一個HttpSession
我們可以使用一個HttpSession對象儲存跨多個請求的工作階段狀態。也即,儲存於該使用者的整個會話期間的工作階段狀態。
讓我們以一個購買貨物的例子來說明會話如何工作。
1)使用者A選擇了一個物品,容器向servlet的一個新線程發送請求,該servlet線程發現與使用者A相關的會話,並把她的選擇(“牛奶”)作為一個屬性儲存到會話中。
2)servlet運行其商務邏輯,並返回一個響應,在這裡返回一個問題:“什麼品牌?”
3)使用者A考慮了一下,選擇了“蒙牛”,並點擊了提交按鈕。容器向servlet的一個新線程發送請求,servlet線程發現與使用者A相關的會話,並把他的選擇(“蒙牛”),作為一個屬性儲存到會話中。
4)servlet運行期商務邏輯,並返回一個響應,然後又返回一個問題。
假設,此時使用者B也來到購物網站。
5)使用者A的會話還是活動的,但是此時使用者B選擇了“書籍”,並點擊了提交按鈕。容器把使用者B的請求發給servlet的一個新線程,servlet線程為使用者B開始一個新的會話,並把他的選擇作為一個屬性儲存到會話中。
此時,我們不希望使用者A和使用者B的回答混在一起,所以他們需要不同的會話對象。我們先回答一個問題,容器怎麼知道使用者是誰?
HTTP協議使用的是無狀態串連,這點我們在文章HTTP解析中已經提及。使用者瀏覽器與伺服器建立串連,發送請求,得到響應,然後關閉串連。也即,串連只針對一個請求/響應。由於串連不會持久保留,所以容器認不出第二個請求的使用者與第一個請求的使用者是同一個使用者。為什麼不使用客戶的IP地址呢?IP地址不也是請求的一部分嗎?
對於伺服器來說,你的IP地址是路由器的地址,所以你和這個網路中的其他人的IP地址都是一樣的,所以靠IP地址是不行的。那麼使用HTTPS呢?如果使用HTTPS,那麼伺服器就能認出使用者,並把它與一個會話關聯。但是這個條件一般不滿足。除非有特定需求,否則網站不會使用HTTPS。所以,客戶需要唯一的會話ID。道理很簡單:
對於使用者的第一個請求,容器會產生一個唯一的會話ID,並通過響應把它返回給使用者。使用者在以後的每一個請求中發回這個會話ID,容器看到這個ID後,就會找到匹配的會話,並把這個會話與請求關聯。
容器與使用者如何交換會話ID資訊?
容器必須以某種方式把會話ID作為響應的一部分交給使用者。而使用者必須把會話ID作為請求的一部分發回。最簡單且最常用的方法是通過cookie交換這個會話ID資訊。容器會做幾乎所有的cookie工作,包括產生會話ID,建立新的cookie對象,把會話ID放到cookie中等等。
在響應中發送一個會話cookie,使用如下語句:
HttpSession session = request.getSession();
這個方法不只是建立一個會話,在請求上第一次調用這個方法時,會導致隨響應發出一個cookie。還不能保證使用者會介紹cookie,不過假定客戶支援cookie。
從請求中得到會話ID,使用如下語句:
HttpSession session = request.getSession();
這和之前發送會話cookie的方法完全一樣。那麼,我怎麼知道會話是已經存在,還是剛建立?
可以調用isNew()方法來查看。代碼如下:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException {response.setContentType("text/html;charset=utf-8"); PrintWriter out=response.getWriter();HttpSession session = request.getSession();if(session.isNew()) {out.println("This is a new session");} else {out.println("Weclome back!");}}
我們可以通過以下語句判定來測試是否已經存在一個會話。
//傳遞false表示,這個方法會返回一個已有的會話,如果沒有與客戶關聯的會話,則返回nullHttpSession session = request.getSession(false);if(session == null) {out.println("No session was available..");out.println("making one..");session = request.getSession();} else {out.println("There was a session..");}
如果瀏覽器沒啟用cookie呢?我們怎麼辦?
如果使用者不接受cookie,可以把URL重寫作為一條後路。結社你的做法正確,URL重寫就總能起作用-客戶並不關心具體發生了什麼。可以使用URL+;jsessionid=1234567890的做法。將會話ID放到請求URL的最後作為額外的資訊返回。
//擷取一個會話HttpSession session = request.getSession(false);//向這個URL添加額外的會話ID資訊response.encodeURL("/test.do");
可能有這樣一種情況,你想把請求重新導向到另外一個URL,但是還想使用一個會話,為此有一個特殊的URL編碼方法:
response.encodeRedirectURL("/test.do");
注意,不能對靜態頁面完成URL重寫,使用URL重寫只有在作為會話一部分的所有頁面都是動態產生的情況下才可以。不能寫入程式碼會話ID,因為ID在運行時之前並不存在。
如何刪除會話?
我們肯定不會希望會話一直保留下去,因為會話對象會佔用資源。HTTP協議沒有提供任何機制讓伺服器知道使用者是不是已經走了,那容器是如何知道使用者什麼時候走的呢?如何知道瀏覽器何時崩潰呢?如何知道何時能安全撤銷一個會話呢?
會話有三種死法。白綾,匕首,毒藥?No,是1)逾時;2)在會話對象上調用invalidate();3)應用結束(崩潰或取消部署)。
逾時有兩種設定方法:
1)在web.xml中配置會話逾時,如下:
<?xml version='1.0' encoding='utf-8'?><web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0" metadata-complete="true"><servlet><servlet-name>TestServlet</servlet-name><servlet-class>com.shan.web.TestServlet</servlet-class></servlet><servlet-mapping><servlet-name>TestServlet</servlet-name><url-pattern>/Test.do</url-pattern></servlet-mapping><session-config><session-timeout>15</session-timeout></session-config></web-app>
註:15是指15分鐘。也即,如果使用者15分鐘沒對這個會話做任何請求,就殺死這個會話。
2)設定一個特定會話的會話逾時,如下:
session.setMaxInactiveInterval(20*60);
註:只針對session這個特定會話。這個方法的參數以秒為單位。如果這個參數設定為負數,那表示把時間設定為無窮大。
在會話對象上調用invalidate()如下:
session.invalidate();
應用結束就不用再說了。我們在看看關鍵的HttpSession方法都有哪些,如表1所示。
|
它做什麼 |
你用它做什麼 |
getCreationTime() |
返回第一次建立會話的時間 |
得出這個會話有多老,你可能想把某些會話時間限定在一個固定時間內,如必須在10min內填完表單。 |
getLastAccessTime() |
返回容器最後一次得到有此會話ID的請求的時間(毫秒數) |
得出使用者最後一次訪問這個會話是什麼時候。用這個方法來確定使用者是否已經離開很長時間,然後決定是否調用invalidate()來結束會話。 |
setMaxInactiveInterval() |
對於此會話,指定使用者請求的最大間隔時間(秒數) |
如果過去了指定時間,而使用者未對此會話做任何請求,就會導致會話撤銷。 |
getMaxInactiveInterval() |
對於此會話,返回客戶請求的最大時間間隔(秒數) |
得到這個會話可以保持多久不活動,而且仍然活著。 |
invalidate() |
結束會話。當前儲存在這個會話中的所有會話屬性頁面會解除綁定。 |
如果使用者已經不活動,活著會話已結束,可以用這個方法結束會話。 |
表1 關鍵的HttpSession方法
cookie是否只能用於會話?
其實儘管設計cookie是為了協助支援工作階段狀態,不過也可以使用定製cookie來完成其他工作。需要知道,cookie實際上就是在使用者和伺服器之間交換的一小段資料(一個名-值對。)cookie的一個好處是,使用者不必介入,cookie交換是自動的(當然,瀏覽器必須支援cookie才行)。cookie預設與會話的壽命一樣長。但是cookie可以活的更長一些,甚至在瀏覽器已經關閉後仍存活。
利用servlet API使用cookie
可以從HTTP請求和響應得到與cookie相關的首部,但最好不要這樣做。對於cookie,你要做的工作都已經封裝在了3個類的servlet API中:HttpServletRequest、HttpServletReponse、Cookie。
建立一個新的cookie:
Cookie cookie = new Cookie("username",name);
設定cookie在用戶端上活多久:
cookie.setMaxAge(30*60);
註:參數以秒為單位。如果把參數設定為-1,則瀏覽器退出時cookie就會消失。
把cookie發送到使用者:
response.addCookie(cookie);
從使用者請求得到cookie(一個或多個cookie):
Cookie[] cookies = response.getCookies();for(int i = 0; i < cookies.length; i++) {Cookie cookie = cookies[i];if(cookie.getName().equals("username")) {String username = cookie.getValue();out.println("Hello, " + username);break;}}
HttpSessionBindingListener
我們之前講過的都是在會話生命週期中的關鍵時刻。如果我的屬性想知道自己什麼時候增加到了一個會話,怎麼辦?使用監聽者即可。如下:
public class Dog implements HttpSessionBindingListener{private String breed = null;public Dog(String breed) {this.breed = breed;}public String getBreed(){return breed;}public void valueBound(HttpSessionBindingEvent event){//我知道我在一個會話中時要啟動並執行代碼}public void valueUnBound(HttpSessionBindingEvent event){//我知道我已經不在一個會話中時要啟動並執行代碼}}
如果一個屬性類(如Dog類)實現了HttpSessionBindingListener,當這個類的一個執行個體增加到了一個會話或從會話刪除時,容器就會調用時間處理回調方法(valueBound()和valueUnBound())。
會話遷移
在分布式web應用環境中,容器會完成Server Load Balancer,去的客戶的請求,並把請求發到多個JVM上(可能在同一個物理主機上,也可能在不同物理主機上,我們不關心)。如果每次同一個客戶做請求時,最後這個請求都有可能到達一個servlet的不通事理。也即,指向servlet A的請求A可能在一個VM中,而指向servlet A的請求B可能在另一個不同的VM中。此時,ServletContext、ServletConfig、HttpSession對象會如何呢?
只有HttpSession對象(及其屬性)會從一個VM移到另一個VM。每個VM中有一個ServletContext。每個VM上每個servlet有一個ServletConfig。但是對於每個web應用的一個戈定會話ID,只有一個HttpSession對象,而不論應用分布在多少個VM上。
會話遷移過程
會話遷移具體過程如下:
1)使用者A選擇了“牛奶”,點擊提交按鈕。Server Load Balancer伺服器決定向VM-1中的容器A-1發送請求。容器建立一個新的會話,ID#123。這個“jsessionid”cookie放在響應中發回給使用者A。
2)使用者A選擇了“電器”,點擊提交按鈕。她的請求包括“jsessionid”#123。這次,Server Load Balancer伺服器決定把請求發送給VM-2中的容器A-2。容器得到請求看到會話ID發現會話在另一個VM中,即VM-1。
3)會話#123從VM-1遷移到VM-2(一旦移到VM-2中,VM-1中就沒有這個會話了)。
4)容器為servlet A-2建立一個新線程,並把新請求與剛遷移過來的會話#123關聯。使用者A並不知道新請求發送給這個線程,,只不過在遷移時稍微有點延遲。
HttpSessionActivationListener
因為HttpSession有可能遷移,所以如果有人能告訴會話中的屬性它們也可以移動會更好一些。如果你的屬性都是直接的Serializable對象,並不關心它們最後會放在那裡,那麼可能不會用到這個監聽者。實際上,大部分web應用都不會使用這個監聽者。
session的持久化
如果你在網上買了一本書,放到了購物車裡,此時,網店管理員重啟了web伺服器,當你再次給購物車添加書籍時,發現之前的圖書資訊沒了,你是什麼感覺?所以,一個健壯的web伺服器會提供session持久化機制。這是通過對象序列化的技術實現的。當需要重新載入session對象時,通過對象還原序列化的技術在記憶體中重新構造session對象。這就要求HttpSession的實作類別要實現Serializable介面,同時session中儲存的對象所屬的類也要實現Serialization介面。
幾個問題
1)session和cookie的區別
session和cookie的最大區別是,session在服務端儲存資訊,cookie在用戶端儲存資訊。
2)會話cookie的共用
有些人可能會有一些誤解,認為以不同方式開啟瀏覽器視窗,或者使用其他非IE瀏覽器就可以在不同的瀏覽器進程之間共用工作階段cookie。實際不是的,對於儲存在記憶體中的cookie,是不能被不同的瀏覽器進程所共用的。共用只能發生在同一個瀏覽器的不同視窗中(因為這些視窗共用同一個進程地址空間)。對於儲存在硬碟上的cookie,因為是在外部的存放裝置中儲存,所以可以在多個瀏覽器進程間共用。
3)瀏覽器關閉後,session就消失?
因為有人發現,關閉瀏覽器之後,再開啟一個瀏覽器,就開始了一次新的會話。其實主要是因為儲存session ID的cookie是儲存在瀏覽器記憶體中,一旦瀏覽器關閉,cookie將被刪除,session ID也就丟失了。當再次開啟瀏覽器串連伺服器時,伺服器沒有收到session ID,也就無法找到先前的session,所以伺服器會建立一個新的session。而此時先前的session仍是存在的,知道設定的session逾時時間間隔發生,session才會被清除。如果我們將會話cookie儲存到硬碟上,或者改寫瀏覽器發送給伺服器的請求前序,將原來的session
ID發送給伺服器,則再次開啟瀏覽器就能看到原來的session了。
轉載請註明出處:http://blog.csdn.net/iAm333