1. 程式人生 > >源碼分析Session的臺前幕後(Asp .Net MVC5)

源碼分析Session的臺前幕後(Asp .Net MVC5)

加工 splay text gif mic close bsp nag pre

  在這篇文章裏,我們從源代碼的角度重點分析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                     }
14     
CompleteAcquireState

該函數將把返回的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)