一、会话状态Session
Session用于服务器端状态管理,使用Session之后,每个客户端都可以将实际的数据保存在服务器上,对于每个客户端的数据,将会生成一个对应的唯一的key(保存在客户端)。客户端与服务器端就是通过这个key来确认客户端的身份,通常这个key为SessionID。
一般情况下,SessionID以Cookie的形式保存在浏览器中,在不使用Cookie的情况下,也可以将这个SessionID嵌入到访问网页的URL中。
二、服务器端Session
在页面对象或者HttpContext对象中,都有一个名为Session的属性,在一次会话中,它们引用的都是同一个对象。
public HttpSessionState Session { get; }
Session对象是HttpSessionState类的实例。Session是保存在服务器端的,对每个登录到网站的用户都有一份,是独有的,而其他用户无法共享。
HttpSessionState来自于HttpModule的SessionStateModule。在每次请求处理过程中,HttpApplication的请求的处理管道中会检查当前请求的处理程序是否实现了接口IRequiresSessionState,如果实现的话,那么SessionStateModule将为这个请求分配HttpSessionState。同时SessionStateModule还负责SessionID的生成、Cookieless会话管理、从外部状态提供程序中检索会话数据以及将数据绑定到请求的调用上下文。
- 对于一般处理程序,默认情况下没有实现IRequiresSessionState接口。所以如果想要在一般处理程序中使用Session,可以通过实现IRequiresSessionState接口来解决这个问题,这个接口是一个标记接口,并没有定义任何内容。
- 对于页面处理程序,可以将页面指令@Page的EnableSessionState属性设置为true,以允许页面可以请求会话状态的写入权限。这是默认的设置。还可以将EnableSessionState属性设置为ReadOnly,此时派生的实际页面类将会实现接口IReadOnlySessionState,在这种情况下,页面可以拥有会话状态的只读权限。
SessionStateModule模块从特定状态提供程序中读取数据。在程序代码中实际上访问的是会话数据在本地内存中的副本,如果其他页面也视图同步访问该会话状态就可能会导致数据冲突。为了避免这种情况,SessionStateModule模块实现了一个读取器/写入器的锁定机制,并对状态值的访问进行排队。对会话状态具有写入权限的页面将保留该会话的写入器锁定,直到请求终止。
如果页面请求设置一个读取器锁定,同一会话中同时处理的其他请求将无法更新会话状态,但是至少可以进行读取。如果页面请求为会话状态设置一个写入锁,那么所有其他页面都被阻止,无论他们是否要读取或写入内容。例如,如果同时有两段程序视图在同一个Session中写入内容,一段程序必须等到另一段程序完成后才能写入。在AJAX程序设计中,必须注意这种情况的发生。
来看一个非常有趣的示例:Asp.net设置session后变单线程执行
新建一个MVC项目,添加一个Controller如下:
public class HomeController : Controller { public ActionResult Index() { //Session["User"] = "张三"; //特别注意这行代码 return View(); } public ActionResult Test1() { Thread.Sleep(5000); return Content("长任务Test1完成"); } public ActionResult Test2() { return Content("短任务Test2完成"); } }
/Home/Index视图代码如下:
<html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Index</title> <script src="/Scripts/jquery-1.4.1.min.js" type="text/javascript"></script> <script type="text/javascript"> $(function() { $("#btn1").click(function() { $.ajax({ url: "/Home/Test1", dataType: "text", success: function(response) { $("#div1").html($("#div1").html() + response + "<br/>"); } }) $.ajax({ url: "/Home/Test2", dataType: "text", success: function(response) { $("#div1").html($("#div1").html() + response + "<br/>"); } }) }) }) </script> </head> <body> <div id="div1" style="width:200px; height:200px; border:1px solid #000;"> </div> <input type="button" id="btn1" value="开始" /> </body> </html>
在刚开始的时候,Index的Session[]那行代码是注释掉的,输出如下:
乍眼一看,这很正常jQuery的AJAX默认是异步执行,那个先执行完就哪个先显示,没问题。
下面,启用那行被注释掉的Session代码,输入如下:
这次点击按钮没有反应,虽然jQuery的AJAX是已经发送出去了,但是Asp.net必须要等到第一个请求执行完毕之后,第二个请求才开始执行。这从google浏览器的请求信息可以看到,两个请求几乎是同时发出去的,是Asp.net使用了Session导致的问题:
特别说明:只有写Session时,Asp.net才会阻塞请求,但是只要你访问过写Session的页面,比如是用Session登录的系统之后的操作(直到Session失效都一直锁定,当然只是SessionID相同的情况)。都会存在这个问题。光读Session不会出现这种情况。
这个问题在开发一些并发的Asp.net功能时需要注意,例如进度条。
在MVC中有一个办法可以解决这个问题,在MVC中,可以为本Controller增加以下特性,但是本Controller都不能修改Session了,只能读取。
[SessionState(System.Web.SessionState.SessionStateBehavior.ReadOnly)]
对于WebForm来说是在aspx顶部的Page后面加上(仅仅加载那个阻塞页面):
EnableSessionState="ReadOnly"
三、客户端的SessionID
对于会话的每个客户端来说,需要在浏览器中保存一个会话的标识,以便在后继的请求中区分不同的会话,这个标识我们成为SessionID。
SessionID是由SessionStateModule创建的,在创建一个新的SessionID之后,SessionStateModule将会触发Session_Start事件,我们可以在Global.asax中处理这个事件。
void Session_Start(object sender, EventArgs e) { // 在新会话启动时运行的代码 }
如果会话超时或被放弃,下次访问应用程序时,SessionID并不会发生改变。即使会话状态过期,会话SessionID也能持续到浏览器会话结束。也就是说,只要浏览器实例相同,就始终使用同一个会话SessionID表示多个会话。
Session_End事件标志着会话的结束,并用于执行终止该会话所需的所有清除代码。但请注意,只有InProc模式支持该事件,也就是说,只有将会话数据存储在Asp.net辅助进程中时才支持该事件。对于要引发Session_End事件来说,必须首先存在会话状态,这意味着必须在改会话状态中存储一些数据,并且必须至少完成一个请求,才会触发这个事件。
void Session_End(object sender, EventArgs e) { // 在会话结束时运行的代码。 // 注意: 只有在 Web.config 文件中的 sessionstate 模式设置为 // InProc 时,才会引发 Session_End 事件。如果会话模式设置为 StateServer // 或 SQLServer,则不会引发该事件。 }
InProc是最常用的也是Asp.net默认的模式,添加到缓存中的会话状态被赋予一个滑动过期策略。服务器将会进行一个倒计时,当倒计时到0的时候,意味着会话过期。滑动过期时间表示如果在一定时间内没有使用,将被删除。在过期之前,处理任何请求的时候,过期时间都将重新设置为会话的过期时间。
例如,如果Session的过期时间是20分钟,那么在过期之前,每次请求都会导致这个事件被重新设置为20分钟。
过期的会话数据将自动被删除。状态会话模块也包含一个删除会话的回调函数。当会话数据被删除的时候,将自动调用删除函数,然后删除函数将引发Session_End事件。但是,如果应用程序没有通过InProc模式来进行会话管理,将永远不会引发结束事件。 SessionID由URL允许的120位字符串组成。默认情况下,SessionID以Cookie的形式发送到客户端保存起来,如果没有设置Cookie的Expires过期时间属性,那么SessionID在关闭浏览器的时候就失效了。
SessionID字符串被发送到浏览器,然后通过以下两种方式之一返回服务器应用程序:
- 使用Cookie。
- 经过修改的URL。
配置会话设置使用sessionState元素的Cookieless特性:
<sessionState cookieless="true" />
cookieless false表示使用Cookie,true表示使用URL。如果使用了URL则路径如:
http://localhost:9090/website/(S(qifjgkvmcnfu8j93ks74ieu7))/SessionID.aspx
四、Session的过期问题
关于Asp.net会话管理,重要的一点是,仅当将第一个项目添加到内存词典中时,会话状态对象的生命周期才开始。如:仅在执行如下代码片段后,才可以认为Asp.net会话开始。
Session["XXX"] = "XXX";
Session词典通常包含Object类型,要向后读取数据,需要将返回的值转换为更具体的类型。
string data = Session["XXX"].ToString();
当将数据保存到Session中时,值会加载到HttpSessionState类包含的特制的词典类中。完成当前处理的请求时,会将词典的内容加载到状态提供程序中。 如果会话超时或被放弃,下次访问无状态应用程序时,其会话ID不会变,即使会话状态过期,SessionID也能持续到浏览器会话结束。也就是说,只要浏览器实例相同,就始终使用同一个会话ID表示多个会话。
Session_OnEnd事件标志着会话的结束,并用于执行终止该会话所需的所有清除代码。但要注意,只有InProc模式支持该事件,也就是说,只有将会话数据存储在Asp.net辅助进程中时才支持该事件。对于要引发的Session_OnEnd事件来说,必须首先存在会话状态,这意味着必须在该会话状态中存储一些数据,并且必须至少完成一个请求。
五、sessionState配置节
sessionState配置节主要用于配置Session方面的信息,部分节点如下:
配置节点 | 说明 |
mode |
mode 取值如下,默认为 InProc 。 Custom :会话状态正在使用自定义数据存储来存储会话状态信息。 InProc :会话状态正在处理 ASP.NET 辅助进程。 Off :会话状态被禁用。 SQLServer :会话状态正在使用进程外 SQL Server 数据库存储状态信息。 StateServer : 会话状态将使用进程外 ASP.NET 状态服务来存储状态信息。 |
cookieName |
指定存储会话标识符的 Cookie 的名称(就是客户端的Key),默认值为 "ASP.NET_SessionId"。 |
cookieless |
当网站用到AJAX时,该属性仅能够使用UseCookies。 AutoDetect :Asp.net自动判断,如果浏览器支持Cookie,则使用 Cookie 来传输;否则,使用Uri传输。 如果浏览器支持Cookie,但禁用了Cookie,则仍然使用 Cookie。 UseCookies :无论浏览器或设备是否支持 Cookie,都使用 Cookie 来保留用户数据。 UseDeviceProfile :ASP.NET 根据 HttpBrowserCapabilities自动判断。 如果 HttpBrowserCapabilities 设置指示浏览器或设备支持 Cookie,将使用 Cookie;否则,将在查询字符串中使用一个标识符。 UseUri :无论浏览器或设备是否支持Cookie,都使用UseUri来传输SessionID |
customProvider |
自定义会话状态提供程序的名称 |
timeout |
指定一个会话多长时间空闲会失效。 对于进程内和状态服务器模式,timeout 特性不能设置为大于 525,600 分钟(1 年)的值。 |
compressionEnabled |
进程外的Session是否先压缩后传输。如是否压缩后再发送到SQLServer |
http://msdn.microsoft.com/zh-cn/library/h6bb9cz9.aspx
StateServer的Session Mode模式的StateServer会将Session数据存在于aspnet_state.exe里面。与w3wp.exe/aspnet_wp.exe进程相互独立。如果存放于InProc,则是存放于Asp.net进程内。
六、HttpSessionState类
属性 | 说明 |
CodePage | 获取或设置当前会话的字符集标识符 |
Contents | 获取对当前会话状态对象的引用 |
CookieMode | 获取一个值,该值指示是否为无 Cookie 会话配置应用程序 |
Count | 获取会话状态集合中的项数 |
IsCookieless | 获取一个值,该值指示会话 ID 是嵌入在 URL 中还是存储在 HTTP Cookie 中。 如果会话嵌入在 URL 中,则为 true;否则,为 false |
IsNewSession | 获取一个值,该值指示会话是否是与当前请求一起创建的。 如果会话是与当前请求一起创建的,则为 true;否则,为 false |
IsReadOnly | 获取一个值,该值指示会话是否为只读 |
IsSynchronized | 获取一个值,该值指示对会话状态值的集合的访问是否是同步(线程安全)的 |
Item | 获取或设置个别会话值Session[] |
Keys | 获取存储在会话状态集合中所有值的键的集合 |
LCID | 获取或设置当前会话的区域设置标识符 (LCID) |
Mode | 获取当前会话状态模式 |
SessionID | 获取会话的唯一标识符 |
StaticObjects | 获取由 ASP.NET 应用程序文件 Global.asax 中的 <object Runat="Server" Scope="Session"/> 标记声明的对象的集合 |
SyncRoot | 获取一个对象,该对象可用于同步对会话状态值的集合的访问 |
Timeout | 获取并设置在会话状态提供程序终止会话之前各请求之间所允许的时间(以分钟为单位) |
方法:
方法 | 说明 |
Abandon | 取消当前会话 |
Add | 向会话状态集合添加一个新项 |
Clear | 从会话状态集合中移除所有的键和值 |
CopyTo | 将会话状态值的集合复制到一维数组中(从数组的指定索引处开始) |
GetEnumerator | 返回一个枚举数,可用来读取当前会话中所有会话状态的变量名称 |
Remove | 删除会话状态集合中的项 |
RemoveAll | 从会话状态集合中移除所有的键和值 |
RemoveAt | 删除会话状态集合中指定索引处的项 |
一旦调用 HttpSessionState.Abandon方法,当前会话不再有效,同时会启动新的会话。Abandon 使 SessionStateModule.End 事件被引发。发送下一次请求后将引发新的 SessionStateModule.Start 事件。如果要用Session.Abandon();最好放在一个独立的页面。
代码示例:
protected void Page_Load(object sender, EventArgs e) { Session.Add("username","admin"); Session.Add("password","123456"); Response.Write(Session.Count); //输出2 Response.Write(Session.IsCookieless); //输出 False 表示是嵌入在cookie里 Response.Write(Session.CodePage); //输出 65001 Response.Write(Session.Contents["username"]); //输出 username Response.Write("<br/>"); Response.Write(Session.CookieMode); //UserCookies Response.Write(Session.IsNewSession); //True Response.Write(Session.IsReadOnly); //False Response.Write(Session.IsSynchronized); //False foreach (string str in Session.Keys) { Response.Write(str + ":" + Session[str]); //username:admin password:123456 } Response.Write("<br/>"); Response.Write(Session.LCID); //2052 Response.Write(Session.Mode); //InProc Response.Write(Session.SessionID); //udtst2kwyltquymd40alduyw Response.Write("<br/>"); Response.Write(Session.StaticObjects.Count); //0 Response.Write(Session.SyncRoot); // Response.Write(Session.Timeout); //20 string[] strArr = new string[Session.Count]; Session.CopyTo(strArr,0); foreach (string str in strArr) { Response.Write(str); //输出 username password 输出由key组成的字符串数组 } Session.Remove("password"); Response.Write(Session.Count); //输出1 可以看到少了一个 Session.RemoveAt(0); //按索引号删除一个Session对象 Response.Write(Session.Count); //输出0 可以看到又少了一个 Session.Add("one","刘备"); Session.Abandon(); Response.Write(Session["one"]); Response.Write("<a href=‘/new.aspx‘>新页面</a>"); //由于调用了ababdon()方法,因此点击此页面过去,将获取不到Session["one"] }
new.aspx后台代码:
protected void Page_Load(object sender, EventArgs e) { Response.Write(Session["one"]); //虽然输出cookie中的值,但是不会输出任何值 }