ASP.NET狀態管理的總結


由於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變量就能維護頁面狀態。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM