--A low-level Look at the ASP.NET Architecture
原文地址:http://blog.ncuhome.cn/llj098/Logs/2008/11/13/24898.html
英文地址:http://www.west-wind.com/presentations/howaspnetworks/howaspnetworks.asp
从更低的角度
这篇文章在一个底层的角度来关注一个web请求怎样到达asp.net框架,从web服务器,通过ISAPI。看看这些后面发生了什么,让我们停止对asp.net的黑箱猜想。
ASP.NET是一个非常强大用来创建web应用程序的平台,它为创建web应用程序提供了大量的灵活强大的支持。大多数人仅仅熟悉表层的WebForm和webservice,他们位于整个ASP.NET架构的最表层。在这篇文章里,我将会描述非常底层的ASP.NET并且解释一个web请求是如何从web服务器到达ASP.NET运行时,并且通过ASP.NET管道(pipeline)处理请求的。
对我来说理解一个平台的内部机制,能够然我自己得到相应的满意和舒适,同时也可以帮助我们写出更好的应用程序。了解这些工具是怎样作为整个框架的某一部分而互相配合,并且更加容易的找到问题的解决方案,更加重要的,在发生错误的时候,能够帮助你定位以及调试这个错误。这篇文章的目标就是从系统的角度来看ASP.NET,并且帮助理解,请求是如何到达ASP.NET处理管道的。就这点而言,我们将会看到核心引擎,以及一个web请求是如何终结的。很多东西都是你在日常的工作中不需要知道的,但是它有利于理解ASP.NET架构怎样路由到你经常编写的应用程序的高层代码的。
大多数使用ASP.NET的人都是对Webforms和WebService熟悉。这些高层的实现能够简化创建以web为基础的应用程序,并且,ASP.NET是一个驱动引擎,提供了对web服务器的底层接口,也为你在你的程序中用到的典型的高层前端服务的路由机制提供接口。WebForm和WebService仅仅是两个在ASP.NET框架核心上构建的非常经久耐用的HttpHandlers。
然而,ASP.NET在低层提供了更多的灵活性。HTTP Runtime和请求管道提供了所有的相同创建WebForm和WebService的能力,它们实际上也是由.NET托管代码实现。而且,你如果决定自己定制、创建一个比WebForm稍低层的平台,所有的ASP.NET的低层的这些功能、机制,你也同样可以使用。
WebForm显然是创建大多数web应用程序的最容易的方式,但是你在创建自定义内容的handlers或者对于进、出内容需要特殊的处理,或者你需要为另外一个应用程序创建一个定制的应用程序接口,使用低层的handlers或者modules能够给你更好的性能以及对一个web请求的更好的控制。你也可以绕过WebForm和WebService的这些高层实现提供的这些功能、机制直接在底层进行操作。
什么是ASP.NET
让我们以一个简单的定义开始:什么事ASP.NET?我喜欢把ASP.NET定义如下:
ASP.NET是一个使用托管代码完成的,从前到后处理web请求的,久经考验的框架。它并不仅仅是WebForm和WebService…
ASP.NET是一个请求处理引擎,它通过它的内部的管道将一个请求传送到一个开发者的代码上。实际上这个引擎完全独立于HTTP或者web服务器。事实上,HTTP Runtime是一个在IIS或者其他任何服务器之外的,您的应用程序的宿主环境。举一个例子,您可以将ASP.NET runtime放到一个Windows窗口中(点击获得更多详情http://www.west-wind.com/presentations/ASP.NETruntime/ASP.NETruntime.asp )
运行时为请求通过这个管道提供了一个复杂而又优雅的机制。有一系列的相关对象,大多数都是可以在请求的每一个层次,通过实现其子类或者实现事件接口来进行扩展。通过这个机制能够接触到非常低层的接口,例如缓存,权限验证等。你甚至能在接受请求的前后过滤内容,或者将满足特定要求的请求转到你的代码或者其他的URL地址。有很多不同的方法来完成相同的事情,而且所有的这些方法实现的都非常直接,这样,就可以灵活的根据性能以及开发难度来选择最好的方法。
整个的ASP.NET引擎都是托管代码完成的,并且,可以支持通过托管代码进行拓展
整个的ASP.NET引擎都是托管代码完成的,并且,可以支持通过托管代码进行拓展.这是一个对.NET框架是否能够开发出久经考验的、性能良好的框架的有力的证明。然而,给人印象最深刻的部分是ASP.NET的深思熟虑的架构,能够使得这个结构非常易用,提供了处理请求的任何一个部分的能力。
使用ASP.NET你能够完成 以前是ISAPI扩展和ISAPI筛选器领域的工作,虽然带有一些局限性,但是比ASP要好很多。ISAPI是一个非常底层的Win32形式的API,它仅有非常贫乏的接口,非常难创建经久耐用的应用程序。由于ISAPI非常的底层而且非常快速,它处在非托管开发层。这样ISAPI有些时候主要用做连接其他的应用程序平台的桥。这并不意味着ISAPI已经死了。事实上ASP.NET在微软的平台上,正是通过ISAPI的一个扩展和ASP.NET的运行时和IIS进行交互的。ISAPI提供了Web服务器的核心接口,并且ASP.NET使用非托管的ISAPI代码来向客户端接收,发送数据。ISAPI提供的数据,是通过一些通用的对象暴露出去的,像HttpRequest和HtttpReponse,他们通过托管代码对象,以一个非常好的,易接触的接口形式,对外暴露非托管代码的内容。
从浏览器到ASP.NET
让我们从一个典型的ASP.NET Web Request的生命周期的最初开始。一个请求,在浏览器里,在一个用户输入一个URL地址或者点击一个超链接,或者提交一个HTML表单(一个post类型的请求)。或者一个客户端的程序会调用ASP.NET的WebService,这个WebService也使用过ASP.NET进行服务的。在服务器端,IIS5或者6接收到请求。在最底层,ASP.NET通过一个ISAPI扩展和IIS进行交互。这样的一个请求通常会被路由到一个以aspx为扩展名的页面文件,但是如何处理这个请求,完全取决与HTTP handler的实现,这个handler为了处理指定的扩展名而创立起来。在IIS里,.aspx 被‘应用程序扩展’(也可以成为脚本映射) 映射到ASP.NET ISAPI dll - ASP.NET_isapi.dll.每一个触发ASP.NET的请求都是必须通过在ASP.NET_isapi.dll指明和注册的扩展名。
根据扩展名,ASP.NET将请求路由到相应的,负责响应请求的handler。举一个例子,asmx这是一个WebService的扩展名,它不会被路由到磁盘上面的一个页面文件,而是路由到一个WebService类里面。其他很多的映射已经被ASP.NET安装了,而且你也可以定义你自己的。所有的这些HttpHandlers都是在ASP.NET ISAPI里面指出,从而在IIS里面被映射,或者在web.config文件里面设置,路由到指定的HTTP Handler的实现。每一个handler,是处理指定的扩展名的一个.NET类,这个类可以简单到一个HelloWorld程序,也可以非常复杂,像一个ASP.NET page类或者 WebService 的实现。现在,理解扩展名是这种映射机制的基础,这种机制是ASP.NET用来从IIS获得一个用户请求然后将其路由到指定的处理请求的handler。
ISAPI是第一个也是性能最高的定制web请求处理的切入点。
ISAPI 连接
ISAPI是一个非常低层的非托管的win32API。这个接口根据ISAPI定义规范,非常的简单,还有优化过的性能。他们非常的底层-处理指针,用函数指针来进行回调-它们为开发者和工具提供最底层的,最好性能的,来处理IIS的接口。由于ISAPI非常的底层,他并适合创建应用程序级别的代码,而且,ISAPI趋向主要被用作 为高层工具提供应用程序服务器功能的 桥接口。举个例子,ASP和ASP.NET都是建立在ISAPI上面,还有Cold Fusion,运行在IIS上面的,大多数的Perl,PHP以及JSP的实现还有很多的第三方的解决方案,比如我的Web Connection framework for Visual FoxPro都是建立在ISAPI上面的。ISAPI是一个为高层应用程序提供接口的非常优秀的工具,这些接口抽象了ISAPI提供的信息。在ASP.NET和ASP中,这些引擎将ISAPI提供的信息抽象为像Request和Response这样的对象,使他们读取到ISAPI的Request信息。把ISAPI想象成铅锤。对于ASP.NET来说,ISAPI dll非常瘦小,仅仅是作为一个路由机制,以管道形式传送请求到ASP.NET运行时,所有的重型的处理甚至请求的线程管理,都在ASP.NET引擎和你的代码中。
依照协议,ISAPI支持ISAPI扩展和ISAPI筛选器。扩展是一个请求处理接口,并且提供处理web服务器的传入传出的逻辑,它本质上是一个事务接口。ASP.NET和ASP都是做为ISAPI扩展被实现的。ISAPI过滤器是一组接口,他们能 查看每一个进入IIS的请求,修改内容,或者改变类似于验证功能的行为。顺便提一句,在ASP.NET中,通过两个概念映射了类似ISAPI的功能:HTTPHandlers(扩展)和HttpModules(筛选器)。一会,我们看详细的内容。
ISAPI是标记着ASP.NET的请求的初始代码。ASP.NET映射了各种扩展名到ISAPI的扩展里,这些映射都在.NET Framework的目录下:
<.NET FrameworkDir>\ASP.NET_isapi.dll
你可以交互式的在IIS服务管理器里面看到这些映射,如图一.选择你的网站,然后“主目录”,“配置”,“映射”。
图一: IIS 映射各种扩展名到ASP.NET ISAPI,像 .ASPX 。通过这个机制,请求在web服务器层被路由到ASP.NET的处理管道。
你不应该手动的设置它们,因为.NET需要他们。另外你也可以使用ASP.NET_regiis.exe 工具来使得各种脚本映射得到正确的注册:
cd <.NetFrameworkDirectory>
ASP.NET_regiis – i
这就将会为整个的站点注册特定版本的ASP.NET运行时,创建各种客户端脚本库。注意,这里是注册在上面的文件夹安装的特定版本的CLR。ASP.NET_regiis命令的选项允许你可以单独的设置一个虚拟目录。每一个版本的.NET框架都有他自己版本的ASP.NET_regiis,你需要运行一个恰当版本的来注册一个网站或者一个虚拟目录。以ASP.NET2.0为例,你可以在IIS配置页面里面的ASP.NET选项选择.NET的版本。
IIS5和IIS6工作方式不同
当一个请求进入,IIS检查脚本映射,然后将请求路由到ASP.NET_isapi.dll。这个DLL进行的操作在IIS6和IIS5中明显不同,图2大概的展示了这个流程。
IIS5中直接宿主ASP.NET_isapi.dll在inetInfo.exe进程中,或者一个独立的进程中。当第一个请求来到这个DLL文件的时候,将会产生另外的一个新的进程– ASP.NET_wp.exe –并且路由请求到这个新生成的进程中。这个进程一次的加载,宿主.NET运行时。每一个请求都是先来到ISAPI然后通过命名管道路由到工作进程
图二– 从IIS到ASP.NET运行时的. IIS 5 and IIS 6以不同的方式处理 ASP.NET,但是总的来说,一旦到了ASP.NET的管道,就相同了。
IIS6,不像以前的服务器,它是完全为ASP.NET做过优化的
IIS 6 –应用程序池万岁
IIS6明显的改变了处理模型,IIS不再像ISAPI的扩展一样直接的处理任何不相关的可执行代码。取而代之的是,IIS6总是创建一个独立的工作进程—一个应用程序池—并且所有的请求都在这个进程里面,包括ISAPI dll的执行。
应用程序池是IIS6的一个重要改善,因为它们允许非常细粒度的控制指定进程执行的东西。可以为每一个虚拟目录或者一个站点设置应用程序池,所以你能够将每一个web应用程序分割到每一个进程中,这个进程和其他的应用程序的进程完全独立。如果其中的一个进程死掉了,也不会影响到其他的进程。
另外,应用程序池是高度可设置化的。你可以通过设置它的执行模拟级别 来设置其执行的安全环境,你可以为每一个Web应用程序定制权限。为ASP.NET的一个重要的改进就是应用程序池取代了大多数的在machine.config的进程模型。这在IIS里面比较难于管理,因为这个设置是全局的,而且不能在应用程序web.config中被继承。当IIS6运行的时候,进程模型设置大部分被忽略了忽略,取而代之的是从应用程序池中读取。我这里是说“大部分”,对于一些设置,像进程池的大小,IO线程仍旧在里面(配置文件)设置,因为应用程序池里面没有对应的设置。
因为应用程序池是外部的可执行的,而且这些可以很容易的进行监视和管理。IIS6提供了一些健康检测,重启,超时的选项,这些可以检测,大多数可以修正应用程序的错误。最后,IIS6的应用程序池并不依赖COM+,和IIS5的独立进程一样,它改进了性能和稳定性,尤其是内部使用了COM对象的应用程序。
尽管IIS6的应用程序池区分于可执行文件,他们通过直接的接触HTTP.SYS核心模块,而进行了高度的HTTP操作优化。进入的请求,直接的路由到相应的应用程序池。InetInfo仅仅扮演了一个管理、设置服务-大多说交互实际发生在HTTP.SYS和应用程序之间,所有的这些构成了一个比IIS5更加稳定,更加高性能的环境。尤其是对于一些静态内容和ASP.NET应用程序。
一个IIS6的应用程序池也有ASP.NET内在的认识,而且一个ASP.NET能够和新的底层的API进行交互,这使得ASP.NET直接接触到HTTP Cache的API,也就使得能够从ASP.NET层来控制Web服务器的缓存。
在IIS6,ISAPI扩展运行在一个应用程序池的工作进程中。.NET运行时也运行在这个进程,所以.NET运行时和ISAPI 扩展的交互是进程内的,这样肯定是比一定要使用命名管道接口的IIS5效率更高。尽管IIS两个版本的宿主模型是不同的,但是到达托管代码之后都是相同的了,只有在做请求路由的时候有一些不同。
ISAPIRuntime.ProcessRequest() 方法是进入ASP.NET的第一个入口
进入到.NET运行时
实际上进入.NET运行时,是通过一系列没有给出文档说明的类和接口。通过微软,很少能够了解这些接口和类,而且微软并不想谈论这些细节,因为他们认为这些实现的细节几乎对创建一个ASP.NET应用程序没有影响。
工作进程ASP.NET_WP.EXE (IIS5)和W3WP.EXE (IIS6)宿主.NET运行时,并且ISAPI DLL 通过底层的COM调用了一些少量的非托管接口,而最终调用到了一个ISAPIRuntime的子类。第一个入口就是这个没有文档说明的ISAPIRuntime类,它通过COM将IISAPIRuntime接口暴露给调用者。这些COM接口,底层的以不了解的接口,意味着从ISAPI扩展到ASP.NET的内部调用。图3显示了这个接口,在Lutz Roeder的优秀的.NET Reflector 工具(http://www.aisto.com/roeder/dotnet/)。反射程序集视图和反编译器,这使得我们能够非常容易的看到反编译的代码(用IL,C#,VB),这是一个非常好的方法来谈就这样的过程。
图3 –如果你想深入研究底层的接口,打开Reflector,并且打开 System.Web.Hosting 命名空间。进入ASP.NET的入口点通过COM接口而被ISAPI dll调用,这就获得了一个非托管的指针,指向了ISAPI ECB.。ECB包含了访问ISAPI的全部接口,能够获得请求的数据,也能够将数据返回给IIS。
IISAPIRuntime接口在ISAPI扩展的非托管代码和托管代码之间。如果你看一个这个类,你会发现有一个签名如下的方法:
[return: MarshalAs(UnmanagedType.I4)]
int ProcessRequest([In] IntPtr ecb,
[In, MarshalAs(UnmanagedType.I4)] int useProcessModel);
ecb这个参数是ISAPI扩展控制模块(Extension Control Block –ECB),这个方法将非托管的资源传到ProcessRequest方法中。这个方法获得ECB并且 通过Request和Response对象将它作为输入输出的基础。一个ISAPI ECB包含了所有的底层请求信息,包括服务器信息,一个来组织变量的输入流同时也有一个用来写回数据给客户端的输出流。这个单独的ecb基本提供了所有的ISAPI请求的所有的功能,而ProcessRequest方法是这个资源和托管代码交互的出口和入口。
ISAPI扩展异步的处理请求。这个模型中,ISAPI扩展立刻回报给工作进程或者IIS线程,而保持当前的请求的ECB存活。ECB包括一个机制允许ISAPI知道请求已经完成(通过ecb.ServerSupportFunction),然后释放ECB。这个异步处理立刻释放ISAPI工作进程,并且卸载进程,而转到由一个ASP.NET管理的独立的线程。
ASP.NET接收到这个ecb引用,用它来获取一些关于当前请求的信息,比如服务器变量,POST数据,也包括向服务器返回输出数据。ECB存活到请求完成,或者IIS超时并且,ASP.NET会继续和其进行交互,直至请求完毕。输出被写入到ISAPI的输出流(ecb.WriteClient()),并且,当请求完成的时候,ISAPI扩展被通知请求完成,ECB可以被释放。这个实现非常的高效,因为.NET类完全扮演的是对高性能的非托管代码简单的封装,
加载.NET - 有一些神秘Loading .NET
让我们回到这里的一个步骤:我跳过了.NET运行时是怎么加载的。这里的事情有一点模糊,这个过程,我没有获得任何的文档,并且我们在谈本地代码,而又没有简单的方法反编译ISAPI DLL把它找出来。
我的最好的猜想是,当第一个ASP.NET映射扩展名被请求的时候,ISAPI扩展引导了.NET运行时。一旦这个运行时存在了之后,如果当前没有,非托管代码可以为给定的虚拟路径请求一个ISAPIRuntime的实例对象。每一个虚拟目录都会有它自己的AppDomain(应用程序域),在AppDomain里,ISAPIRuntime存在于一个独立的应用程序引导的过程。实例化看起来像发生在COM里面,因为接口方法是一个COM可调用的方法。
为了创建ISAPIRuntime实例,System.Web.Hosting.AppDomainFactory.Create()方法被调用,当某一个虚拟目录第一次被请求的时候。这开始了‘应用程序’引导过程。这个调用获得了参数的类型,模块的名称以及虚拟路径的信息—对于应用程序来说,这个ASP.NET用来创建一个AppDomain以及运行指定虚拟目录的ASP.NET应用程序。这个HttpRuntime派生对象在一个新的AppDomain里面被创建。每一个虚拟目录都被宿主在一个独立的AppDomain中,仅仅加载指定的应用程序的请求。ISAPI扩展来管理HttpRuntime对象的实例,路由相应的请求到正确的请求的虚拟路径上。
图4 – 从ISAPI请求到ASP.NET的HTTP管道的传输过程,使用到了一些没有文档说明的类和接口,并且需要一些工厂方法调用。通过其调用者把一个引用放在IISAPIRuntime接口中(这个接口触发了ASP.NET请求处理)每一个Web应用程序运行在其自身的AppDomain中。
回到运行时
在这时,我们已经有了一个被ISAPI扩展激活,可被其调用的ISAPIRuntime的实例。一旦运行时运行起来,ISAPI代码将会调用到ISAPIRuntime.ProcessRequest()这个方法,这个方法是真正的进入ASP.NET管道的入口。这个流程已经显示在图4中。
记住,ISAPI是一个多线程的,这样请求将会以多线程的方式进如ASP.NET,通过ApplicationDomainFactory.Create()返回的引用。列表1显示了IsapiRuntime.ProcessRequest反编译的结果,这个方法接收一个ISAPI ecb对象,这个方法是线程安全的,所以,多线程的ISAPI可以同时的安全的调用这个单独返回的对象实例。
Listing 1: 处理请求的方法获得了一个ISAPI Ecb并且将其传入工作进程
public int ProcessRequest(IntPtr ecb, int iWRType) { HttpWorkerRequest request1 = ISAPIWorkerRequest.CreateWorkerRequest(ecb, iWRType); string text1 = request1.GetAppPathTranslated(); string text2 = HttpRuntime.AppDomainAppPathInternal; if (((text2 == null) || text1.Equals(".")) || (string.Compare(text1, text2, true, CultureInfo.InvariantCulture) == 0)) { HttpRuntime.ProcessRequest(request1); return 0; } HttpRuntime.ShutdownAppDomain("Physical application path changed from " + text2 + " to " + text1); return 1; }
这里的代码并不重要,并且记住,这是你将从来不会直接接触到的,反编译出来的框架内部的代码,而且这些代码可能在以后会改变。这意味着证实在后台发生了什么。ProcessRequest接收到非托管的ECB引用,并且将它传送到ISAPIWorkerRequest对象中,这个对象负责为当前请求创建请球上下文,就像在Listing2中显示的一样。
System.Web.Hosting.ISAPIWorkerRequest类是HttpWorkerRequest的一个抽象子类。它的职责是为输入输出创建一个抽象的视图,这个作为整个web应用程序的输入。注意另外一个工厂方法: CreateWorkerRequest,做为第二个参数,它接收到了要创建的worker request的类型,这里有三种不用的版本:ISAPIWorkerRequestInProc, ISAPIWorkerRequestInProcForIIS6, ISAPIWorkerRequestOutOfProc。这个对象在每一个请求进来之后被创建,这个对象是Request和Response对象基础,他们从WorkerRequest里接收他们的数据和流。
这个HttpWorkerRequest抽象类用来为底层的接口提供一个高层的抽象,所以不管这个数据是来自一个CGI Web服务器,一个浏览器,或者一些订制的机制。关键的是ASP.NET能够以同样的方式来获得信息。
对于IIS的抽象,以一个ISAPI ECB模块。在我们的请求过程中,ISAPIWorkRequst和IISAPI ECB交互,当需要的时候,通过它获得数据。Listing2显示展示了如何获得query string 的值。
Listing 2: 一个使用非托管的ISAPIWorkerRequest 的方法
// *** Implemented in ISAPIWorkerRequest public override byte[] GetQueryStringRawBytes() { byte[] buffer1 = new byte[this._queryStringLength]; if (this._queryStringLength > 0) { int num1 = this.GetQueryStringRawBytesCore(buffer1, this._queryStringLength); if (num1 != 1) { throw new HttpException("Cannot_get_query_string_bytes"); } } return buffer1; } // *** Implemented in a specific implementation class ISAPIWorkerRequestInProcIIS6 internal override int GetQueryStringCore(int encode, StringBuilder buffer, int size) { if (this._ecb == IntPtr.Zero) { return 0; } return UnsafeNativeMethods.EcbGetQueryString(this._ecb, encode, buffer, size); }
ISAPIWorkerRequest实现了一个高层的封装的方法,这个方法调用底层的用来和访问底层非托管的API的Core方法。Core 方法在ISAPIWorkerRequest实例的子类中实现,这样,为其宿主的环境提供实现方法。这样构造了一个更加容易插拔的环境,方便添加一些新的服务器或者服务ASP.NET的其他平台的接口的实现。也有一个辅助类System.Web.UnsafeNativeMethods。很多的这些方法,他们就是通过操作ISAPI ECB结构体来完成对ISAPI扩展的调用。
HttpRuntime, HttpContext, and HttpApplication – Oh my
当一个请求到达,被路由到ISAPIRuntime.ProcessRequest()方法。这个方法继而调用HttpRuntime.ProcessRequest,这个方法做了很多重要的事情(使用Reflector 查看System.Web.HttpRuntime.ProcessRequestInternal):
- 为请求创建一个新的HttpContext实例
- 得到一个HttpApplication 实例
- 调用 HttpApplication.Init() 来建立管道事件。
- Init() 触发 HttpApplication.ResumeProcessing(),这个方法开始ASP.NET管道处理
首先,一个新的HttpContext对象被封装了ISAPI ECB的 ISAPIWorkerRequest创建,传递。这个Context在整个的请求生命周期中都可用,总是可以通过静态的HttpContext.Current 属性来获得。就像名字提示的,HttpContext对象表示了当前激活的请求的上下文,因为它包含了在请求的生命周期中,所有的你要接触到的重要的典型的对象: Request, Response, Application, Server, Cache.在处理请求的任何时候,HttpContext.Current让你接触到所有的这些对象。
HttpContext对象同时包含了一个非常有用的集合,允许你用来存储一些请求指定的数据。Context对象从请求周期被创建,当请求完成的时候释放,所以存储在这个集合的数据仅仅对应当前的请求。一个很好的应用的例子是一个时间请求记录的机制,这里你想记录请求开始和请求结束的时间,通过在Listing3所示,在Global.asax 里面,Application_BeginRequest 和Application_EndRequest 方法。HttpContext是你的朋友,你可以自由的使用,在不同的请求或者页面处理的部分。
Listing 3 – 使用 HttpContext.Items 结合来让你在管道事件之间保存数据
protected void Application_BeginRequest(Object sender, EventArgs e) { //*** Request Logging if (App.Configuration.LogWebRequests) Context.Items.Add("WebLog_StartTime", DateTime.Now); } protected void Application_EndRequest(Object sender, EventArgs e) { // *** Request Logging if (App.Configuration.LogWebRequests) { try { TimeSpan Span = DateTime.Now.Subtract( (DateTime)Context.Items["WebLog_StartTime"]); int MiliSecs = Span.TotalMilliseconds; // do your logging WebRequestLog.Log(App.Configuration.ConnectionString, true, MilliSecs); } catch (Exception ee) { throw ee; } } }
一旦Context对象被创立,ASP.NET需要路由进入的请求到相应的应用程序/虚拟目录,通过一个HttpApplication对象。每一个ASP.NET应用程序被需建立一个虚拟目录(或者根根目录)每一个‘application’独立的处理。
HttpApplication像一个典礼的主人,处理动作在这里开始
你的域的主人: HttpApplication
每一个请求都被路由到一个HttpApplication对象。HttpApplicationFactory类根据你的ASP.NET应用程序的负载情况,为其创建一个HttpApplication对象池,并且为每一个请求处理这些引用。这个池的容量受限于设置在machine.config的ProcessModel键里面的MaxWorkerThreads的值,默认值是20.
然而这个池只启动了少量对象,通常是一个然后同时进入的请求多了,池中对象将会增长。池是被监视的,在负载量大的时候,将会增长到它的容量的最大值,当负载下降的时候,池的容量又会降低。
HttpApplication是你指定的Web应用程序的外部容器,并且它映射到定义在Global.asax的文件中。他是进入HttpRuntime的第一个点,如果你看Global.asax(或者其后置代码)你会发现,他是派生自HttpApplication的一个类:
public class Global : System.Web.HttpApplication
HttpApplication的主要目的是扮演了Http管道的事件控制者,所以它的接口主要由事件组成。事件是多方面的,包括:
- BeginRequest
- AuthenticateRequest
- AuthorizeRequest
- ResolveRequestCache
- AquireRequestState
- PreRequestHandlerExecute
- …Handler Execution…
- PostRequestHandlerExecute
- ReleaseRequestState
- UpdateRequestCache
- EndRequest
这些事件都在Global.asax文件中通过以Application_为前缀的空方法实现。举个例子,Application_BeginRequest(), Application_AuthorizeRequest().这样的处理方法,非常的方便,因为在应用程序中,他们经常会被用到,这样你就不用显示的创建事件处理的委托。
理解每一个ASP.NET虚拟用应程序运行在它自己的AppDomain中,然而在这个AppDomain中,有多个HttpApplication实例在同时运行,被一个ASP.NET的一个池来进行管理。这样,多个请求就可以同时被处理,而且没有互相的影响。
来看一看AppDomain,线程和HttpApplication的关系,看看Listing4的代码。
Listing 4 – 显示了AppDomain,线程和HttpApplication实例的关系
private void Page_Load(object sender, System.EventArgs e) { // Put user code to initialize the page here this.ApplicationId = ((HowASP.NETWorks.Global) HttpContext.Current.ApplicationInstance).ApplicationId; this.ThreadId = AppDomain.GetCurrentThreadId(); this.DomainId = AppDomain.CurrentDomain.FriendlyName; this.ThreadInfo = "ThreadPool Thread: " + System.Threading.Thread.CurrentThread.IsThreadPoolThread.ToString() + "<br>Thread Apartment: " + System.Threading.Thread.CurrentThread.ApartmentState.ToString(); // *** Simulate a slow request so we can see multiple // requests side by side. System.Threading.Thread.Sleep(3000); }
这部分代码运行的结果在图5中显示,在两个不同的浏览器中来访问这个示例页面,来看这不用的Id
图 5 –通过同时用两个浏览器访问,你可以看到AppDomain,Application池还有请求进程怎样互相影响,当多个请求到达的时候,你会发现线程,Application的Id都改变了,AppDomain却保持不变。
你会注意到,AppDomain 的ID保持不变,而HttpApplication的ID在大多数请求中都变化了,尽管他们有可能会重复。这些HttpApplication用过了,会在后面的请求中复用,所以Id是会重复出现的。注意,Application实例并不绑定到指定的线程,他们只是被分配到当前请求的线程中。
线程来自 .NET的ThreadPool,而且,默认的是Multithreaded Apartment (MTA)这种形式的线程。你可以在ASP.NET页面中重写这部分,通过在@Page指令设置ASPCOMPAT="true" 属性,ASPCOMPAT意味着提供给COM组建一个安全的环境来运行,并且,ASPCOMPAT使用特殊的Single Threaded Apartment (STA)线程来服务这些请求。STA的线程被搁置、合并,因为他们需要特殊的处理。
事实上,HttpApplication对象都在同一个AppDomain中非常重要。这正是ASP.NET怎样保证修改了web.config或者独立的页面,可以被整个的AppDomain所识别。在web.config里面修改值,可以引起AppDomain的关闭和重启,这使得所有的HttpApplication都发现了这个变化,因为AppDomain重新加载的时候,他重新读取了信息。所有的静态成员都重新加载了,因为AppDomain重新加载,所以,应用程序从应用程序配置文件读取设置的时候这些值都被更新了。
来看这个例子。访问ApplicationPoolsAndThreads.aspx页面,并且注意AppDomain的Id。然后修改一下web.config(添加一个空格并且保存)。然后重新读取这个页面,你会发现AppDomain已经被重新创建了。
本质上,当这发生后,web应用程序/虚拟目录完全的‘重启’了。所有的已经在管道中的请求,将会继续的通过现在已经存在的管道继续运行,同时,所有的新的请求都会被路由到新的AppDomain。为了处理那些‘挂起的请求’,在这些请求超时后或者甚至请求还在进行的时候,ASP.NET会强制关闭旧的AppDomain。这样,实际上在某一特定时间点,可以存在两个相同的AppDomain,为同一个HttpApplication服务,旧的AppDomain关闭,新的AppDomain里面的Application对象将会急剧增加。两个AppDomain都继续服务,直到旧的一个将所有的请求都运行完毕,旧的将会被关闭,只留下新的AppDomain。
ASP.NET管道的流程
HttpApplication通过触发指示你的应用程序状态的事件,来负责请求的流程。这发生在HttpApplication.Init()方法中(用Reflector看System.Web.HttpApplication.InitInternal 和 HttpApplication.ResumeSteps() ),这个方法连续的创建并触发了一系列的事件,包括了调用执行所有的handlers.事件处理自动的映射在global.asax里面的事件,并且,他们也映射所有已经附加了的HTTPModule,本质上,HTTPModule是一个形象化了事件槽。
HttpModules 和 HttpHandlers a都通过web.config里面条目而被动态加载,并且将其绑定到事件链。HttpModules是实际的HttpApplication的事件处理者,而HttpHandlers是一个终点,用来处理‘应用程序级的请求处理’的。HttpModules 实际是HttpApplication的事件处理器。
Modules 和 Handlers的加载,附加到调用链都做为HttpApplication.Init()方法的一部分、图6显示了各种事件以及他们出发的时间和触发影响的部分。
图 6 – ASP.NET HTTP管道的事件流程。HttpApplication对象的事件驱动贯穿管道。 Http Modules能够拦截这些事件,进而重写或者增强已有的功能。
HttpContext, HttpModules 和 HttpHandlers
HttpApplication自身并不知道传送进来的数据,他仅仅是一个通信对象,通过事件来进行交互。他触发事件,并且将信息通过HttpContext对象传递到被调用的方法中。当前请求的状态数据存储在我们前面提到的Httpcontext对象。它提供了所有请求的数据,并且在管道中,伴随着每一个请求从开始到结束。图7显示了通过ASP.NET管道的流程,注意Context对象从开始到请求的结束,都可以用来存储信息,在一个事件方法中存贮信息,在后面的事件方法中获得这个数据。
一旦管道开始,HttpApplication如图6一样,开始一个接着一个的触发事件。每一个事件处理器被调用,如果事件被调用这些处理器执行他们的任务。这个过程的主要目的是最终调用HttpHandler处理一个请求。Handlers是处理ASP.NET请求的核心的机制,经常位于应用程序级代码的执行。记住,ASP.NET 页面和WebService框架,都是作为HTTPHandler的实现,在这里请求的核心处理过程被执行。Modules往往是更核心的性质,用来准备或者发送处理交付于Handlers的Context。ASP.NET中,典型的默认的Handlers是Authentication, pre-processing的Caching 和 发送处理请求的编码机制。
有很多的信息在HttpHandlers 和 HttpModules中,但是为了保持这篇文章的一个合理的长度,我下面只是简短介绍一下handlers。
HttpModules
随着请求通过管道,一系列的事件在HttpApplication对象中被触发。我们已经在Global.asax中看到了这些事件。这种方法是应用程序指定的,这样,就不一定总是你想要的。如果你想创建一个广义的HttpApplication事件处理,而且能够可插拔的放到任何一个应用程序中,你可以使用HttpModules,他们是可以复用的,而且,不需要程序代码指定,只需要在web.config里面设置一下。
Modules本质上像筛选器,就像一个在ASP.NET请求层的ISAPI Filter。Modules允许附加事件到每一个通过ASP.NET HttpApplication对象的请求。这些Module在外部的程序集的类里面,在web.config里面设置,并且当应用程序启动的时候,随着应用程序的启动而加载。通过实现指定的接口和方法,将事件添加到HttpApplication事件链中。多个HttpModules能够附加事件处理代码到相同的事件上,这些附加的事件处理的顺序根据在web.config里面设置的一样:
<configuration> <system.web> <httpModules> <add name= "BasicAuthModule" type="HttpHandlers.BasicAuth,WebStore" /> </httpModules> </system.web> </configuration>
注意,你需要指定一个类型全名,还有一个程序集的名字.
Modules允许你看到每一个请求并且基于触发事件形式的执行动作。Module非常适于修改request或者response的内容,用以提供定制的身份验证或者为每一个请求执行预处理。很多的ASP.NET的特性,像身份验证以及Sesion引擎都是通过HTTPModule来实现的。
虽然,HTTPModule感觉上像ISAPI Filter,他们可以查看每一个通过ASP.NET应用程序的请求,但是他们只是能够监视映射到ASP.NET应程序或者ASP.NET虚拟目录的请求。这样,你可以查看ASPX文件,或者其他的映射到ASP.NET的扩展名。但是你不能监视一个标准的.HTM或者图片文件,除非你把他们的扩展名显式的映射到ASP.NET ISAPI dll,就像图1显式的。一个Module的经常被用作,过滤指定文件夹下面的图片,然后显式一个‘SAMPLE’覆盖在每一个图片上面,通过使用GDI+。
实现一个HTTPModule非常的容易:你必须实现IHttpModule接口,这个接口仅仅有两个方法,Init()和Dispose().这个时间参数是一个HttpApplication对象,通过他你可以访问Httpcontext对象,在这个方法中你可以接触到HttpApplication的事件。举个例子,如果你想附加AuthenticateRequest事件你可以像Listing5一样。
Listing 5: 一个基础的HTTP Module非常好实现
public class BasicAuthCustomModule : IHttpModule { public void Init(HttpApplication application) { // *** Hook up any HttpApplication events application.AuthenticateRequest += new EventHandler(this.OnAuthenticateRequest); } public void Dispose() { } public void OnAuthenticateRequest(object source, EventArgs eventArgs) { HttpApplication app = (HttpApplication)source; HttpContext Context = HttpContext.Current; //... do what you have to do ... } }
记住,你的Module访问的是HttpContext对象,通过其就能获得其他的管道对象,就如Response和Request,这样你可以获得输入等等,但是记住,这些不一定在后面的事件链中还有效。
你可以附件多个事件在Init()方法中,这样你就可以通过一个Module来管理不同的功能操作。然而,最好将不同的逻辑放在不同的类中使得Module真正的模块化。在你要实现的很多功能中,你需要附加到多个事件-举个例子,一个日志filter需要早BeginRequest中记录Request的开始时间,而在EndRequest中记录请求的完成时间。
注意HttpModules 和 HttpApplication 的事件events: Response.End() 或者 HttpApplication.CompleteRequest()将会切断HttpApplication对象或者Module的事件链。See the sidebar “Watch out for Response.End() “ for more info.
HttpHandlers
Modules相当的底层而且,对应的是对应ASP.NET应用程序的每一个请求。HTTP Handler则更加侧重于对于一个指定的请求的操作,通常一个页面都被映射到Handler.
实现Http Handler要求非常基础,但是通过访问HttpContext对象可以获得强大的功能。Http Handler通过实现一个非常简单的接口IHttpHandler来实现(或者其异步的版本的IHttpAsyncHandler),它仅仅包含了一的方法ProcessRequest()和一个IsReusagable属性。关键的是,ProcessRequest()获得了一个HttpContext对象的实例,这个方法负责处理Web请求,从开始到结束。
一个简单的方法?非常的简单,对么?确实,一个简单的接口,但是却不是那么的简单!记得WebForm和WebService都是做为HTTPHandler的实现的,所以,很强大的功能封装在这一个看似简单的接口中。关键点是,接触到HTTP Handler的时候,ASP.NET的内置对象已经为开始处理请求而创建和设置好了。关键就是HttpContext对象,他提供了所有的请求相关的功能获得输入信息,输出信息到Web服务器。
一个HTTPHandler所有的动作发生都是通过调用这个单独的ProcessRequest().这个可以简单的像:
public void ProcessRequest(HttpContext context) { context.Response.Write("Hello World"); }
也可以完全实现一个象WebForm Page引擎,可以输出复杂格式HTML模板。这点完全取决与你的决定,你到底如何用这个简单,却有强大的接口!
因为你可以使用Context对象,你可以获得Request,Response,Session和Cache对象,这样你有了所有的ASP.NET请求的特性, 你可以找到用户提交的内容,也可以设置返回客户端的内容。记住Context对象,他是你的朋友,在这个ASP.NET请求的生命周期中!
Handler的关键性的操作应带是最终的把output输出结果到Response对象,或者更具体的说是Response对象的OutputStream。这个output返回客户端的信息。在背后,ISAPIWorkerRequest负责将OutputStream返回到ISAPI ecb.WriteClient方法,执行了IIS输出的过程。
![](http://www.west-wind.com/presentations/howaspnetworks/Figure%207.png)
图 7 – ASP.NET请求管道流程通过一系列事件接口,提供了很大的灵活性。Application扮演了一个宿主容器的角色,它加载了Web应用程序,并且随着请求的进入和在管道中的传递而触发事件。每一个请求都是通过相同的路径,通过HTTP Filters和设置了的Modules。Filters能够检测通过管道的每一个请求,而Handlers允许实现用用程序的逻辑和接口,就像WebForm和WebService。为了提供应用程序的输入和输出Context在整个过程提供了请求的相关信息。
WebForm在这个基础框架上面,通过实现一个HTTPHandler以及更高层的接口,然而最终,Wenforms的Render()方法简单的使用一个HtmlTextWriter对象将其最终的输出结果写入到context.Response.OutputStream。这样,最终,一个高层的工具,像WebForms仅仅是一个Request和Response对象的高层的抽象。
你可能想知道,这点上,你是否需要处理HTTPHandler。毕竟,WebForm提供了简单的可访问的HTTPHandler实现,那么我们为什么要放弃这个灵活性而不厌其烦的做一些底层的事情呢?
WebForm对于生成复杂的HTML页面和需要图形布局工具,模板化页面的商业逻辑非常的好。但是,WebForm执行了很多的增加消耗的任务。如果你仅仅想在系统中读取一个文件,并将其返回,那么你可以跳过Web Form page框架,直接处理文件。如果你做的事情像从数据库提供图片,你也不需要Page框架—你不需要模板,而且没有一个UI。没有理由创建一个页面对象和Seesion并且处理页面级别的事件.
所以handlers更加高效。Handler也可以完成WebForm不能完成的任务。例如,他能够处理一个请求,不需要磁盘上有物理文件。 做这个,你需要在图1中的应用程序扩展对话框中。
关闭“检查文件是否存在”选项。
对于内容提供者是通用的,就像动态图片处理,XML服务器,Url重定向提供构造的Url,下载管理等等,这些都不是适合Wenform引擎。
对你来说,我介绍的足够了么?
恩,我们这里已经介绍了处理整个请求的过程。有很多的底层信息,我没有仔细的讲HTTPHandler和HTTPModule具体工作细节。挖掘这些信息需要一些时间,在理解ASP.NET怎样工作上面,希望能给你和我自己一样的满意程度。
在结束之前,让我们简短的回顾一下从IIS到handler的事件序列:
- IIS获得请求
- 检测脚本映射,映射到ASP.NET_isapi.dll
- 触发工作进程(ASP.NET_wp.exe 在 IIS5 或者 w3wp.exe 在 IIS6)
- .NET运行时加载
- IsapiRuntime.ProcessRequest()通过非托管代码调用
- IsapiWorkerRequest created once per request
- IsapiWorkerRequest 每一次请求创建一次
- HttpRuntime.ProcessRequest() called with Worker Request
- 通过传进Work Request, HttpContext对象被创建
- HttpApplication.GetApplicationInstance() called with Context to retrieve instance from pool
- HttpApplication.Init() 调用,并且启动管道事件序列,附加Modules和Handler
- 被调用,开始处理进请求
- 管道事件触发
- Handlers被调用,并且ProcessRequest 方法执行
- 控件返回管道并且发送请求事件触发
通过这个简单的列表,把这些是如何组合起来的记住会更容易。我不时的来看它来记忆。现在,我们回到工作上,继续做一些不抽象的…
尽管,这里我说的是基于ASP.NET1.1,但是ASP.NET2.0中,并没有改变这些底层的处理过程。
非常感谢微软的Mike Volodarsky来审阅这篇文章,并且提了一些附件的提示并且 Michele Leroux Bustamante提供了ASP.NET管道请求基础信息。