从更低的角度
这篇文章在一个底层的角度来关注一个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的人都是对Wenforms和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并且将其传入工作进程
![](https://i-blog.csdnimg.cn/blog_migrate/0be121fa5b8988fbabbbc526af3b0fc0.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/34031c708bfe702fe82d01ff5c6593aa.gif)