众所周知,HTTP是一种无状态(stateless)的协议。首先,先搞清楚什么是状态(state),按照金山词霸的解释,state意为“A condition of being in a stage or form, as of structure, growth, or development”;按照汉语词典的解释,状态的意思是“状貌特征与动作情态,或物质系统所处的状况”。
对于网页来说,网页在某一时刻的状态即网页当前呈现在用户面前全部内容,如字体类型、字体大小、背景和前景颜色、数据、排版等等。页面初始化时呈现的是页面的初始状态,用户在初始状态的基础上所做的任何改变就是“用户状态”。之所以说HTTP是一种无状态的协议,是因为HTTP并不负责维护用户状态,当把页面返回给服务器时,用户在初始状态的基础上所作的任何改变(即状态)都将丢失,从服务器再次返回的将又是页面的初始状态。对于静态网页来说,状态的丢失并不会产生太大问题;但对于网络应用程序来说,未来的状态往往以当前的状态为依据,状态的丢失会使得应用程序可能得不到预期结果。因此,状态管理始终是网络应用程序的一个核心问题。
关于状态管理,ASP.NET中有两个相近的词:State Management和Cache Management,翻译成中文分别为状态管理和缓存管理。两种说法实际上是一致的,State Management说明了管理的内容;Cache Management则说明了状态管理实现的方式,即将表达状态信息的对象缓存起来以实现对状态的维护。但二者之间在语意上也存在一点点区别,State Management只强调状态的管理,而Cache Management除了指出状态管理的方式以外,对于提高网络应用程序的性能也有很重要的意义。
在ASP.NET中提供了丰富的状态管理和缓存机制,下面分别讨论各个具体的状态管理和缓存机制,重点在于各种机制实现的原理、特点、适用范围,以及它们之间的异同点。
1.Page Output Caching
Page Output Caching,即页面输出缓存,是指将已生成的页面存放在输出缓存(Output Cache)中,当对该页面新的请求出现时,不需要再重复页面的生成过程,直接从输出缓存中取出并返回给客户端。页面输出缓存可以实现对整个页面或部分页面的缓存,当对整个页面进行缓存时,更多的是出于性能的考虑,而非页面状态的维护。图1显示了客户端如何从输出缓存中获取页面的过程,图中的序号表示了工作的流程:
开始的几步是客户端对某一页面发出第一次请求的情况:
(1)客户端向服务器端发送请求(Request),请求通过HTTP runtime中功能模块的处理,向Page Handler请求页面;
(2)由于是第一次发送请求,Page Handler不能在中找到已编译好(precompiled)的页面,因此从服务器硬盘中获取对应的页面文件,并将该页面文件发送到ASP.NET引擎中进行解析;
(3)ASP.NET页面文件通过解析生成页面类;
(4)页面类被编译成.NET装配件(Assembly),并存储在硬盘中;
(5)为页面类创建一个实例(class instance);
(6)将页面实例作为客户响应(response)发送回客户端;
下面的步骤是没有使用输出缓存的情况:
(7)与第(1)步一样,客户端请求通过处理被传送到Page Handler请求页面;
(8)Page Handler发现了之前已编译好的页面,则创建该页面的实例;
(9)将页面实例发送回客户端;
下面的步骤是使用输出缓存的情况:
(10)和(11)与上面的步骤一样,创建页面的实例;
(12)将页面实例存储在ASP.NET的输出缓存中;
(13)页面实例作为响应返回客户端;
下面可以直接从输出缓存中获取页面:
(14)客户端向服务器发送请求(Request);
(15)在HTTP runtime处理请求的过程中,会在输出缓存中查找是否有之前生成好的页面实例,如果找到,则直接从输出缓存中取出该实例,发送回客户端。
图 1 客户端从输出缓存中获取页面的过程
从上面的过程我们可以看到,通过ASP.NET的页面输出缓存,相同的页面只需在第一次请求时进行编译和实例化,后续的请求通过输出缓存就能得到满足,大大减少了服务器端的工作量,提高了系统性能。我们还可以注意到,在ASP.NET的架构中,即使不使用输出缓存,页面也只需编译一次,如(7)(8)(9)步所示,性能与以前相比已有很大提高,而使用输出缓存则使服务器对请求的响应速度更快。此外,输出缓存的作用域为整个应用程序(Application Scope),因此,所有的客户端请求和会话都可以从输出缓存中获取缓存的页面。
页面输出缓存有两种使用方式:对整个页面缓存和对部分(partial)页面缓存。部分页面缓存适用于页面中有一部分内容每次请求都可能不同的情况。部分页面缓存又分为两种实现方式:control caching(自定义控件缓存)和post-cache substitution。Control Caching又称为fragment caching(片段缓存),实现的方式是通过将需要缓存的内容放在自定义控件中,并指定自定义控件是可缓存的(cacheable)。比如,我们现在要创建一个页面,页面上所要显示的信息大部分为要即时更新的信息(dynamic sections),还有一部分是基本不会更新的信息(static sections),那么就可以将静态的部分放在自定义控件中,并整个页面设置为不可缓存的,而将控件设置为可缓存的。自定义控件的创建方法请参见MSDN中的ASP.NET User Controls。Post-cache substitution与Control Caching实现的方式相反,即整个页面设置为可缓存的,要动态更新的部分则不被缓存。这种方式可以通过三种方法实现:Substitution Control;Substitution API;AdRotator Control。具体请参见MSDN中的Dynamically Updating Portions of a Cached Page。
页面输出缓存可以缓存同一页面的多个版本。同一ASP.NET页面往往由于参数等的不同而产生不同的输出,输出缓存允许根据指定的条件缓存同一页面的多个版本。如根据查询字符串(HTTP Get)的不同、根据返回到服务器端的控件的值(HTTP Post values)的不同,根据随请求一起发送到服务器的HTTP头(HTTP header)的不同,根据发送请求的浏览器的主版本号(是IE6.0还是IE5.0,或NetScape等)的不同,以及一些自定义的字符串,来确定对同一页面的多个版本的缓存。具体实现请查阅Caching Multiple Versions of a Page和Page Output Caching, Part 1。
页面输出缓存中存储的页面只能通过Duration属性控制其有效的时间,而没有其它的方法使其失效。但在服务器内存不够的情况下,根据最近最少算法(Least Recently Used,LRU)自动的从输出缓存中删除最近最少使用的页面。
2.Cache Object
Cache (Response.Cache)是ASP.NET提供的一种新特性,实际上也是通过输出缓存(output cache)是实现的,它存在与System.Web.Caching命名空间。
与Page Output Caching相比,二者的相同点为:实现的原理相同,都是通过输出缓存实现;作用域相同,都是整个应用程序(Application Scope),可以被所有的用户请求访问;当内存不足时,Output Cache中的内容会按照LRU算法进行回收。
二者的区别首先是使用方式不同。Page Output Caching主要是通过页面顶部的命令实现,如:<% OutputCache Duration=”10” VaryByParam=”none”>,也可以通过配置文件对页面进行设置,设置方式请参见Page Output Cache ConfigurationMSDN。且存储在输出缓存中的页面的获取由HTTP Runtime根据客户请求实现,不能通过程序控制。而Cache对象则是采用key/value对实现,如Cache[“temp”] = var,与Application和Session等的使用方式相同。其次,也是最重要的一点,Page Output Caching只提供了Duration属性限制缓存页面有效的时间,否则只能在内存不足时通过内存回收自动从输出缓存中删除数据;而Cache对象提供了过期(expiration)和依赖(dependency)机制,能灵活的实现对输出缓存中的内容的控制。依赖机制主要有:基于文件的依赖(File-based dependency),当指定的文件发生改变时,缓存的对象自动删除;基于关键字的依赖(Key-based dependency),当指定的另一个存放在输出缓存中的对象发生改变时,该缓存的对象自动删除;基于时间的依赖(time-based dependency),分为两种,一种是基于绝对时间(absolute),如从当前时间开始10分钟后失效;另一种是基于客户请求从输出缓存中获取该对象的时间(sliding time)。此外。还可以设置每个被缓存对象的优先级,内存回收时会根据优先级从输出缓存中删除对象。这几种依赖机制的具体实现,请参阅ASP.NET Caching MSDN。而且Cache对象还支持Callback,即当一个缓存对象由于发生更新而被删除时,可以重新在输出缓存中建立该对象,并将对象的值设置为更新后的值。Callback的实现可以参阅Cache Management in ASP.NET MSDN。
使用Cache对象还要注意的一点是,每次在从输出缓存中获取数据时,都需要检查目标对象是否存在,因为有可能由于对该对象设置的依赖策略或内存不足时自动回收,已经自定被删除,这是使用Cache对象需要遵循的一个模式。如下面的代码所示:
Public Function GetProductData () As DataSet
If (IsNothing (Cache ("ProductData")) Then
Cache ("ProductData") = LoadDataSet ()
Return Cache ("ProductData")
End Function
3.Session State
Session,即会话。首先让我们先弄清楚什么是Session和Session State。在Michael Volodarsky的文章Fast, Scalable, and Secure Session State Management for Your Web Applications MSDN中,Session 的定义为:“A session is defined as a series of requests issued by the same client within a certain period of time”。直观的说,Session就是一个用户某次,在有限的时间内,访问一个网络应用程序的全过程。即当用户通过浏览器向一个网络应用程序发送第一个请求时,一个会话(Session)就开始了;当用户关闭该网络应用程序时(无论出于任何原因,如用户浏览完毕主动关闭,或中途更换浏览器,甚至由于客户端异常而关闭),就称为该会话终止;当该用户再一次访问该网络应用程序时,就启动了新的会话。可见,Session是与单个客户端绑定的,且一个完整的会话过程不存在中断(中途没有关闭过该网络应用程序)。那么Session State就是在一个Session中由用户产生并由用户使用的数据,文章Underpinnings of the Session State Implementation in ASP.NET MSDN给出的定义为“The session state is the collection of persistent data that user generated and used during the session”。Session state对应于单个的Session,不同的Session有不同的Session State,Session结束Session State也就消失了。因此,与上述基于Output Cache实现的状态管理机制相比,最大的区别在于,Session State不是跨用户(cross clients)的。而相同点在于,Session State也是跨请求的(cross requests),也就是说它是应用程序级别的缓存,但只局限于同一会话中的一系列请求。
ASP.NET提供的Session State是一个功能强大、使用方便的状态管理机制,同时也是ASP.NET提供的最重要的状态管理机制之一。它本身并不是HTTP基础架构的一部分,而是在HTTP基础架构上构建了一个新的抽象层,通过该抽象层实现Session State与每一个请求的绑定。
实际上,ASP已经实现了Session State,其调用方法与ASP.NET一致,都是通过一个Session对象来存取状态信息,但底层的实现却有很大差别。在ASP中,session state是作为一个自由线程的COM对象实现的,对象中存储着表示状态信息的键/值对(key/value pairs)的集合,并将它们按Session ID(Session的唯一标识符)分组,使每个用户只能看见自己创建的键/值对集合。ASP.NET中的Session State则拥有更强大的功能,以及更大的灵活性和扩展性。图2显示了ASP.NET中Session State的基本架构,我们通过该图详细解释它的实现原理。
在对图2进行详细阐述之前,我们先大致介绍一下ASP.NET的Session State架构中的一些重要内容。当一个请求发送到服务器时,客户请求被提交到由若干HTTP注册模块(registered module)组成的管道(pipeline)中,即图1中Modules模块中的内容。这些模块能够对请求中携带的信息进行过滤和处理。客户请求中携带的信息的总体又被称为“调用上下文”(Call Context),在编程时与HttpContext对象对应。虽然HttpContext对象提供了Items属性可以对页面信息进行存取,但是不能把它看作与Session、Cache、Application等相同的用于储存状态信息的容器。当请求通过HTTP注册模块管道之后,HttpContext对象就拥有了对各种状态对象的引用。这个过程对于ASP.NET中所有的状态管理机制都是相同的,当最终对请求进行处理时,与请求关联的调用上下文(Call context,也就是HttpContext对象)不但与特定的Session对象绑定,也与全局(global)的Cache对象、Application对象绑定。3.Session State(续)
下面详细介绍在“之三中图2”的各主要部件:
SessionStateModule:是实现Session State的关键模块,它是Http的注册模块之一,位于图1中Modules的位置,它负责为每个用户设置会话状态。SessionStateModule模块为ASP.NET应用程序提供了许多与会话状态相关的服务,如会话标识符的生成(session ID generation)、cookieless会话管理(cookieless session management)、从外部会话状态提供程序(state provider)检索会话数据以及将数据绑定到请求的调用上下文(call context)。
Session ID Manager:负责从请求中获取session ID或给请求分配session ID。默认的Session ID Manager支持基于Cookie和基于URL的session ID管理模式。此外,ASP.NET也允许自定义实现Session ID Manager,以提供对自定义session ID管理模式的支持。如将session ID存储在查询字符串(qurey string)中,或存储在窗体(form)的一个字段(field)中。
Session State Store Provider:会话状态存储提供程序,主要负责会话状态数据的存取。ASP.NET提供了三种存储会话状态数据的方式:工作进程内(InProc)、状态服务器(StateServer)和SQLServer数据库,后面我们会详细讨论这三种方式。会话状态存储提供程序也可以由开发人员自定义实现,实现的方法请参照MSDN文档:实现会话状态存储程序MSDN。
Session State的运行时操作(runtime operation)主要由SessionStateModule模块完成,SessionStateModule模块被插入到对请求的处理过程中,并在页面处理程序(IHttpHandler和CustomHandler)执行前后起作用。整个过程可以分为下面三个阶段:
&216; AcquireRequestState阶段:SessionStateModule先在请求中查找是否存在session ID,如果有则根据session ID从会话状态存储提供程序中提取该session ID对应的状态数据,并通过反串行化(deserialization)操作生成会话状态字典(session state dictionary,图2中)。
&216; Session State数据存取阶段:由SessionStateModule会话状态字典在程序中对应于HttpSessionState类型的对象,该对象可以在编程方式下由我们熟悉的Page或HttpContext对象的Session属性获得。生成的会话状态字典包含代表状态信息的键/值对的集合,由页面处理程序(handler)进行读写(read/write)。
&216; ReleaseSessionState阶段:页面处理程序执行完后,Session又通过串行化(serialization)操作存储到SessionStateModule中。如果在执行页面处理程序的过程中Session中的值发生了改变,则Session ID Manager将为输出(Response)赋予一个新的Session ID。当Session ID Manager没有在请求中找到session ID,而在页面处理程序中创建了session以后,Session ID Manager也将为输出(Response)赋予一个新的Session ID。
3.Session State(续2)
图 3 Session State架构
图 3 Session State架构
图
3显示了ASP.NET页面与Session中存储的值之间交互的过程。在图3中,除了“Lock”以外,其它的内容在上面都已涉及到,这里再对Lock做一个简单的讨论。Lock是ASP.NET提供的一种读/写锁(reader/writer lock)机制,以处理对同一Session State的并发访问(Synchronizing Access)。当具有写权限的页面访问Session的时候,页面就对该Session添加了写锁(writer lock),其它的页面只能读取Session中的内容而不能修改,同样具有写权限的页面将在队列(queue)中等待,直到当前具有写权限的页面处理完毕,该Session的写锁(writer lock)被释放为止;当具有读权限的页面访问Session时,页面就对该Session添加了读锁(reader lock),此时其它的页面也只能读取该Session中的内容而不能修改,具有写权限的页面将在队列(queue)中等待,知道当前页面处理完毕,读锁(reader lock)被释放为止。想要了解如何设置读写锁,请参阅Underpinnings of the Session State Implementation in ASP.NET MSDN中“Synchronizing Access to the Session State”部分的内容。
3.Session State(续3)
以上已经详细描述了Session State的体系架构、以及实现的原理和过程。下面对Session State另一个重要的方面——三种会话状态存储模式,进行深入的探讨。
前面已经提到Session State有三种会话状态存储模式:InProc、StateServer和SQLServer。在对每一种模式深入探讨之前,先对它们做一个综合的评价。总体来说,这三种模式分别属于两种类型,其中InProc模式属于工作进程内的存储模式,而StateServer和SQLServer属于工作进程外的存储模式,这两种类型都有其自身的特点。由于采用工作进程外模式存储的状态信息需要在当前工作进程外存取,显然从实现的复杂程度和状态信息的读写速度上来看,InProc模式都要优于StateServer和SQLServer模式。但由于工作进程有可能在运行的过程中被回收或崩溃,存储在工作进程中的状态信息也会一同消失,而采用工作进程外的存储模式就没有这种担心。一旦工作进程崩溃,还可以再恢复到崩溃前的状态。StateServer模式和SQLServer模式之间还有区别,StateServer的状态信息是存储在工作进程外的一个单独的进程中,如果该进程崩溃状态信息也会丢失。因此,从应用程序可靠性(robust)上来讲,InProc、StateServer和SQLServer依次增强。此外,工作进程外的存储模式使得工作进程之间可以共享状态信息,这与ASP.NET中的Web园(Web Garden)模型和Web场(Web Farm)模型的实现密切相关。Web园是指多个ASP.NET工作进程运行在一台服务器上的多个CPU上,每个CPU运行一个工作进程;Web场是指多个ASP.NET工作进程运行在多个服务器上,每个服务器运行一个工作进程。在Web园和Web场中,运行在不同CPU或不同服务器上的工作进程必须协同工作,它们可以通过StateServer或SQLServer实现会话状态的共享。下面开始深入讨论这三种模式实现的原理。
1.InProc模式:这是ASP.NET默认的模式。在这种模式中,Session State被存储在工作进程中,更确切的讲,存储在Cache对象的一个私有插槽(private slot)中,这个私有插槽以session ID命名。也就是说InProc模式下的Session实际上是通过Cache对象实现的,SessionStateModule模块只是根据session ID从Cache中读取数据。那么什么是Cache对象的私有插槽呢?Cache对象中有两种类型的插槽(slot),一种是公共(public)的,一种是私有(private)的。当我们显式的采用Cache对象存取数据时,使用的就是公共插槽。与公共插槽不同的是,存储在私有插槽中的数据不能通过遍历Cache对象获取(ASP.NET1.0中,存储在私有插槽中的数据是可以通过遍历Cache对象获取到的,ASP.NET1.1以上的版本已经更正了这个bug)。上面我们讲到,SessionStateModule从会话状态存储提供程序(Session State Store Provider)中存取会话状态数据,是通过串行化(serialization)和反串行化(deserialization)操作完成的。实际上,在InProc模式中,并没有真正执行串行化和反串行化的操作。Cache对象的私有插槽中存储的值实际上是一个SessionStateItem类型的对象实例,可以在生成会话状态字典(HttpSessionState类型的对象)的时候直接插入到字典中。也就是说,你创建的任何类型的对象实例都可以在InProc模式中存储,包括不能被串行化的对象。另外要注意的是,由于在这种模式下,状态信息是直接存储在服务器的内存中。因此,存储的数据越多,占用服务器的内存资源就越多,对于应用程序的性能有潜在的影响。
2.StateServer模式:在这种模式下,会话状态被存储在工作进程外的一个单独的服务进程中,进程的名称为aspnet_state.exe。要采用这种模式,首先需要在网络应用程序的配置文件(web.config)中指定提供aspnet_state.exe服务的主机名和端口号(默认端口为42424),然后在指定的主机上启动aspnet_state.exe服务,默认它需要手动启动。需要注意的是,如果服务暂停,会话状态信息仍会保存在进程中;但如果服务停止会崩溃,会话状态就会全部丢失。而且这种模式下,会话状态的存取需要串行化和反串行化操作。
3.SQLServer模式:使用SQLServer模式则是直接将会话状态数据存储在SQLServer数据库表中,这是三种模式中可靠性最高的一种模式。使用这种模式,需要在网络应用程序的配置文件(web.config)中指定数据库连接字符串。ASP.NET提供了两类脚本,用于在SQL Server中配置存储会话状态的数据库环境:一类是InstallSqlState.sql和UninstallSqlState.sql,它们可以在SQL Server中创建一个名为ASPState的数据库和一些存储过程,而会话状态数据却存储在SQL Server的临时数据库TempDB中。这就意味着如果装有SQL Server的机器重启,会话状态就会丢失。另一类是InstallPersistSqlState.sql和UninstallPersistSqlState.sql,它们同样可以在SQL Server中创建ASPState数据库和存储过程,不同的是,会话状态数据存储在创建的数据库中而不是TempDB数据库中,这样就算机器重启会话状态也不会丢失。删除过时的会话状态数据的任务由一直保持运行状态的ASPState_Job_DeleteExpiredSessions负责,它要求SQLServerAgent服务处于运行状态。在这种模式下,会话状态数据的串行化和反串行化也是必须的。
以上三种模式的配置方式可以参见MSDN的Session-State Modes。
3.Session State(续4)
在StateServer和SQL Server模式中,主要的开销(overhead)在于串行化和反串行化操作。下面我们对串行化和反串行化做一个简单的介绍。
首先需要明确的是,并不是所有的对象都可以被串行化,且可以被串行化的对象效率也是不一样的。ASP.NET根据对象的类型提供了两种串行化方式:对于基本类型的对象,即String、DateTime、Boolean、byte、char以及所有的数字类型,ASP.NET采用一种内部的经过优化的串行化(serializer)程序;对于其它类型的对象,包括用户自定义类型,则采用.NET提供的二进制格式化程序(binary formatter)进行串行化。因此,基本类型对象的串行化效率较非基本类型的对象要高。大致估计,串行化和反串行化操作对应用程序性能的影响在15%到25%,如果采用的对象更加复杂,影响可能更大。因此,在使用工作进程外模式存储会话状态时,应尽可能将复杂对象分解为基本类型对象。要了解有关串行化和反串行化,请参阅Serialization and Deserialization MSDN。
下面再谈谈Session对象的生命周期。一般情况下,不管向服务器发送了多少次请求,只有当开始向Session中存储会话状态数据时,Session对象的生命周期才算真正开始,这一点不同于Cache和Application对象(Application对象的生命周期开始于向网络应用程序发送第一个请求时)。当Session对象为空,Session对象的生命周期还没有开始,Session ID Manager会为每一个请求页面赋予一个新的session ID,且每一次请求都不相同,直到有数据存储在Session对象中时,就按照图2所示的过程进行会话状态维护。有两个事件与Session对象的生命周期密切相关,分别是Session_OnStart(会话开始)和Session_OnEnd(会话结束)。如果为Session_OnStart事件定义了处理程序(event handler),那么Session的生命周期从第一次请求该页面就开始,该页面的Session ID一被赋予就不再改变,Session即使为空也会被存储,因此,从性能的角度考虑,只有在必要时才为Session_OnStart事件定义事件处理程序。Session_OnEnd事件只有在InProc模式下,且Session已经存在的前提下才能被触发,这是因为该事件是由Cache对象在删除失效的Session对象时触发,可在该事件的处理程序中通过编码终止Session对象。
最后,再补充一点。由上面的讨论可知,Session 对象的实现与Cache对象有着密切联系,尤其是再InProc模式下,但是Session不能提供和Cache那样丰富的缓存过期和依赖机制。在InProc模式下,Session对象中的数据是否失效,只能通过设定sliding time来控制,即在指定的时间间隔内没用使用Session中的数据,数据自动删除。且考虑Cache自身的特点,Session中的数据也可能在服务器端内存资源不足的情况下,由于内存回收而丢失。
至此,已经深入探讨了Session State的各个方面,这里推荐MSDN中关于Session State的两篇文章:一篇是Dino Esposito和Wintellect的Underpinnings of the Session State Implementation in ASP.NET MSDN;另一篇是Michael Volodarsky的Fast, Scalable, and Secure Session State Management for Your Web ApplicationsMSDN。本文有很多内容取自这两篇文章。