?
Session實現原理
session和Cookie是我們做web開發中常用到的兩個對象,它們之間會不會有聯系呢?
php中文網學習專題:php session (包含圖文、視頻、案例)
Cookie是什么??
Cookie 是一小段文本信息,伴隨著用戶請求和頁面在 Web 服務器和瀏覽器之間傳遞。Cookie 包含每次用戶訪問站點時 Web 應用程序都可以讀取的信息。
注:Cookie 會隨每次http請求一起被傳遞服務器端,排除JS,css,image等靜態文件,這個過程可以從fiddler或者ie自帶的網絡監控里面分析到,考慮性能的化可以從盡量減少cookie著手
Cookie寫入瀏覽器的過程:我們可以使用如下代碼在Asp.net項目中寫一個Cookie 并發送到客戶端的瀏覽器(為了簡單我沒有設置其它屬性)。
HttpCookie?cookie?=?new?HttpCookie("redisSessionId",?"string?value");Response.Cookies.Add(cookie);
我們可以看到在服務器寫的cookie,會通過響應頭Set-Cookie的方式寫入到瀏覽器。
Session是什么??
Session我們可以使用它來方便地在服務端保存一些與會話相關的信息。比如常見的登錄信息。
Session實現原理??
HTTP協議是無狀態的,對于一個瀏覽器發出的多次請求,WEB服務器無法區分 是不是來源于同一個瀏覽器。所以服務器為了區分這個過程會通過一個sessionid來區分請求,而這個sessionid是怎么發送給服務端的呢?
前面說了cookie會隨每次請求發送到服務端,并且cookie相對用戶是不可見的,用來保存這個sessionid是最好不過了,我們通過下面過程來驗證一下。
Session["UserId"]?=?123;
通過上圖再次驗證了session和cookie的關系,服務器產生了一次設置cookie的操作,這里的sessionid就是用來區分瀏覽器的。為了實驗是區分瀏覽器的,可以實驗在IE下進行登錄,然后在用chrome打開相同頁面,你會發現在chrome還是需要你登錄的,原因是chrome這時沒有sessionid。httpOnly是表示這個cookie是不會在瀏覽器端通過js進行操作的,防止人為串改sessionid。
asp.net默認的sessionid的鍵值是ASP.NET_SessionId,可以在web.config里面修改這個默認配置
<sessionstate></sessionstate>
服務器端Session讀取
服務器端是怎么讀取session的值呢?,Session[“鍵值”]。那么問題來了,為什么在Defaule.aspx.cs文件里可以獲取到這個Session對象,這個Session對象又是什么時候被初始化的呢。
為了弄清楚這個問題,我們可以通過轉到定義的方式來查看。
System.Web.ui.Page?->HttpSessionState(Session)
protected?internal?override?HttpContext?Context?{ [System.Runtime.TargetedPatchingOptOut("Performance?critical?to?inline?across?NGen?image?boundaries")] get?{ ???????if?(_context?==?null)?{ ???????????_context?=?HttpContext.Current; ???????} ???????return?_context; ????} ?} ?public?virtual?HttpSessionState?Session?{ ????????get?{ ????????????if?(!_sessionRetrieved)?{ ????????????????/*?try?just?once?to?retrieve?it?*/ ????????????????_sessionRetrieved?=?true; ????????????????try?{ ????????????????????_session?=?Context.Session; ????????????????} ????????????????catch?{ ????????????????????//??Just?ignore?exceptions,?return?null. ????????????????} ????????????} ????????????if?(_session?==?null)?{ ????????????????throw?new?HttpException(SR.GetString(SR.Session_not_enabled)); ????????????} ????????????return?_session; ????????} ????}
上面這一段是Page對象初始化Session對象的,可以看到Session的值來源于HttpContext.Current,而HttpContext.Current又是什么時候被初始化的呢,我們接著往下看。
public?sealed?class?HttpContext?:?IServiceProvider,?IPrincipalContainer ????{ ????????internal?static?readonly?Assembly?SystemWebAssembly?=?typeof(HttpContext).Assembly; ????????private?static?volatile?bool?s_eurlSet; ????????private?static?string?s_eurl; ????????private?IHttpAsyncHandler??_asyncAppHandler;???//?application?as?handler?(not?always?HttpApplication) ????????private?AsyncPreloadModeFlags?_asyncPreloadModeFlags; ????????private?bool???????????????_asyncPreloadModeFlagsSet; ????????private?HttpApplication????_appInstance; ????????private?IHttpHandler???????_handler; ????????[DoNotReset] ????????private?HttpRequest????????_request; ????????private?HttpResponse???????_response; ????????private?HttpServerUtility??_server; ????????private?Stack??????????????_traceContextStack; ????????private?TraceContext???????_topTraceContext; ????????[DoNotReset] ????????private?Hashtable??????????_items; ????????private?ArrayList??????????_errors; ????????private?Exception??????????_tempError; ????????private?bool???????????????_errorCleared; ????????[DoNotReset] ????????private?IPrincipalContainer?_principalContainer; ????????[DoNotReset] ????????internal?ProfileBase???????_Profile; ????????[DoNotReset] ????????private?DateTime???????????_utcTimestamp; ????????[DoNotReset] ????????private?HttpWorkerRequest??_wr; ????????private?VirtualPath????????_configurationPath; ????????internal?bool??????????????_skipAuthorization; ????????[DoNotReset] ????????private?CultureInfo????????_dynamicCulture; ????????[DoNotReset] ????????private?CultureInfo????????_dynamicUICulture; ????????private?int????????????????_serverExecuteDepth; ????????private?Stack??????????????_handlerStack; ????????private?bool???????????????_preventPostback; ????????private?bool???????????????_runtimeErrorReported; ????????private?PageInstrumentationService?_pageInstrumentationService?=?null; ????????private?ReadOnlyCollection<string>?_webSocketRequestedProtocols; }</string>
HttpContext包含了我們常用的Request,Response等對象。HttpContext得從ASP.NET管道說起,以iis 6.0為例,在工作進程w3wp.exe中,利用Aspnet_ispai.dll加載.NET運行時(如果.NET運行時尚未加載)。
IIS 6.0引入了應用程序池的概念,一個工作進程對應著一個應用程序池。一個應用程序池可以承載一個或多個Web應用,每個Web應用映射到一個IIS虛擬目錄。與IIS 5.x一樣,每一個Web應用運行在各自的應用程序域中。
如果HTTP.SYS接收到的HTTP請求是對該Web應用的第一次訪問,在成功加載了運行時后,會通過AppDomainFactory為該Web應用創建一個應用程序域(AppDomain)。
隨后,一個特殊的運行時IsapiRuntime被加載。IsapiRuntime定義在程序集System.Web中,對應的命名空間為System.Web.Hosting。
IsapiRuntime會接管該HTTP請求。IsapiRuntime會首先創建一個IsapiWorkerRequest對象,用于封裝當前的HTTP請求,并將該IsapiWorkerRequest對象傳遞給ASP.NET運行時:HttpRuntime,從此時起,HTTP請求正式進入了ASP.NET管道。
根據IsapiWorkerRequest對象,HttpRuntime會創建用于表示當前HTTP請求的上下文(Context)對象:HttpContext。
至此相信大家對Session初始化過程,session和cookie的關系已經很了解了吧,下面開始進行Session共享實現方案。?
Session共享實現方案
一.StateServer方式
這種是asp.net提供的一種方式,還有一種是sqlserver方式(不一定程序使用的是SQLServer數據庫,所以通用性不高,這里就不介紹了)。也就是將會話數據存儲到單獨的內存緩沖區中,再由單獨一臺機器上運行的windows服務來控制這個緩沖區。
狀態服務全稱是“ASP.NET State Service ”(aspnet_state.exe)。它由Web.config文件中的stateConnectionString屬性來配置。該屬性指定了服務所在的服務器,以及要監視的端口。
<sessionstate></sessionstate>
在這個例子中,狀態服務在當前機器的42424端口(默認端口)運行。要在服務器上改變端口和開啟遠程服務器的該功能,可編輯HKEY_LOCAL_MACHINESYSTEMCurrentControlSetservicesaspnet_stateParameters注冊表項中的Port值和AllowRemoteConnection修改成1。
?顯然,使用狀態服務的優點在于進程隔離,并可在多站點中共享。 使用這種模式,會話狀態的存儲將不依賴于iis進程的失敗或者重啟,然而,一旦狀態服務中止,所有會話數據都會丟失(這個問題redis不會存在,重新了數據不會丟失)。
這里提供一段bat文件幫助修改注冊表,可以復制保存為.bat文件執行
reg?add?"HKEY_LOCAL_MACHINESYSTEMCurrentControlSetservicesaspnet_stateParameters"?/v?"AllowRemoteConnection"?/t?REG_DWORD??/d?1?/f reg?add?"HKEY_LOCAL_MACHINESYSTEMCurrentControlSetservicesaspnet_stateParameters"?/v?"Port"?/t?REG_DWORD??/d?42424?/f net?stop?aspnet_state net?start?aspnet_state pause
完成這些配置以后還是不能實現共享,雖然站點間的SessionId是一致的,但只有一個站點能夠讀取的到值,而其它站點讀取不到。下面給出解決方案,在Global文件里面添加下面代碼
public?override?void?Init() ?{ ??????base.Init(); ??????foreach?(string?moduleName?in?this.Modules) ??????{ ???????????string?appName?=?"APPNAME"; ???????????IHttpModule?module?=?this.Modules[moduleName]; ???????????SessionStateModule?ssm?=?module?as?SessionStateModule; ???????????if?(ssm?!=?null) ???????????{ ????????????????FieldInfo?storeInfo?=?typeof(SessionStateModule).GetField("_store",?BindingFlags.Instance?|?BindingFlags.NonPublic); ????????????????FieldInfo?configMode?=?typeof(SessionStateModule).GetField("s_configMode",?BindingFlags.Instance?|?BindingFlags.NonPublic?|?BindingFlags.Static); ????????????????SessionStateMode?mode?=?(SessionStateMode)configMode.GetValue(ssm); ????????????????if?(mode?==?SessionStateMode.StateServer) ????????????????{ ????????????????????SessionStateStoreProviderBase?store?=?(SessionStateStoreProviderBase)storeInfo.GetValue(ssm); ????????????????????if?(store?==?null)//In?IIS7?Integrated?mode,?module.Init()?is?called?later ????????????????????{ ????????????????????????FieldInfo?runtimeInfo?=?typeof(HttpRuntime).GetField("_theRuntime",?BindingFlags.Static?|?BindingFlags.NonPublic); ????????????????????????HttpRuntime?theRuntime?=?(HttpRuntime)runtimeInfo.GetValue(null); ????????????????????????FieldInfo?appNameInfo?=?typeof(HttpRuntime).GetField("_appDomainAppId",?BindingFlags.Instance?|?BindingFlags.NonPublic); ????????????????????????appNameInfo.SetValue(theRuntime,?appName); ????????????????????} ????????????????????else ????????????????????{ ???????????????????????Type?storeType?=?store.GetType(); ???????????????????????if?(storeType.Name.Equals("OutOfProcSessionStateStore")) ???????????????????????{ ???????????????????????????FieldInfo?uribaseInfo?=?storeType.GetField("s_uribase",?BindingFlags.Static?|?BindingFlags.NonPublic); ???????????????????????????uribaseInfo.SetValue(storeType,?appName); ???????????????????????????object?obj?=?null; ???????????????????????????uribaseInfo.GetValue(obj); ????????????????????????} ????????????????????} ????????????????} ????????????????break; ????????????} ????????} ??}
二.redis實現session共享
下面我們將使用redis來實現共享,首先要弄清楚session的幾個關鍵點,過期時間,SessionId,一個SessionId里面會存在多組key/value數據。基于這個特性我將采用Hash結構來存儲,看看代碼實現。用到了上一篇提供的RedisBase幫助類。
using?System; using?System.Collections.Generic; using?System.Linq; using?System.Web; using?System.Web.SessionState; using?ServiceStack.Redis; using?Com.Redis; namespace?ResidSessionDemo.RedisDemo { ????public?class?RedisSession ????{ ????????private?HttpContext?context; ????????public?RedisSession(HttpContext?context,?bool?IsReadOnly,?int?Timeout) ????????{ ????????????this.context?=?context; ????????????this.IsReadOnly?=?IsReadOnly; ????????????this.Timeout?=?Timeout; ????????????//更新緩存過期時間 ????????????RedisBase.Hash_SetExpire(SessionID,?DateTime.Now.AddMinutes(Timeout)); ????????} ????????///?<summary> ????????///?SessionId標識符 ????????///?</summary> ????????public?static?string?SessionName?=?"Redis_SessionId"; ????????// ????????//?摘要: ????????//?????獲取會話狀態集合中的項數。 ????????// ????????//?返回結果: ????????//?????集合中的項數。 ????????public?int?Count ????????{ ????????????get ????????????{ ????????????????return?RedisBase.Hash_GetCount(SessionID); ????????????} ????????} ????????// ????????//?摘要: ????????//?????獲取一個值,該值指示會話是否為只讀。 ????????// ????????//?返回結果: ????????//?????如果會話為只讀,則為?true;否則為?false。 ????????public?bool?IsReadOnly?{?get;?set;?} ????????// ????????//?摘要: ????????//?????獲取會話的唯一標識符。 ????????// ????????//?返回結果: ????????//?????唯一會話標識符。 ????????public?string?SessionID ????????{ ????????????get ????????????{ ????????????????return?GetSessionID(); ????????????} ????????} ????????// ????????//?摘要: ????????//?????獲取并設置在會話狀態提供程序終止會話之前各請求之間所允許的時間(以分鐘為單位)。 ????????// ????????//?返回結果: ????????//?????超時期限(以分鐘為單位)。 ????????public?int?Timeout?{?get;?set;?} ????????///?<summary> ????????///?獲取SessionID ????????///?</summary> ????????///?<param>SessionId標識符 ????????///?<returns>HttpCookie值</returns> ????????private?string?GetSessionID() ????????{ ????????????HttpCookie?cookie?=?context.Request.Cookies.Get(SessionName); ????????????if?(cookie?==?null?||?string.IsNullOrEmpty(cookie.Value)) ????????????{ ????????????????string?newSessionID?=?Guid.NewGuid().ToString(); ????????????????HttpCookie?newCookie?=?new?HttpCookie(SessionName,?newSessionID); ????????????????newCookie.HttpOnly?=?IsReadOnly; ????????????????newCookie.Expires?=?DateTime.Now.AddMinutes(Timeout); ????????????????context.Response.Cookies.Add(newCookie); ????????????????return?"Session_"+newSessionID; ????????????} ????????????else ????????????{ ????????????????return?"Session_"+cookie.Value; ????????????} ????????} ????????// ????????//?摘要: ????????//?????按名稱獲取或設置會話值。 ????????// ????????//?參數: ????????//???name: ????????//?????會話值的鍵名。 ????????// ????????//?返回結果: ????????//?????具有指定名稱的會話狀態值;如果該項不存在,則為?null。 ????????public?object?this[string?name] ????????{ ????????????get ????????????{ ????????????????return?RedisBase.Hash_Get<object>(SessionID,?name); ????????????} ????????????set ????????????{ ????????????????RedisBase.Hash_Set<object>(SessionID,?name,?value); ????????????} ????????} ????????//?摘要: ????????//?????判斷會話中是否存在指定key ????????// ????????//?參數: ????????//???name: ????????//?????鍵值 ????????// ????????public?bool?IsExistKey(string?name) ????????{ ????????????return?RedisBase.Hash_Exist<object>(SessionID,?name); ????????} ????????// ????????//?摘要: ????????//?????向會話狀態集合添加一個新項。 ????????// ????????//?參數: ????????//???name: ????????//?????要添加到會話狀態集合的項的名稱。 ????????// ????????//???value: ????????//?????要添加到會話狀態集合的項的值。 ????????public?void?Add(string?name,?object?value) ????????{ ????????????RedisBase.Hash_Set<object>(SessionID,?name,?value); ????????} ????????// ????????//?摘要: ????????//?????從會話狀態集合中移除所有的鍵和值。 ????????public?void?Clear() ????????{ ????????????RedisBase.Hash_Remove(SessionID); ????????} ????????// ????????//?摘要: ????????//?????刪除會話狀態集合中的項。 ????????// ????????//?參數: ????????//???name: ????????//?????要從會話狀態集合中刪除的項的名稱。 ????????public?void?Remove(string?name) ????????{ ????????????RedisBase.Hash_Remove(SessionID,name); ????????} ????????// ????????//?摘要: ????????//?????從會話狀態集合中移除所有的鍵和值。 ????????public?void?RemoveAll() ????????{ ????????????Clear(); ????????} ????} }</object></object></object></object>
下面是實現類似在cs文件中能直接使用Session[“UserId”]的方式,我的MyPage類繼承Page實現了自己的邏輯主要做了兩件事? 1:初始化RedisSession? 2:實現統一登錄認證,OnPreInit方法里面判斷用戶是否登錄,如果沒有登錄了則跳轉到登陸界面
using?System; using?System.Collections.Generic; using?System.Linq; using?System.Web; using?System.Web.UI; namespace?ResidSessionDemo.RedisDemo { ????///?<summary> ????///?自定義Page?實現以下功能 ????///?1.初始化RedisSession ????///?2.實現頁面登錄驗證,繼承此類,則可以實現所有頁面的登錄驗證 ????///?</summary> ????public?class?MyPage:Page ????{ ????????private?RedisSession?redisSession; ????????///?<summary> ????????///?RedisSession ????????///?</summary> ????????public?RedisSession?RedisSession ????????{ ????????????get ????????????{ ????????????????if?(redisSession?==?null) ????????????????{ ????????????????????redisSession?=?new?RedisSession(Context,?true,?20); ????????????????} ????????????????return?redisSession; ????????????} ????????} ????????protected?override?void?OnPreInit(EventArgs?e) ????????{ ????????????base.OnPreInit(e); ????????????//判斷用戶是否已經登錄,如果未登錄,則跳轉到登錄界面 ????????????if?(!RedisSession.IsExistKey("UserCode")) ????????????{ ????????????????Response.Redirect("Login.aspx"); ????????????} ????????} ????} }
我們來看看default.aspx.cs是如何使用RedisSession的,至此我們實現了和Asp.netSession一模一樣的功能和使用方式。
RedisSession.Remove("UserCode");
相比StateServer,RedisSession具有以下優點
1、redis服務器重啟不會丟失數據? 2.可以使用redis的讀寫分離個集群功能更加高效讀寫數據 ?
測試效果,使用nginx和iis部署兩個站點做負載均衡,iis1地址127.0.0.1:8002 iis2地址127.0.0.1:9000? nginx代理服務地址127.0.0.1:8003,不懂如何配置的可以去閱讀我的nginx+iis實現負載均衡這篇文章。我們來看一下測試結果。
訪問127.0.0.1:8003 需要進行登錄?? 用戶名為admin? 密碼為123
登錄成功以后,重點關注端口號信息
刷新頁面,重點關注端口號信息
可以嘗試直接訪問iis1地址127.0.0.1:8002 iis2地址127.0.0.1:9000?這兩個站點,你會發現都不需要登錄了。至此我們的redis實現session功能算是大功告成了。
問題拓展
使用redis實現session告一段落,下面留個問題討論一下方案。微信開發提供了很多接口,參考下面截圖,可以看到獲取access_token接口每日最多調用2000次,現在大公司提供的很多接口針對不對級別的用戶接口訪問次數限制都是不一樣的,至于做這個限制的原因應該是防止惡意攻擊和流量限制之類的。
那么我的問題是怎么實現這個接口調用次數限制功能。大家可以發揮想象力參與討論哦,或許你也會碰到這個問題。
先說下我知道的兩種方案:
1、使用流量整形中的令牌桶算法,大小固定的令牌桶可自行以恒定的速率源源不斷地產生令牌。如果令牌不被消耗,或者被消耗的速度小于產生的速度,令牌就會不斷地增多,直到把桶填滿。后面再產生的令牌就會從桶中溢出。最后桶中可以保存的最大令牌數永遠不會超過桶的大小。
說淺顯點:比如上面的獲取access_token接口,一天2000次的頻率,即1次/分鐘。我們令牌桶容量為2000,可以使用redis 最簡單的key/value來存儲 ,key為用戶id,value為整形存儲還可使用次數,然后使用一個定時器1分鐘調用client.Incr(key) 實現次數自增;用戶每訪問一次該接口,相應的client.Decr(key)來減少使用次數。
但是這里存在一個性能問題,這僅僅是針對一個用戶來說,假設有10萬個用戶,怎么使用定時器來實現這個自增操作呢,難道是循環10萬次分別調用client.Incr(key)嗎?這一點沒有考慮清楚。
2、直接用戶訪問一次 先進行總次數判斷,符合條件再就進行一次自增
更多redis知識請關注入redis門教程欄目。
?