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 mode="InProc" cookieName="MySessionId"></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; }
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請求是對該網路應用程式的第一次訪問,在成功載入了執行時間後,會透過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 mode="StateServer" stateConnectionString="tcpip=127.0.0.1:42424" cookieless="false" timeout="20" />
在这个例子中,状态服务在当前机器的42424端口(默认端口)运行。要在服务器上改变端口和开启远程服务器的该功能,可编辑HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters注册表项中的Port值和AllowRemoteConnection修改成1。
显然,使用状态服务的优点在于进程隔离,并可在多站点中共享。 使用这种模式,会话状态的存储将不依赖于iis进程的失败或者重启,然而,一旦状态服务中止,所有会话数据都会丢失(这个问题redis不会存在,重新了数据不会丢失)。
这里提供一段bat文件帮助修改注册表,可以复制保存为.bat文件执行
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters" /v "AllowRemoteConnection" /t REG_DWORD /d 1 /f reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\aspnet_state\Parameters" /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 name="key">SessionId标识符</param> /// <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(); } } }
下面是实现类似在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門教學專欄。
以上是redis實作session共享的方法介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!