ASP.NET會話(Session)保存模式
今日抽空就說一下 Session 在 .Net v1.0/v1.1 中的存儲模式。大家可在 MSDN 2003 中搜索一下<sessionState>即可看到關於 Web.config 中的<sessionState>節點元素的描述,共有 Off、InProc、StateServer、SQLServer 四種模式。Off、InProc 分別指“不啟用”、“進程內保存(默認值)”,此兩種模式沒啥講的,所謂 InProc 就是把 Session 保存在 aspnet_wp.exe (Windows 2000 解析 ASP.NET頁面所用的進程) 或 w3wp.exe (Win2003 的進程) 中,一旦進程被中止或被重置,Session 將丟失。
一、 引發 Session 丟失的幾種原因
動過手寫代碼的人都知道,Session 丟失是比較常見的事。以下是本人這幾年所遇到的,能夠引發 Session 丟失的原因,不敢說是百分百,丟失概率還是特別高的。錯…,簡直可以說是“相…當…”高哇 ^_^"
1、 存放 Session 的電腦重啟(廢話,若這樣都不丟,你神仙啊)
2、 InProc 模式:aspnet_wp.exe 或 w3wp.exe 在“任務管理器”中或其它情況下導致其進程被終止運行。
3、 InProc 模式:修改 .cs 文件后,編譯了兩次(只編譯一次,有時不會丟失)
4、 InProc 模式:修改了 Web.config
5、 InProc 模式,Windows 2003 環境:應用程序池回收、停止后重啟
6、 InProc 模式:服務器上 bin 目錄里的 .dll 文件被更新
以上列舉的都是 InProc 模式下,容易引發解析 ASP.NET 應用程序重置的原因。是不是覺得很窩火?之前我也有這種感覺,慢慢就習慣啦,再后來就干脆不用這種模式了。於是乎,就有了使用下列兩種模式的嘗試,現寫出來與大家一起分享。
二、 使用 StateServer 保存 Session
StateServer 模式的實質是,把Session 存放在一個單獨的進程里,此進程獨立於 aspnet_wp.exe 或 w3wp.exe 。啟用此服務后,在“任務管理器”中可以看到一個名為 aspnet_state.exe 的進程,下面開始說明一下設置的具體步驟:
1、 修改注冊表(關鍵步驟,如下圖)
運行 regedit →打開注冊表→找到HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Services/aspnet_state/Parameters 節點→將AllowRemoteConnection 的鍵值設置成“1”(1 為允許,0 代表禁止)→設置Port (端口號)
注意事項:
a)、若ASP.NET State Service 正在運行,修改注冊表內容后,則需要重新啟動該服務
b)、注意端口號的鍵值是以十六進制儲存的,可以使用十進制進行修改,42424 是默認的端口
c)、AllowRemoteConnection 的鍵值設置成“1”后,意味着允許遠程電腦的連接,也就是說只要知道你的服務端口,就可享用你的ASP.NET State Service,即把 Session 存放在你的電腦進程內,因此請大家慎用;鍵值為“0”時,僅有stateConnectionString為“tcpip=localhost: 42424”與“tcpip=127.0.0.1:42424”的情況,方可使用ASP.NET State Service
2、 開啟 ASP.NET State Service(如下圖)
右鍵點擊“我的電腦”→管理→服務與應用程序→服務→雙擊“ASP.NET State Service”→啟動(可設為“自動”)
說明:只要安裝了 .Net Framework v1.0/v1.1 ,都擁有此服務。
3、 更改 Web.config
打開 Web.config →找到<sessionState>節點內容
<sessionState
mode="InProc"
stateConnectionString="tcpip=127.0.0.1:42424"
sqlConnectionString="data source=127.0.0.1;Trusted_Connection=yes"
cookieless="false"
timeout="20" />
→將其改為以下內容
<sessionState mode="StateServer" stateConnectionString="tcpip=192.168.0.2:42424" timeout="20" />
注意事項:
a)、設成StateServer 后,必須要有對應的stateConnectionString
b)、注意 IP 地址(可以是遠程計算機 IP、計算機名稱、域名)與端口號,端口號需與ASP.NET State Service 的服務端口一致
三、 將 Session 放入 SQLServer 保存
SQLServer 模式就是,把Session 存放在 SQL Server 數據庫里(注意不是 Oracle ,動動腳趾都能猜到原因啦),下面開始說明一下設置的具體步驟:
1、 啟動相關的數據庫服務(如圖)
運行SQL Server 服務管理器→啟動 SQL Server (最好設為開機自動運行)→啟動 SQL Server Agent 服務(最好設為開機自動運行)
注意事項:
a)、注意啟動順序,也可通過下列方式設置:右鍵點擊“我的電腦”→管理→服務與應用程序→服務→找到“MSSQLSERVER”與“SQLSERVERAGENT”→啟動並設置啟動類型為“自動”
b)、SQL Server Agent在此處的作用是清除數據庫中已過期的 Session
2、 建立存放 Session 的 DataBase
運行“SQL 查詢分析器”→使用“sa”或是擁有“master”的 db_owner 權限的用戶登錄數據庫→打開查詢文件 C:/WINNT/Microsoft.NET/Framework/v1.1.4322/InstallSqlState.sql (存放在 Windows 系統目錄的 .Net 安裝目錄下可找到)→直接運行該 sql 腳本→刷新數據庫即可看到名為 ASPState 的 DataBase
3、 建立連接數據庫 ASPState 的用戶,並為此用戶授權(此步驟可跳過)
進行此步的原因是:一是不想在 Web.config 中出現 sa 的密碼;二是 tempdb 在數據庫啟動后僅保留 sa 一個帳號的使用權限,其余帳號的權限統統被清除,但保存 Session又需要用到此 DataBase;
A)、運行 SQL Server 的企業管理器→展開數據庫的安全性→右擊“登錄”→新建“登錄”→輸入“名稱”→選擇“SQL Server 身份驗證”→輸入“密碼”→指定“數據庫”→點擊“數據庫訪問”→勾選“ASPState”→選中“db_owner”角色→點擊“確定”→再一次輸入“密碼”→點擊“確定”后即可建立 ASPState 的用戶(此處建立名為“SessionStateUser”,密碼為“123456”的測試用戶)
B)、運行 SQL Server 的企業管理器→展開“管理”→展開“SQL Server 代理”→右擊“作業”→點擊“新建作業”→輸入“名稱”(此例為 GrantSessionUser )→點擊標簽“步驟”→新建→輸入“步驟名”(此例為 Grant01)→選擇數據庫“tempdb”→編寫 SQL 腳本“exec sp_adduser 'SessionStateUser', 'SessionUser' ,'db_owner' ”→確定→點擊標簽“調度”→新建→輸入“名稱”(此例為 Start01 )→選擇類型“SQL Server 代理啟動時自動啟動”→確定→最后點擊“確定”新增完畢
C)、也可運行以下腳本一次性搞定以上 A、B 兩個步驟
/******腳本開始******/
--新建數據庫帳號 SessionStateUser ,默認登錄 ASPState
EXEC sp_addlogin 'SessionStateUser', '123456', 'ASPState'
use ASPState --切換 DataBase
--將 SessionStateUser 授予 db_owner 的權限
exec sp_adduser 'SessionStateUser', 'SessionUser' ,'db_owner'
use master --切換 DataBase
BEGIN TRANSACTION
/******聲明變量******/
DECLARE @JobID BINARY(16)
DECLARE @ReturnCode INT
SELECT @ReturnCode = 0
-- 若沒有,則添加作業的分類
IF (SELECT COUNT(*) FROM msdb.dbo.syscategories WHERE name = N'[Uncategorized (Local)]') < 1
EXECUTE msdb.dbo.sp_add_category @name = N'[Uncategorized (Local)]'
-- 新建作業
EXECUTE @ReturnCode = msdb.dbo.sp_add_job --調用存儲過程 sp_add_job
@job_id = @JobID OUTPUT, --將返回的 JobID,賦值給變量
@job_name = N'GrantSessionUser', --作業名稱
@owner_login_name = NULL, --默認為當前用戶所有
@description = null,
@category_name = N'[Uncategorized (Local)]', --作業分類歸屬
@enabled = 1, --是否啟用
@notify_level_email = 0,
@notify_level_page = 0,
@notify_level_netsend = 0,
@notify_level_eventlog = 0,
@delete_level= 0
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback --出錯則回滾
-- 新建步驟
EXECUTE @ReturnCode = msdb.dbo.sp_add_jobstep --調用存儲過程 sp_add_jobstep
@job_id = @JobID, --傳入剛剛新建的 JobID
@step_id = 1,
@step_name = N'Grant01', --步驟名稱
@command = N'exec sp_adduser ''SessionStateUser'', ''SessionUser'' ,''db_owner''',
--需要執行的 SQL 腳本(注意用兩個連續的單引號表示 SQL 中的單引號)
@database_name = N'tempdb', --執行上述 SQL 所用的 DataBase
@server = N'',
@database_user_name = N'',
@subsystem = N'TSQL', --執行類型為“Transact-SQL 腳本”
@cmdexec_success_code = 0,
@flags = 0,
@retry_attempts = 0,
@retry_interval = 1,
@output_file_name = N'',
@on_success_step_id = 0,
@on_success_action = 1,
@on_fail_step_id = 0,
@on_fail_action = 2
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
-- 新建調度
EXECUTE @ReturnCode = msdb.dbo.sp_add_jobschedule
@job_id = @JobID,
@name = N'Start01', --調度名稱
@enabled = 1,
@freq_type = 64 --“64”表示當 SQLServerAgent 服務啟動時運行
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
-- 將新建的作業添加到本地數據庫
EXECUTE @ReturnCode = msdb.dbo.sp_add_jobserver @job_id = @JobID, @server_name = N'(local)'
IF (@@ERROR <> 0 OR @ReturnCode <> 0) GOTO QuitWithRollback
COMMIT TRANSACTION
GOTO EndSave
QuitWithRollback:
IF (@@TRANCOUNT > 0) ROLLBACK TRANSACTION
EndSave:
/******腳本結束******/
4、 設置 Web.config 內容
打開 Web.config →找到<sessionState>節點內容→修改為以下內容即可:
<sessionState mode="SQLServer" sqlConnectionString ="data source=192.168.0.2; user id=SessionStateUser; password=123456" timeout="20" />
注意事項:
a)、sqlConnectionString中不能出現 initial catalog 選項
b)、SQL Server Agent在此處的作用是清除數據庫中已過期的 Session
c)、你若跳過了第三步,則user id 需要用sa進行登錄
d)、若sqlConnectionString為“data source=127.0.0.1;Trusted_Connection=yes”,則使用本地計算機ASPNET(Windows 2000 系統帳戶)或 Network Service(Windows 2003 系統帳戶)的身份登錄數據庫。要是數據庫不允許上述用戶登錄,則報錯;同樣,即使上述帳戶能成功登錄,也要分配其 tempdb 的權限,理由是 Session 是保存在 tempdb 中的,若沒有該 DataBase 的存取權限是行不滴。見下圖:
關於SESSION共享的問題個人總結。
1.不管上面采用哪種模式要達到SESION共享必須是同一個站點或者主域名相同,要不絕對不可能實現SESSION。
2.同時相關子模塊項目是不允許在IIS設置虛擬目錄,設置了虛擬目錄也是不可能達到共享條件的。
3.在本地開發時怎么達到SESSION共享的條件時布置呢?舉例如下:
3.1有個大項目A站點(假設他的域名為EXAMPLE.COM.CN)
3.2其中有2個單獨的項目為B,C(假設他的域名為B.EXAMPLE.COM.CN和C.EXAMPLE.COM.CN或是EXAMPLE.COM.CN/B和EXAMPLE.COM.CN/C),他們之間要共享SESSION。
采取做法如下(詳情可以參考自己參與的DP項目):
3.3在同一個解決方案中在A中把增加對 B,C兩個Web工程的引用,注意:是工程引用,不是其他引用(且這2個工程都在A功根目錄地下,但B,C工程也在這解決方案中,打開VS A工程站點是看不到B,C文件夾的,同時出現與A同等級的B,C工程站點在解決方案中,這樣開發就互不影響,從而達到分工程團隊開發)。
3.4.運行時在主站點直接運行看效果。那樣在本地就可以看到SESSION的共享了。
3.5.發布到IIS上要布置3個站點,好像同時要設置COOKIE的主域名DOMIAN為主站點的域名且有以下操作
protected void Session_Start(object sender, EventArgs e)
{
Response.Cookies["ASP.NET_SessionId"].Value = Session.SessionID.ToString();
Response.Cookies["ASP.NET_SessionId"].Domain =".local.com";
}
或寫個httpModules層處理這個問題
namespace Primepress.FontSearch.Services {
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.SessionState;
using System.Reflection;
/// <summary>
/// 二級域名會話共享
/// </summary>
public class SessionSharedHttpModule : IHttpModule {
string _rootDomain = null; //一級域名
public void Dispose() {
//throw new NotImplementedException();
}
public void Init(HttpApplication context) {
_rootDomain = "localhost"; //一級域名賦值
//去除一級域名以外信息(將www.dhlx.cn改為dhlx.cn,如果一級域名不是常量賦值的話)
//_rootDomain = _rootDomain.Substring(_rootDomain.LastIndexOf('.', _rootDomain.LastIndexOf('.') - 1) + 1);
//要實現會話共享還得修改OutOfProcSessionStateStore類下的一個私有的靜態字段s_uribase
//OutOfProcSessionStateStore的聲明為:
//internal sealed class OutOfProcSessionStateStore : SessionStateStoreProviderBase
//s_uribase的聲明為:
//static string s_uribase;
//關於OutOfProcSessionStateStore類以及s_uribase字段的內容請查閱OutOfProcStateClientManager.cs文件
//文件路徑:Framework源代碼\V2.0.5727\dd\ndp\fx\src\xsp\System\Web\State\OutOfProcStateClientManager.cs
Type stateServerSessionProvider = typeof(HttpSessionState).Assembly.GetType("System.Web.SessionState.OutOfProcSessionStateStore");
FieldInfo uriField = stateServerSessionProvider.GetField("s_uribase", BindingFlags.Static | BindingFlags.NonPublic);
if (uriField == null)
throw new ArgumentException("UriField was not found");
uriField.SetValue(null, _rootDomain);
context.EndRequest += new EventHandler(context_EndRequest);
}
/// <summary>
/// 從發送給客戶端的Cookie集合中找出記錄會話ID的Cookie
/// 並修改它的Domain屬性值為要共享的一級域名
/// </summary>
void context_EndRequest(object sender, System.EventArgs e) {
HttpApplication app = sender as HttpApplication;
for (int i = app.Context.Response.Cookies.Count - 1; i >= 0; i--) {
//ASP.NET_SessionId是默認的存儲會話ID的key,如果修改了默認值這里要修改成一致的
if (app.Context.Response.Cookies[i].Name.Equals("ASP.NET_SessionId")) {
app.Context.Response.Cookies[i].Domain = _rootDomain;
return;
}
}
}
}
}