由于HTTP協(xié)議的無狀態(tài)特性,導(dǎo)致在ASP.NET編程中,每個請求都會在服務(wù)端從頭到執(zhí)行一次管線過程, 對于ASP.NET頁面來說,Page對象都會重新創(chuàng)建,所有控件以及內(nèi)容都會重新生成, 因此,如果希望上一次的頁面狀態(tài)能夠在后續(xù)頁面中保留,則必需引入狀態(tài)管理功能。
ASP.NET為了實現(xiàn)狀態(tài)管理功能,提供了8種方法,可幫助我們在頁面之間或者整個用戶會話期間保留狀態(tài)數(shù)據(jù)。 這些方法分為二類:視圖狀態(tài)、控件狀態(tài)、隱藏域、Cookie 和查詢字符串會以不同方式將數(shù)據(jù)發(fā)送到客戶端上。 而應(yīng)用程序狀態(tài)、會話狀態(tài)和配置文件屬性(Profile)則會將數(shù)據(jù)存儲到服務(wù)端。 雖然每種方法都有不同的優(yōu)點和缺點,對于小的項目來說,可以選擇自己認為最容易使用的方法, 然而,對于有著較高要求的程序,尤其是對于性能與擴展性比較關(guān)注的程序來說, 選擇不同的方法最終導(dǎo)致的差別可能就非常大了。
在這篇博客中,我將談?wù)勛约簩SP.NET狀態(tài)管理方面的一些看法。
注意:本文的觀點可能并不合適開發(fā)小型項目,因為我關(guān)注的不是易用性。
hidden-input( 隱藏域)
hidden-input 這個名字我是取的,表示所有type="hidden"的input標(biāo)簽元素。 在中文版的MSDN中,也稱之為 隱藏域 。 hidden-input通常存在于HTML表單之內(nèi),它不會顯示到頁面中, 但可以隨表單一起提交,因此,經(jīng)常用于維護當(dāng)前頁面的相關(guān)狀態(tài),在服務(wù)端我們可以使用Request.Form[]來訪問這些數(shù)據(jù)。
一般說來,我通常使用hidden-input來保存一些中間結(jié)果,用于在多次提交中維持一系列狀態(tài), 或者用它來保存一些固定參數(shù)用來提交給其它頁面(或網(wǎng)站)。 在這些場景中,我不希望用戶看到這些數(shù)據(jù),因此,使用hidden-input是比較方便的。
關(guān)于表單的更多介紹可參考我的博客:細說 Form (表單)
在ASP.NET WebForm框架中,我們可以使用HiddenField控件來創(chuàng)建一個hidden-input控件,并可以在服務(wù)端操作它, 還可以直接以手寫的方式使用隱藏域,例如:
<input type="hidden" name="hidden-1" value="aaaaaaa" /> <input type="hidden" name="hidden-2" value="bbbbbbb" /> <input type="hidden" name="hidden-3" value="ccccccc" />
另外,我們還可以調(diào)用ClientScript.RegisterHiddenField()方法來創(chuàng)建隱藏域:
ClientScript.RegisterHiddenField("hidden-4", "ddddddddd");
輸出結(jié)果:
<input type="hidden" name="hidden-4" id="hidden-4" value="ddddddddd" />
這三種方法對于生成的HTML代碼來說,主要差別在于它們出現(xiàn)位置不同:
1. HiddenField控件:由HiddenField的出現(xiàn)位置來決定(在form內(nèi)部)。
2. RegisterHiddenField方法:在form標(biāo)簽的開頭位置。
3. hidden-input:你寫在哪里就是哪里。
優(yōu)點:
1. 不需要任何服務(wù)器資源:隱藏域隨頁面一起發(fā)送到客戶端。
2. 廣泛的支持:幾乎所有瀏覽器和客戶端設(shè)備都支持具有隱藏域的表單。
3. 實現(xiàn)簡單:隱藏域是標(biāo)準(zhǔn)的 HTML 控件,不需要復(fù)雜的編程邏輯。
缺點:
1. 不能在多頁面跳轉(zhuǎn)之間維持狀態(tài)。
2. 用戶可見,保存敏感數(shù)據(jù)時需要加密。
QueryString
查詢字符串是存在于 URL 結(jié)尾的一段數(shù)據(jù)。下面是一個典型的查詢字符串示例(紅色部分文字):
http://www.abc.com/demo.aspx?k1=aaa&k2=bbb&k3=ccc
查詢字符串經(jīng)常用于頁面的數(shù)據(jù)過濾,例如:
1. 給列表頁面增加分頁參數(shù),list.aspx?page=2
2. 給列表頁面增加過慮范圍,Product.aspx?categoryId=5
3. 顯示特定記錄,ProductInfo.aspx?page=3
關(guān)于查詢字符串的用法,我補充二點:
1. 可以調(diào)用HttpUtility.ParseQueryString()來解析查詢字符串。
2. 允許參數(shù)名重復(fù):list.aspx?page=2&page=3,因此在修改URL參數(shù)時,使用替換方式而不是追加。
關(guān)于參數(shù)重名的讀取問題,請參考我的博客:細說 Request[]與Request.Params[]
優(yōu)點:
1. 不需要任何服務(wù)器資源:查詢字符串的數(shù)據(jù)包含在每個URL中。
2. 廣泛的支持:幾乎所有的瀏覽器和客戶端設(shè)備均支持使用查詢字符串傳遞參數(shù)值。
3. 實現(xiàn)簡單:在服務(wù)端直接訪問Request.QueryString[]可讀取數(shù)據(jù)。
4. 頁面?zhèn)髦岛唵危?lt;a href="url">或者 Response.Redirect(url) 都可以實現(xiàn)。
缺點:
1. 有長度限制。
2. 用戶可見,不能保存敏感數(shù)據(jù)。
Cookie
由于HTTP協(xié)議是無狀態(tài)的,對于一個瀏覽器發(fā)出的多次請求,web服務(wù)器無法區(qū)分它們是不是來源于同一個瀏覽器。所以,需要額外的數(shù)據(jù)用于維護會話。 Cookie 正是這樣的一段隨HTTP請求一起被傳遞的額外數(shù)據(jù)。 Cookie 是一小段文本信息,它的工作方式就是伴隨著用戶請求和頁面在 Web 服務(wù)器和瀏覽器之間傳遞。Cookie 包含每次用戶訪問站點時 Web 應(yīng)用程序都可以讀取的信息。
與hidden-input, QueryString相比,Cookie有更多的屬性,許多瀏覽器可以直接查看這些信息:
由于Cookie擁有這些屬性,因此在客戶端狀態(tài)管理中可以實現(xiàn)更多的功能,尤其是在實現(xiàn)客戶端會話方面具有不可替代的作用。
關(guān)于Cookie的更多講解,請參考我的另一篇博客:細說Cookie
優(yōu)點:
1. 可配置到期規(guī)則:Cookie可以在客戶端長期存在,也可以在瀏覽器關(guān)閉時清除。
2. 不需要任何服務(wù)器資源:Cookie 存儲在客戶端。
3. 簡單性:Cookie 是一種基于文本的輕量結(jié)構(gòu),包含簡單的鍵值對。
4. 數(shù)據(jù)持久性:與其它的客戶端狀態(tài)數(shù)據(jù)相比,Cookie可以實現(xiàn)長久保存。
5. 良好的擴展性:Cookie的讀寫要經(jīng)過ASP.NET管線,擁有無限的擴展性。
這里我要解釋一下Cookie 【良好的擴展性】是個什么概念,比如:
1. 我可以實現(xiàn)把Cookie保存到數(shù)據(jù)庫中而不需要修改現(xiàn)有的項目代碼。
2. 把SessionId這樣由ASP.NET產(chǎn)生的臨時Cookie讓它變成持久保存。
缺點:
1. 大小受到限制。
2. 增加請求頭長度。
3. 用戶可見,保存敏感數(shù)據(jù)時需要加密。
ApplicationState
應(yīng)用程序狀態(tài)是指采用HttpApplicationState實現(xiàn)的狀態(tài)維持方式,使用代碼如下:
Application.Lock(); Application["PageRequestCount"] = ((int)Application["PageRequestCount"]) + 1; Application.UnLock();
對于這種方法,我不建議使用,因為:
1. 與使用靜態(tài)變量差不多,直接使用靜態(tài)變量可以不需要字典查找。
2. 選擇強類型的集合或者變量可以避免裝箱拆箱。
ViewState,ControlState
視圖狀態(tài),控件狀態(tài),二者是類似,在頁面中表現(xiàn)為一個hidden-input元素:
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="......................" />
控件狀態(tài)是ASP.NET 2.0中引入,與視圖狀態(tài)相比,它不允許關(guān)閉。
由于它們使用方式一致,而且視圖狀態(tài)是基于控件狀態(tài)的實現(xiàn)邏輯,所以我就不區(qū)分它們了。
在ASP.NET的早期,微軟為了能幫助廣大開發(fā)人員提高開發(fā)效率,引用入一大批的服務(wù)端控件,并為了能將事件編程機制引入ASP.NET中,又發(fā)明了ViewState。
這種方式雖然可以簡化開發(fā)工作量,然而卻有一些限制和缺點:
1. 視圖狀態(tài)的數(shù)據(jù)只能用于回發(fā)(postback)。
2. 視圖狀態(tài)的【濫用】容易導(dǎo)致生成的HTML較大,這會引起一個惡性循環(huán):
a. 過大的ViewState在序列化過程中會消耗較多的服務(wù)器CPU資源,
b. 過大的ViewState最終生成的HTML輸出也會很大,它會浪費服務(wù)端網(wǎng)絡(luò)資源,
c. 過大的ViewState輸出導(dǎo)致表單在下次提交時,會占用客戶端網(wǎng)絡(luò)資源。
d. 過大的ViewState數(shù)據(jù)上傳到服務(wù)端后,反序列化又會消耗較多的服務(wù)器CPU資源。
因此,整個交互過程中,用戶一直在等待,用戶體驗極差。
在ASP.NET興起的年代,ViewState絕對是個了不起的發(fā)明。
然而,現(xiàn)在很多關(guān)于ASP.NET性能優(yōu)化的方法中,都會將【關(guān)閉ViewState】放在頭條位置。
為什么會這樣呢,大家可以自己思考一下了。
有些人認為:我現(xiàn)在做的程序只是在局域網(wǎng)內(nèi)使用,使用ViewState完全沒有問題!
然而,那些人或許沒有想過:
1. 未來用戶可能會把它部署在互聯(lián)網(wǎng)上運行(對于產(chǎn)品來說就是遇到大客戶了)。
2. 項目早期的設(shè)計與規(guī)劃,對后期的開發(fā)與維護來說,影響是巨大的,因為許多基礎(chǔ)部分通常是在早期開發(fā)的。
當(dāng)這二種情況的任何一種發(fā)生時,想再禁用ViewState,可能已經(jīng)晚了。
對于視圖狀態(tài),我認為它解決的問題比它引入的問題要多要復(fù)雜,
因此,我不想花時間整理它的優(yōu)缺點,我只想說一句:把它關(guān)了,在web.config中關(guān)了。
另外,我不排斥使用服務(wù)器控件,我認為:你可以使用服務(wù)端控件顯示數(shù)據(jù),但不要用它處理回發(fā)。
如果你仍然認為視圖狀態(tài)是不可缺少的,那我還是建議你看看ASP.NET MVC框架,看看沒有視圖狀態(tài)是不是照樣可以寫ASP.NET程序。
Session是ASP.NET實現(xiàn)的一種服務(wù)端會話技術(shù),它允許我們方便地在服務(wù)端保存與用戶有關(guān)的會話數(shù)據(jù)。
我認為Session只有一個優(yōu)點:最簡單的服務(wù)端會話實現(xiàn)方式。
缺點:
1. 當(dāng)mode="InProc"時,容易丟失數(shù)據(jù),為什么?因為網(wǎng)站會因為各種原因重啟。
2. 當(dāng)mode="InProc"時,Session保存的東西越多,就越占用服務(wù)器內(nèi)存,對于用戶在線人數(shù)較多的網(wǎng)站,服務(wù)器的內(nèi)存壓力會比較大。
3. 當(dāng)mode="InProc"時,程序的擴展性會受到影響,原因很簡單:服務(wù)器的內(nèi)存不能在多臺服務(wù)器間共享。
4. 當(dāng)采用進程外模式時,在每次請求中,不管你用不用會話數(shù)據(jù),所有的會話數(shù)據(jù)都為你準(zhǔn)備好了(反序列化),這其實很是浪費資源的。
5. 如果你沒有關(guān)閉Session,SessionStateModule就一直在工作中,尤其是全采用默認設(shè)置時,會對每個請求執(zhí)行一系列的調(diào)用,浪費資源。
6. 阻塞同一客戶端發(fā)起的多次請求(默認方式)。
7. 無Cookie會話可能會丟失數(shù)據(jù)(重新生成已過期的會話標(biāo)識符)。
Session的這些缺點也提醒我們:
1. 當(dāng)網(wǎng)站的在線人數(shù)較多時,一定不要用Session保存較大的對象。
2. 在密集型的AJAX型網(wǎng)站或者大量使用iframe的網(wǎng)站中,要關(guān)注Session可能引起的服務(wù)端阻塞問題。
3. 當(dāng)采用進程外模式時,不需要訪問Session的頁面,一定要關(guān)閉,否則會浪費服務(wù)器資源。
如果想了解更多的Session特點,以及我對Session的看法,可以瀏覽我的博客:Session,有沒有必要使用它?
Session的本質(zhì)有二點:
1. SessionId + 服務(wù)端字典:服務(wù)端字典保存了某個用戶的所有會話數(shù)據(jù)。
2. 用SessionId識別不同的客戶端:SessionId通常以Cookie形式發(fā)送到客戶端。
我認為了解Sesssion本質(zhì)非常有用,因為可以借鑒并實現(xiàn)自己的服務(wù)端會話方法。
關(guān)于Session我還想說一點:
有些新手喜歡用Session來實現(xiàn)身份認證功能,這是一種【不正確】的方法。
如果你的ASP.NET應(yīng)用程序需要身份認證功能,請使用 Forms身份認證 或者 Windows身份認證
Profile 在中文版的MSDN中被稱為 配置文件屬性,這個功能是在 ASP.NET 2.0 中引入的。
ASP.NET提供這個功能主要是為了簡化與用戶相關(guān)的個性化信息的讀寫方式。
簡化主要體現(xiàn)在3個方面:
1. 自動與某個用戶關(guān)聯(lián),已登錄用戶或者未登錄都支持。
2. 不需要我們設(shè)計用戶的個性化信息的保存表結(jié)構(gòu),只要修改配置文件就夠了。
3. 不需要我們實現(xiàn)數(shù)據(jù)的加載與保存邏輯,ASP.NET框架替我們實現(xiàn)好了。
為了使用Profile,我們首先在web.config中定義所需要的用戶個性化信息:
<profile> <properties> <add name="Address"/> <add name="Tel"/> </properties> </profile>
然后,就可以在頁面中使用了:
為什么會這樣呢?
原因是ASP.NET已經(jīng)根據(jù)web.config為我們創(chuàng)建了一個新類型:
using System; using System.Web.Profile; public class ProfileCommon : ProfileBase { public ProfileCommon(); public virtual string Address { get; set; } public virtual string Tel { get; set; } public virtual ProfileCommon GetProfile(string username); }
有了這個類型后,當(dāng)我們訪問HttpContext.Profile屬性時,ASP.NET會創(chuàng)建一個ProfileCommon的實例。 也正是由于Profile的強類型機制,在使用Profile時才會有智能提示功能。
如果我們希望為未登錄的匿名用戶也提供這種支持,需要將配置修改成:
<profile> <properties> <add name="Address" allowAnonymous="true" /> <add name="Tel" allowAnonymous="true"/> </properties> </profile> <anonymousIdentification enabled="true" />
Profile中的每個屬性還允許指定類型和默認值,以及序列化方式,因此,擴展性還是比較好的。
盡管Profile看上去很美,然而,使用Profile的人卻很少。
比如我就不用它,我也沒見有人有過它。
為什么會這樣?
我個人認為:它與MemberShip一樣,是個雞肋。
通常說來,我們會為用戶信息創(chuàng)建一張User表,增加用戶信息時,會通過增加字段的方式解決。
我認為這樣集中的數(shù)據(jù)才會更好,而不是說,有一部分數(shù)據(jù)由我維護,另一部分數(shù)據(jù)由ASP.NET維護。
另一個特例是:我們根本不創(chuàng)建User表,直接使用MemberShip,那么Profile用來保存MemberShip沒有信息是有必要的。
還是給Profile做個總結(jié)吧:
優(yōu)點:使用簡單。
缺點:不實用。
各種狀態(tài)管理的對比與總結(jié)
前面分別介紹了ASP.NET的8種狀態(tài)管理技術(shù),這里打算給它們做個總結(jié)。
客戶端 | 服務(wù)端 | |
數(shù)據(jù)安全性 | 差 | 好 |
數(shù)據(jù)長度限制 | 有 | 受硬件限制 |
占用服務(wù)器資源 | 否 | 是 |
集群擴展性 | 好 | 差 |
表格中主要考察了數(shù)據(jù)保存與服務(wù)端水平擴展的相關(guān)重要指標(biāo)。
下面我來解釋表格的結(jié)果。
1. 客戶端方式的狀態(tài)數(shù)據(jù)(hidden-input, QueryString, Cookie):
a. 數(shù)據(jù)對用戶來說,可見可修改,因此數(shù)據(jù)不安全。
b. QueryString, Cookie 都有長度限制。
c. 數(shù)據(jù)在客戶端,因此不占用服務(wù)端資源。這個特性對于在線人數(shù)很多的網(wǎng)站非常重要。
d. 數(shù)據(jù)在客戶端,因此和服務(wù)端沒有耦合關(guān)系,WEB服務(wù)器可以更容易實現(xiàn)水平擴展。
2. 服務(wù)端方式的狀態(tài)數(shù)據(jù)(ApplicationState,ViewState,ControlState,Session,Profile):
a. 數(shù)據(jù)對用戶不可見,因此安全性好。(ApplicationState,Session,Profile)
b. 數(shù)所長度只受硬件限制,因此,對于在線人數(shù)較多的網(wǎng)站,需謹慎選擇。
c. 對于存放在內(nèi)存中的狀態(tài)數(shù)據(jù),由于不能共享內(nèi)存,因此會限制水平擴展能力。
d. 如果狀態(tài)數(shù)據(jù)保存到一臺機器,會有單點失敗的可能,也會限制了水平擴展能力。
從這個表格我們還可以得到以下結(jié)論:
1. 如果很關(guān)注數(shù)據(jù)的安全性,應(yīng)該首選服務(wù)端的狀態(tài)管理方法。
2. 如果你關(guān)注服務(wù)端的水平擴展性,應(yīng)該首選客戶端的狀態(tài)管理方法。
會話狀態(tài)的選擇
接下來,我們再來看看會話狀態(tài),它與狀態(tài)管理有著一些關(guān)系,屬于比較類似的概念。
談到會話狀態(tài),首先我要申明一點:會話狀態(tài)與狀態(tài)不是一回事。
本文前面所說的狀態(tài)分為二種:
1. 頁面之間的狀態(tài)。
2. 應(yīng)用程序范圍內(nèi)的狀態(tài)。
而會話狀態(tài)是針對某個用戶來說,他(她)在多次操作之間的狀態(tài)。
在用戶的操作期間,有可能狀態(tài)需要在頁面之間持續(xù)使用,
也有可能服務(wù)端程序做過重啟,但數(shù)據(jù)仍然有效。
因此,這種狀態(tài)數(shù)據(jù)更持久。
在ASP.NET中,使用會話狀態(tài)有二個選擇:Session 或者 Cookie 。
前者由ASP.NET實現(xiàn),并有可能依賴后者。
后者則由瀏覽器實現(xiàn),ASP.NET提供讀寫方法。
那么到底選擇哪個呢?
如果你要問我這個問題,我肯定會說:我選 Cookie !
下面是我選擇Cookie實現(xiàn)會話狀態(tài)的理由:
1. 不會有服務(wù)端阻塞問題。
2. 不占用服務(wù)端資源。
3. 水平擴展沒有限制。
4. 也支持過期設(shè)置,而且更靈活。
5. 可以在客戶端直接使用會話數(shù)據(jù)。
6. 可以實現(xiàn)更靈活的會話數(shù)據(jù)加載策略。
7. 擴展性較好(源于ASP.NET管線的擴展性)
如果選擇使用Cookie實現(xiàn)會話狀態(tài),有3點需要特別注意:
1. 不建議保存敏感數(shù)據(jù),除非已加密。
2. 只適合保存短小簡單的數(shù)據(jù)。
3. 如果會話數(shù)據(jù)較大,可以在客戶端保存用戶標(biāo)識,由服務(wù)端實現(xiàn)數(shù)據(jù)的加載保存邏輯。
或許有些人認為:每種技術(shù)都有它們的優(yōu)缺點,有各自的適用領(lǐng)域。
我表示贊同這句話。
但是,我們要清楚一點:每個項目的規(guī)模不一樣,性能以及擴展性要求也不同。
對于一個小的項目來說,選擇什么方法都不是問題,
但是,對于規(guī)模較大的項目,我們一定需要取舍。
取舍的目標(biāo)是:包裝越少越好,因為人家做了過多的包裝,就會有較多的限制,
所以,不要只關(guān)注現(xiàn)在的調(diào)用是否方便,其實只要你愿意包裝,你也可以讓復(fù)雜的調(diào)用簡單化。
改變開發(fā)方式,發(fā)現(xiàn)新方法
回想一下:為什么在ASP.NET中需要狀態(tài)管理?
答:因為與HTTP協(xié)議有關(guān),服務(wù)端沒有保存每個請求的上次頁面狀態(tài)。
為什么Windows計算器(這類)程序不用考慮會話問題呢?
答:因為這類程序的界面不需要重新生成,任何變量都可表示狀態(tài)。
再來看這樣一個場景:
圖片左邊是一個列表頁面,允許調(diào)整每條記錄的優(yōu)先級,但是有2個要求:
1. 在移動每條記錄時,必須輸入一個調(diào)整理由。
2. 只要輸入理由后,那條記錄可以任意調(diào)整多次。
顯然,完成這個任務(wù)必須要有狀態(tài)才能實現(xiàn)。
面對這個問題,你可以思考一下:選擇哪種ASP.NET支持的狀態(tài)管理方法都很麻煩。
怎么辦?
我的解決方法:創(chuàng)建一個JavaScript數(shù)組,用每個數(shù)組元素保存每條記錄的狀態(tài),
所有用戶交互操作用AJAX方式實現(xiàn),這樣頁面不會刷新,JavaScript變量中的狀態(tài)一直有效。
因此,很容易就能解決這個問題。
這個案例也提醒我們:當(dāng)發(fā)現(xiàn)ASP.NET提供的狀態(tài)管理功能全部不合適時, 我們需要改變開發(fā)方式了。
為什么WEB編程都有【無狀態(tài)】問題,而桌面程序沒有?
我認為與HTTP協(xié)議有關(guān),但沒有絕對的關(guān)系。
只要你能保證頁面不刷新,也能像桌面程序那樣,用JavaScript變量就能維護頁面狀態(tài)。