Asp.net狀態管理

狀態管理概述
 1.1  狀態管理與數據庫
狀態管理是你對同一頁或不同頁的多個請求維護狀態和頁信息的過程。與所有基於 HTTP 的技術一樣,Web 窗體頁是無狀態的,這意味着它們不自動指示序列中的請求是否全部來自相同的客戶端,或者單個瀏覽器實例是否一直在查看頁或站點。此外,到服務器的每一往返過程都將銷燬並重新創建頁;因此,如果超出了單個頁的生命週期,頁信息將不存在。比如,我們在代碼中聲明一個DataSet從數據庫獲取記錄,頁面回發(也就是重新請求)後這個DataSet是空的,這就是爲什麼在ASP.NET應用程序中,甚至在一個頁面中需要多次連接數據庫獲取記錄。正是由於這個原因,狀態管理對於Web編程來說非常重要,從第一代動態Web編程語言開始就支持多種狀態管理以彌補HTTP無狀態的不足。
現在的Web應用程序,通常都是數據驅動的,但是在狀態處理中,我們應該儘量減少對數據庫的依賴,原因如下。
·      數據庫是存放在磁盤上的。如果把數據存放在數據庫中的話,性能會比較差。
·      很多數據是和用戶相關的。如果把數據存放在數據庫中的話,我們沒有一個唯一的標誌來區分哪條記錄對應哪個客戶端(瀏覽器)。
·      很多數據是臨時的,用戶關閉了瀏覽器這些數據就不再需要了。如果把數據存放在數據庫中的話,我們不知道是哪個用戶關閉了瀏覽器,也就不能及時把數據刪除。
n  通常來講,狀態管理的作用主要概括爲以下幾點。
·      指示用戶信息,關聯瀏覽器實例。
·      使得頁與頁之間,請求與請求之間能夠共享信息。
·      更爲快速的數據存儲與讀取。
1.2  狀態管理的比較參數
ASP.NET提供了很多狀態管理機制,各有各的特點。一般來說,我們是在以下幾個方面來比較各種狀態管理機制:
·      存儲的物理位置。比如是存儲在客戶端還是服務端。
·      存儲的類型限制。比如是可以存放任意類型還是僅僅可以存放字符串。
·      狀態使用的範圍。比如是否可以跨應用程序?是否可以跨用戶?是否可以跨頁面?
·      存儲的大小限制。比如是任意大小還是有一定字節限制。
·      生命週期。什麼時候建立?什麼時候銷燬?
·      安全與性能。比如是否加密存儲?是否適合存儲大量數據?
·      優點缺點與注意事項。
只有對這些概念有一個清晰的認識後,我們才能靈活使用各種狀態管理機制。狀態管理是Web應用程序的利器,但是它是一把雙刃劍,不合理的使用各種狀態管理機制會對整個網站的性能造成很大的影響,甚至使網站癱瘓。因此,使用狀態管理的重要原則就是在有必要的時候用,而不能因爲方便到處都用。
 
===============================================================================
===============================================================================
===============================================================================
 

2  Cookie

2.1  Cookie概述

Cookie 爲 Web 應用程序保存用戶相關信息提供了一種有用的方法。例如,當用戶訪問站點時,可以利用 Cookie 保存用戶首選項或其他信息,這樣,當用戶下次再訪問站點時,應用程序就可以檢索以前保存的信息。

從技術上講,Cookie是小段保存在客戶端的數據(如果你安裝的是XP,可以看一下<安裝Windows的盤>:/Documents and Settings/<用戶名>/Cookies文件夾)。用戶訪問網站的時候,網站會給用戶一個包含過期時間的Cookie,瀏覽器收到Cookie後就存放在客戶端的文件夾下。以後用戶每次訪問網站頁面的時候,瀏覽器會根據網站的URL在本地Cookie文件夾內查找是否存在當前網站關聯的Cookie,如果有的話就連同頁面請求一起發送到服務器。

關於Cookie的知識還需要了解以下幾點。

·      Cookie只是一段字符串,並不能執行。

·      大多數瀏覽器規定Cookie大小不超過4K,每個站點能保存的Cookie不超過20個,所有站點保存的Cookie總和不超過300個。

·      除了Cookie外,幾乎沒有其他的方法在客戶端的機器上寫入數據(就連Cookie的寫入操作也是瀏覽器進行的)。當然,連Cookie都可以通過瀏覽器安全配置來禁止。如果你使用IE瀏覽器,可以看一下“工具”→“Internet”選項→“隱私”一頁。現在的大多數網站都利用Cookie來保存一些數據(比如你的ID),以便你下一次訪問網站時能直接“繼續”以前的配置,所以我還是建議你不要輕易關閉Cookie。

在使用Cookie時,必須意識到其固有的安全弱點。Cookie畢竟是存放於客戶端的。因此,不要在Cookie中保存保密信息,如用戶名、密碼、信用卡號等。在Cookie中不要保存不應該由用戶掌握的內容,也不要保存可能被其他竊取Cookie的人控制的內容。

2.2  Cookie的使用

下面,我們就來討論如何保存、讀取、刪除和修改Cookie。首先在頁面上添加4個按鈕用來完成這4個操作。

<asp:Button ID="btn_SaveCookie" runat="server" OnClick="btn_SaveCookie_Click"

Text="保存Cookie" />

<asp:Button ID="btn_ReadCookie" runat="server" Text="讀取Cookie"

OnClick="btn_ReadCookie_Click" />

<asp:Button ID="btn_ModifyCookie" runat="server" OnClick="btn_ModifyCookie_Click"

Text="修改Cookie" />

<asp:Button ID="btn_DelCookie" runat="server" Text="刪除Cookie"

OnClick="btn_DelCookie_Click" />

保存Cookie的方法如下。

protected void btn_SaveCookie_Click(object sender, EventArgs e)

{

    HttpCookie SingleValueCookie = new HttpCookie("test1", "單值Cookie");

    SingleValueCookie.Expires = DateTime.Now.AddDays(1);

    Response.Cookies.Add(SingleValueCookie);

    HttpCookie MultiValueCookie = new HttpCookie("test2");

    MultiValueCookie.Values.Add("key1", "value1");

    MultiValueCookie.Values.Add("key2", "value2");

    MultiValueCookie.Expires = DateTime.Now.AddDays(1);

    Response.Cookies.Add(MultiValueCookie);

}

我們可以看到,一個Cookie中允許保存單個值也可以保存多個值。HttpCookie類型表示一個Cookie,Expires屬性用於修改Cookie的過期時間。對於單值Cookie,既可以直接在構造方法中指定值也可以使用Value屬性指定值。對於多值Cookie,既可以使用Values屬性的Add方法添加子鍵和值,也可以直接使用Values屬性的索引設置子鍵和值。上面這段代碼等價於下面這段代碼。

protected void btn_SaveCookie_Click(object sender, EventArgs e)

{

    HttpCookie SingleValueCookie = new HttpCookie("test1");

    SingleValueCookie.Value = "單值Cookie";

    SingleValueCookie.Expires = DateTime.Now.AddDays(1);

    Response.Cookies.Add(SingleValueCookie);

    HttpCookie MultiValueCookie = new HttpCookie("test2");

    MultiValueCookie.Values["key1"] = "value1";

    MultiValueCookie.Values["key2"] = "value2";

    MultiValueCookie.Expires = DateTime.Now.AddDays(1);

    Response.Cookies.Add(MultiValueCookie);

}

在添加完值以後,務必記得使用Response對象把Cookie重新返回給瀏覽器。我們的服務器不能直接在客戶端機器上寫Cookie,而是由瀏覽器完成這一工作,當然用戶也可以設置是否允許瀏覽器讀寫Cookie。

下面是讀取Cookie的操作。

protected void btn_ReadCookie_Click(object sender, EventArgs e)

{

    HttpCookie SingleValueCookie = Request.Cookies["test1"];

    if (SingleValueCookie != null)

    {

        Response.Write(string.Format("Key:{0} Value:{1} Expires:{2}<br/>", "test1",

        SingleValueCookie.Value, SingleValueCookie.Expires));

    }

    HttpCookie MultiValueCookie = Request.Cookies["test2"];

    if (MultiValueCookie!= null)

    {

        Response.Write(string.Format("Key:{0} Value:{1}<br/>", "test2", MultiValueCookie.

          Value));

        foreach (string subkey in MultiValueCookie.Values.AllKeys)

        {

            Response.Write(string.Format("SubKey:{0} Value:{1} Expires:{2}<br/>",

            subkey, MultiValueCookie.Values[subkey], MultiValueCookie.Expires));

        }

    }

}

對於多值Cookie,我們通過遍歷AllKeys屬性返回的字符串數組獲取所有子鍵Key,從而獲得子鍵的值。要注意的是,在訪問Cookie以前,需要檢測一下Cookie是否存在。打開頁面,先單擊“保存Cookie”按鈕,然後單擊“讀取Cookie”按鈕,得到以下輸出:

Key:test1 Value:單值Cookie Expires:0001-1-1 0:00:00

Key:test2 Value:key1=value1&key2=value2

SubKey:key1 Value:value1 Expires:0001-1-1 0:00:00

SubKey:key2 Value:value2 Expires:0001-1-1 0:00:00

這裏要說明以下幾點。

·      我們發現,所有Cookie的過期時間都不能正常顯示。這是因爲瀏覽器返回給服務器的Cookie是不包含過期時間的,而服務器返回給瀏覽器的Cookie是包含過期時間的。過期時間只對客戶端瀏覽器有意義,對服務器來說沒有什麼意義。

·      直接讀取多值Cookie的Value,它會把所有子鍵和子鍵值都使用key=value方法顯示,多個子鍵使用“&”連接(類似URL的方式)。

下面是刪除Cookie的操作。

protected void btn_DelCookie_Click(object sender, EventArgs e)

{

    HttpCookie SingleValueCookie = Request.Cookies["test1"];

    SingleValueCookie.Expires = DateTime.MinValue;

    Response.Cookies.Add(SingleValueCookie);

}

如果你想刪除所有Cookie,可以遍歷刪除。

protected void btn_DelCookie_Click(object sender, EventArgs e)

{

    foreach (string key in Request.Cookies.AllKeys)

    {

        HttpCookie cookie = Request.Cookies[key];

        cookie.Expires = DateTime.MinValue;

        Response.Cookies.Add(cookie);

    }

}

我們始終要記住,服務器不能直接刪除Cookie,刪除Cookie的操作是瀏覽器進行的。說是刪除,其實是把它的過期時間設置爲過去的時間,讓Cookie過期。因此,對於刪除操作來說有三個步驟。

n  1.從Request對象中獲取Cookie。

n  2.把Cookie的過期時間設置爲過去的時間。

n  3.把Cookie重新寫回Response中。

4.修改Cookie的操作也非常簡單。

protected void btn_ModifyCookie_Click(object sender, EventArgs e)

{

    HttpCookie SingleValueCookie = Request.Cookies["test1"];

    SingleValueCookie.Value = "修改後的單值Cookie";

    Response.Cookies.Add(SingleValueCookie);

}

2.3  Cookie總結

Cookie雖然是一個簡單實用的對象,但是我們也要注意Cookie的工作原理、大小限制以及安全性等,大致可以歸納爲以下幾點。

·      存儲的物理位置。客戶端的Cookies文件夾內。

·      存儲的類型限制。字符串。

·      狀態使用的範圍。當前請求上下文的上下文都能訪問到Cookie,Cookie對每個用戶來說都是獨立的。

·      存儲的大小限制。每個Cookie不超過4K數據。每個網站不超過20個Cookie。所有網站的Cookie總和不超過300個。

·      生命週期。每個Cookie都有自己的過期時間,超過了過期時間後失效。

·      安全與性能。存儲在客戶端,安全性差。對於敏感數據建議加密後存儲。

·      優點缺點與注意事項。可以很方便地關聯網站和用戶,長久保存用戶設置。

===============================================================================
===============================================================================
===============================================================================
 
3  Session
3.1  Session概述
Session又稱爲會話狀態,是Web系統中最常用的狀態,用於維護和當前瀏覽器實例相關的一些信息。舉個例子來說,我們可以把已登錄用戶的用戶名放在Session中,這樣就能通過判斷Session中的某個Key來判斷用戶是否登錄,如果登錄的話用戶名又是多少。
我們知道,Session對於每一個客戶端(或者說瀏覽器實例)是“人手一份”,用戶首次與Web服務器建立連接的時候,服務器會給用戶分發一個SessionID作爲標識。SessionID是一個由24個字符組成的隨機字符串。用戶每次提交頁面,瀏覽器都會把這個SessionID包含在HTTP頭中提交給Web服務器,這樣Web服務器就能區分當前請求頁面的是哪一個客戶端。那麼,ASP.NET 2.0提供了哪些存儲SessionID的模式呢:
·      Cookie(默認)。如果客戶端禁止了Cookie的使用,Session也將失效。
·      URL。Cookie是否開啓不影響Session使用,缺點是不能再使用絕對鏈接了。
前面說了SessionID可以存儲在客戶端的Cookie或者URL中,那麼Session真正的內容存儲在哪裏呢?ASP.NET 2.0對於Session內容的存儲也提供了多種模式。
·      InProc(默認)。Session存儲在IIS進程中(Web服務器內存)。
·      StateServer。Session存儲在獨立的Windows服務進程中(可以不是Web服務器)。
·      SqlServer。Session存儲在SqlServer數據庫的表中(SqlServer服務器)。
雖然InProc模式的Session直接存儲在Web服務器IIS進程中,速度比較快,但是每次重新啓動IIS都會導致Session丟失。利用後兩種模式,我們就完全可以把Session從Web服務器中獨立出來,從而減輕Web服務器的壓力,同時減少Session丟失的概率。
因此,SessionID存儲在客戶端(可以是Cookie或者URL),其他都存儲在服務端(可以是IIS進程、獨立的Windows服務進程或者SQL Server數據庫中)。
3.2  Session的使用
讓我們先來實踐一下如何使用Session,進而回答第二個問題:Session存儲的類型限制。Session不需要進行任何配置就可以使用(默認是InProc模式並且依賴Cookie)。首先,在頁面上建立兩個按鈕。
<asp:Button ID="btn_WriteSession" runat="server"Text="寫入Session" />
<asp:Button ID="btn_ReadSession" runat="server" Text="讀取Session" />
在btn_WriteSession按鈕的Click事件處理方法中,寫入兩個Session,一個是簡單的字符串,另外一個是自定義的類。
protected void btn_WriteSession_Click(object sender, EventArgs e)
{
    Session["SimpleString"] = "編程快樂";
    MyUser user = new MyUser();
    user.sUserName = "小朱";
    user.iAage = 24;
    Session["CustomClass"] = user;
}
Session的使用非常簡單,直接對某個Key的Session進行賦值即可。自定義類MyUser如下:
class MyUser
{
    public string sUserName;
    public int iAage;
    public override string ToString()
    {
        return string.Format("姓名:{0},年齡:{1}", sUserName, iAage);
    }
}
在這裏,我們覆寫了ToString()方法直接返回實例的一些信息。然後,雙擊btn_ReadSession按鈕來實現從Session中讀取數據的代碼:
protected void btn_ReadSession_Click(object sender, EventArgs e)
{
    if (Session["SimpleString"]==null)
    {
        Response.Write("讀取簡單字符串失敗<br/>");
    }
    else
    {
        string s=Session["SimpleString"].ToString();
        Response.Write(s + "<br/>");
    }
    if (Session["CustomClass"]==null)
    {
        Response.Write("讀取簡單自定義類失敗<br/>");
    }
    else
    {
        MyUser user=Session["CustomClass"] as MyUser;
        Response.Write(user.ToString()+"<br/>");
    }
}
在每次讀取Session的值以前請務必先判斷Session是否爲空,否則很有可能出現“未將對象引用設置到對象的實例”的異常。我們看到,從Session中讀出的數據都是object類型的,我們需要進行類型轉化後才能使用。打開頁面,先單擊寫入Session按鈕,再單擊讀取Session按鈕,頁面輸出如    圖12-1所示。
文本框: 圖12-1  從Session中讀取字符串和自定義類
 
 
 
 
 
 
 
3  把Session存儲在獨立的進程中
由此看來,Session能存儲任意對象,是這樣嗎?現在得出這個結論還太早了一點,因爲我們並沒有實踐過StateServer和SqlServer模式的Session。要把Session存儲在Windows服務進程中需要進行以下幾個步驟。
n  第1步是打開狀態服務。依次打開“控制面板”→“管理工具”→“服務”命令,找到ASP.NET狀態服務一項,右鍵單擊服務選擇啓動,如圖12-2所示。
圖12-2  啓動ASP.NET狀態服務
n  如果你正式決定使用狀態服務存儲Session前,別忘記修改服務爲自啓動(在操作系統重啓後服務能自己啓動)以免忘記啓動服務而造成網站Session不能使用,如圖12-3所示,雙擊服務把服務的啓動類型設置爲自動。
圖12-3  修改服務啓動類型爲自動
服務正常啓動後可以觀察任務管理器的進程頁,其中的aspnet_state.exe進程就是狀態服務進程,如圖12-4所示。
圖12-4  觀察任務管理器的進程頁
n  第2步,在system.web節點中加入:
<sessionState mode="StateServer" stateConnectionString="tcpip=127.0.0.1:42424"
stateNetworkTimeout="20"></sessionState>
n  stateConnectionString表示狀態服務器的通信地址(IP:服務端口號)。由於我們現在在本機進行測試,這裏設置成本機地址127.0.0.1。狀態服務默認的監聽端口爲42422。當然,您也可以通過修改註冊表來修改狀態服務的端口號。
n  1.在運行中輸入regedit啓動註冊表編輯器。
n  2.依次打開HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/aspnet_state/Parameters節點,雙擊Port選項,如圖12-5所示。
選擇基數爲十進制,然後輸入一個端口號即可。stateNetworkTimeout屬性表示從狀態服務器請求Session數據最長的時間,默認爲10秒,如果網絡連接不是很好,請把這個數字適當設置得大一點。
n  第3步打開頁面,單擊“寫入Session”按鈕,系統會報錯,如圖12-6所示。
   
        圖12-5  修改狀態服務端口號            圖12-6  向StateServer默認的Session中寫入自定義類出錯
提示已經說得很清楚了,只有把對象標註爲可序列化後才能在服務中進行存儲。什麼是序列化呢?序列化是指將對象實例的狀態存儲到存儲媒體的過程。在此過程中,先將對象的公共字段和私有字段以及類的名稱轉換爲字節流,然後再把字節流寫入數據流。在隨後對對象進行反序列化時,將創建出與原對象完全相同的副本。要使一個類可序列化,最簡單的方法是使用 Serializable 屬性對它進行標記。
[Serializable]
class MyUser
{
    public string sUserName;
    public int iAage;
    public override string ToString()
    {
        return string.Format("姓名:{0},年齡:{1}", sUserName, iAage);
    }
}
n  第4步現在重新打開頁面進行測試,得到的結果和使用InProc模式是一樣的。
3.4  把Session存儲在數據庫中
要把Session存儲在SqlServer中,基本上也是這麼幾個步驟。
n  1.在命令行窗口輸入cmd並在命令行中運行如下命令。
C:/WINDOWS/Microsoft.NET/Framework/v2.0.50727/aspnet_regsql.exe -S ./SqlExpress -E –ssadd
其中C:/Windows用你自己Windows的目錄代替,v2.0.50727用你安裝的2.0框架的版本號代替。-S指定SqlServer服務器地址,-E表示採用信任連接,-ssadd表示爲SqlServer服務器添加狀態服務的支持。操作結束後,你可以使用IDE的服務器資源管理器連接SqlExpress數據庫,可以看到多了一個ASPState數據庫,但是奇怪的是數據庫中沒有任何表卻有很多存儲過程,如圖12-7所示。
其實,所有Session的數據都存放在了tempdb數據庫內,如圖12-8所示。
           
    圖12-7  使用服務器資源管理器瀏覽ASPState數據庫      圖12-8  存放Session數據的tempdb數據庫
其實,aspnet_regsql.exe有一個-sstype參數可以用來指定Session的內容和操作的存儲過程存放的表。由於篇幅關係,在這裏就不詳細介紹了,讀者可以使用aspnet_regsql.exe/?來瀏覽程序詳細的使用方式。
n  2.打開Web.config文件,修改前面建立的sessionState節點。
<sessionState mode="SQLServer" sqlConnectionString="server=(local)/SQLEXPRESS;
Trusted_Connection=True" sqlCommandTimeout="60"></sessionState>
爲sqlConnectionString屬性指定以前一直用的連接字符串,唯一不同的是不需要再指定數據表的名字了。sqlCommandTimeout屬性表示允許執行Sql命令最長的時間,默認爲30秒,可以根據自己的需要適當調整這個數字。最後,重新打開頁面進行測試,得到的結果和使用InProc模式是一樣的(同樣你需要確保在自定義類前標註了[Serializable]),不過我們能感到速度有些慢了,畢竟數據是從數據庫中進行讀取或保存的,而且在使用前還需要經過序列化和反序列化操作。
因此Session能存儲的類型爲: 對於InProc模式是一切類型,而對於StateServer和SqlServer模式是一切可以序列化的類型。
3.5  Session的使用範圍與大小限制
那麼,會話狀態使用的範圍和大小限制又是怎麼樣的呢?我們可以分析一下圖12-8,系統使用兩個表來存儲Session的狀態。其中有一個ASPStateTempApplication表,用來存儲Session所在的應用程序,一定程度上反映了Session是不能跨應用程序的。舉例來說,我們在計算機上建立了兩個網站,同時都使用Session[“UserName”]來保存登錄的用戶名,一個網站的用戶登錄後,另一個網站直接訪問Session[“UserName”]是取不到任何值的。那麼,Session是否可以跨用戶呢?通過前面的分析我們知道,肯定是不行的,Session通過SessionID來區分用戶,一般來說SessionID是不可能出現重複的現象,也就是說Session一般是不會“串號”的。既然頁面每次提交的時候都會附加上當前用戶的SessionID,那麼Session應該是可以跨頁面的,也就是說一個網站中所有的頁面都使用同一份Session。你可以自己來做個試驗,請讀者打開剛纔那個頁面,然後按Ctrl+N組合鍵再打開第二個同樣的頁面,單擊第一個頁面中的“寫入Session”按鈕,單擊第二個頁面中的“讀取Session”按鈕,可以發現Session的值被正確讀出了。第三個問題的答案有了。
·      Session狀態使用的範圍:使用同一個客戶端(瀏覽器實例)訪問同一個應用程序的所有頁面。
我們再來做一個試驗,看看Session的容量有多大,在測試以前請修改Web.config,把Session設置爲StateServer模式。然後,把寫入Session的代碼修改成如下(別忘記using System.Data.SqlCient):
DataSet ds = new DataSet();
using (SqlConnection conn = new SqlConnection(@"server=(local)/SQLEXPRESS;database=Forum;
Trusted_Connection=True"))
{
    SqlDataAdapter da = new SqlDataAdapter("select * from tbUser;select * from tbBoard;
    select * from tbTopic;", conn);
    da.Fill(ds);
}
ArrayList al = new ArrayList();
for(int i = 0;i<10000000;i++)
    al.Add(ds);
Session["LargeData"] = al;
我們把包含三個表的DataSet重複加入ArrayList中1000萬次。由於這些表幾乎每個表只有幾條記錄,這樣可以模擬大數據量的情況。啓動頁面,單擊“寫入Session”按鈕後可以發現,Windows服務進程一下子佔用了多達70MB的內存,如圖12-9所示。
圖12-9  把大量數據存放到Session中
Session對於網站和用戶是獨立的,試想一下,如果服務器上有兩個網站,每個網站的在線人數是100人,那麼佔用內存就要14G。是不是很恐怖的數字?因此,雖然Session的大小沒有限制,但是我們千萬不能濫用Session。筆者推薦你在Session中存儲少於100K的數據。
·      如果你使用InProc模式的Session,存儲過多的數據會導致IIS進程被回收,引發Session不斷丟失。
·      如果你使用StateServer存儲Session,那麼數據在存入Session以前需要進行序列化,序列化會消耗大量的CPU資源。
·      如果你使用SqlServer模式的Session,數據不但要序列化而且還是存儲在磁盤上,更不適合存儲大量數據。
3.6  Session的生命週期
在瞭解了Session中存儲的數據無大小限制後,我們可能要更多地關心Session的生命週期了。我們已經知道,Session是在用戶第一次訪問網站的時候創建的,那麼Session是什麼時候銷燬的呢?Session使用一種平滑超時的技術來控制何時銷燬Session。默認情況下,Session的超時時間(Timeout)是20分鐘,用戶保持連續20分鐘不訪問網站,則Session被收回,如果在這20分鐘內用戶又訪問了一次頁面,那麼20分鐘就重新計時了,也就是說,這個超時是連續不訪問的超時時間,而不是第一次訪問後20分鐘必過時。這個超時時間同樣也可以通過調整Web.config文件進行修改:
<sessionState timeout="30"></sessionState>
當然你也可以在程序中進行設置:
Session.Timeout = "30";
一旦Session超時,Session中的數據將被回收,如果再使用Session系統,將給你分配一個新的SessionID。本節一開始我們就介紹了可以在URL中存儲SessionID,現在請你配置Web.config文件,設置Session超時時間爲1分鐘,SessionID在URl中存放。打開頁面後單擊“寫入Session”按鈕,過1分鐘再次單擊按鈕並觀察SessionID是否變化。
<sessionState timeout="1" cookieless="true"></sessionState>
如圖12-10所示,SessionID的確發生了變化。
圖12-10  超時後SessionID發生變化
不過,你可別太相信Session的Timeout屬性,如果你把它設置爲24小時,則很難相信24小時之後用戶的Session還在。Session是否存在,不僅僅依賴於Timeout屬性,以下的情況都可能引起Session丟失(所謂丟失就是在超時以前原來的Session無效)。
·      bin目錄中的文件被改寫。asp.net有一種機制,爲了保證dll重新編譯之後,系統正常運行,它會重新啓動一次網站進程,這時就會導致Session丟失,所以如果有access數據庫位於bin目錄,或者有其他文件被系統改寫,就會導致Session丟失。
·      SessionID丟失或者無效。如果你在URL中存儲SessionID,但是使用了絕對地址重定向網站導致URL中的SessionID丟失,那麼原來的Session將失效。如果你在Cookie中存儲SessionID,那麼客戶端禁用Cookie或者Cookie達到了IE中Cookie數量的限制(每個域20個),那麼Session將無效。
·      如果使用InProc的Session,那麼IIS重啓將會丟失Session。同理,如果使用StateServer的Session,服務器重新啓動Session也會丟失。
一般來說,如果在IIS中存儲Session而且Session的Timeout設置得比較長,再加上Session中存儲大量的數據,非常容易發生Session丟失的問題。
最後,Session的安全性怎麼樣呢?我們知道,Session中只有SessionID是存儲在客戶端的,並且在頁面每次提交的過程中加入HTTP頭髮送給服務器。SessionID只是一個識別符,沒有任何內容,真正的內容是存儲在服務器上的。總的來說安全性還是可以的,不過筆者建議你不要使用cookieless和SqlServer模式的Session。把SessionID暴露在URL中,把內容存儲在數據庫中可能會發生攻擊隱患。
3.7  遍歷與銷燬Session
Session雖然很方便,但是要用好Session還需要自己不斷實踐,根據自己網站的特點靈活使用各種模式的Session。關於使用程序訪問Session,筆者還想補充兩點。
·      如何遍歷當前的Session集合。
System.Collections.IEnumerator SessionEnum = Session.Keys.GetEnumerator();
while (SessionEnum.MoveNext())
{
    Response.Write(Session[SessionEnum.Current.ToString()].ToString() + "<br/>");
}
對於我們這個例子,輸出和圖12-1一樣。如果你僅僅爲了監視Session,也可以通過trace來獲得詳細信息。在Web.config的system.Web節點中添加:
<trace enabled="true"  pageOutput="true"/>
打開頁面後單擊“寫入Session”按鈕,頁面顯示如圖12-11所示。
圖12-11  使用trace觀察會話狀態
·      如何立刻讓Session失效。比如用戶退出系統後,Session中保存的所有數據全部失效,可以使用以下代碼來讓Session失效。
Session.Abandon();
3.8  Session的常見問題與總結
Session的基本知識就介紹到這裏,現在再回頭看第一節中的幾個問題,你是否都能回答了呢?爲了強化大家的概念,筆者就三種模式的Session進行了一個比較(假設都使用Cookie來存儲SessionID)。
表12.1  三種模式的Session比較
 
InProc
StateServer
SQLServer
存儲物理位置
IIS進程(內存)
Windows服務進程(內存)
SQLServer數據庫(磁盤)
存儲類型限制
無限制
可以序列化的類型
可以序列化的類型
存儲大小限制
無限制
使用範圍
當前請求上下文,對於每個用戶獨立
生命週期
第一次訪問網站的時候創建Session超時後銷燬
優點
性能比較高
Session不依賴Web服務器,不容易丟失
缺點
容易丟失
序列化與反序列化消耗CPU資源
序列化與反序列化消耗CPU資源,從磁盤讀取Session比較慢
使用原則
不要存放大量數據
在使用Session的過程中你可能還會遇到很多奇怪的問題,結束本節之前筆者列出了幾條常見的FAQ,供大家參考:
·      爲什麼每次請求的SessionID都不相同?
n  可能是沒有在Session裏面保存任何信息引起的,即程序中任何地方都沒有使用Session。只有在Session中保存了內容後,Session纔會和瀏覽器進行關聯,此時的SessionID將不會再變化。
·      爲什麼當我設置cookieless爲true後,在重定向的時候會丟失Session?
n  當使用cookieless時,你必須使用相對路徑替換程序中的絕對路徑,如果使用絕對路徑,ASP.NET將無法在URL中保存SessionID。
·      有辦法知道應用程序的Session在運行時佔用了多少內存嗎?
n  沒有辦法,你可以通過觀察IIS進程(InProc模式)或者aspnet_state進程(StateServer模式)大致估計。
·      有沒有可能知道整個網站使用Session的用戶列表?
n  對於InProc模式和StateServer模式很難,對於SqlServer模式你可以查詢存儲Session的表進行嘗試。
·      當頁面中設了frameset,發現在每個frame中顯示頁面的SessionID在第一次請求時都不相同,爲什麼?
n  原因是你的frameset是放在一個HTML頁面上而不是ASPX頁面。在一般情況下,如果frameset是aspx頁面,當你請求頁面時,它首先將請求發送到Web服務器,此時已經獲得了SessionID,接着瀏覽器會分別請求Frame中的其他頁面,這樣所有頁面的SessionID就是一樣的,就是FrameSet頁面的SessionID。然而如果你使用HTML頁面做FrameSet頁面,第一個請求將是HTML頁面,當該頁面從服務器上返回時並沒有任何Session產生,接着瀏覽器會請求Frame裏面的頁面,這樣,這些頁面都會產生自己的SessionID,所以在這種情況下就可能出現這種問題。當你重新刷新頁面時,SessionID就會一樣,並且是最後一個請求頁面的SessionID。 
 
===============================================================================
===============================================================================
===============================================================================
 
4  Application
4.1  全局應用程序類
從Application這個單詞上大致可以看出Application狀態是整個應用程序全局的。在ASP時代我們通常會在Application中存儲一些公共數據,而ASP.NET中Application的基本意義沒有變:在服務器內存中存儲數量較少又獨立於用戶請求的數據。由於它的訪問速度非常快而且只要應用程序不停止,數據一直存在,我們通常在Application_Start的時候去初始化一些數據,在以後的訪問中可以迅速訪問和檢索。
我們可以來實踐一下。首先,右鍵單擊網站,選擇“添加新項”命令,如圖12-12所示,選擇全局應用程序類。
圖12-12  添加一個Global.asax
Global.asax(通常我們不改名)是一個用來處理應用程序全局的事件。打開文件,系統已經爲我們定義了一些事件的處理方法。
<script runat="server">
    void Application_Start(object sender, EventArgs e)
    {
        // 在應用程序啓動時運行的代碼
    }   
    void Application_End(object sender, EventArgs e)
    {
        //  在應用程序關閉時運行的代碼
    }       
    void Application_Error(object sender, EventArgs e)
    {
        // 在出現未處理的錯誤時運行的代碼
    }
    void Session_Start(object sender, EventArgs e)
    {
        // 在新會話啓動時運行的代碼
    }
    void Session_End(object sender, EventArgs e)
    {
        // 在會話結束時運行的代碼
        // 注意: 只有在 Web.config 文件中的 sessionstate 模式設置爲InProc 時,纔會引發 Session_
           End 事件
        // 如果會話模式設置爲 StateServer 或 SQLServer,則不會引發該事件
    }
</script>
通過這些註釋我們可以看到,這些事件是整個應用程序的事件,和某一個頁面沒有關係。
4.2  使用Application統計網站訪問
假設我們希望使用Application統計網站的訪問情況。
·      頁面單擊數。頁面被單擊一次+1,不管是否是同一個用戶多次單擊頁面。
·      用戶訪問數。來了一個用戶+1,一個用戶打開多個頁面不會影響這個數字。
我們首先需要在Application_Start中去初始化兩個變量。
void Application_Start(object sender, EventArgs e)
{
    // 在應用程序啓動時運行的代碼
    Application["PageClick"]=0;
    Application["UserVisit"]=0;
}
用戶訪問數根據Session來判斷,因此可以在Session_Start的時候去增加這個變量:
void Session_Start(object sender, EventArgs e)
{
    Application.Lock();
    Application["UserVisit"]=(int)Application["UserVisit"]+1;
    Application.UnLock();
}
我們看到,Application的使用方法和Session差不多。唯一要注意的是,Application的作用範圍是整個應用程序,可能有很多用戶在同一個時間訪問Application造成併發混亂,因此在修改Application的時候需要先鎖定Application,修改完成後再解鎖。
頁面單擊數則在頁面Page_Load的時候去修改。
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        Application.Lock();
        Application["PageClick"] = (int)Application["PageClick"] + 1;
        Application.UnLock();
        Response.Write(string.Format("頁面單擊數:{0}<br/>", Application["PageClick"]));
        Response.Write(string.Format("用戶訪問數:{0}<br/>", Application["UserVisit"]));
    }
}
由於在應用程序開始的時候我們已經爲兩個變量初始化了,所以在這裏可以直接使用。首次執行效果如圖12-13所示。
連續刷新頁面幾次,效果如圖12-14所示。
使用Ctrl+N組合鍵打開幾個頁面,可以發現用戶訪問數還是沒有變化。前一節中介紹過,Session是每個客戶端一份,而不是每個瀏覽器一份。
關閉頁面,再重新打開。由於前一個用戶的Session還沒有超時,所以這次用戶訪問數增加了1,如圖12-15所示。
                           

圖12-15  Session_Start導致 用戶訪問數增長
 
圖12-14 頁面單擊數隨着 頁面刷新增長
 
圖12-13  使用Application 進行站點統計
 
                             
我們知道,Visual Studio 2005有一個內置的服務器(不依賴IIS)。因此,我們不能通過IIS來重新啓動應用程序。如圖12-16所示。
單擊“停止”選項,然後重新打開頁面,如圖12-17所示,我們可以看到兩個變量都重新初始      化了。
                       
          圖12-16  停止IDE內置的Web服務器      圖12-17  重新啓動Web服務器導致Application_Start觸發
4.3  Application總結
在ASP.NET 2.0中,Application已經變得不是非常重要了。因爲Application的自我管理功能非常薄弱,它沒有類似Session的超時機制。也就是說,Application中的數據只有通過手動刪除或者修改才能釋放內存,只要應用程序不停止,Application中的內容就不會消失。在下一節中,我們會看到,可以使用Cache實現類似Application的功能,同時Cache又有豐富而強大的自我管理機制。
在結束本節以前,讓我們來根據第一節中提出的幾個問題總結一下Application的特性。
·      存儲的物理位置。服務器內存。
·      存儲的類型限制。任意類型。
·      狀態使用的範圍。整個應用程序。
·      存儲的大小限制。任意大小。
·      生命週期。應用程序開始的時候創建(準確來說是用戶第一次請求某URL的時候創建),應用程序結束的時候銷燬。
·      安全與性能。數據總是存儲在服務端,安全性比較高,但不易存儲過多數據。
·      優缺點與注意事項。檢索數據速度快,但缺乏自我管理機制,數據不會自動釋放。
===============================================================================
===============================================================================
===============================================================================
 

5  Cache

5.1  Cache概述

Cache和Application一樣是整個應用程序共用一份的,而且所有用戶訪問的都是相同的Cache。Cache從字面上說是緩存的意思,我們知道計算機系統本身就是一個多級緩存的結構。CPU的緩存中存放了部分內存中的數據,內存中又存放了部分硬盤中的數據。把最常用的數據放在讀取最快速的硬件中存儲能大大提高效率。對於Web系統來說也一樣,從數據庫(硬盤)中讀取數據的速度肯定比從Cache(內存)中讀取的效率低,基於這個特性,我們通常把改動不大而查詢次數又比較多的數據放到Cache中。

既然緩存中的數據其實是來自數據庫的,那麼緩存中的數據如何和數據庫進行同步呢?一般來說,緩存中應該存放改動不大或者對數據的實時性沒有太多要求的數據。這樣,我們只需要定期更新緩存就可以了。相反,如果緩存的更新頻率過快的話,使用緩存的意義就不是很大了,因此更新緩存的時候需要一次性從數據庫中讀取大量的數據,過於頻繁地更新緩存反而加重了數據庫的負擔。

那麼ASP.NET中的Cache又提供了哪些緩存的過期策略呢?

·      永不過期。和Application一樣,緩存永不過期。

·      絕對時間過期。緩存在某一時間過期,比如5分鐘後。

·      變化時間過期(平滑過期)。緩存在某一時間內未訪問則超時過期,這個和Session有點類似,比如我們可以設定緩存5分鐘沒有人訪問則過期。

·      依賴過期。緩存依賴於數據庫中的數據或者文件中的內容。一旦數據庫中某些表的數據發生變動或者文件內容發生變動,則緩存自動過期。

緩存過期後我們就要更新緩存了,ASP.NET提供了兩種更新策略。

·      被動更新。緩存過期以後手動進行更新。

·      主動更新。緩存過期以後在回調方法中更新。

5.2  Cache性能與過期策略

首先,在頁面上添加兩個按鈕,並雙擊按鈕實現Click事件處理方法。

<asp:Button ID="btn_GetDataFromCache" runat="server" OnClick="btn_GetData_Click"

Text="從緩存中讀取數據" />

<asp:Button ID="btn_GetDataFromDb" runat="server" OnClick="btn_GetDataFromDb_Click"

Text="從數據庫中讀取數據" />

第一個按鈕實現從緩存讀取數據。

注意:本例需要using以下命名空間。

using System.Diagnostics;   // 用於精確測定時間間隔

using System.Web.Caching;   // 用於緩存的策略

using System.IO;            // 用於文件操作

protected void btn_GetData_Click(object sender, EventArgs e)

{

    InsertRecord();

    Stopwatch sw=new Stopwatch();

    sw.Start();

    if (Cache["Data"]==null)

    {

        Response.Write("緩存無效<br/>");

    }

    else

    {

        DataSet ds = Cache["Data"] as DataSet;

        Response.Write(string.Format("查詢結果:{0}<br/>", ds.Tables[0].Rows[0][0]));

        Response.Write(string.Format("耗費時間:{0}<br/>", sw.ElapsedTicks));

    }

}

在這裏有幾點需要說明。

·      一開始的InsertRecord()方法是我們自己創建的,用來向數據庫插入一條記錄。這樣,我們就能看出來數據是否是從緩存中讀取的了。

InsertRecord()方法如下:

private void InsertRecord()

{

    using (SqlConnection conn = new SqlConnection(@"server=(local)/SQLEXPRESS;

    database=Forum;Trusted_Connection=True"))

    {

        conn.Open();

        using (SqlCommand cmd = new SqlCommand("Insert into CacheTest (Test) values

       ('Test')", conn))

        {

            cmd.ExecuteNonQuery();

        }

    }

}

·      如果緩存存在則輸出查詢結果和查詢耗費的時間,如果緩存不存在則輸出“緩存無效”。

·      Stopwatch類用於精確測定逝去的時間,ElapsedTicks屬性返回了間隔的計數器刻度,所謂計數器刻度就是系統的計數器走過了多少次。當然,Stopwatch還有ElapsedMilliseconds能返回間隔的總毫秒數。之所以使用ElapsedTicks,因爲它是一個更小的時間單位。

第二個按鈕直接從數據庫讀取數據。

protected void btn_GetDataFromDb_Click(object sender, EventArgs e)

{

    InsertRecord();

    Stopwatch sw = new Stopwatch();

    sw.Start();

    DataSet ds = GetData();

    Response.Write(string.Format("查詢結果:{0}<br/>", ds.Tables[0].Rows[0][0]));

    Response.Write(string.Format("耗費時間:{0}<br/>", sw.ElapsedTicks));

}

在這裏,我們把讀取數據的操作使用一個GetData()方法進行了封裝,方法實現如下:

private DataSet GetData()

{

    DataSet ds = new DataSet();

    using (SqlConnection conn = new SqlConnection(@"server=(local)/SQLEXPRESS;

    database=Forum;Trusted_Connection=True"))

    {

        SqlDataAdapter da = new SqlDataAdapter("select count(*) from CacheTest", conn);

        da.Fill(ds);           

    }

    return ds;

}

爲了能體現出緩存的效率,我們在Forum數據庫中又新建立了一個CacheTest數據表,表結構很簡單,如圖12-18所示。

圖12-18  CacheTest表結構

我們在表中插入了10萬條以上的記錄,使得表的大小達到了100MB左右。

運行程序,單擊“從數據庫中讀取數據”按鈕,如圖12-19所示,我們可以看到,這個操作耗費了相當多的時間。

文本框: 圖12-19  從數據庫讀取數據 需要花費大量的時間因爲我們直接從數據庫讀取count(*),所以每次單擊按鈕查詢結果顯示的數字都會+1。現在你單擊“從緩存中讀取數據”肯定是顯示“緩存無效”,因爲我們還沒有添加任何緩存。

然後,我們在頁面上添加三個按鈕並雙擊按鈕創建事件處理方法,三個按鈕使用不同的過期策略添加緩存。

<asp:Button ID="btn_InsertNoExpirationCache" runat="server" Text="插入永不過期緩存"

OnClick="btn_InsertNoExpirationCache_Click" />

<asp:Button ID="btn_InsertAbsoluteExpirationCache" runat="server" Text="插入絕對時間

過期緩存" OnClick="btn_InsertAbsoluteExpirationCache_Click" />

<asp:Button ID="btn_InsertSlidingExpirationCache" runat="server" Text="插入變化時間

過期緩存" OnClick="btn_InsertSlidingExpirationCache_Click" />

三個按鈕的Click事件處理方法如下:

protected void btn_InsertNoExpirationCache_Click(object sender, EventArgs e)

{

    DataSet ds = GetData();

    Cache.Insert("Data", ds);

}

protected void btn_InsertAbsoluteExpirationCache_Click(object sender, EventArgs e)

{

    DataSet ds = GetData();

    Cache.Insert("Data", ds,null, DateTime.Now.AddSeconds(10), TimeSpan.Zero);

}

protected void btn_InsertSlidingExpirationCache_Click(object sender, EventArgs e)

{

    DataSet ds = GetData();

    Cache.Insert("Data", ds, null, DateTime.MaxValue, TimeSpan.FromSeconds(10));

}

我們來分析一下這三種過期策略。

·      永不過期。直接賦值緩存的Key和Value即可

·      絕對時間過期。DateTime.Now.AddSeconds(10)表示緩存在10秒後過期,TimeSpan.Zero表示不使用平滑過期策略。

·      變化時間過期(平滑過期)。DateTime.MaxValue表示不使用絕對時間過期策略,TimeSpan.FromSeconds(10)表示緩存連續10秒沒有訪問就過期。

在這裏,我們都使用了Insert()方法來添加緩存。其實,Cache還有一個Add()方法也能向緩存中添加項。不同之處在於Add()方法只能添加緩存中沒有的項,如果添加緩存中已有的項將失敗(但不會拋出異常),而Insert()方法能覆蓋原來的項。

n  注意:和Application不同,這裏不需要使用在插入緩存的時候進行鎖操作,Cache會自己處理     併發。

現在,我們就可以打開頁面對這三種過期策略進行測試了。

n  1.單擊“從緩存中讀取數據”按鈕,提示“緩存無效”。

n  2.單擊“從數據庫中讀取數據”按鈕,查詢結果顯示現在記錄總數爲100646。

n  3.單擊“插入永不過期緩存”按鈕,然後連續單擊“從緩存中讀取數據”按鈕,可以發現,無論過去多久,緩存始終沒有過期,而且觀察記錄查詢結果可以發現值始終沒有發生變化。不同的是,從緩存中讀取數據的效率比從數據庫中讀取數據提高了幾個數量級,如圖12-20所示,你可以和圖12-19進行比較。

文本框: 圖12-20  從緩存中讀取數據 所花費的時間4.單擊“插入絕對時間過期緩存”,然後連續單擊“從緩存中讀取數據”按鈕,大約10秒過期後,頁面提示“緩存無效”,說明緩存過期了。

n  5.單擊“插入變化時間過期緩存”,然後連續單擊“從緩存中讀取數據”按鈕,緩存始終不過期,如果我們等待10秒後再去單擊按鈕,頁面提示“緩存無效”,說明緩存過期了。

我們再來看一下依賴過期策略。所謂依賴過期就是緩存的依賴項(比如一個文件)的內容改變之後緩存也就失效了。由於篇幅關係,這裏只介紹文件依賴。我們在頁面上再加兩個按鈕並雙擊按鈕添加Click事件處理方法。

<asp:Button ID="btn_ModifyFile" runat="server" Text="修改文件" OnClick="btn_ModifyFile_

Click" />

<asp:Button ID="btn_AddFileDependencyCache" runat="server" Text="插入文件依賴緩存"

OnClick="btn_AddFileDependencyCache_Click" />

在本例中,我們將使緩存依賴一個txt文本文件。因此,首先在項目中添加一個test.txt文本文件。單擊“修改文件”按鈕實現文件的修改。

protected void btn_ModifyFile_Click(object sender, EventArgs e)

{

    FileStream fs = new FileStream(Server.MapPath("test.txt"), FileMode.Append,

    FileAccess.Write);

    StreamWriter sw = new StreamWriter(fs);

    sw.WriteLine(DateTime.Now.ToString());

    sw.Close();

    fs.Close();

}

我們通過在文件的最後寫入當前的時間來修改文件。插入文件依賴緩存按鈕的事件處理方法如下:

protected void btn_AddFileDependencyCache_Click(object sender, EventArgs e)

{

    CacheDependency cd = new CacheDependency(Server.MapPath("test.txt"));

    DataSet ds = GetData();

    Cache.Insert("Data", ds, cd);

}

添加文件依賴緩存同樣簡單,通過CacheDependency關聯了一個文件依賴。

現在就可以打開頁面進行測試了。

n  1.單擊“從緩存中讀取數據”按鈕,提示“緩存無效”。

n  2.單擊“從數據庫中讀取數據”按鈕,查詢結果顯示現在記錄總數爲100710。

n  3.單擊“插入文件依賴緩存”按鈕,然後連續單擊“從緩存中讀取數據”按鈕,可以發現,無論過去多久,緩存始終沒有過期,而且觀察記錄查詢結果可以發現值始終沒有發生變化。

n  4.單擊“修改文件”按鈕,然後單擊“從緩存中讀取數據”按鈕,提示“緩存無效”。由於文件已經修改了,依賴這個文件的緩存立刻失效了。

5.3  Cache的更新策略

最後,我們來討論緩存的更新策略。在Web程序中我們通常會使用被動更新。所謂被動更新,就是在調用數據的時候判斷緩存是否爲空,如果爲空則先更新緩存然後再從緩存中讀取數據,如果不爲空則直接從緩存中讀取數據。可以把“從緩存中讀取數據”按鈕的Click事件處理方法修改成如下,實現被動更新。

protected void btn_GetData_Click(object sender, EventArgs e)

{

    InsertRecord();

    DataSet ds = new DataSet();

    Stopwatch sw = new Stopwatch();

    sw.Start();

    if (Cache["Data"] == null)

    {

        ds = GetData();

        Cache.Insert("Data", ds, null, DateTime.Now.AddSeconds(10), TimeSpan.Zero);

    }

    else

    {

        ds = Cache["Data"] as DataSet;

    }

    Response.Write(string.Format("查詢結果:{0}<br/>", ds.Tables[0].Rows[0][0]));  

    Response.Write(string.Format("耗費時間:{0}<br/>", sw.ElapsedTicks));

}

我們可以看出,如果沒有人訪問數據緩存是不會更新的,只有緩存被訪問的時候發現緩存無效纔會去更新。這樣很明顯的一個缺點就是,如果緩存過期了更新操作將花費很長時間,這個時候的查詢也需要花費很多時間。我們可以利用緩存的回調功能讓緩存過期後自動續建實現自動更新的目的。

protected void btn_InsertActiveUpdateCache_Click(object sender, EventArgs e)

{

    DataSet ds = GetData();

    Cache.Insert("Data", ds, null, DateTime.Now.AddSeconds(10), TimeSpan.Zero,

    CacheItemPriority.Default, CacheRemovedCallback);

}

最後一個參數表明緩存被移除以後自動調用CacheRemovedCallback()方法,方法實現如下。

private void CacheRemovedCallback(String key, object value, CacheItemRemovedReason

removedReason)

{

    DataSet ds = GetData();

    Cache.Insert(key, ds, null, DateTime.Now.AddSeconds(10), TimeSpan.Zero, CacheItemPriority.

    Default, CacheRemovedCallback);

}

在回調方法中,我們再次插入一個支持回調的緩存。這樣,緩存被移除以後又能自動更新了。說了這麼多創建緩存的方法,讀者可能會問怎麼手動移除緩存呢?比如我們要移除Key="Data"的緩存只需要:

Cache.Remove("Data");

你可能會馬上想到用Cache.RemoveAll()方法移除所有緩存,可是Cache沒有提供這樣的方法,我們只能通過遍歷來實現移除所有緩存。

IDictionaryEnumerator CacheEnum = HttpRuntime.Cache.GetEnumerator();

while (CacheEnum.MoveNext())

{

    Cache.Remove(CacheEnum.Key.ToString());

}

5.4  Cache總結

同樣,我們以第一節中的幾個問題結束對Cache的討論。

·      存儲的物理位置。服務器內存。

·      存儲的類型限制。任意類型。

·      狀態使用的範圍。當前請求上下文,所有用戶共用一份。

·      存儲的大小限制。任意大小。

·      生命週期。有多種過期策略控制緩存的銷燬。

·      安全與性能。數據總是存儲在服務端,安全性比較高,但不易存儲過多數據。

·      優缺點與注意事項。檢索數據速度快,過期策略豐富。注意別把對實時性要求很高的數據放到Cache中,不斷更新Cache會對數據庫造成壓力。

 
===============================================================================
===============================================================================
===============================================================================
 

6  隱藏域/ViewState/ControlState——保存數據的另一個場所

6.1  使用隱藏域

Session、Application和Cache都是保存在服務器內存中的。一般來說我們是無權訪問客戶端的機器,把數據直接保存在客戶端的(Cookie是一個例外,不過Cookie只能保存不超過4K的字符串)。我們可以想一下還有哪裏可以讓我們暫時保存數據的?那就是頁面!如果我們在Web頁面中放置一個Label控件,然後設置它隱藏。那麼我們就可以使用這個Label來保存一些臨時數據,供當前頁面的程序使用。

在ASP.NET中,我們還可以使用隱藏域來進行類似的工作,和Label不同的是,在隱藏域中填寫的內容不會直接顯示在IDE的設計視圖中。由於我們保存的這些數據根本不需要顯示給用戶看,所以用隱藏域更合理一些。

<asp:HiddenField ID="HiddenField1" runat="server" Value="編程快樂" />

在代碼中可以直接訪問隱藏域的Value屬性獲得其值。

Response.Write(HiddenField1.Value);

不過,這樣做還有幾個不合理的地方。

·      數據直接暴露給用戶。

·      只能存儲字符串數據。

6.2  使用ViewState

ASP.NET引入了ViewState(視圖狀態)的概念。從這個名字上我們大概可以體會出,ViewState主要是用來存放和視圖有關的一些狀態。比如,在用戶註冊時用戶填寫了一大堆數據,提交頁面後系統返回了一個“用戶名重複”的出錯信息,此時先前用戶在頁面上填寫的一些註冊資料全部沒有了。用戶會是什麼感覺呢?我想大多數用戶會很惱火。ASP.NET通過ViewState自動保存控件的狀態。你可能也發現了,文本框中的數據在頁面提交後還是存在的。

同時,我們也可以利用ViewState來保存一些程序需要的數據。ViewState中的數據默認是使用base64進行編碼的,因此,用戶不能直接看到裏面的數據。我們在代碼中可以這樣添加一個ViewState項:

ViewState["test"] = "編程快樂";

打開頁面,觀察源代碼,ViewState就在這裏:

<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE" value="XT+Q3cCGrb+qjUKNB1N7x

CMUgAMjbpmAwtMtwPE+b5Ii8uRFaO42AgKyR+u9T0Be" />

既然ViewState是存在頁面上的,那麼ViewState肯定是不能跨頁面使用的,而且每個用戶訪問到的ViewState都是獨立的。此外,ViewState也沒有什麼聲明週期的概念,頁面在ViewState就在,頁面關閉了ViewState就關閉了。

觀察上面的ViewState,是不是找不到“編程快樂”這幾個字的影子呢?請在頁面上隨便加入一個按鈕,按鈕的Click事件處理方法如下:

Response.Write(System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(

Request["__VIEWSTATE"])));

如圖12-21所示,單擊按鈕後頁面顯示如下。

圖12-21  對ViewState數據進行base64解碼

我們對ViewState數據進行base64解碼後就能看到“編程快樂”的字樣了。不過,現在的那串字符串還是很亂。其實,ASP.NET首先對ViewState中的數據進行序列化,然後再使用base64編碼後存儲在頁面的隱藏域中。base64不是什麼加密算法,只是一種編碼算法,任何人都能對base64進行反     編碼。

6.3  ViewState的安全與性能

如果我們需要在ViewState中保存一些相對比較機密的數據(當然,非常機密的數據不建議你保存在ViewState中),又如何保證ViewState的安全性呢?一般來說可以從兩個方面入手。

n  1.保證客戶端提交過來的ViewState沒有被修改。我們做Web應用程序,心中要有這樣一個意識,那就是客戶端的一切都是不可相信的。大家可能以爲只有我們提供了諸如TextBox等控件,用戶才能修改。其實這種觀點是錯誤的,雖然DropDownList中的內容只允許選擇不允許修改,但完全可以僞造一個頁面進行提交。對於ViewState也是同樣道理,爲了進一步的安全,我們需要驗證客戶端發回的ViewState是否已經被修改了。

n  2.保證用戶不能直接看到ViewState中的數據。說白了就是對ViewState進行加密。

在ASP.NET 2.0中,我們只需要進行簡單地配置就能對ViewState進行驗證和加密,在頁面頭部添加EnableViewStateMac(驗證)和ViewStateEncryptionMode(加密)屬性:

<%@ Page Language="C#"   …  EnableViewStateMac="true" ViewStateEncryptionMode="Always" %>

當然,如果你希望爲所有頁面的ViewState應用驗證和加密,可以在Web.config的system.Web節點中添加:

<pages enableViewStateMac="true" viewStateEncryptionMode="Always"></pages>

既然ViewState中的數據是序列化後加入的,那麼我們就可以把一些複雜的類型也存放到ViewState中。在介紹Session的時候我們曾建立過一個MyUser自定義類,並把它的實例存放到了Session中,後來爲了讓StateServer和SqlServer模式的Session也能保存MyUser類型,我們又爲MyUser標記了[Serializable]。在ViewState中保存自定義類型同樣需要爲類型標記[Serializable],那麼在這裏我們使用ViewState保存MyUser實例的代碼就和使用Session差不多。

MyUser user = new MyUser();

user.sUserName = "小朱";

user.iAage = 24;

ViewState["CustomClass"] = user;

讀取代碼:

MyUser user = ViewState["CustomClass"] as MyUser;

Response.Write(user.ToString());

那麼,ViewState中能保存多少數據呢?暫且不說表單Post的數據是有大小上限的,ViewState是經過序列化和編碼後保存在頁面中的。如果我們在ViewState中保存一個擁有100條記錄的DataSet,恐怕頁面就很難打開了。不信,你可以自己做一個試驗。

DataSet ds = new DataSet();

using (SqlConnection conn = new SqlConnection(@"server=(local)/SQLEXPRESS;database=

Forum;Trusted_Connection=True"))

{

    SqlDataAdapter da = new SqlDataAdapter("select top 100 * from CacheTest", conn);

    da.Fill(ds);

}

ViewState["Data"] = ds;

僅僅只有100條記錄,在ViewState就成這樣了,如圖12-22所示。

圖12-22  濫用ViewState的結果

而且這些數據還要在瀏覽器和服務器之間往返,佔用的網絡流量很客觀。因此,筆者建議你在ViewState中保存儘量少的數據。如果實在需要在ViewStatge中放置大量數據建議使用maxPageState- FieldLength對ViewState啓用分塊傳輸。

<%@ Page Language="C#"   …  maxPageStateFieldLength="100"%>

如圖12-23所示就設置了單個ViewState,不超過100字節,ViewState分成了幾個部分。

圖12-23  使用maxPageStateFieldLength控制每個ViewState不超過100字節

我們知道,ViewState不僅僅是我們在使用,ASP.NET會把控件交互相關的一些數據都存放到ViewState中,但是對於一些不實現任何交互的控件(比如顯示10條記錄的GridView),你可以設置控件的EnableViewState屬性爲false來讓控件不使用ViewState,從而減少頁面體積。

6.4  ControlState概述

最後,我們再簡單提一下,ASP.NET 2.0提供了ControlState。它用於保存(自定義)控件的關鍵信息。就算頁面或者控件的ViewState被關閉它還能起作用,彌補了ViewState能被禁止的不足。不過使用ControlState稍顯複雜,我們需要自己序列化複雜對象進行存儲。下面的代碼演示瞭如何在ControlState中保存和讀取簡單字符串:

PageStatePersister.ControlState = "編程快樂";

Response.Write(PageStatePersister.ControlState.ToString());

6.5  總結

其實隱藏域、ViewState和ControlState的原理差不多,我們來總結一下。

·      存儲的物理位置。表單隱藏域。

·      存儲的類型限制。可序列化類型(直接在隱藏域中保存內容需要自己序列化)。

·      狀態使用的範圍。當前頁面(當前控件),對用戶獨立。

·      存儲的大小限制。存儲過大數據會導致頁面不能正常打開,不能正常提交。

·      生命週期。頁面在就在,頁面不在也就不在了。三者始終是依附在頁面的隱藏域中的。

·      安全與性能。在客戶端存儲,安全性低。不過,ViewState提供了驗證和加密。

·      優缺點與注意事項。存儲少量數據非常方便簡單。但需要注意不要存儲敏感數據,不要存儲過大的數據。它們和前面說的Cookie、Session與Application不同。雖然Cookie也是存儲在客戶端,每次提交都附加在HTTP頭中進行提交,但是它的數據量畢竟不大,起了一個標記的作用。Session和Application都是存儲在服務器端的,不會參與頁面往返過程。隱藏域、ViewState和ControlState始終參與往返,而且序列化和反序列化會消耗一定資源,因此,存儲過大的數據會導致網頁加載過慢,浪費服務器帶寬。

===============================================================================
===============================================================================
===============================================================================
 

7  以人爲本的Profile

7.1  使用Profile製作個性化頁面

一個人性化的網站往往提供給用戶很多個性化選擇。比如讓用戶選擇所喜歡的網站風格,讓用戶選擇是否自動彈出消息提醒等。這些數據需要在用戶把瀏覽器關閉後還能保存下來,因此只能選擇數據庫進行保存。對於登錄過的用戶比較好辦,我們可以根據用戶名和用戶的選擇存放在數據庫中,對於非登錄用戶(匿名用戶)怎麼保存用戶的選擇呢?唯一的方法只能像Session那樣分配給用戶一個ID,把這個ID存放在Cookie中(當然也可以放在URL中),然後在數據庫中保存這個ID相關的一些配置信息。

ASP.NET 2.0提供了Profile機制,能幫助我們完成類似的功能。Profile不僅僅支持登錄用戶還支持匿名用戶,存儲的數據也可以是任何可序列化類型。幾乎無需寫一行代碼就能輕鬆實現用戶個性化數據的保存。我們配置了一個Web.config文件,如下所示。

<?xml version="1.0"?>

<configuration>

    <system.web>

    <anonymousIdentification enabled="true"/>

    <profile automaticSaveEnabled="true">

      <properties>

        <group name="UI">

          <add name="ForeColor" defaultValue="Black" allowAnonymous="true" type="string"/>

          <add name="EnableBold" defaultValue="false" allowAnonymous="true" type="bool"/>

        </group>

        <group name="UserInfo">

          <add name="UserName" defaultValue="" allowAnonymous="false" type="string"/>

          <add name="UserAge" defaultValue="0" allowAnonymous="false" type="int"/>

        </group>

      </properties>

    </profile>

        <compilation debug="true"/>

        <authentication mode="Forms"/>

    </system.web>

</configuration>

·      <anonymousIdentification enabled="true"/>表示對匿名用戶也啓用Profile,系統會給匿名用戶分配一個隨機字符串組成的ID。

·      <profile automaticSaveEnabled="true">表示自動在頁面請求結束的時候保存Profile的設置到數據庫中。

·      <properties>中就是正式定義Profile的格式了,我們使用<group>標籤把Profile分成了兩組,<group name="UI">和<group name="UserInfo">。

·      在每一個<group>中的纔是真正的Profile。name表示Profile的名字,defaultValue表示默認值,allowAnonymous表示匿名用戶是否可以使用,type表示數據類型。

·      <authentication mode="Forms"/>表示爲系統啓用了表單認證(這是ASP.NET認證方式的一種,以後的章節中會詳細介紹),

然後,我們爲頁面添加一些控件來個性化頁面。

n  文字顏色:

<asp:DropDownList ID="ddl_TextColor" runat="server">

    <asp:ListItem Selected="True">Black</asp:ListItem>

    <asp:ListItem>Blue</asp:ListItem>

    <asp:ListItem>Red</asp:ListItem>

</asp:DropDownList>

<asp:CheckBox ID="cb_IsBlod" runat="server" Text="粗體" />

<br />

<asp:Button ID="btn_SaveSettings" runat="server" Text="保存個性化設置" OnClick=

"btn_SaveSettings_Click" />&nbsp;

<asp:Button ID="btn_Login" runat="server" OnClick="btn_

Login_Click" Text="登錄" /><br />

<asp:Label ID="lab_Text" runat="server" Font-Names="黑

體" Font-Size="50pt" Height="77px" Text="編程快樂" Width=

"293px"></asp:Label>

效果如圖12-24所示。

文本框: 圖12-24  進行個性化設置通過下拉框我們可以設置文字的顏色爲黑色、藍色或者紅色,通過複選框我們可以設置文字是否是粗體。單擊“保存個性化設置”按鈕保存設置。

protected void btn_SaveSettings_Click(object sender, EventArgs e)

{

    Profile.UI.ForeColor = ddl_TextColor.SelectedValue;

    Profile.UI.EnableBold = cb_IsBlod.Checked;

    ApplyUISettings();

}

看到這裏讀者會不會很驚訝,我們僅僅在Web.config文件中配置了Profile的信息,怎麼在代碼中就直接能訪問到強類型的Profile了呢?其實,系統會在App_Code下生成臨時的代碼文件,如圖12-25所示。

圖12-25  系統生成的臨時代碼文件

在這裏我們又自定義了一個ApplyUISettings()方法來向頁面中的標籤應用樣式。

private void ApplyUISettings()

{

    lab_Text.ForeColor = Color.FromName(Profile.UI.ForeColor);

    lab_Text.Font.Bold = Profile.UI.EnableBold;

    ddl_TextColor.SelectedValue = Profile.UI.ForeColor;

    cb_IsBlod.Checked = Profile.UI.EnableBold;

}

同時爲了保證頁面首次加載的時候也能按照用戶的個性化配置來顯示,我們在Page_Load()的時候也需要應用配置。

protected void Page_Load(object sender, EventArgs e)

{

    if (!IsPostBack)

    {           

        ApplyUISettings();

    }

}

現在打開頁面,並把樣式設置爲藍色文字,粗體顯示,如圖12-26所示。

首次操作的時候比較慢,因爲系統正在爲你生成保存信息的數據庫。默認是使用SQL Express數據庫,文件就放在網站的App_Data文件夾下,如圖12-27所示。

         

              圖12-26  保存自己的個性化設置        圖12-27  App_Data下的ASPNETDB數據庫文件

關閉頁面後再打開,可以發現頁面仍然保持了原來的樣式設置。你可能會問,系統怎麼知道我們還是原來的那個用戶呢?其實,系統爲匿名用戶生成了一個ID字符串保存於Cookie中,頁面加載的時候根據這個ID從數據庫中讀出數據填充Profile。在測試了匿名用戶的Profile後,我們來爲登錄按鈕添加Click事件處理方法。

protected void btn_Login_Click(object sender, EventArgs e)

{

    FormsAuthentication.SetAuthCookie("test", false);

    Response.Redirect(Request.Path);

}

在這裏,我們假設一個名爲test的用戶登錄了系統,並把頁面重定向到本頁。然後,我們在頁面上添加一個PlaceHolder控件,在其中放置一些控件讓用戶輸入Profile的信息和退出登錄。

<asp:PlaceHolder ID="ph_UserInfo" runat="server">

    姓名:<asp:TextBox ID="tb_Name" runat="server"></asp:TextBox>

    年齡:<asp:TextBox ID="tb_Age" runat="server"></asp:TextBox>

    <br />

    <asp:Button ID="btn_SaveUserInfo" runat="server" Text="保存用戶信息" OnClick=

   "btn_SaveUserInfo_Click" />&nbsp;

    <asp:Button ID="btn_Logout" runat="server" OnClick="btn_Logout_Click" Text="退

    出" />

</asp:PlaceHolder>

保存用戶信息按鈕Click事件實現如下:

protected void btn_SaveUserInfo_Click(object sender, EventArgs e)

{

    Profile.UserInfo.UserName = tb_Name.Text;

    Profile.UserInfo.UserAge = int.Parse(tb_Age.Text);

    GetUserInfo();

}

這裏的GetUserInfo用於顯示用戶的信息(登錄用戶的信息和非登錄用戶的信息)。

private void GetUserInfo()

{

    if (User.Identity.IsAuthenticated)

    {

        ph_UserInfo.Visible = true;

        Response.Write("當前登錄用戶:" + User.Identity.Name + "<br/>");

        Response.Write("Profile關聯用戶:" + Profile.UserName + "<br/>");

        Response.Write("Profile.UserInfo.UserName:" + Profile.UserInfo.UserName +

        "<br/>");

        Response.Write("Profile.UserInfo.UserAge:" + Profile.UserInfo.UserAge +

       "<br/>");

    }

    else

    {

        ph_UserInfo.Visible = false;

        Response.Write("Profile關聯用戶:" + Profile.UserName + "<br/>");

    }

}

退出登錄按鈕Click事件實現如下:

protected void btn_Logout_Click(object sender, EventArgs e)

{

    FormsAuthentication.SignOut();

    Profile.UserInfo.UserAge = 0;

    Profile.UserInfo.UserName = "";

    Response.Redirect(Request.Path);

}

同時,我們修改一下Page_Load,讓頁面首次加載的時候也顯示用戶信息。

protected void Page_Load(object sender, EventArgs e)

{

    if (!IsPostBack)

    {           

        ApplyUISettings();

        GetUserInfo();

    }

}

再次打開頁面,如圖12-28所示。

我們看到,對於匿名用戶來說,Profile.UserName屬性爲一個隨機字符串,單擊“登錄”按鈕後如圖12-29所示。

    

    圖12-28  匿名用戶關聯了一個隨機字符串作爲ID           圖12-29  單擊登錄按鈕後顯示用戶信息

現在可以看到Profile關聯的用戶UserName已經爲登錄的test用戶了。現在,我們把姓名設置爲“小朱”,年齡設置爲24,單擊“保存用戶信息”按鈕。然後把樣式修改爲紅色粗體,單擊“保存個性化設置”按鈕。如圖12-30所示。

文本框: 圖12-30  在非匿名Profile中保存了信息如果你現在關閉窗口再重新打開就會發現個性化設置還是原來在匿名狀態下保存的藍色粗體。因此現在已經不在登錄狀態了,比較一下發現現在的ID和上次的ID還是一樣的。再次單擊“登錄”按鈕後,頁面又加載了test用戶的個性化設置。

7.2  Profile總結

Profile的知識遠遠不止這些,我們這裏僅僅對它進行了簡單的介紹,在結束以前我們以第一節中的幾個問題結束對Profile的討論。

·      存儲的物理位置。客戶端Cookie/URL和服務器數據庫。

·      存儲的類型限制。可序列化類型。

·      狀態使用的範圍。當前請求的上下文,對每一個用戶獨立。

·      存儲的大小限制。任意大小,讀取寫入頻繁的數據不建議存入Profile。

·      生命週期。與關聯的Cookie的生命週期一樣。

·      安全與性能。數據總是存儲在服務端,安全性比較高,但不易存儲過多數據。

·      優缺點與注意事項。可以很方便地保存用戶(匿名用戶和已登錄用戶)的設置。

===============================================================================
===============================================================================
===============================================================================
 

8  其他

8.1  QueryString

除了Cookie、Session、Application、Cache、HiddenField、ViewState、ControlState、Profile等重要狀態機制外,ASP.NET還提供了一些其他的方法讓我們暫時保存數據。

在很多時候我們希望跨頁面傳輸數據,最常用的一個辦法就是使用GET方式提交數據。也就是在URL中附加一段QUERYSTRING(類似 news.aspx?ID=1這樣的效果),因此,有的時候我們不需要通過程序就能傳輸QUERYSTRING。不過,我們需要注意以下幾點。

·      IE瀏覽器對URL長度限制在2083個字符內。由於QUERYSTRING是在URL中傳輸內容,所以也就受到了這個限制。

·      在URL中傳輸的數據都是明文的,而且客戶端隨時能修改,因此千萬別使用QUERYSTRING傳敏感數據。

·      在URL中傳漢字或者一些特殊字符需要進行URL編碼後傳遞,接收的時候再反編碼,否則傳遞的數據可能會出現亂碼或者被截斷。

string s = "編程快樂!@#$%^&*()";

Response.Redirect(Request.Url.AbsolutePath+"?data="+HttpUtility.UrlEncode(s));

接收的時候:

if (Request.QueryString["data"] != null)

    Response.Write(HttpUtility.UrlDecode(Request.QueryString["data"]));

8.2  跨頁提交

QUERYSTRING畢竟只能傳輸字符串,如果我們希望在一個頁面中直接訪問另外一個頁面,可以使用ASP.NET 2.0的跨頁提交功能。比如我們建立一個CrossPageSubmitTest.aspx頁面,在任意頁面建立一個按鈕提交到CrossPageSubmitTest.aspx。

<asp:Button ID="btn_CrossPageSubmit" runat="server" OnClick="btn_CrossPageSubmit_

Click" PostBackUrl="CrossPageSubmitTest.aspx" Text="跨頁面提交" />

然後在CrossPageSubmitTest.aspx的Page_Load中加入以下代碼就能輸出前一個頁面上btn_CrossPageSubmit按鈕的Text屬性。

Response.Write((PreviousPage.FindControl("btn_CrossPageSubmit") as Button).Text);

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