17.6 会话状态

会话状态((Sssion)是ASP.NET状态管理技术中的一种,它将一个有限时间窗口内来自同一浏览器的请求标识为一个会话,并在该会话持续期间保留变量的值。默认情况下,将为所有ASP.NET应用程序启用ASP.NET会话状态。

可以在会话状态中存储会话特定的值和对象,该会话状态对象将由服务器来进行管理并可用于浏览器或客户端设备。存储在会话状态变量中的理想数据是特定于单独会话的短期的、敏感的数据。例如,可以把已登录用户的用户名放在Session中,这样就能通过判断Session中的某个Key来判断用户是否登录等。因此,可以使用会话状态来完成以下任务:

1)唯一标识浏览器或客户端设备请求,并将这些请求映射到服务器上的单独会话实例。

2)在服务器上存储特定于会话的数据,以用于同一个会话内的多个浏览器或客户端设备请求。

3)引发适当的会话管理事件。此外,可以利用这些事件编写应用程序代码。

还需要说明的是,Session对于每一个客户端(或者说是浏览器实例)是“人手一份”,用户首次与Web服务器建立连接的时候,服务器会给用户分发一个SessionID作为标识。SessionID是一个由24个字符组成的随机字符串。用户每次提交页面,浏览器都会把这个SessionID包含在HTTP头中提交给Web服务器,这样Web服务器就能区分当前请求页面的是哪一个客户端。

17.6.1 会话变量

会话变量存储在通过HttpContext的Session属性公开的SessionStateItemCollection对象中。在ASP.NET页中,当前会话变量将通过Page对象的Session属性公开。会话变量集合按变量名称或整数索引来进行索引。可以通过按照名称引用会话变量来创建会话变量,而无须声明会话变量或将会话变量显式添加到集合中。如下面的代码所示:


//保存会话状态中的值

Session["book"]="易学C#";

//读取会话状态中的值

string book=Session["book"].ToString();

Response.Write(book);


为了保持良好的代码写作习惯,在每次读取Session的值以前请务必先判断Session是否为空,否则很有可能出现“未将对象引用设置到对象的实例”的异常。因此,应该将上面的代码改写如下:


Session["book"]="易学C#";

if(Session["book"]!=null)

{

string book=Session["book"].ToString();

Response.Write(book);

}


这里还需要注意的是,从Session中读出的数据都是object类型的。因此,需要进行类型转化后才能使用。

除了可以在会话变量中里存储一些简单数据之外,也可以在会话变量中存储一些复杂的数据类型,如数据实体、DataSet等。如下面的代码所示:


DataSet ds=GetDataSet();

Session["book"]=ds;

DataSet book=Session["book"]as DataSet;


17.6.2 会话标识符

在本节的开头就已经阐述过,会话由一个唯一标识符标识,可以使用SessionID属性读取此标识符。为ASP.NET应用程序启用会话状态时,将检查应用程序中每个页面请求是否有浏览器发送的SessionID值。如果未提供任何SessionID值,则ASP.NET将启动一个新会话,并将该会话的SessionID值随响应一起发送到浏览器。

只要一直使用相同的SessionID值来发送请求,会话就被视为活动的。如果特定会话的请求间隔超过指定的超时值(以分钟为单位),则该会话被视为已过期。使用过期的SessionID值发送的请求将生成一个新的会话。

默认情况下,SessionID值存储在Cookie中。但也可以将应用程序配置为在“无Cookie”会话的URL中存储SessionID值。

通过在Web.config配置文件的sessionState节中将cookieless特性设置为true,可以指定不应将会话标识符存储在Cookie中。如下面的代码所示:


<configuration>

<system.web>

<sessionState cookieless="true"

regenerateExpiredSessionId="true"/>

</system.web>

</configuration>


ASP. NET通过自动在页的URL中插入唯一的会话ID来保持无Cookie会话状态。其中,会话ID嵌入在URL中应用程序名称后的斜杠之后,在其余所有文件或虚拟目录标识符之前。这使ASP.NET可以在使用请求中的SessionStateModule之前解析应用程序的名称。例如,下面的URL已被ASP.NET修改,以包含唯一的会话ID 451is2dmzapr0suh5u0uon5z:


http://localhost:2298/(S(451is2dmzapr0suh5u0uon5z))/Form1.aspx


当ASP.NET向浏览器发送页时,ASP.NET将修改页中任何使用相对于应用程序的路径的链接,在链接中嵌入一个会话ID值。只要用户单击已按这种方式修改的链接,即可保持会话状态。但是,如果客户端重新写入应用程序提供的URL, ASP.NET将不能解析此会话ID,也不能将请求与现有的会话相关联。在这种情况下,将为请求启动一个新的会话。

如果使用已过期的会话ID发起一个请求,将使用该请求提供的SessionID值启动一个新的会话。当包含无Cookie SessionID值的链接由多个浏览器使用时,这会导致无意中共享会话。这时候,可以通过将应用程序配置为不回收会话标识符来减少共享会话数据的机会。为此,可以将sessionState配置元素的regenerateExpiredSessionId特性设置为true。这将在使用已过期的会话ID发起无Cookie会话请求时,生成一个新的会话ID。

这里需要特别说明的是,如果通过使用HTTP POST方法发起已使用已过期会话ID发起的请求,则当regenerateExpiredSessionId为true时,将丢失发送的所有数据。这是因为ASP.NET会执行重定向,以确保浏览器在URL中具有新的会话标识符。

注意 无论作为Cookie还是作为URL的一部分,SessionID值都以明文的形式发送。恶意用户通过获取SessionID值并将其包含在对服务器的请求中,可以访问另一位用户的会话。如果将敏感信息存储在会话状态中,建议使用SSL来加密浏览器和服务器之间包含SessionID值的任何通信。

17.6.3 会话状态模式

ASP. NET会话状态支持若干用于会话数据的存储选项,每个选项都由SessionStateMode枚举中的一个值标识。下面的几项描述了可用的会话状态模式:

1)InProc模式将会话状态存储在Web服务器上的内存中,这是默认值。

2)StateServer模式将会话状态存储在一个名为ASP.NET状态服务的单独进程中。这确保了在重新启动Web应用程序时会保留会话状态,并让会话状态可用于网络场中的多个Web服务器。

3)SQLServer模式将会话状态存储到一个SQL Server数据库中。这确保了在重新启动Web应用程序时会保留会话状态,并让会话状态可用于网络场中的多个Web服务器。

4)Custom模式允许你指定自定义存储提供程序。

5)Off模式禁用会话状态。

对于会话模式的设置,通过在应用程序的Web.config文件中为sessionState元素的mode特性分配一个SessionStateMode枚举值,可以指定要让ASP.NET会话状态使用的模式。除了InProc和Off之外,其他模式都需要附加参数。

1.把Session存储在独立的进程中

StateServer模式将会话状态存储在一个称为ASP.NET状态服务的进程中,该进程是独立于ASP.NET辅助进程或IIS应用程序池的单独进程。使用此模式可以确保在重新启动Web应用程序时保留会话状态,并使会话状态可用于网络场中的多个Web服务器。

若要使用StateServer模式,必须首先确保ASP.NET状态服务运行在用于存储会话的服务器上。可以通过打开Windows服务,找到ASP.NET状态服务一项,右击“服务”,并选择“启动”来进行设置,如图17-4所示。

figure_0612_0488

图 17-4 打开ASP.NET状态服务

如果已经决定使用状态服务来存储Session,别忘记修改服务为自启动。这样,在操作系统重启后服务也能自行启动,以免忘记启动服务而造成网站Session不能使用。

接下来,只需要对Web.config配置文件做相应配置就可以了。下面的示例演示了StateServer模式的一种配置设置,其中会话状态存储在本地计算机上:


<configuration>

<system.web>

<sessionState mode="StateServer"

stateConnectionString="tcpip=127.0.0.1:42424"

cookieless="false"

timeout="20"/>

</system.web>

</configuration>


需要说明的是,如果要在网络场中使用StateServer模式,则必须在Web配置文件的machineKey元素中为网络场中的所有应用程序指定相同的加密密钥。

2.把Session存储在SQL Server数据库中

如果选择使用SQL Server模式将会话状态存储到一个SQL Server数据库中。那么,首先必须检查你的SQL Server数据库中有没有安装ASPState数据库。如果没有安装,则需要使用aspnet_regsql.exe(在“C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\”目录下)工具进行配置,如图17-5所示。

如图17-5所示,在aspnet_regsql.exe工具的配置命令((apnet_regsql-S localhost-U sa-P mawei-ssadd-sstype p)中:

1)-S:后跟数据源,如localhost。

2)-U:后跟数据库用户名,如sa。

figure_0613_0489

图 17-5 运行aspnet_regsql.exe工具

3)-P:后跟数据库密码,如mawei。

4)-ssadd:表示添加对SQLServer模式会话状态的支持。

5)-sstype t|p|c:表示支持的会话状态类型,其中:

❑t:表示temporary,会话状态数据存储在tempdb数据库中,管理会话的存储过程安装在ASPState数据库中。如果重新启动SQL,数据不会保存下来(默认)。

❑p:表示persisted,会话状态数据和存储过程都存储在ASPState数据库中。

❑c:表示custom,会话状态数据和存储过程都存储在定制的数据库中,必须指定数据库名。

6)-ssremove:删除对SQLServer模式会话状态的支持。

7)-d<database>:-sstype是c时使用的定制数据库名。

当执行完上面的aspnet_regsql.exe工具的配置命令之后,打开SQL Server数据库,你会发现数据库里面已经自动添加好了ASPState数据库。其中,在ASPState数据库中创建了两张数据表:ASPStateTempApplications与ASPStateTempSessions。除此之外,还创建了一系列存储过程,以支持在SQL和内存之间来回移动会话。

为了演示SQLServer模式的使用方法,我们来创建一个测试页面。如下面的代码所示:


<form id="form1"runat="server">

<div>

<asp:TextBox ID="wSession"runat="server"></asp:TextBox>

<asp:Button ID="Bt_WSession"runat="server"

Text="写入Session"OnClick="Bt_WSession_Click"/>

<br/>

<br/>

从Session中读取值:

<br/>

<asp:Label ID="rSession"runat="server"></asp:Label>

</div>

</form>


其中,Bt_WSession_Click事件处理程序的实现代码很简单。它首先需要将wSession控件里的数据写入Session[“MySession”],然后再将Session[“MySession”]赋给rSession控件显示出来。代码如下所示:


protected void Bt_WSession_Click(object sender, EventArgs e)

{

Session["MySession"]=wSession.Text;

if(Session["MySession"]!=null)

{

rSession.Text=Session["MySession"].ToString();

}

}


创建好测试页面之后,还需要在配置文件里将会话状态配置成SQLServer模式。配置示例如下所示:


<configuration>

<system.web>

<sessionState mode="SQLServer"

cookieless="true"

regenerateExpiredSessionId="true"

timeout="30"

sqlConnectionString="Data Source=.;

Integrated Security=SSPI;"

stateNetworkTimeout="30"/>

</system.web>

</configuration>


示例运行结果如图17-6所示。

从程序运行的表面上看,使用SQLServer模式与其他模式所得到的结果一样。但是,如果现在打开ASPStateTempSessions表,就会看到已串行化的对象,如图17-7所示。

figure_0614_0490

图 17-6 示例运行结果

figure_0614_0491

图 17-7 数据库里插入新记录

除此之外,如果要使用自己的数据库存储会话状态,可以用aspnet_regsql.exe的-d<database>选项指定数据库名,并在连接字符串中包含allowCustomSqlDatabase=“true”属性和数据库名。示例代码如下所示:


<sessionState allowCustomSqlDatabase="true"

mode="SQLServer"

sqlConnectionString="data source=.;

database=MyASPStateDatabase;

Integrated Security=SSPI;"/>


当然,也可以在连接字符串中配置用户ID和密码。最后还需要注意的是,如果模式设置为StateServer,则存储在会话状态中的对象必须是可序列化的。

3.自定义会话状态存储提供程序

ASP. NET会话状态建立在一个可扩展的、基于提供程序的新存储模型上,可以在此基础之上很容易地创建自定义的会话状态存储提供程序。下面就通过一个示例来阐述如何创建自定义会话状态存储提供程序。

在本示例中,选择将会话状态存储到SQL Server数据库中。因此,首要工作就是在ASPNET4数据库中创建一张存储会话状态的表Sessions,如图17-8所示。

figure_0615_0492

图 17-8 Sessions数据表

接下来,必须创建一个继承Ses sionStateStoreProviderBase抽象类的类来实现自定义会话状态存储提供程序。其中,SessionState-StoreProviderBase类属于System.Web.Session-State命名空间中的成员,它定义了数据存储区的会话状态提供程序所需的成员。同时,它又继承了ProviderBase抽象类,因此还必须实现ProviderBase类必需的成员。创建示例如SqlServerSessionStateStore类所示:


using System;

using System.Web;

using System.Web.Configuration;

using System.Configuration;

using System.Configuration.Provider;

using System.Collections.Specialized;

using System.Web.SessionState;

using System.Data;

using System.Data.SqlClient;

using System.Diagnostics;

using System.IO;

using System.Web.Hosting;

namespace MySessionStateStore

{

public class SqlServerSessionStateStore:

SessionStateStoreProviderBase

{

private SessionStateSection pConfig=null;

private string connectionString;

private ConnectionStringSettings

pConnectionStringSettings;

private string eventSource="SqlServerSessionStateStore";

private string eventLog="Application";

private string exceptionMessage="";

private string pApplicationName;

private bool pWriteExceptionsToEventLog=false;

private SqlConnection conn;

private SqlCommand cmd=null;

private SqlCommand deleteCmd=null;

private SqlDataReader reader=null;

public bool WriteExceptionsToEventLog

{

get{return pWriteExceptionsToEventLog;}

set{pWriteExceptionsToEventLog=value;}

}

public string ApplicationName

{

get{return pApplicationName;}

}

private void WriteToEventLog(Exception e, string action)

{

EventLog log=new EventLog();

log.Source=eventSource;

log.Log=eventLog;

string message="数据源发生异常信息.\n\n";

message+="Action:"+action+"\n\n";

message+="Exception:"+e.ToString();

log.WriteEntry(message);

}


Initialize方法是首要且必须要实现的,它是System.Configuration.Provider.ProviderBase类中的成员,在构造函数执行后将被立即调用,用来初始化提供程序。

它采用提供程序的名称和配置设置的NameValueCollection实例作为输入,用于设置提供程序实例的属性值,包括特定于实现的值和在配置文件((Mchine.config或Web.config)中指定的选项。如下面的代码所示:


public override void Initialize(string name,

NameValueCollection config)

{

if(config==null)

throw new ArgumentNullException("config");

if(name==null||name.Length==0)

name="SqlServerSessionStateStore";

if(String.IsNullOrEmpty(config["description"]))

{

config.Remove("description");

config.Add("description","SqlServerSessionStateStore");

}

base.Initialize(name, config);

pApplicationName=HostingEnvironment.ApplicationVirtualPath;

Configuration cfg=WebConfigurationManager.

OpenWebConfiguration(ApplicationName);

pConfig=((SssionStateSection)cfg.GetSection

("system.web/sessionState");

pConnectionStringSettings=ConfigurationManager.

ConnectionStrings[config["connectionStringName"]];

if(pConnectionStringSettings==null||

pConnectionStringSettings.ConnectionString.Trim()=="")

{

throw new ProviderException("连接字符串出错");

}

connectionString=

pConnectionStringSettings.ConnectionString;

conn=new SqlConnection(connectionString);

pWriteExceptionsToEventLog=false;

if(config["writeExceptionsToEventLog"]!=null)

{

if(config["writeExceptionsToEventLog"].ToUpper()=="TRUE")

pWriteExceptionsToEventLog=true;

}

}


SetItemExpireCallback方法用于设置对Global.asax文件中定义的Session_OnEnd事件的SessionStateItemExpireCallback委托的引用。它采用引用Global.asax文件中定义的Session_OnEnd事件的委托作为输入。如果会话状态存储提供程序支持Session_OnEnd事件,则设置对SessionStateItemExpireCallback参数的局部引用,并且此方法返回true;否则,此方法返回false。代码如下所示:


public override bool SetItemExpireCallback(

SessionStateItemExpireCallback expireCallback)

{

return false;

}


SetAndReleaseItemExculsive方法用于将SessionStateStoreData对象保存在定制数据库中。该方法采用当前请求的HttpContext实例、当前请求的SessionID值、包含要存储的当前会话值的SessionStateStoreData对象、当前请求的锁定标识符以及指示要存储的数据是属于新会话还是现有会话的值作为输入。

如果newItem参数为true,则SetAndReleaseItemExclusive方法使用提供的值将一个新项插入到数据存储区中。否则,数据存储区中的现有项使用提供的值进行更新,并释放对数据的任何锁定。需要注意的是,只有与提供的SessionID值和锁定标识符值匹配的当前应用程序的会话数据才会更新。

调用SetAndReleaseItemExclusive方法后,SessionStateModule调用ResetItemTimeout方法来更新会话项数据的过期日期和时间。代码如下所示:


public override void SetAndReleaseItemExclusive(

HttpContext context, string id, SessionStateStoreData item,

object lockId, bool newItem)

{

string sessItems=

Serialize(((SssionStateItemCollection)item.Items);

string dCmdText="";

string cmdText="";

if(newItem)

{

dCmdText=string.Format("DELETE FROM WHERE SessionId='{0}'"

+"AND ApplicationName='{1}'AND Expires<'{2}'",

id, ApplicationName, DateTime.Now);

cmdText=string.Format("INSERT INTO Sessions"

+"((SssionId, ApplicationName, Created, Expires,"

+"LockDate, LockId, Timeout, Locked, SessionItems, Flags)"

+"Values('{0}','{1}','{2}','{3}','{4}',{5},

'{6}',{7},'{8}',{9})",

id, ApplicationName, DateTime.Now,

DateTime.Now.AddMinutes(((Duble)item.Timeout),

DateTime.Now,0,item.Timeout,0,sessItems,0);

deleteCmd=new SqlCommand(dCmdText, conn);

cmd=new SqlCommand(cmdText, conn);

}

else

{

cmdText=string.Format("UPDATE Sessions SET Expires='{0}'"

+",SessionItems='{1}',Locked={2}"

+"WHERE SessionId='{3}'AND ApplicationName='{4}'"

+"AND LockId='{5}'",

DateTime.Now.AddMinutes(((Duble)item.Timeout),

sessItems,0,id, ApplicationName, lockId);

cmd=new SqlCommand(cmdText, conn);

}

try

{

conn.Open();

if(deleteCmd!=null)

deleteCmd.ExecuteNonQuery();

cmd.ExecuteNonQuery();

}

catch(SqlException e)

{

if(WriteExceptionsToEventLog)

{

WriteToEventLog(e,"SetAndReleaseItemExclusive");

throw new ProviderException(exceptionMessage);

}

else

throw e;

}

finally

{

conn.Close();

}

}

private string Serialize(SessionStateItemCollection items)

{

MemoryStream ms=new MemoryStream();

BinaryWriter writer=new BinaryWriter(ms);

if(items!=null)

items.Serialize(writer);

writer.Close();

return Convert.ToBase64String(ms.ToArray());

}


GetItemExclusive方法可以从选中的数据库中获得SessionStateStoreData。它采用当前请求的HttpContext实例和当前请求的SessionID值作为输入。从会话数据存储区中检索会话的值和信息,并在请求持续期间锁定数据存储区中的会话项数据。GetItemExclusive方法设置几个输出参数值,这些参数值将数据存储区中当前会话状态项的状态通知给执行调用的SessionStateModule。

如果数据存储区中未找到任何会话项数据,则GetItemExclusive方法将locked输出参数设置为false,并返回null。这将导致SessionStateModule调用CreateNewStoreData方法来为请求创建一个新的SessionStateStoreData对象。

如果在数据存储区中找到会话项数据但该数据已锁定,则GetItemExclusive方法将locked输出参数设置为true,将lockAge输出参数设置为当前日期和时间与该项锁定日期和时间的差,将lockId输出参数设置为从数据存储区中检索的锁定标识符,并返回null。这将导致SessionStateModule隔半秒后再次调用GetItemExclusive方法,以尝试检索会话项信息和获取对数据的锁定。如果lockAge输出参数的设置值超过ExecutionTimeout值,SessionStateModule将调用ReleaseItemExclusive方法以清除对会话项数据的锁定,然后再次调用GetItemExclusive方法。

如果regenerateExpiredSessionId(它指示当客户端指定过期的SessionID时是否重新生成SessionID)特性设置为true,则actionFlags参数用于其Cookieless属性为true的会话。actionFlags值设置为InitializeItem(1)则指示会话数据存储区中的项是需要初始化的新会话。通过调用CreateUninitializedItem方法可以创建会话数据存储区中未初始化的项。如果会话数据存储区中的项已经初始化,则actionFlags参数设置为零。

如果提供程序支持无Cookie会话,请将actionFlags输出参数设置为当前项从会话数据存储区中返回的值。如果被请求的会话存储项的actionFlags参数值等于InitializeItem枚举值(1),则GetItemExclusive方法在设置actionFlagsout参数之后应将数据存储区中的值设置为零。如下面的代码所示:


public override SessionStateStoreData GetItemExclusive(

HttpContext context, string id, out bool locked,

out TimeSpan lockAge, out object lockId,

out SessionStateActions actionFlags)

{

return GetSessionStoreItem(true, context, id, out locked,

out lockAge, out lockId, out actionFlags);

}

private SessionStateStoreData GetSessionStoreItem(

bool lockRecord, HttpContext context, string id,

out bool locked, out TimeSpan lockAge, out object lockId,

out SessionStateActions actionFlags)

{

SessionStateStoreData item=null;

lockAge=TimeSpan.Zero;

lockId=null;

locked=false;

actionFlags=0;

DateTime expires;

string serializedItems="";

bool foundRecord=false;

bool deleteData=false;

string updateCmdText="";

int timeout=0;

try

{

conn.Open();

if(lockRecord)

{

updateCmdText=string.Format(

"UPDATE Sessions SET Locked={0},LockDate='{1}'"

+"WHERE SessionId='{2}'AND ApplicationName='{3}'"

+"AND Locked={4}AND Expires>'{5}'",

0,DateTime.Now, id, ApplicationName,0,DateTime.Now);

cmd=new SqlCommand(updateCmdText, conn);

if(cmd.ExecuteNonQuery()==0)

locked=true;

else

locked=false;

}

cmd=new SqlCommand(

"SELECT Expires, SessionItems, LockId,"

+"LockDate, Flags, Timeout FROM Sessions"

+"WHERE SessionId='"+id

+"'AND ApplicationName='"

+ApplicationName+"'",conn);

reader=cmd.ExecuteReader(CommandBehavior.SingleRow);

while(reader.Read())

{

expires=reader.GetDateTime(0);

if(expires<DateTime.Now)

{

locked=false;

deleteData=true;

}

else

foundRecord=true;

serializedItems=reader.GetString(1);

lockId=reader.GetInt32(2);

lockAge=DateTime.Now.Subtract(reader.GetDateTime(3));

actionFlags=((SssionStateActions)reader.GetInt32(4);

timeout=reader.GetInt32(5);

}

reader.Close();

if(deleteData)

{

cmd=new SqlCommand("DELETE FROM Sessions"

+"WHERE SessionId='"+id

+"'AND ApplicationName='"

+ApplicationName+"'",conn);

cmd.ExecuteNonQuery();

}

if(!foundRecord)

locked=false;

if(foundRecord&&!locked)

{

lockId=((it)lockId+1;

cmd=new SqlCommand("UPDATE Sessions SET"

+"LockId="+lockId+",Flags=0"

+"WHERE SessionId='"+id

+"'AND ApplicationName='"

+ApplicationName+"'",conn);

cmd.ExecuteNonQuery();

if(actionFlags==SessionStateActions.InitializeItem)

item=CreateNewStoreData(context,

Convert.ToInt32((ponfig.Timeout.TotalMinutes));

else

item=Deserialize(

context, serializedItems, timeout);

}

}

catch(SqlException e)

{

if(WriteExceptionsToEventLog)

{

WriteToEventLog(e,"GetSessionStoreItem");

throw new ProviderException(exceptionMessage);

}

else

throw e;

}

finally

{

if(reader!=null){reader.Close();}

conn.Close();

}

return item;

}

private SessionStateStoreData Deserialize(

HttpContext context, string serializedItems, int timeout)

{

MemoryStream ms=

new MemoryStream(Convert.FromBase64String(serializedItems));

SessionStateItemCollection sessionItems=

new SessionStateItemCollection();

if(ms.Length>0)

{

BinaryReader reader=new BinaryReader(ms);

sessionItems=

SessionStateItemCollection.Deserialize(reader);

}

return new SessionStateStoreData(sessionItems,

SessionStateUtility.GetSessionStaticObjects(context),

timeout);

}


除了不尝试锁定数据存储区中的会话项以外,GetItem方法与GetItemExclusive方法执行的操作相同。GetItem方法在EnableSessionState特性设置为ReadOnly时调用。如下面的代码所示:


public override SessionStateStoreData GetItem(

HttpContext context, string id, out bool locked,

out TimeSpan lockAge, out object lockId,

out SessionStateActions actionFlags)

{

return GetSessionStoreItem(false, context, id, out locked,

out lockAge, out lockId, out actionFlags);

}


在调用GetItem或GetItemExclusive方法,并且数据存储区指定被请求项已锁定,但锁定时间已超过ExecutionTimeout值时会调用ReleaseItemExclusive方法。ReleaseItemExclusive方法用于清除锁定,释放该被请求项以供其他请求使用。

该方法采用当前请求的HttpContext实例、当前请求的SessionID值以及当前请求的锁定标识符作为输入,并释放对会话数据存储区中的项的锁定。代码如下所示:


public override void ReleaseItemExclusive(HttpContext context,

string id, object lockId)

{

cmd=new SqlCommand(

"UPDATE Sessions SET Locked=0,Expires='"

+DateTime.Now.AddMinutes(pConfig.Timeout.TotalMinutes)

+"'"+"WHERE SessionId='"+id

+"'AND ApplicationName='"

+ApplicationName+"'AND LockId="+lockId+"",conn);

try

{

conn.Open();

cmd.ExecuteNonQuery();

}

catch(SqlException e)

{

if(WriteExceptionsToEventLog)

{

WriteToEventLog(e,"ReleaseItemExclusive");

throw new ProviderException(exceptionMessage);

}

else

throw e;

}

finally

{

conn.Close();

}

}


RemoveItem方法在Abandon方法被调用时调用。它采用当前请求的HttpContext实例、当前请求的SessionID值以及当前请求的锁定标识符作为输入,并删除数据存储区中与提供的SessionID值、当前应用程序和提供的锁定标识符相匹配的数据存储项的会话信息。代码如下所示:


public override void RemoveItem(HttpContext context,

string id, object lockId, SessionStateStoreData item)

{

cmd=new SqlCommand("DELETE*FROM Sessions"

+"WHERE SessionId='"+id+"'AND ApplicationName='"

+ApplicationName+"'AND LockId="+lockId+"",conn);

try

{

conn.Open();

cmd.ExecuteNonQuery();

}

catch(SqlException e)

{

if(WriteExceptionsToEventLog)

{

WriteToEventLog(e,"RemoveItem");

throw new ProviderException(exceptionMessage);

}

else

throw e;

}

finally

{

conn.Close();

}

}


CreateUninitializedItem方法用于将新的会话状态项添加到数据存储区中。它采用当前请求的HttpContext实例、当前请求的SessionID值以及当前请求的锁定标识符作为输入,并向会话数据存储区添加一个actionFlags值为InitializeItem的未初始化项。

如果regenerateExpiredSessionId特性设置为true,则CreateUninitializedItem方法用于无Cookie会话,这将导致遇到过期会话ID时,SessionStateModule会生成一个新的SessionID值。

生成新的SessionID值的过程需要浏览器重定向到包含新生成的会话ID的URL。在包含过期的会话ID的初始请求期间,会调用CreateUninitializedItem方法。SessionStateModule获取一个新的SessionID值来替换过期的会话ID之后,它会调用CreateUninitializedItem方法以将一个未初始化项添加到会话状态数据存储区中。然后,浏览器被重定向到包含新生成的SessionID值的URL。如果会话数据存储区中存在未初始化项,则可以确保包含新生成的SessionID值的重定向请求被视为新的会话,而不会被误认为是对过期会话的请求。

会话数据存储区中未初始化的项与新生成的SessionID值关联,并且仅包含默认值,其中包括到期日期和时间以及与GetItem和GetItemExclusive方法的actionFlags参数相对应的值。会话状态存储区中的未初始化项应包含一个与InitializeItem枚举值(1)相等的actionFlags值。此值由GetItem和GetItemExclusive方法传递给SessionStateModule,并针对SessionStateModule指定当前会话是新会话。SessionStateModule随后将初始化该新会话,并引发Session_OnStart事件。代码如下所示:


public override void CreateUninitializedItem(

HttpContext context, string id, int timeout)

{

cmd=new SqlCommand("INSERT INTO Sessions"

+"((SssionId, ApplicationName, Created, Expires,"

+"LockDate, LockId, Timeout, Locked, SessionItems, Flags)"

+"Values('"+id+"','"+ApplicationName+"','"

+DateTime.Now+"','"

+DateTime.Now.AddMinutes(((Duble)timeout)

+"','"+DateTime.Now+"',0,'"+timeout+"',0,'',1)",conn);

try

{

conn.Open();

cmd.ExecuteNonQuery();

}

catch(SqlException e)

{

if(WriteExceptionsToEventLog)

{

WriteToEventLog(e,"CreateUninitializedItem");

throw new ProviderException(exceptionMessage);

}

else

throw e;

}

finally

{

conn.Close();

}

}


CreateNewStoreData方法创建要用于当前请求的新SessionStateStoreData对象。它采用当前请求的HttpContext实例和当前会话的Timeout值作为输入,并返回带有空ISessionStateItemCollection对象的新的SessionStateStoreData对象、一个HttpStaticObjectsCollection集合和指定的Timeout值。

其中,使用SessionStateUtility的GetSessionStaticObjects方法可以检索ASP.NET应用程序的HttpStaticObjectsCollection实例。代码如下所示:


public override SessionStateStoreData CreateNewStoreData(

HttpContext context, int timeout)

{

return new SessionStateStoreData(

new SessionStateItemCollection(),

SessionStateUtility.GetSessionStaticObjects(context),

timeout);

}


ResetItemTimeout方法用于更新会话数据存储区中的项的到期日期和时间。如果请求ASP.NET页并且EnableSessionState特性设置为false,仍然会调用ResetItemTimeout方法,以更新会话数据存储区中数据的到期日期和时间。代码如下所示:


public override void ResetItemTimeout(

HttpContext context, string id)

{

cmd=new SqlCommand("UPDATE Sessions SET Expires='"

+DateTime.Now.AddMinutes(pConfig.Timeout.TotalMinutes)

+"'"+"WHERE SessionId='"+id

+"'AND ApplicationName='"

+ApplicationName+"",conn);

try

{

conn.Open();

cmd.ExecuteNonQuery();

}

catch(SqlException e)

{

if(WriteExceptionsToEventLog)

{

WriteToEventLog(e,"ResetItemTimeout");

throw new ProviderException(exceptionMessage);

}

else

throw e;

}

finally

{

conn.Close();

}

}


InitializeRequest方法采用当前请求的HttpContext实例作为输入,并执行会话状态存储提供程序必需的所有初始化操作。代码如下所示:


public override void InitializeRequest(HttpContext context)

{

}


EndRequest方法采用当前请求的HttpContext实例作为输入,并执行会话状态存储提供程序必需的所有清理操作。代码如下所示:


public override void EndRequest(HttpContext context)

{

}


Dispose方法释放会话状态存储提供程序不再使用的所有资源。代码如下所示:


public override void Dispose()

{

}


到现在为止,一个完整的自定义会话状态存储提供程序SqlServerSessionStateStore就完成了。如果要在项目里使用该自定义会话状态存储提供程序,还需要在配置文件里面做如下配置:


<configuration>

<connectionStrings>

<add name="SqlServerSessionStateStore"connectionString="

server=.;database=ASPNET4;uid=sa;pwd=mawei;pooling=true;"/>

</connectionStrings>

<system.web>

<sessionState

cookieless="true"

regenerateExpiredSessionId="true"

mode="Custom"

customProvider="SqlServerSessionProvider">

<providers>

<add name="SqlServerSessionProvider"

type="MySessionStateStore.SqlServerSessionStateStore"

connectionStringName="SqlServerSessionStateStore"

writeExceptionsToEventLog="false"/>

</providers>

</sessionState>

<compilation debug="true"targetFramework="4.0"/>

</system.web>

</configuration>


仍然以17.6.3节中第2部分的测试页面为例,运行测试页面,结果如图17-9所示。

从程序运行的表面上看,所得到的结果与其他模式的相同。但是,如果现在打开Sessions表,就会看到存储的数据,如图17-10所示。

figure_0625_0493

图 17-9 示例运行结果

figure_0625_0494

图 17-10 Sessions数据表

最后还需要说明的是,如果示例提供程序在使用数据源时遇到异常,它会将异常的详细信息写入到应用程序事件日志中,而不是将异常返回到ASP.NET应用程序。这是一种安全措施,用来避免在ASP.NET应用程序中公开有关数据源的私有信息。

本示例提供程序指定了“SqlServerSessionStateStore”的事件Source属性值。在ASP.NET应用程序能够成功写入应用程序事件日志之前,需要创建下面的注册表项:


HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Eventlog\

Application\SqlServerSessionStateStore


如果不想让示例提供程序将异常写入事件日志,则可以在Web.config文件中将自定义writeExceptionsToEventLog特性设置为false。

17.6.4 会话状态事件

ASP. NET提供两个可帮助管理用户会话的事件:Session_OnStart事件和Session_OnEnd事件。其中,Session_OnStart事件在开始一个新会话时引发。可以通过向Global.asax文件添加一个名为Session_OnStart的子例程来处理Session_OnStart事件。如果请求开始一个新会话,Session_OnStart子例程会在请求开始时运行。如果请求不包含SessionID值或请求所包含的SessionID属性引用一个已过期的会话,则会开始一个新会话。因此,还可以使用Session_OnStart事件初始化会话变量并跟踪与会话相关的信息。

Session_OnEnd事件在一个会话被放弃或过期时引发。同样可以通过向Global.asax文件添加一个名为Session_OnEnd的子例程来处理Session_OnEnd事件。Session_OnEnd子例程在Abandon方法已被调用或会话已过期时运行。如果超过了某一会话Timeout属性指定的分钟数,并且在此期间内没有请求该会话,则该会话过期。

这里需要特别注意的是,只有会话状态属性Mode设置为InProc(默认值)时,才支持Session_OnEnd事件。如果会话状态属性Mode为StateServer或SQLServer,则忽略Global.asax文件中的Session_OnEnd事件。如果会话状态属性Mode设置为Custom,则由自定义会话状态存储提供程序以决定是否支持Session_OnEnd事件。因此,可以使用Session_OnEnd事件清除与会话相关的信息,如由SessionID值跟踪的数据源中的用户信息。

在下面的示例代码中,创建了一个计数器,用来跟踪正在使用应用程序的应用程序用户的数量。注意,只有当会话状态属性Mode设置为InProc时,此示例才会正常运行,因为只有进程内会话状态存储才支持Session_OnEnd事件。


protected void Application_Start(object sender, EventArgs e)

{

Application["Users"]=0;

}

protected void Session_Start(object sender, EventArgs e)

{

Application.Lock();

Application["Users"]=((it)Application["Users"]+1;

Application.UnLock();

}

protected void Session_End(object sender, EventArgs e)

{

Application.Lock();

Application["Users"]=((it)Application["Users"]-1;

Application.UnLock();

}


17.6.5 会话状态的生命周期

我们已经知道,Session是在用户第一次访问网站的时候创建的,那么Session是什么时候销毁的呢?

其实,Session使用一种平滑超时的技术来控制何时销毁Session。默认情况下,Session的超时时间((Tmeout)是20分钟,即用户保持连续20分钟不访问网站,则Session被收回。如果在这20分钟内用户又访问了一次页面,那么20分钟就重新计时了。也就是说,这个超时是连续不访问的超时时间,而不是第一次访问的20分钟之内必然过时。当然,可以通过修改Web.config文件的配置项来调整这个超时时间。如下面的代码所示:


<sessionState timeout="30"></sessionState>


同样也可以在程序中进行设置。如下面的代码所示:


Session.Timeout="30";


一旦Session超时,Session中的数据将被回收,如果再次使用Session,将分配一个新的SessionID。

不过,你可别太相信Session的Timeout属性,如果把它设置为24小时,则很难相信24小时之后用户的Session还在。Session是否存在,不仅仅依赖于Timeout属性,以下的情况都可能引起Session丢失:

1)bin目录中的文件被改写。asp.net有一种机制,为了保证dll重新编译之后,系统正常运行,它会重新启动一次网站进程,这时就会导致Session丢失。

2)SessionID丢失或者无效。如果在URL中存储SessionID,但是使用了绝对地址重定向网站导致URL中的SessionID丢失,那么原来的Session将失效。如果在Cookie中存储SessionID,那么客户端禁用Cookie或者Cookie达到了IE中Cookie数量的限制(每个域20个),那么Session将无效。

3)如果使用InProc的Session,那么IIS重启将会丢失Session。同理,如果使用StateServer的Session,服务器重新启动Session也会丢失。

17.6.6 遍历与销毁会话状态

如果需要遍历当前的Session集合,可以这样来处理。如下面的代码所示:


IEnumerator SessionEnum=Session.Keys.GetEnumerator();

while(SessionEnum.MoveNext())

{

Response.Write(

Session[SessionEnum.Current.ToString()].ToString()

+"<br/>");

}


有时候,还需要立刻让Session失效。比如用户退出系统后,Session中保存的所有数据需要全部失效。处理方法如下面的代码所示:


Session.Abandon();


17.6.7 会话状态的优点与局限性

从上面的阐述中,可以看出会话状态具有许多的优点。主要表现在以下几方面:

1)实现简单。会话状态功能易于使用,为ASP开发人员所熟悉,并且与其他.NET Framework类一致。

2)会话特定的事件。会话管理事件可以由应用程序引发和使用。

3)数据持久性。放置于会话状态变量中的数据可以经受得住Internet信息服务((Iternet Information Services, IIS)重新启动和辅助进程重新启动,而不丢失会话数据,这是因为这些数据可以存储在另一个进程空间中。此外,会话状态数据可跨多进程保持。

4)平台可伸缩性。会话状态可在多计算机和多进程配置中使用,因而优化了可伸缩性方案。

5)无须Cookie支持。尽管会话状态最常见的用途是与Cookie一起向Web应用程序提供用户标识功能,但会话状态可用于不支持HTTP Cookie的浏览器。但是,使用无Cookie的会话状态需要将会话标识符放置在查询字符串中,这就导致了安全问题。

6)可扩展性。可通过编写自己的会话状态提供程序自定义和扩展会话状态。然后可以通过多种数据存储机制(例如,数据库、XML文件甚至Web服务)将会话状态数据以自定义数据格式存储。

当然,除了上面这些优点之外,会话状态也存在着一些局限性:

1)会话状态变量在被移除或替换前保留在内存中,因而可能降低服务器性能。如果会话状态变量包含诸如大型数据集之类的信息块,则可能会因服务器负荷的增加影响Web服务器的性能。

2)容易丢失。