源碼分析Session的臺前幕後(Asp .Net MVC5)
在這篇文章裏,我們從源代碼的角度重點分析Session的創建、緩存、銷毀、管理。
通常我們說的Session指的是在控制器中使用的Session字段,該字段的類型是HttpSessionState。可以獲取SessionID,可以存儲數據,可以增加刪除數據等等。Session字段中使用的HttpSessionState對象,就是在下面介紹的模塊中產生的。
在.Net Framework內部,來自瀏覽器的請求像工廠的流水線加工產品一樣,在不同的部位進行不同的處理,最終被加工為一件成品。我們的請求也是如此,內部有許多的模塊(實現了接口IHttpModule)對我們的請求進行處理,比如映射路由的模塊(UrlRoutingModule),該模塊的作用是從用戶請求的url中獲取信息,找到能處理這個IHttpHandler對象,用於後續處理處理請求。而我們這裏討論的則是關於會話Session的一個模塊--SessionStateModule。
在這裏模塊裏,有諸如這樣的功能,生成SessionID、生成用於HttpSessionState內部存儲數據的ISessionStateItemCollection對象。然後將這樣的不同對象封裝起來。
在IIS中,Session的存儲生成有4中模式。
1-InProc進程內模式,將Session存儲在web應用程序進程內部。這也是系統默認的模式。這種方式優點顯而易見---存取速度很快,缺點也顯而易見--一旦因為某種原因應用程序被關閉,不管是否重啟應用程序,所有用戶的Session全部都會丟失,這是不能接受的。
而且因為是進行內存儲,當我們想做分布式的時候,Session無法共享。
2-StateServer狀態服務器模式。在這種模式下,應用程序會啟動一個會話狀態服務器,專門用於存儲Session。這種方式解決了重啟web應用程序後session丟失的問題,但狀態服務器本身會有問題,如:不支持故障轉移等。
3-SQLServer數據庫模式。在這種模式下,所有的會話數據都被存儲在SQLServer數據庫中,但是要求session中存儲的時候是可以序列化的。在這種模式下,數據庫強大的存儲、搜索、故障轉移群集等穩定功能,會給session帶來強大的支持。
4-Custom自定義模式。在這種模式下,用戶可以定義自己的session存儲方式。
這些內容僅在這裏做簡單介紹,有興趣的朋友可以查看微軟官方的文檔。https://msdn.microsoft.com/zh-cn/library/ms178586(v=vs.100).aspx
1 void InitModuleFromConfig(HttpApplication app, SessionStateSection config) { 2 if (config.Mode == SessionStateMode.Off) { 3 return; 4 } 5 6 app.AddOnAcquireRequestStateAsync( 7 new BeginEventHandler(this.BeginAcquireState), 8 new EndEventHandler(this.EndAcquireState)); 9 10 app.ReleaseRequestState += new EventHandler(this.OnReleaseState); 11 app.EndRequest += new EventHandler(this.OnEndRequest); 12 13 _partitionResolver = InitPartitionResolver(config); 14 15 switch (config.Mode) { 16 case SessionStateMode.InProc: 17 if (HttpRuntime.UseIntegratedPipeline) { 18 s_canSkipEndRequestCall = true; 19 } 20 _store = new InProcSessionStateStore(); 21 _store.Initialize(null, null); 22 break; 23 24 case SessionStateMode.StateServer: 25 if (HttpRuntime.UseIntegratedPipeline) { 26 s_canSkipEndRequestCall = true; 27 } 28 _store = new OutOfProcSessionStateStore(); 29 ((OutOfProcSessionStateStore)_store).Initialize(null, null, _partitionResolver); 30 break; 31 32 case SessionStateMode.SQLServer: 33 _store = new SqlSessionStateStore(); 34 ((SqlSessionStateStore)_store).Initialize(null, null, _partitionResolver); 35 break; 36 37 case SessionStateMode.Custom: 38 _store = InitCustomStore(config); 39 break; 40 default: 41 break; 42 } 43 44 // 依賴SessionIDManager管理session id,所以在這裏對管理器進行初始化 45 _idManager = InitSessionIDManager(config); 46 47 if ((config.Mode == SessionStateMode.InProc || config.Mode == SessionStateMode.StateServer) && 48 _usingAspnetSessionIdManager) { 49 //如果我們使用InProc模式或者StateServer模式,並且也使用我們自己的會話ID模塊, 50 //我們知道我們不關心在所有會話狀態存儲讀/寫和會話ID讀/寫中的模擬。 51 _ignoreImpersonation = true; 52 } 53 }模塊初始化進行的設置之一
模塊進行初始化時,會進行許多設置,上面的方法時其中的設置之一。上面的方法中,如果會話狀態的模式是關閉,則直接返回,不再進行任何操作。
然後向HttpApplication對象註冊事件,當這個對象需要獲取會話狀態時,就調用這裏註冊的方法,進行會話狀態的創建等共走。供後面的步驟使用。
然後根據會話狀態模式加載不同的會話狀態存儲提供程序,默認的模式是SessionStateMode.InProc,則加載的提供程序是類是InProcSessionStateStore。
InProcSessionStateStore類內部實現的存儲會話狀態的方式是存儲在運行時的緩存中。
從緩存中讀取會話狀態:
HttpRuntime.CacheInternal.Get(key)。
將回話狀態寫入緩存:
HttpRuntime.CacheInternal.UtcInsert(
key,/*緩存的key,此處緩存會話,所以是在sessionid的基礎上進行了其他操作組合而成。*/
state,/*這就是我們的會話狀態*/
null,/*依賴屬性,這裏不需要,所以傳空值*/
Cache.NoAbsoluteExpiration,/*絕對過期時間。在這裏我們不希望有絕對過期時間,所以傳遞了Cache.NoAbsoluteExpiration,該字段的值是 DateTime.MaxValue;*/
new TimeSpan(0, state._timeout, 0),/*滑動到期時間,傳遞的是會話狀態的過期值。*/
CacheItemPriority.NotRemovable,/*緩存優先級,告訴運行時緩存,在進行內存優化時,不能刪除這個緩存值*/
_callback /*緩存項被移除的回調方法。正是因為InProcSessionStateStore類內部使用運行時緩存且運行時緩存帶有緩存項移除回調方法,所以在SessionStateMode.InProc模式下,會話狀態過期時,會調用在Global.asax中註冊的Session_End方法*/
);
繼續看代碼。
緊接著,會根據配置文件對SessionIDManager進行初始化,默認情況下會初始化一個SessionIDManager的對象。我們使用這個會話管理器進行sessionid的管理。創建sessionid,驗證sessionid的合法性等功能。
模塊的初始化工作已經介紹完畢,這裏介紹的只是最基礎的一部分,其他還涉及的會話狀態的鎖定、優化等擴展開來則過於復雜。
下面我們按一個請求的處理過程為順序,介紹會話狀態的相關處理。從獲得會話狀態開始。上代碼。
1 IAsyncResult BeginAcquireState(Object source, EventArgs e, AsyncCallback cb, Object extraData) { 2 bool requiresState; 3 bool isCompleted = true; 4 bool skipReadingId = false; 5 6 _acquireCalled = true; 7 _releaseCalled = false; 8 ResetPerRequestFields(); 9 10 _rqContext = ((HttpApplication)source).Context; 11 _rqAr = new HttpAsyncResult(cb, extraData); 12 13 ChangeImpersonation(_rqContext, false); 14 15 try { 16 17 /* Notify the store we are beginning to get process request */ 18 _store.InitializeRequest(_rqContext); 19 20 /* determine if the request requires state at all */ 21 requiresState = _rqContext.RequiresSessionState; 22 23 // SessionIDManager may need to do a redirect if cookieless setting is AutoDetect 24 if (_idManager.InitializeRequest(_rqContext, false, out _rqSupportSessionIdReissue)) { 25 _rqAr.Complete(true, null, null); 26 return _rqAr; 27 } 28 29 // See if we can skip reading the session id. See inline doc of s_allowInProcOptimization 30 // for details. 31 if (s_allowInProcOptimization && 32 !s_sessionEverSet && 33 (!requiresState || // Case 1 34 !((SessionIDManager)_idManager).UseCookieless(_rqContext)) ) { // Case 2 35 36 skipReadingId = true; 37 } 38 else { 39 /* Get sessionid */ 40 _rqId = _idManager.GetSessionID(_rqContext); 41 } 42 43 if (!requiresState) { 44 if (_rqId == null) { 45 } 46 else { 47 // Still need to update the sliding timeout to keep session alive. 48 // There is a plan to skip this for perf reason. But it was postponed to 49 // after Whidbey. 50 _store.ResetItemTimeout(_rqContext, _rqId); 51 } 52 _rqAr.Complete(true, null, null); 53 return _rqAr; 54 } 55 56 _rqExecutionTimeout = _rqContext.Timeout; 57 58 if (_rqExecutionTimeout == DEFAULT_DBG_EXECUTION_TIMEOUT) { 59 _rqExecutionTimeout = s_configExecutionTimeout; 60 } 61 62 /* determine if we need just read-only access */ 63 _rqReadonly = _rqContext.ReadOnlySessionState; 64 65 if (_rqId != null) { 66 /* get the session state corresponding to this session id */ 67 isCompleted = GetSessionStateItem(); 68 } 69 else if (!skipReadingId) { 70 /* if there‘s no id yet, create it */ 71 bool redirected = CreateSessionId(); 72 73 _rqIdNew = true; 74 75 if (redirected) { 76 if (s_configRegenerateExpiredSessionId) { 77 // See inline comments in CreateUninitializedSessionState() 78 CreateUninitializedSessionState(); 79 } 80 _rqAr.Complete(true, null, null); 81 return _rqAr; 82 } 83 } 84 85 if (isCompleted) { 86 CompleteAcquireState(); 87 _rqAr.Complete(true, null, null); 88 } 89 90 return _rqAr; 91 } 92 finally { 93 RestoreImpersonation(); 94 } 95 }BeginAcquireState
這是獲取Session的入口,每當有請求到達時,這是必須要有的步驟,經過這個方法處理後,請求上下文會擁有一個與當前請求匹配的Session。
下面我們具體分析方法中的一些代碼。
requiresState = _rqContext.RequiresSessionState;
上面這行代碼判斷請求是否需要會話狀態。主要取決於兩方面,一方面是我們的控制器是否需要會話狀態.控制器本身是否設置了SessionStateAttribute特性或者用於處理請求的實現了IHttpHandler接口的類是否標記了IRequiresSessionState這個接口。
當我們沒有給控制器標記SessionStateAttribute特性時,會默認使用SessionStateBehavior.Default這個選項。這個選項的意思參考第一個截圖。
我把這個屬性的內部代碼貼出來,有興趣的可以看看。
internal bool RequiresSessionState { get { switch (SessionStateBehavior) { case SessionStateBehavior.Required: case SessionStateBehavior.ReadOnly: return true; case SessionStateBehavior.Disabled: return false; case SessionStateBehavior.Default: default: return _requiresSessionStateFromHandler; } } } //從上面的代碼中可以看出,當設置SessionStateBehavior.Default時, //會返回_requiresSessionStateFromHandler這個變量的值。 //我們再看看這個值是如何被設置的. public IHttpHandler Handler { get { return _handler;} set { _handler = value; _requiresSessionStateFromHandler = false; _readOnlySessionStateFromHandler = false; InAspCompatMode = false; if (_handler != null) { if (_handler is IRequiresSessionState) { _requiresSessionStateFromHandler = true; } if (_handler is IReadOnlySessionState) { _readOnlySessionStateFromHandler = true; } Page page = _handler as Page; if (page != null && page.IsInAspCompatMode) { InAspCompatMode = true; } } } } //從上面代碼中的可以看出, // if (_handler is IRequiresSessionState) { // _requiresSessionStateFromHandler = true; // } //而我們的MvcHandler是擴展了IRequiresSessionState這個接口的,所以_requiresSessionStateFromHandler是true;_rqContext.RequiresSessionState屬性的內部
BeginAcquireState方法中的代碼主要是用來判斷是否滿足優化的目的。比如是否要延遲獲取一個session id,是否要加載session等。
我們假設是第一次請求,走一次完整的流程。那麽,我們的代碼就要進入CompleteAcquireState這個方法中了。
在介紹這個方法前,首先介紹一個點。
在控制器中訪問的Session共分兩個部分,一個是用來存儲數據的部分,例如這樣的形式--Session["key1"] = 123;
一個是Session的id---Session.SessionID
他們雖然都歸屬在Session下面,但是它們的生成、存儲、刪除等機制完全不同。
1 // Called when AcquireState is done. This function will add the returned 2 // SessionStateStore item to the request context. 3 void CompleteAcquireState() { 4 bool delayInitStateStoreItem = false; 5 try { 6 //_rqItem就是session中用來存儲數據的 7 //當第一次訪問時,這個值肯定是空值,所以會走else的分支 8 if (_rqItem != null) { 9 _rqSessionStateNotFound = false; 10 11 if ((_rqActionFlags & SessionStateActions.InitializeItem) != 0) { 12 _rqIsNewSession = true; 13 } 14CompleteAcquireState
該函數將把返回的SessionStateStore項添加到請求上下文中。
這個方法的主要作用是向請求上下文中(HttpContext)添加一個會話狀態存儲,Session中存儲數據的功能就是靠這個對象實現的。
這個方法中,添加會話狀態存儲的功能是靠InitStateStoreItem這個方法來實現的。我們也看看這個方法的內部。
1 internal void InitStateStoreItem(bool addToContext) { 2 try { 3 4 if (_rqItem == null) { 5 _rqItem = _store.CreateNewStoreData(_rqContext, s_timeout); 6 } 7 8 _rqSessionItems = _rqItem.Items; 9 if (_rqSessionItems == null) { 10 throw new HttpException(SR.GetString(SR.Null_value_for_SessionStateItemCollection)); 11 } 12 13 // No check for null because we allow our custom provider to return a null StaticObjects. 14 _rqStaticObjects = _rqItem.StaticObjects; 15 16 _rqSessionItems.Dirty = false; 17 18 _rqSessionState = new HttpSessionStateContainer( 19 this, 20 _rqId, // could be null if we‘re using InProc optimization 21 _rqSessionItems, 22 _rqStaticObjects, 23 _rqItem.Timeout, 24 _rqIsNewSession, 25 s_configCookieless, 26 s_configMode, 27 _rqReadonly); 28 29 if (addToContext) { 30 SessionStateUtility.AddHttpSessionStateToContext(_rqContext, _rqSessionState); 31 } 32 } 33 finally { 34 RestoreImpersonation(); 35 } 36 } 37 public static class SessionStateUtility { 38 // Called by custom session state module 39 static public void AddHttpSessionStateToContext(HttpContext context, IHttpSessionState container) { 40 HttpSessionState sessionState = new HttpSessionState(container); 41 42 //在這個方法中,將用於存取數據的數據結構存儲在了context.Items中。 43 //存儲的key是變量SESSION_KEY 44 try { 45 context.Items.Add(SESSION_KEY, sessionState); 46 } 47 catch (ArgumentException) { 48 throw new HttpException(SR.GetString(SR.Cant_have_multiple_session_module)); 49 } 50 } 51 } 52 53 54 //這是HttpContext中Session的內部代碼 55 public HttpSessionState Session { 56 get { 57 if (HasWebSocketRequestTransitionCompleted) { 58 // Session is unavailable at this point 59 return null; 60 } 61 //省略了部分代碼 62 63 //訪問session時,從context.Items中取出這一數據結構 64 //存儲的key是變量SESSION_KEY 65 return(HttpSessionState)Items[SessionStateUtility.SESSION_KEY]; 66 } 67 }InitStateStoreItem
經過這個方法後,我們的請求上下文真正有了用了可以讓我們用於訪問,用於存取數據的對象。
經過上面的兩個大方法,我們的請求上下文終於獲得了session,然後代碼進入了我們的控制器,我們在控制器中會具體的處理的代碼,與session相關的可能就是從session中讀取數據,但是這個數據只是存儲在這個用來存儲數據的對象中了,並沒有存儲在緩存中。當我們再次請求時,數據從哪裏來呢?下面介紹的方法就是負責session相關的收尾工作---OnReleaseState。
當控制器中的代碼處理完畢後。應用程序就會進行其他的一些收尾步驟,比如將session中的數據存儲起來,再次查詢時使用。
在下面的代碼中,對部分關鍵代碼做了相應的註釋。
這個方法的主要作用就是判斷是否要把會話存儲放進系統緩存中,是否把session id繼續保存在cookie中
1 void OnReleaseState(Object source, EventArgs eventArgs) { 2 HttpApplication app; 3 HttpContext context; 4 bool setItemCalled = false; 5 6 _releaseCalled = true; 7 8 app = (HttpApplication)source; 9 context = app.Context; 10 11 ChangeImpersonation(context, false); 12 13 try { 14 if (_rqSessionState != null) { 15 bool delayedSessionState = (_rqSessionState == s_delayedSessionState); 16 SessionStateUtility.RemoveHttpSessionStateFromContext(_rqContext, delayedSessionState); 17 18 if ( 19 //如果會話狀態是新的,並且沒有被訪問過, 20 //那麽這樣的會話狀態存儲不用保存到系統的緩存中,保存了沒有意義。 21 //所以不做任何處理(不保存在系統緩存中) 22 _rqSessionStateNotFound 23 && _sessionStartEventHandler == null 24 // Nothing has been stored in session state 25 && (delayedSessionState || !_rqSessionItems.Dirty) 26 && (delayedSessionState || _rqStaticObjects == null || _rqStaticObjects.NeverAccessed) 27 ) { 28 } 29 //如果會話被丟棄了,即我們在控制器中調用了Session.Abandon(),常用於註銷操作。 30 else if (_rqSessionState.IsAbandoned) { 31 if (_rqSessionStateNotFound) { 32 // The store provider doesn‘t have it, and so we don‘t need to remove it from the store. 33 34 // However, if the store provider supports session expiry, and we have a Session_End in global.asax, 35 // we need to explicitly call Session_End. 36 if (_supportSessionExpiry) { 37 if (delayedSessionState) { 38 InitStateStoreItem(false /*addToContext*/); 39 } 40 _onEndTarget.RaiseSessionOnEnd(ReleaseStateGetSessionID(), _rqItem); 41 } 42 } 43 else { 44 // Remove it from the store because the session is abandoned. 45 _store.RemoveItem(_rqContext, ReleaseStateGetSessionID(), _rqLockId, _rqItem); 46 } 47 } 48 else if (!_rqReadonly || 49 (_rqReadonly && 50 _rqIsNewSession && 51 _sessionStartEventHandler != null && 52 !SessionIDManagerUseCookieless)) { 53 54 // We save it only if there is no error, and if something has changed (unless it‘s a new session) 55 if ( context.Error == null // no error 56 && ( _rqSessionStateNotFound 57 || _rqSessionItems.Dirty // SessionItems has changed. 58 || (_rqStaticObjects != null && !_rqStaticObjects.NeverAccessed) // Static objects have been accessed 59 || _rqItem.Timeout != _rqSessionState.Timeout // Timeout value has changed 60 ) 61 ) { 62 63 if (delayedSessionState) { 64 InitStateStoreItem(false /*addToContext*/); 65 } 66 if (_rqItem.Timeout != _rqSessionState.Timeout) { 67 _rqItem.Timeout = _rqSessionState.Timeout; 68 } 69 //該標識設置為true,說明該模塊設置過session,該標識用於一些優化 70 s_sessionEverSet = true; 71 //為true說明我們已經把當前的session保存進系統緩存中了 72 setItemCalled = true; 73 //將這個會話狀態存儲放入系統緩存中,在這個代碼中,我們看到使用了ReleaseStateGetSessionID()這個方法去獲取session id 74 //有這樣的意思。如果已經有session id了,則直接使用,如果沒有則會創建一個session id,且寫入cookie ,則瀏覽器端就有了這個id 75 //如果這是一個全新的請求,而且也沒有在控制器中也沒有Session.SessionID這樣的訪問,那麽到了此處,session id就是空值 76 _store.SetAndReleaseItemExclusive(_rqContext, ReleaseStateGetSessionID(), _rqItem, _rqLockId, _rqSessionStateNotFound); 77 } 78 else { 79 // Can‘t save it because of various reason. Just release our exclusive lock on it. 80 if (!_rqSessionStateNotFound) { 81 _store.ReleaseItemExclusive(_rqContext, ReleaseStateGetSessionID(), _rqLockId); 82 } 83 } 84 } 85 } 86 //上面是決定是否保存會話狀態存儲的代碼,下面是決定是否將session id保存在cookie中的代碼 87 //有這樣一種情況,在一次全新的請求中,如果我們在控制器中有了Session.SessionID這樣的訪問 88 //那麽,此時會創建一個SessionID且寫入cookie中,但是如果我們沒有向Session中存儲數據 89 //那麽這樣的SessionID繼續保存在cookie中,就是沒有意義的,所以就從cookie中刪除這個id 90 if (_rqAddedCookie && !setItemCalled && context.Response.IsBuffered()) { 91 _idManager.RemoveSessionID(_rqContext); 92 } 93 } 94 finally { 95 RestoreImpersonation(); 96 } 97 98 bool implementsIRequiresSessionState = context.RequiresSessionState; 99 if (HttpRuntime.UseIntegratedPipeline 100 && (context.NotificationContext.CurrentNotification == RequestNotification.ReleaseRequestState) 101 && (s_canSkipEndRequestCall || !implementsIRequiresSessionState)) { 102 context.DisableNotifications(RequestNotification.EndRequest, 0 /*postNotifications*/); 103 _acquireCalled = false; 104 _releaseCalled = false; 105 ResetPerRequestFields(); 106 } 107 }OnReleaseState
上面對於初次的訪問關於session的獲取等已經介紹完畢,而對於非初次訪問(已經想session中存儲過數據),則簡單很多。
因為已經存儲過數據,所以cookie中有session id,系統緩存中數據。
則先從cookie中讀取session id,然後根據此id去緩存中讀取數據,然後返回。
結束語:SessionStateModule模塊已經介紹完畢,這個模塊的主要作用,就是在正式處理請求前,為我們生成存儲數據用的session內部對象,請求處理完畢後,將
存儲數據的對象保存在系統緩存中。這些功能也是這個模塊的部分功能,還有其他很重要的功能。比如用於同步的鎖機制,比如生成sessionid的類等。這些內容 就放在其他文章裏再做介紹。
源碼分析Session的臺前幕後(Asp .Net MVC5)