状态序列化和反序列化
使用进程内模式时,对象作为各自类的活动实例存储在会话状态中。如果未发生真正的序列化和反序列化,则表示您实际上可以在 Session 中存储您创建的任何对象(包括无法序列化的对象和 COM 对象),并且访问它们的开销也不会太高。如果您选择进程外状态提供程序,又是另外一种情况。
在进程外体系结构中,会话值将从本地存储介质(外部 AppDomain 数据库)复制到处理请求的 AppDomain 的内存中。需要使用序列化/反序列化图层完成该任务,并表示进程外状态提供程序的某项主要成本。这种情况对代码产生的主要影响是只能在会话词典中存储可序列化的对象。
根据所涉及的数据类型,ASP.NET 使用两种方法对数据进行序列化和反序列化。对于基本类型,ASP.NET 使用经过优化的内部序列化程序;对于其他类型(包括对象和用户定义的类),ASP.NET 使用 .NET 二进制格式化程序。基本类型包括字符串、日期时间、布尔值、字节、字符以及所有的数字类型。对于这些类型,使用量身制作的序列化程序要比使用默认的常用 .NET 二进制格式化程序更快。
经过优化的序列化程序没有公开发布,也没有以文档形式提供。它仅仅是二进制读取器/写入器,并且使用简单但有效的存储架构。该序列化程序使用 BinaryWriter 类写入一个字节表示类型,然后写入一个字节表示该类型对应的值。读取序列化的字节时,该类首先提取一个字节,检测要读取的数据类型,然后对 BinaryReader 类调用特定类型的 ReadXxx 方法。
请注意,布尔值和数字类型的大小是众所周知的,但对字符串并非如此。在基础数据流上,字符串始终带有一个固定长度的前缀(一次编写 7 位整数代码),读取器根据这一事实来确定字符串的正确大小。而日期值是通过只写入构成日期的标记总数来保存的。因此,要对会话执行序列化操作,日期应为 Int64 类型。
只要将包含的类标记为可序列化的类,便可以使用 BinaryFormatter 类对更复杂的对象(以及自定义对象)执行序列化操作。所有非基本类型都采用相同的类型 ID 进行标识并与基本类型存储在同一个数据流中。总之,序列化操作会导致性能下降 15% 至 25%。但请注意,这是基于假定使用基本类型所进行的粗略估计。使用的类型越复杂,开销越大。
如果不大量使用基本类型,很难实现有效的会话数据存储。因此,至少在理论上,使用三个会话槽保存对象的三个不同的字符串属性要比对整个对象进行序列化好。但是,如果要序列化的对象包含 100 个属性,那该怎么办呢?是要使用 100 个槽,还是只使用一个槽?在许多情况下,更好的方法是将复杂的类型转换为多个简单的类型。这种方法基于类型转换器。“类型转换器”是一种轻便的序列化程序,它以字符串集合的形式返回类型的关键属性。类型转换器是使用特性与基类绑定在一起的外部类。由类型编写者决定保存哪些属性以及如何保存。类型转换器对于 ViewState 存储也有帮助,它代表的是比二进制格式化程序更有效的会话存储方法。
会话的生命周期 关于 ASP.NET 会话管理,重要的一点是,仅当将第一个项目添加到内存词典中时,会话状态对象的生命周期才开始。仅在执行如下代码片断后,才可以认为 ASP.NET 会话开始。
Session["MySlot"] = "Some data";
Session 词典通常包含 Object 类型,要向后读取数据,需要将返回的值转换为更具体的类型。
string data = (string) Session["MySlot"];
当页面将数据保存到 Session 中时,会将值加载到 HttpSessionState 类包含的特制的词典类中。完成当前处理的请求时,会将词典的内容加载到状态提供程序中。如果由于未通过编程方式将数据放入词典而导致会话状态为空,则不会将数据序列化到存储介质中,而且更重要的是,不会在 ASP.NET Cache、SQL Server 或 NT 状态服务中创建槽来跟踪当前会话。这是出于性能方面的原因,但会对处理会话 ID 的方式产生重要影响:将为每个请求生成一个新的会话 ID,直到将某些数据存储到会话词典中。
需要将会话状态与正在处理的请求连接时,HTTP 模块会检索会话 ID(如果它不是启动请求),并在配置的状态提供程序中寻找它。如果没有返回数据,HTTP 模块将为请求生成一个新的会话 ID。这可以很容易地通过以下页面进行测试:
<%@ Page Language="C#" Trace="true" %>;
</html>;
<body>;
<form runat="server">;
<asp:button runat="server" text="Click" />;
</form>;
</body>;
</html>;
无论何时单击该按钮并返回页面,都将生成新的会话 ID,同时记录跟踪信息。
图 3:在没有将数据存储到会话词典中的应用程序中,为每个请求生成一个新的会话 ID。
Session_OnStart 事件的情况如何呢?也会为每个请求引发该事件吗?如果应用程序定义 Session_OnStart 处理程序,则会始终保存会话状态,即使会话状态为空。因此,对于第一个请求之后的所有请求来说,会话 ID 始终为常量。仅在确实必要时,才使用 Session_OnStart 处理程序。
如果会话超时或被放弃,下次访问无状态应用程序时,其会话 ID 不会发生改变。经过设计后,即使会话状态过期,会话 ID 也能持续到浏览器会话结束。也就是说,只要浏览器实例相同,就始终使用同一个会话 ID 表示多个会话。
Session_OnEnd 事件标志着会话的结束,并用于执行终止该会话所需的所有清除代码。但请注意,只有 InProc 模式支持该事件,也就是说,只有将会话数据存储在 ASP.NET 辅助进程中时才支持该事件。对于要引发的 Session_OnEnd 事件来说,必须首先存在会话状态,这意味着必须在该会话状态中存储一些数据,并且必须至少完成一个请求。
在 InProc 模式下,作为项目添加到缓存中的会话状态被赋予一个可变过期时间策略。可变过期时间表示如果某个项目在一定时间内没有使用,将被删除。在此期间处理的任何请求的过期时间都将被重置。会话状态项目的时间间隔被设置为会话超时。用来重置会话状态过期时间的技术非常简单和直观:会话 HTTP 模块只读取 ASP.NET Cache 中存储的会话状态项目。如果知道 ASP.NET Cache 对象的内部结构,该模块将进行计算以重新设置可变过期时间。因此,当缓存项目过期时,会话已超时。
过期的项目将自动从缓存中删除。状态会话模块作为此项目的过期时间策略的一部分,也代表了一个删除回调函数。缓存将自动调用删除函数,删除函数然后将引发 Session_OnEnd 事件。如果应用程序通过进程外组件来执行会话管理,则永远不会引发结束事件。