關於session、cookie

今天在工作中遇到了web session的問題,看了網上一些資料,整理整理。

一、session、cookie的概念

1.1 cookie是什麼?
Cookie是WEB上最常用的跟蹤用戶會話方式,Cookie是一種由服務器發送給客戶的片段信息,存儲在客戶環境中,並在客戶所有對服務器的請求中都要發回它。hrome瀏覽器把Cookie信息保存在類似於C:\Users\username\AppData\Local\Google\Chrome\User Data\Default\Cookies的地方。

1.2 session是什麼

session一般譯作會話,牛津詞典對其的解釋是進行某活動連續的一段時間。從不同的層面看待session,它有着類似但不全然相同的含義。比如,在web應用的用戶看來,他打開瀏覽器訪問一個電子商務網站,登錄、並完成購物直到關閉瀏覽器,這是一個會話。而在web應用的開發者開來,用戶登錄時我需要創建一個數據結構以存儲用戶的登錄信息,這個結構也叫做session。因此在談論session的時候要注意上下文環境。而本文談論的是一種基於HTTP協議的用以增強web應用能力的機制或者說一種方案,它不是單指某種特定的動態頁面技術,而這種能力就是保持狀態,也可以稱作保持會話。
session是服務端的解決方案,它是通過服務器來保持狀態的。

1.3 爲什麼需要session或cookie?

談及session一般是在web應用的背景之下,我們知道web應用是基於HTTP協議的,而HTTP協議恰恰是一種無狀態協議。也就是說,用戶從A頁面跳轉到B頁面會重新發送一次HTTP請求,而服務端在返回響應的時候是無法獲知該用戶在請求B頁面之前做了什麼的。

對於HTTP的無狀態性的原因,相關RFC裏並沒有解釋,但聯繫到HTTP的歷史以及應用場景,我們可以推測出一些理由:

①設計HTTP最初的目的是爲了提供一種發佈和接收HTML頁面的方法。那個時候沒有動態頁面技術,只有純粹的靜態HTML頁面,因此根本不需要協議能保持狀態;
②用戶在收到響應時,往往要花一些時間來閱讀頁面,因此如果保持客戶端和服務端之間的連接,那麼這個連接在大多數的時間裏都將是空閒的,這是一種資源的無端浪費。所以HTTP原始的設計是默認短連接,即客戶端和服務端完成一次請求和響應之後就斷開TCP連接,服務器因此無法預知客戶端的下一個動作,它甚至都不知道這個用戶會不會再次訪問,因此讓HTTP協議來維護用戶的訪問狀態也全然沒有必要;
③將一部分複雜性轉嫁到以HTTP協議爲基礎的技術之上可以使得HTTP在協議這個層面上顯得相對簡單,而這種簡單也賦予了HTTP更強的擴展能力。

事實上,session技術從本質上來講也是對HTTP協議的一種擴展。
隨着網絡技術的蓬勃發展,人們再也不滿足於死板乏味的靜態HTML,他們希望web應用能動起來,於是客戶端出現了腳本和DOM技術,HTML裏增加了表單,而服務端出現了CGI等等動態技術。
而正是這種web動態化的需求,給HTTP協議提出了一個難題:一個無狀態的協議怎樣才能關聯兩次連續的請求呢?也就是說無狀態的協議怎樣才能滿足有狀態的需求呢?因此我們需要一些方案來解決這個矛盾,來保持HTTP連接狀態,於是出現了cookie和session。

在此談2點疑惑:

無狀態性和長連接
現在被廣泛使用的HTTP1.1默認使用長連接,它還是無狀態的嗎?
連接方式和有無狀態是完全沒有關係的兩回事。因爲狀態從某種意義上來講就是數據,而連接方式只是決定了數據的傳輸方式,而不能決定數據。長連接是隨着計算機性能的提高和網絡環境的改善所採取的一種合理的性能上的優化,一般情況下,web服務器會對長連接的數量進行限制,以免資源的過度消耗。
無狀態性和session
Session是有狀態的,而HTTP協議是無狀態的,二者是否矛盾呢?
Session和HTTP協議屬於不同層面的事物,後者屬於ISO七層模型的最高層應用層,前者不屬於後者,前者是具體的動態頁面技術來實現的,但同時它又是基於後者的。

二、工作原理

2.1 cookie工作原理

cookie是在RFC2109(已廢棄,被RFC2965取代)裏初次被描述的,每個客戶端最多保持三百個cookie,每個域名下最多20個Cookie(實際上一般瀏覽器現在都比這個多,如Firefox是50個),而每個cookie的大小爲最多4K,不過不同的瀏覽器都有各自的實現。對於cookie的使用,最重要的就是要控制cookie的大小,不要放入無用的信息,也不要放入過多信息。

無論使用何種服務端技術,只要發送回的HTTP**響應中**包含如下形式的頭,則視爲服務器要求設置一個cookie:

**Set-cookie:name=name;expires=date;path=path;domain=domain**

支持cookie的瀏覽器都會對此作出反應,即創建cookie文件並保存(也可能是內存cookie),用戶以後在每次發出請求時,瀏覽器都要判斷當前所有的cookie中有沒有沒失效(根據expires屬性判斷)並且匹配了path屬性的cookie信息,如果有的話,會以下面的形式加入到請求頭中發回服務端:

**Cookie: name="zj"; Path="/linkage"**

cookie的保存有臨時性的和持久性的,大多都是臨時性的,也就是cookie只保存在客戶端的內存中,而沒有保存在硬盤上,當關閉瀏覽器,cookie也就銷燬。以下是有關cookie機制的一些具體說明:
cookie的內容主要包括:名字,值,過期時間,路徑和域。
其中域可以指定某一個域比如.google.com,相當於總店招牌,比如寶潔公司,也可以指定一個域下的具體某臺機器比如www.google.com或者froogle.google.com,可以用飄柔來做比。
路徑就是跟在域名後面的URL路徑,比如/或者/foo等等,可以用某飄柔專櫃做比。路徑與域合在一起就構成了cookie的作用範圍。
如果不設置過期時間,則表示這個cookie的生命期爲瀏覽器會話期間,只要關閉瀏覽器窗口,cookie就消失了。這種生命期爲瀏覽器會話期的 cookie被稱爲會話cookie。會話cookie一般不存儲在硬盤上而是保存在內存裏,當然這種行爲並不是規範規定的。如果設置了過期時間,瀏覽器就會把cookie保存到硬盤上,關閉後再次打開瀏覽器,這些cookie仍然有效直到超過設定的過期時間。
存儲在硬盤上的cookie可以在不同的瀏覽器進程間共享,比如兩個IE窗口。而對於保存在內存裏的cookie,不同的瀏覽器有不同的處理方式。對於微軟的IE瀏覽器,在一個打開的窗口上按Ctrl-N(或者從文件菜單)打開的窗口可以與原窗口共享,而使用其他方式新開的IE進程則不能共享已經打開的窗口的內存cookie;對於火狐狸firefox瀏覽器,所有的進程和標籤頁都可以共享同樣的cookie。一般來說是用javascript的window.open打開的窗口會與原窗口共享內存cookie。瀏覽器對於會話cookie的這種只認cookie不認人的處理方式經常給採用Session機制的web應用程序開發者造成很大的困擾。

需要注意的是,出於安全性的考慮,cookie可以被瀏覽器禁用。

2.2 session工作原理

服務端爲每一個session維護一份會話信息數據,而客戶端和服務端依靠一個全局唯一的標識來訪問會話信息數據。用戶訪問web應用時,服務端程序決定何時創建session,創建session可以概括爲三個步驟:

1 生成全局唯一標識符(sessionid);
2 開闢數據存儲空間。一般會在內存中創建相應的數據結構,但這種情況下,系統一旦掉電,所有的會話數據就會丟失,如果是電子商務網站,這種事故會造成嚴重的後果。不過也可以寫到文件裏甚至存儲在數據庫中,這樣雖然會增加I/O開銷,但session可以實現某種程度的持久化,而且更有利於session的共享;
3 將session的全局唯一標示符發送給客戶端。
問題的關鍵就在服務端如何發送這個session的唯一標識上。聯繫到HTTP協議,數據無非可以放到請求行、頭域或Body裏,基於此,一般來說會有兩種常用的方式:cookie和URL重寫。

3.1 Cookie
讀者應該想到了,對,服務端只要設置Set-cookie頭就可以將session的標識符傳送到客戶端,而客戶端此後的每一次請求都會帶上這個標識符,由於cookie可以設置失效時間,所以一般包含session信息的cookie會設置失效時間爲0,即瀏覽器進程有效時間。至於瀏覽器怎麼處理這個0,每個瀏覽器都有自己的方案,但差別都不會太大(一般體現在新建瀏覽器窗口的時候);

3.2 URL重寫
所謂URL重寫,顧名思義就是重寫URL。試想,在返回用戶請求的頁面之前,將頁面內所有的URL後面全部以get參數的方式加上session標識符(或者加在path info部分等等),這樣用戶在收到響應之後,無論點擊哪個鏈接或提交表單,都會在再帶上session的標識符,從而就實現了會話的保持。讀者可能會覺得這種做法比較麻煩,確實是這樣,但是,如果客戶端禁用了cookie的話,URL重寫將會是首選。

三、session保持與銷燬

當首次創建了session後,客戶端會在後續的請求中將session的標識符帶到服務端,服務端程序只要在需要session的時候調用getSession,服務端就可以將對應的session綁定到當前請求,從而實現狀態的保持。當然這需要客戶端的支持,如果禁用了cookie而又不採用url重寫的話,session是無法保持的。
如果幾次請求之間有一個servlet未調用getSession(或者乾脆請求一個靜態頁面)會不會使得會話中斷呢?這個不會發生的,因爲客戶端只會將合法的cookie值傳送給服務端,至於服務端拿cookie做什麼事它是不會關心的,當然也無法關心。Session建立之後,客戶端會一直將session的標識符傳送到服務器,無論請求的頁面是動態的、靜態的,甚至是一副圖片。

此處談到的銷燬是指會話的廢棄,至於存儲會話信息的數據結構是回收被重用還是直接釋放內存我們並不關心。Session的銷燬有兩種情況:超時和手動銷燬

由於HTTP協議的無狀態性,服務端無法得知一個session對象何時將再次被使用,可能用戶開啓了一個session之後再也沒有後續的訪問,而且session的保持是需要消耗一定的服務端開銷的,因此不可能一味地創建session而不去回收無用的session。這裏就引入了一個超時機制。Tomcat中的超時在web.xml裏做如下配置:

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

上述配置是指session在30分鐘沒有被再次使用就將其銷燬。Tomcat是怎麼計算這個30分鐘的呢?原來在getSession之後,都要調用它的access方法,修改lastAccessedTime,在銷燬session的時候就是判斷當前時間和這個lastAccessedTime的差值。
手動銷燬是指直接調用其invalidate方法,此方法實際上是調用expire方法來手動將其設置爲超時。

當用戶手動請求了session的銷燬時,客戶端是無法知道服務端的session已經被銷燬的,它依然會發送先前的session標識符到服務端。而此時如果再次請求了某個調用了getSession的servlet,服務端是無法根據先前的session標識符找到相應的session對象的,這是又要重新創建新的session,分配新的標識符,並告知服務端更新session標識符開始保持新的會話。

四、發送sessionId的三種方式

A.保存session id的方式可以採用cookie,這樣在交互過程中瀏覽器可以自動的按照規則把這個標識發送給服務器。
B.由於cookie可以被人爲的禁止,必須有其它的機制以便在cookie被禁止時仍然能夠把session id傳遞迴服務器,經常採用的一種技術叫做URL重寫,就是把session id附加在URL路徑的後面,附加的方式也有兩種,一種是作爲URL路徑的附加信息,另一種是作爲查詢字符串附加在URL後面。網絡在整個交互過程中始終保持狀態,就必須在每個客戶端可能請求的路徑後面都包含這個session id。
C.另一種技術叫做表單隱藏字段。就是服務器會自動修改表單,添加一個隱藏字段,以便在表單提交時能夠把session id傳遞迴服務器。

五、在實際運用中session所帶來的問題

服務端會佔用一定的內存和cpu用來存儲和處理session計算的開銷,這也就是tomcat這個的web容器的併發連接那麼低(tomcat官方文檔裏默認的連接數是200)原因之一。因此很多java語言編寫的網站,在生產環境裏web容器之前會加一個靜態資源服務器,例如:apache服務器或nginx服務器,靜態資源服務器沒有解決http無狀態問題的功能,因此部署靜態資源的服務器也就不會讓出內存或cpu計算資源專門去處理像session這樣的功能,這些內存和cpu資源可以更有效的處理每個http請求,因此靜態資源服務器的併發連接數更高,所以我們可以讓那些沒有狀態保持要求的請求直接在靜態服務器裏處理,而要進行狀態保持的請求則在java的web容器裏進行處理,這樣能更好的提升網站的效率。

當下的互聯網網站爲了提高網站安全性和併發量,服務端的部署的服務器的數量往往是大於或等於兩臺,多臺服務器對外提供的服務是等價的,但是不同的服務器上面肯定會有不同的web容器,由上面的講述我們知道session的實現機制都是web容器裏內部機制,這就導致一個web容器裏所生成的session的id值是不同的,因此當一個請求到了A服務器,瀏覽器得到響應後,客戶端存下的是A服務器上所生成的session的id,當在另一個請求分發到了B服務器B服務器上的web容器是不能識別這個session的id值,更不會有這個sessionID所對應記錄下來的信息,這個時候就需要兩個不同web容器之間進行session的同步。Tomcat容器有一個官方的解決方案就是使用apache+tomcat+mod_jk方案,當一個web容器裏session的信息發生變化後,該web容器會向另一個web容器進行廣播,另一個web收到廣播後將session信息同步到自己的容器裏,這個過程是十分消耗系統資源,當訪問量增加會嚴重影響到網站的效率和穩定性。

一個可能的解決方案,當用戶請求網站的時候,會先將請求發送給硬件的負載均衡設備,該設備可以截獲客戶端發送過來的session的id值,然後我們根據這個id值找到產生這個session的服務器,將請求直接發送給這臺服務器。這種解決方案看起來解決了session共享問題,其實結果是將集羣系統最終變回了單點系統,如果處理請求的web容器掛掉了,那麼用戶的相關會話操作也就廢掉了。此外,這種做法也干擾了負載均衡服務器的負載均衡的計算,讓請求的分發並不是公平的。

  一般大型互聯公司的網站都是有一個個獨立的頻道所組成的,例如我們常用的百度,會有百度搜索,百度音樂,百度百科等等,我相信他們不會把這些不同頻道都給一個開發團隊完成,應該每個頻道都是一個獨立開發團隊,因爲每個頻道的應用的都是獨立的web應用,那麼就存在一個跨站點的session同步的問題,跨站點的登錄可以使用單點登錄的(SSO)的解決方案,但是不管什麼解決方案,跨站點的session共享任然是逃避不了的問題。

由上所述,session一共有兩個問題需要解決:
1) session的存儲應該獨立於web容器,也要獨立於部署web容器的服務器
2)如何進行高效的session同步

在講到解決這些問題之前,我們首先要考慮下session如何存儲纔是高效,是存在內存、文件還是數據庫了?文件和數據庫的存儲方式都是將session的數據固化到硬盤上,操作硬盤的方式就是IO,IO操作的效率是遠遠低於操作內存的數據,因此文件和數據庫存儲方式是不可取的,所以將session數據存儲到內存是最佳的選擇。因此最好的解決方案就是使用分佈式緩存技術,例如:memcached和redis,將session信息的存儲獨立出來也是解決session同步問題的方法。
Tomcat的session同步也有使用memcache的解決方案,大家可以參加下面的文章:

  http://blog.sina.com.cn/s/blog_5376c71901017bqx.html
但是該方案只是解決了同步問題,session機制任然和web容器緊耦合,我們需要一個高效、可擴展的解決方案,那麼我們就應該不是簡單的把session獨立出來存儲而是設計一個完全獨立的session機制,它既能給每個web應用提供session的功能又可以實現session同步,下面是一篇用zookeeper實現的分佈式session方案:
http://www.open-open.com/lib/view/open1378556537303.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章