由於HTTP協議的無狀態特性,導致在ASP.NET編程中,每個請求都會在服務端從頭到執行一次管線過程,
對於ASP.NET頁面來說,Page對象都會重新建立,所有控制項以及內容都會重建,
因此,如果希望上一次的頁面狀態能夠在後續頁面中保留,則必需引入狀態管理功能。
ASP.NET為了實現狀態管理功能,提供了8種方法,可協助我們在頁面之間或者整個使用者會話期間保留狀態資料。
這些方法分為二類:檢視狀態、控制項狀態、隱藏欄位、Cookie 和查詢字串會以不同方式將資料發送到用戶端上。
而應用程式狀態、工作階段狀態和設定檔屬性(Profile)則會將資料存放區到服務端。
雖然每種方法都有不同的優點和缺點,對於小的項目來說,可以選擇自己認為最容易使用的方法,
然而,對於有著較高要求的程式,尤其是對於效能與擴充性比較關注的程式來說,
選擇不同的方法最終導致的差別可能就非常大了。
在這篇部落格中,我將談談自己對ASP.NET狀態管理方面的一些看法。
注意:本文的觀點可能並不合適開發小型項目,因為我關注的不是易用性。
hidden-input
hidden-input 這個名字我是取的,表示所有type="hidden"的input標籤元素。
在中文版的MSDN中,也稱之為 隱藏欄位 。
hidden-input通常存在於HTML表單之內,它不會顯示到頁面中,
但可以隨表單一起提交,因此,經常用於維護當前頁面的相關狀態,在服務端我們可以使用Request.Form[]來訪問這些資料。
一般說來,我通常使用hidden-input來儲存一些中間結果,用於在多次提交中維持一系列狀態,
或者用它來儲存一些固定參數用來提交給其它頁面(或網站)。
在這些情境中,我不希望使用者看到這些資料,因此,使用hidden-input是比較方便的。
關於表單的更多介紹可參考我的部落格:細說 Form (表單)
在ASP.NET WebForm架構中,我們可以使用HiddenField控制項來建立一個hidden-input控制項,並可以在服務端操作它,
還可以直接以手寫的方式使用隱藏欄位,例如:
<input type="hidden" name="hidden-1" value="aaaaaaa" /><input type="hidden" name="hidden-2" value="bbbbbbb" /><input type="hidden" name="hidden-3" value="ccccccc" />
另外,我們還可以調用ClientScript.RegisterHiddenField()方法來建立隱藏欄位:
ClientScript.RegisterHiddenField("hidden-4", "ddddddddd");
輸出結果:
<input type="hidden" name="hidden-4" id="hidden-4" value="ddddddddd" />
這三種方法對於產生的HTML代碼來說,主要差別在於它們出現位置不同:
1. HiddenField控制項:由HiddenField的出現位置來決定(在form內部)。
2. RegisterHiddenField方法:在form標籤的開頭位置。
3. hidden-input:你寫在哪裡就是哪裡。
優點:
1. 不需要任何伺服器資源:隱藏欄位隨頁面一起發送到用戶端。
2. 廣泛的支援:幾乎所有瀏覽器和用戶端裝置都支援具有隱藏欄位的表單。
3. 實現簡單:隱藏欄位是標準的 HTML 控制項,不需要複雜的編程邏輯。
缺點:
1. 不能在多頁面跳轉之間維持狀態。
2. 使用者可見,儲存敏感性資料時需要加密。
QueryString
查詢字串是存在於 URL 結尾的一段資料。下面是一個典型的查詢字串樣本(紅色部分文字):
http://www.abc.com/demo.aspx?k1=aaa&k2=bbb&k3=ccc
查詢字串經常用於頁面的資料過濾,例如:
1. 給列表頁面增加分頁參數,list.aspx?page=2
2. 給列表頁面增加過慮範圍,Product.aspx?categoryId=5
3. 顯示特定記錄,ProductInfo.aspx?page=3
關於查詢字串的用法,我補充二點:
1. 可以調用HttpUtility.ParseQueryString()來解析查詢字串。
2. 允許參數名重複:list.aspx?page=2&page=3,因此在修改URL參數時,使用替換方式而不是追加。
關於參數重名的讀取問題,請參考我的部落格:細說 Request[]與Request.Params[]
優點:
1. 不需要任何伺服器資源:查詢字串的資料包含在每個URL中。
2. 廣泛的支援:幾乎所有的瀏覽器和用戶端裝置均支援使用查詢字串傳遞參數值。
3. 實現簡單:在服務端直接存取Request.QueryString[]可讀取資料。
4. 頁面傳值簡單:<a href="url">或者 Response.Redirect(url) 都可以實現。
缺點:
1. 有長度限制。
2. 使用者可見,不能儲存敏感性資料。
Cookie
由於HTTP協議是無狀態的,對於一個瀏覽器發出的多次請求,WEB伺服器無法區分它們是不是來源於同一個瀏覽器。所以,需要額外的資料用於維護會話。 Cookie 正是這樣的一段隨HTTP請求一起被傳遞的額外資料。
Cookie 是一小段文本資訊,它的工作方式就是伴隨著使用者請求和頁面在 Web 服務器和瀏覽器之間傳遞。Cookie 包含每次使用者訪問網站時 Web 應用程式都可以讀取的資訊。
與hidden-input, QueryString相比,Cookie有更多的屬性,許多瀏覽器可以直接查看這些資訊:
由於Cookie擁有這些屬性,因此在用戶端狀態管理中可以實現更多的功能,尤其是在實現用戶端工作階段方面具有不可替代的作用。
關於Cookie的更多講解,請參考我的另一篇部落格:細說Cookie
優點:
1. 可配置到期規則:Cookie可以在用戶端長期存在,也可以在瀏覽器關閉時清除。
2. 不需要任何伺服器資源:Cookie 儲存在用戶端。
3. 簡單性:Cookie 是一種基於文本的輕量結構,包含簡單的索引值對。
4. 資料持久性:與其它的用戶端狀態資料相比,Cookie可以實現長久儲存。
5. 良好的擴充性:Cookie的讀寫要經過ASP.NET管線,擁有無限的擴充性。
這裡我要解釋一下Cookie 【良好的擴充性】是個什麼概念,比如:
1. 我可以實現把Cookie儲存到資料庫中而不需要修改現有的項目代碼。
2. 把SessionId這樣由ASP.NET產生的臨時Cookie讓它變成持久儲存。
缺點:
1. 大小受到限制。
2. 增加要求標頭長度。
3. 使用者可見,儲存敏感性資料時需要加密。
ApplicationState
應用程式狀態是指採用HttpApplicationState實現的狀態維持方式,使用代碼如下:
Application.Lock();Application["PageRequestCount"] = ((int)Application["PageRequestCount"]) + 1;Application.UnLock();
對於這種方法,我不建議使用,因為:
1. 與使用靜態變數差不多,直接使用靜態變數可以不需要字典尋找。
2. 選擇強型別的集合或者變數可以避免裝箱拆箱。
ViewState,ControlState
檢視狀態,控制項狀態,二者是類似,在頁面中表現為一個hidden-input元素:
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="......................" />
控制項狀態是ASP.NET 2.0中引入,與檢視狀態相比,它不允許關閉。
由於它們使用方式一致,而且檢視狀態是基於控制項狀態的實現邏輯,所以我就不區分它們了。
在ASP.NET的早期,微軟為了能協助廣大開發人員提高開發效率,引用入一大批的服務端控制項,並為了能將事件編程機制引入ASP.NET中,又發明了ViewState。
這種方式雖然可以簡化開發工作量,然而卻有一些限制和缺點:
1. 檢視狀態的資料只能用於回傳(postback)。
2. 檢視狀態的【濫用】容易導致產生的HTML較大,這會引起一個惡性迴圈:
a. 過大的ViewState在序列化過程中會消耗較多的伺服器CPU資源,
b. 過大的ViewState最終產生的HTML輸出也會很大,它會浪費服務端網路資源,
c. 過大的ViewState輸出導致表單在下次提交時,會佔用用戶端網路資源。
d. 過大的ViewState資料上傳到服務端後,還原序列化又會消耗較多的伺服器CPU資源。
因此,整個互動過程中,使用者一直在等待,使用者體驗極差。
在ASP.NET興起的年代,ViewState絕對是個了不起的發明。
然而,現在很多關於ASP.NET效能最佳化的方法中,都會將【關閉ViewState】放在頭條位置。
為什麼會這樣呢,大家可以自己思考一下了。
有些人認為:我現在做的程式只是在區域網路內使用,使用ViewState完全沒有問題!
然而,那些人或許沒有想過:
1. 未來使用者可能會把它部署在互連網上運行(對於產品來說就是遇到大客戶了)。
2. 項目早期的設計與規劃,對後期的開發與維護來說,影響是巨大的,因為許多基礎部分通常是在早期開發的。
當這二種情況的任何一種發生時,想再禁用ViewState,可能已經晚了。
對於檢視狀態,我認為它引入的問題比它解決的問題要多要複雜,
因此,我不想花時間整理它的優缺點,我只想說一句:把它關了,在web.config中關了。
另外,我不排斥使用伺服器控制項,我認為:你可以使用服務端控制項顯示資料,但不要用它處理回傳。
如果你仍然認為檢視狀態是不可缺少的,那我還是建議你看看ASP.NET MVC架構,看看沒有檢視狀態是不是照樣可以寫ASP.NET程式。
Session
Session是ASP.NET實現的一種服務端會話技術,它允許我們方便地在服務端儲存與使用者有關的會話資料。
我認為Session只有一個優點:最簡單的服務端會話實現方式。
缺點:
1. 當mode="InProc"時,容易遺失資料,為什嗎?因為網站會因為各種原因重啟。
2. 當mode="InProc"時,Session儲存的東西越多,就越佔用伺服器記憶體,對於使用者線上人數較多的網站,伺服器的記憶體壓力會比較大。
3. 當mode="InProc"時,程式的擴充性會受到影響,原因很簡單:伺服器的記憶體不能在多台伺服器間共用。
4. 當採用進程外模式時,在每次請求中,不管你用不用會話資料,所有的會話資料都為你準備好了(還原序列化),這其實很是浪費資源的。
5. 如果你沒有關閉Session,SessionStateModule就一直在工作中,尤其是全採用預設設定時,會對每個請求執行一系列的調用,浪費資源。
6. 阻塞同一用戶端發起的多次請求(預設)。
7. 無Cookie會話可能會遺失資料(重建已到期的工作階段識別項)。
Session的這些缺點也提醒我們:
1. 當網站的線上人數較多時,一定不要用Session儲存較大的對象。
2. 在密集型的AJAX型網站或者大量使用iframe的網站中,要關注Session可能引起的服務端阻塞問題。
3. 當採用進程外模式時,不需要訪問Session的頁面,一定要關閉,否則會浪費伺服器資源。
如果想瞭解更多的Session特點,以及我對Session的看法,可以瀏覽我的部落格:Session,有沒有必要使用它?
Session的本質有二點:
1. SessionId + 服務端字典:服務端字典儲存了某個使用者的所有會話資料。
2. 用SessionId識別不同的用戶端:SessionId通常以Cookie形式發送到用戶端。
我認為瞭解Sesssion本質非常有用,因為可以借鑒並實現自己的服務端會話方法。
關於Session我還想說一點:
有些新手喜歡用Session來實現身份認證功能,這是一種【不正確】的方法。
如果你的ASP.NET應用程式需要身份認證功能,請使用 Forms身份認證 或者 Windows身份認證
Profile
Profile 在中文版的MSDN中被稱為 設定檔屬性,這個功能是在 ASP.NET 2.0 中引入的。
ASP.NET提供這個功能主要是為了簡化與使用者相關的個人化資訊的讀寫方式。
簡化主要體現在3個方面:
1. 自動與某個使用者關聯,已登入使用者或者未登入都支援。
2. 不需要我們設計使用者的個人化資訊的儲存表結構,只要修改設定檔就夠了。
3. 不需要我們實現資料的載入與儲存邏輯,ASP.NET架構替我們實現好了。
為了使用Profile,我們首先在web.config中定義所需要的使用者個人化資訊:
<profile> <properties> <add name="Address"/> <add name="Tel"/> </properties></profile>
然後,就可以在頁面中使用了:
為什麼會這樣呢?
原因是ASP.NET已經根據web.config為我們建立了一個新類型:
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);}
有了這個類型後,當我們訪問HttpContext.Profile屬性時,ASP.NET會建立一個ProfileCommon的執行個體。
也正是由於Profile的強型別機制,在使用Profile時才會有智能提示功能。
如果我們希望為未登入的匿名使用者也提供這種支援,需要將配置修改成:
<profile> <properties> <add name="Address" allowAnonymous="true" /> <add name="Tel" allowAnonymous="true"/> </properties></profile><anonymousIdentification enabled="true" />
Profile中的每個屬性還允許指定類型和預設值,以及序列化方式,因此,擴充性還是比較好的。
儘管Profile看上去很美,然而,使用Profile的人卻很少。
比如我就不用它,我也沒見有人有過它。
為什麼會這樣?
我個人認為:它與MemberShip一樣,是個雞肋。
通常說來,我們會為使用者資訊建立一張User表,增加使用者資訊時,會通過增加欄位的方式解決。
我認為這樣集中的資料才會更好,而不是說,有一部分資料由我維護,另一部分資料由ASP.NET維護。
另一個特例是:我們根本不建立User表,直接使用MemberShip,那麼Profile用來儲存MemberShip沒有資訊是有必要的。
還是給Profile做個總結吧:
優點:使用簡單。
缺點:不實用。
各種狀態管理的對比與總結
前面分別介紹了ASP.NET的8種狀態管理技術,這裡打算給它們做個總結。
|
用戶端 |
服務端 |
資料安全性 |
差 |
好 |
資料長度限制 |
有 |
受硬體限制 |
佔用伺服器資源 |
否 |
是 |
叢集擴充性 |
好 |
差 |
表格中主要考察了資料儲存與服務端水平擴充的相關重要指標。
下面我來解釋表格的結果。
1. 用戶端方式的狀態資料(hidden-input, QueryString, Cookie):
a. 資料對使用者來說,可見可修改,因此資料不安全。
b. QueryString, Cookie 都有長度限制。
c. 資料在用戶端,因此不佔用服務端資源。這個特性對於線上人數很多的網站非常重要。
d. 資料在用戶端,因此和服務端沒有耦合關係,WEB伺服器可以更容易實現水平擴充。
2. 服務端方式的狀態資料(ApplicationState,ViewState,ControlState,Session,Profile):
a. 資料對使用者不可見,因此安全性好。(ApplicationState,Session,Profile)
b. 數所長度只受硬體限制,因此,對於線上人數較多的網站,需謹慎選擇。
c. 對於存放在記憶體中的狀態資料,由於不能共用記憶體,因此會限制水平擴充能力。
d. 如果狀態資料儲存到一台機器,會有單點失敗的可能,也會限制了水平擴充能力。
從這個表格我們還可以得到以下結論:
1. 如果很關注資料的安全性,應該首選服務端的狀態管理方法。
2. 如果你關注服務端的水平擴充性,應該首選用戶端的狀態管理方法。
工作階段狀態的選擇
接下來,我們再來看看工作階段狀態,它與狀態管理有著一些關係,屬於比較類似的概念。
談到工作階段狀態,首先我要申明一點:工作階段狀態與狀態不是一回事。
本文前面所說的狀態分為二種:
1. 頁面之間的狀態。
2. 應用程式範圍內的狀態。
而工作階段狀態是針對某個使用者來說,他(她)在多次操作之間的狀態。
在使用者的操作期間,有可能狀態需要在頁面之間持續使用,
也有可能服務端程式做過重啟,但資料仍然有效。
因此,這種狀態資料更持久。
在ASP.NET中,使用工作階段狀態有二個選擇:Session 或者 Cookie 。
前者由ASP.NET實現,並有可能依賴後者。
後者則由瀏覽器實現,ASP.NET提供讀寫方法。
那麼到底選擇哪個呢?
如果你要問我這個問題,我肯定會說:我選 Cookie !
下面是我選擇Cookie實現工作階段狀態的理由:
1. 不會有服務端阻塞問題。
2. 不佔用服務端資源。
3. 水平擴充沒有限制。
4. 也支援到期設定,而且更靈活。
5. 可以在用戶端直接使用會話資料。
6. 可以實現更靈活的會話資料載入策略。
7. 擴充性較好(源於ASP.NET管線的擴充性)
如果選擇使用Cookie實現工作階段狀態,有3點需要特別注意:
1. 不建議儲存敏感性資料,除非已加密。
2. 只適合儲存短小簡單的資料。
3. 如果會話資料較大,可以在用戶端儲存使用者標識,由服務端實現資料的載入儲存邏輯。
或許有些人認為:每種技術都有它們的優缺點,有各自的適用領域。
我表示贊同這句話。
但是,我們要清楚一點:每個項目的規模不一樣,效能以及擴充性要求也不同。
對於一個小的項目來說,選擇什麼方法都不是問題,
但是,對於規模較大的項目,我們一定需要取捨。
取捨的目標是:封裝越少越好,因為人家做了過多的封裝,就會有較多的限制,
所以,不要只關注現在的調用是否方便,其實只要你願意封裝,你也可以讓複雜的調用簡單化。
改變開發方式,發現新方法
回想一下:為什麼在ASP.NET中需要狀態管理?
答:因為與HTTP協議有關,服務端沒有儲存每個請求的上次頁面狀態。
為什麼Windows計算機(這類)程式不用考慮會話問題呢?
答:因為這類程式的介面不需要重建,任何變數都可表示狀態。
再來看這樣一個情境:
圖片左邊是一個列表頁面,允許調整每條記錄的優先順序,但是有2個要求:
1. 在移動每條記錄時,必須輸入一個調整理由。
2. 只要輸入理由後,那條記錄可以任意調整多次。
顯然,完成這個任務必須要有狀態才能實現。
面對這個問題,你可以思考一下:選擇哪種ASP.NET支援的狀態管理方法都很麻煩。
怎麼辦?
我的解決方案:建立一個JavaScript數組,用每個數組元素儲存每條記錄的狀態,
所有使用者互動操作用AJAX方式實現,這樣頁面不會重新整理,JavaScript變數中的狀態一直有效。
因此,很容易就能解決這個問題。
這個案例也提醒我們:當發現ASP.NET提供的狀態管理功能全部不合適時,
我們需要改變開發方式了。
為什麼WEB編程都有【無狀態】問題,而傳統型程式沒有?
我認為與HTTP協議有關,但沒有絕對的關係。
只要你能保證頁面不重新整理,也能像傳統型程式那樣,用JavaScript變數就能維護頁面狀態。
如果,您認為閱讀這篇部落格讓您有些收穫,不妨點擊一下右下角的【推薦】按鈕。
如果,您希望更容易地發現我的新部落格,不妨點擊一下右下角的【關注 Fish Li】。
因為,我的寫作熱情也離不開您的肯定支援。
感謝您的閱讀,如果您對我的部落格所講述的內容有興趣,請繼續關注我的後續部落格,我是Fish Li 。