ASP.NET底层架构 22
PS:不知道翻译的文章能不能放到首页,如果不行的话还请各位大大移走,谢谢.
原文地址:http://www.west-wind.com/presentations/howaspnetworks/howaspnetworks.asp
了解ASP.NET底层架构
进入底层
这篇文章以非常底层的视角讲述了Web请求(request)在ASP.NET框架中是如何流转的,从Web服务器,通过ISAPI直到请求处理器(handler)和你的代码.看看在幕后都发生了些什么,不要再把ASP.NET看成一个黑盒了.
ASP.NET是一个非常强大的构建Web应用的平台,它提供了极大的灵活性和能力以致于可以用它来构建所有类型的Web应用.绝大多数的人只熟悉高层的框架如WebForms和WebServices-这些都在ASP.NET层次结构在最高层.在这篇文章中我将会讨论ASP.NET的底层机制并解释请求(request)是怎么从Web服务器传送到ASP.NET运行时然后如何通过ASP.NET管道来处理请求.
对我而言了解平台的内幕通常会带来满足感和舒适感,深入了解也能帮助我写出更好的应用.知道可以使用哪些工具以及他们是怎样作为整个复杂框架的一部分来互相配合的可以更容易地找出最好的解决方案,更重要的是可以在出现问题时更好的解决它们.这篇文章的目标是从系统级别了解ASP.NET并帮助理解请求(request)是如何在ASP.NET的处理管道中流转的.同样,我们会了解核心引擎和Web请求如何在那里结束.这些信息大部分并不是你在日常工作时必须了解的,但是它对于理解ASP.NET架构如何把请求路由到你的代码(通常是非常高层的)中是非常有益的.
不管怎么样,ASP.NET从更低的层次上提供了更多的灵活性.HTTP运行时和请求管道在构建WebForms和WebServices上提供了同样的能力-它们事实上都是建立在.NET托管代码上的.而且所有这些同样的功能对你也是可用的,你可用决定你是否需要建立一个比WebForms稍低一点层次的定制的平台.
WebForms显然是最简单的构建绝大多数Web接口的方法,不过如果你是在建立自定义的内容处理器(handler),或者有在处理输入输出内容上有特殊的要求,或者你需要为另外的应用建立一个定制的应用程序服务接口,使用这些更低级的处理器(handler)或者模块(module)能提供更好的性能并能对实际请求处理提供更多的控制.在WebForms和WebServices这些高层实现提供它们那些能力的同时,它们也对请求增加了一些额外负担,这些都是在更底层可以避免的.
ASP.NET是什么
让我们以一个简单的定义开始:什么是ASP.NET?我喜欢这样定义ASP.NET:
ASP.NET是一个复杂的使用托管代码来从头到尾处理Web请求的引擎.
它并不只是WebForms和WebServies…
ASP.NET是一个请求处理引擎.它接收一个发送过来的请求,把它传给内部的管道直到终点,作为一个开发人员的你可以在这里附加一些代码来处理请求.这个引擎是和HTTP/Web服务器完全分隔的.事实上,HTTP运行时是一个组件,使你可以摆脱IIS或者任何其他的服务器程序,将你自己的程序寄宿在内.例如,你可以将ASP.NET运行时寄宿在一个Windows form程序中(查看http://www.west-wind.com/presentations/aspnetruntime/aspnetruntime.asp可以得到更加详细的信息)
运行时提供了一个复杂但同时非常优雅的在管道中路由请求的机制.其中有很多相关的对象,大多数都是可扩展的(通过继承或者事件接口),在几乎所有的处理流程上都是如此.所以这个框架具有高度可扩展性.通过这个机制,挂接到非常底层的接口(比如缓存,认证和授权)都变得可能了.你甚至可以在预处理或者处理后过滤内容,也可以简单的将符合特殊标记的请求直接路由你的代码或者另一个URL上.存在着许多不同的方法来完成同一件事,但是所有这些方法都是可以简单直接地实现的,同时还提供了灵活性,可以得到最好的性能和开发的简单性.
整个ASP.NET引擎是完全建立在托管代码上的,所有的扩展功能也是通过托管代码扩展来提供的
整个ASP.NET引擎是完全建立在托管代码上的,所有的扩展功能也是通过托管代码扩展来提供的.这是对.NET框架具有构建复杂而且高效的框架的能力的最好的证明.ASP.NET最令人印象深刻的地方是深思熟虑的设计,使得框架非常的容易使用,又能提供挂接到请求处理的几乎所有部分的能力.
通过ASP.NET你可以从事从前属于ISAPI扩展和IIS过滤器领域的任务-有一些限制,但是比起ASP来说是好多了.ISAPI是一个底层的Win32风格的API,有着非常粗劣的接口而且难以用来开发复杂的程序.因为ISAPI非常底层,所以它非常的快,但是对于应用级的开发者来说是十分难以管理的.所以,ISAPI通常用来提供桥接的接口,来对其他应用或者平台进行转交.但是这并不意味者ISAPI将消亡.事实上,ASP.NET在微软的平台上就是通过ISAPI扩展来和IIS进行交互的,这个扩展寄宿着.NET运行时和ASP.NET运行时.ISAPI提供了核心的接口,ASP.NET使用非托管的ISAPI代码通过这个接口来从Web服务器获取请求,并发送响应回客户端.ISAPI提供的内容可以通过通用对象(例如HttpRequest和HttpResponse)来获取,这些对象通过一个定义良好并有很好访问性的接口来暴露非托管数据.
从浏览器到ASP.NET
让我们从一个典型的ASP.NET Web请求的生命周期的起点开始.当用户输入一个URL,点击了一个超链接或者提交了一个HTML表单(form)(一个POST请求,相对于前两者在一般意义上都是GET请求).或者一个客户端程序可能调用了一个基于ASP.NET的WebService(同样由ASP.NET来处理).在Web服务器端,IIS5或6,获得这个请求.在最底层,ASP.NET和IIS通过ISAPI扩展进行交互.在ASP.NET环境中这个请求通常被路由到一个扩展名为.aspx的页面上,但是这个流程是怎么工作的完全依赖于处理特定扩展名的HTTP Handler是怎么实现的.在IIS中.aspx通过’应用程序扩展’(又称为脚本映射)被映射到ASP.NET的ISAPI扩展DLL-aspnet_isapi.dll.每一个请求都需要通过一个被注册到aspnet_isapi.dll的扩展名来触发ASP.NET(来处理这个请求).
依赖于扩展名ASP.NET将请求路由到一个合适的处理器(handler)上,这个处理器负责获取这个请求.例如,WebService的.asmx扩展名不会将请求路由到磁盘上的一个页面,而是一个由特殊属性(Attribute)标记为WebService的类上.许多其他处理器和ASP.NET一起被安装,当然你也可以自定义处理器.所有这些HttpHandler在IIS中被配置为指向ASP.NET ISAPI扩展,并在web.config(译著:ASP.NET中自带的handler是在machine.config中配置的,当然可以在web.config中覆盖配置)被配置来将请求路由到指定的HTTP Handler上.每个handler都是一个处理特殊扩展的.NET类,可以从一个简单的只包含几行代码的Hello World类,到非常复杂的handler如ASP.NET的页面或者WebService的handler.当前,只要了解ASP.NET的映射机制是使用扩展名来从ISAPI接收请求并将其路由到处理这个请求的handler上就可以了.
对在IIS中自定义Web请求处理来说,ISAPI是第一个也是最高效的入口
ISAPI连接
ISAPI是底层的非托管Win32 API.ISAPI定义的接口非常简单并且是为性能做了优化的.它们是非常底层的-处理指针和函数指针表来进行回调-但是它们提供了最底层和面向效率的接口,使开发者和工具提供商可以用它来挂接到IIS上.因为ISAPI非常底层所以它并不适合来开发应用级的代码,而且ISAPI倾向于主要被用于桥接接口,向上层工具提供应用服务器类型的功能.例如,ASP和ASP.NET都是建立在ISAPI上的,Cold Fusion,运行在IIS上的多数Perl,PHP以及JSP实现,很多第三方解决方案(如我的Wisual FoxPro的Web连接框架)都是如此.ISAPI是一个杰出的工具,可以为上层应用提供高效的管道接口,这样上层应用可以抽象出ISAPI提供的信息.在ASP和ASP.NET中,将ISAPI接口提供的信息抽象成了类型Request和Response这样的对象,通过它们来读取ISAPI请求中对应的信息.将ISAPI想像成管道.对ASP.NET来说,ISAPI dll是非常的”瘦”的,只是作为一个路由机制来将原始的请求转发到ASP.NET运行时.所有那些沉重的负担和处理,甚至请求线程的管理都发生在ASP.NET引擎内部和你的代码中.
作为最为协议,ISAPI同时支持ISAPI扩展和ISAPI过滤器(Filter).扩展是一个请求处理接口,提供了处理Web服务器的输入输出的逻辑-它本质上是一个处理(事物?)接口.ASP和ASP.NET都被实现为ISAPI扩展.ISAPI过滤器是挂接接口,提供了查看进入IIS的每一个请求的能力,并能修改请求的内容或者改变功能型的行为,例如认证等.顺便提一下,ASP.NET通过了两种概念映射了类似ISAPI的功能:Http Handler类似扩展,Http Module类似过滤器.我们将在后面详细讨论它们.
ISAPI是开始一个ASP.NET请求的最初的入口.ASP.NET映射了好几个扩展名到它的ISAPI扩展,此扩展位于.NET框架的目录下:
<.NET FrameworkDir>\aspnet_isapi.dll
你可以在IIS服务管理界面上看到这些映射,如图1.查看网站根目录的属性中的主目录配置页,然后查看配置|映射.
图1:IIS映射了多种扩展名如.ASPX到ASP.NET的ISAPI扩展.通过这个机制请求会在Web服务器这一层被路由到ASP.NET的处理管道.
由于.NET需要它们中的一部分,你不应该设置手动这些扩展名.使用aspnet_regiis.exe这个工具来确保所有的映射都被正确的设置了:
cd <.NetFrameworkDirectory>
aspnet_regiis – i
这个命令将为整个Web站点注册特定版本的ASP.NET运行时,包括脚本 (扩展名) 映射和客户端脚本库(包括进行控件验证的代码等).注意它注册的是<.NetFrameworkDirectory>中安装的特定版本的CLR(如1.1,2.0).aspnet_regiis的选项令您可以对不同的虚拟目录进行配置.每个版本的.NET框架都有自己不同版本的aspnet_regiis工具,你需要运行对应版本的aspnet_regiis来为web站点或者虚拟目录来配置指定版本的.NET框架.从ASP.NET2.0开始提供了ASP.NET配置页面,可以通过这个页面在IIS管理控制台来交互的配置.NET版本.
IIS 5 和6以不同的方式工作
当一个请求来到时,IIS检查脚本映射(扩展名映射)然后把请求路由到aspnet_isapi.dll.这个DLL的操作和请求如何进入ASP.NET运行时在IIS5和6中是不同的.图2显示了这个流程的一个粗略概览.
在IIS5中,aspnet_isapi.dll直接寄宿在inetinfo.exe进程中,如果你设置了Web站点或虚拟目录的隔离度为中或高,则会寄宿在IIS单独的(被隔离的)工作进程中.当第一个ASP.NET请求来到,DLL(aspnet_isapi.dll)会开始另一个新进程aspnet_wp.exe并将请求路由到这个进程中来进行处理.这个进程依次加载并寄宿.NET运行时.每个转发到ISAPI DLL的请求都会通过命名管道调用被路由到这个进程来.
图2-从较高层次来看请求从IIS到ASP.NET运行时,并通过请求处理管道的流程.IIS5和IIS6通过不同的方式与ASP.NET交互,但是一旦请求来到ASP.NET管道,整个处理流程就是一样的了.
不同于以前版本的服务器,IIS6为ASP.NET做了全面的优化
IIS6-应用程序池万岁
IIS6对处理模型做了意义重大的改变,IIS不再直接寄宿象ISAPI扩展这样的外部可执行代码.IIS总是创建一个独立的工作线程-一个应用程序池-所有的处理都发生在这个进程中,包括ISAPI dll的执行.应用程序池是IIS6的一个很大的改进,因为它允许对指定线程中将会执行什么代码进行非常细粒度的控制.应用程序池可以在每个虚拟路径上或者整个Web站点上进行配置,这样你可以将每个Web应用隔离到它们自己的进程中,这样每个应用都将和其他运行在同一台机器上的Web应用完全隔离.如果一个进程崩溃了,不会影响到其他进程(至少在Web处理的观点上来看是如此).
不止如此,应用程序池还是高度可配置的.你可以通过设置池的执行扮演级别(execution impersonation level )来配置它们的运行安全环境,这使你可以定制赋予一个Web应用的权限(同样,粒度非常的细).对于ASP.NET的一个大的改进是,应用程序池覆盖了在machine.config文件中大部分的ProcessModel节的设置.这一节的设置在IIS5中非常的难以管理,因为这些设置是全局的而且不能在应用程序的web.config文件中被覆盖.当运行IIS6是,ProcessModel相关的设置大部分都被忽略了,取而代之的是从应用程序池中读取.注意这里说的是大部分-有些设置,如线程池的大小还有IO线程的设置还是从machine.config中读取,因为它们在线程池的设置中没有对应项.
因为应用程序池是外部的可执行程序,这些可执行程序可以很容易的被监控和管理.IIS6提供了一系列的进行系统状况检查,重启和超时的选项,可以很方便的用来检查甚至在许多情况下可以修正程序的问题.最后IIS6的应用程序池并不像IIS5的隔离模式那样依赖于COM+,这样做一来可以提高性能,二来提高了稳定性(特别对某些内部需要调用COM组件的应用来说)
尽管IIS6的应用程序池是单独的EXE,但是它们对HTTP操作进行了高度的优化,它们直接和内核模式下的HTTP.SYS驱动程序进行通讯.收到的请求被直接路由给适当的应用程序池.InetInfo基本上只是一个管理程序和一个配置服务程序-大部分的交互实际上是直接在HTTP.SYS和应用程序池之间发生,所有这些使IIS6成为了比IIS5更加的稳定和高效的环境.特别对静态内容和ASP.NET程序来说这是千真万确的.
一个IIS6应用程序池对于ASP.NET有着天生的认识,ASP.NET可以在底层的API上和它进行交互,这允许直接访问HTTP缓存API,这样做可以将ASP.NET级别的缓存直接下发到Web服务器.
在IIS6中,ISAPI扩展在应用程序池的工作进程中运行. .NET运行时也在同一个进程中运行,所以ISAPI扩展和.NET运行时的通讯是发生在进程内的,这样做相比IIS5使用的命名管道有着天生的性能优势.虽然IIS的寄宿模型有着非常大的区别,进入托管代码的接口却异常的相似-只有路由消息的过程有一点区别.
ISAPIRuntime.ProcessRequest()函数是进入ASP.NET的第一站
进入.NET运行时
进入.NET运行时的真正的入口发生在一些没有被文档记载的类和接口中(译著:当然,你可以用Reflector来查看J).除了微软,很少人知道这些接口,微软的家伙们也并不热衷于谈论这些细节,他们认为这些实现细节对于使用ASP.NET开发应用的开发人员并没有什么用处.
工作进程(IIS5中是ASPNET_WP.EXE,IIS6中是W3WP.EXE)寄宿.NET运行时和ISAPI DLL,它(工作进程)通过调用COM对象的一个小的非托管接口最终将调用发送到ISAPIRuntime类的一个实例上(译注:原文为an instance subclass of the ISAPIRuntime class,但是ISAPIRuntime类是一个sealed类,疑为作者笔误,或者这里的subclass并不是子类的意思).进入运行时的第一个入口就是这个没有被文档记载的类,这个类实现了IISAPIRuntime接口(对于调用者说明来说,这个接口是一个COM接口)这个基于Iunknown的底层COM接口是从ISAPI扩展到ASP.NET的一个预定的接口.图3展示了IISAPIRuntime接口和它的调用签名.(使用了Lutz Roeder出色的.NET Reflector 工具http://www.aisto.com/roeder/dotnet/).这是一个探索这个步步为营过程的很好的方法.
图3-如果你想深入这个接口,打开Reflector,指向System.Web.Hosting命名空间. ISAPI DLL通过调用一个托管的COM接口来打开进入ASP.NET的入口,ASP.NET接收一个指向ISAPI ECB的非托管指针.这个ECB包含访问完整的ISAPI接口的能力,用来接收请求和发送响应回到IIS.
IISAPIRuntime接口作为从ISAPI扩展来的非托管代码和ASP.NET之间的接口点(IIS6中直接相接,IIS5中通过命名管道).如果你看一下这个类的内部,你会找到含有以下签名的ProcessRequest函数:
[return: MarshalAs(UnmanagedType.I4)]
int ProcessRequest([In] IntPtr ecb,
[In, MarshalAs(UnmanagedType.I4)] int useProcessModel);
其中的ecb参数就是ISAPI扩展控制块(Extention Control Block),被当作一个非托管资源传递给ProcessRequest函数.这个函数接过ECB后就把它做为基本的输入输出接口,和Request和Response对象一起使用.ISAPI ECB包含有所有底层的请求信息,如服务器变量,用于表单(form)变量的输入流和用于回写数据到客户端的输出流.这一个ecb引用基本上提供了用来访问ISAPI请求所能访问的资源的全部功能,ProcessRequest是这个资源(ecb)最初接触到托管代码的入口和出口.
ISAPI扩展异步地处理请求.在这个模式下ISAPI扩展马上将调用返回到工作进程或者IIS线程上,但是在当前请求的生命周期上ECB会保持可用.ECB含有使ISAPI知道请求已经被处理完的机制(通过ecb.ServerSupportFunction方法)(译注:更多信息,可以参考开发ISAPI扩展的文章),这使得ECB被释放.这个异步的处理方法可以马上释放ISAPI工作线程,并将处理传递到由ASP.NET管理的一个单独的线程上.
ASP.NET接收到ecb引用并在内部使用它来接收当前请求的信息,如服务器变量,POST的数据,同样它也返回信息给服务器.ecb在请求完成前或超时时间到之前都保持可访问(stay alive),这样ASP.NET就可以继续和它通讯直到请求处理完成.输出被写入ISAPI输出流(使用ecb.WriteClient())然后请求就完成了,ISAPI扩展得到请求处理完成的通知并释放ECB.这个实现是非常高效的,因为.NET类本质上只是对高效的、非托管的ISAPI ECB的一个非常”瘦”(thin)的包装器.
装载.NET-有点神秘
让我们从这儿往回退一步:我跳过了.NET运行时是怎么被载入的.这是事情变得有一点模糊的地方.我没有在这个过程中找到任何的文档,而且因为我们在讨论本机代码,没有很好的办法来反编译ISAPI DLL并找出它(装载.NET运行时的代码)来.
我能作出的最好的猜测是当ISAPI扩展接受到第一个映射到ASP.NET的扩展名的请求时,工作进程装载了.NET运行时.一旦运行时存在,非托管代码就可以为指定的虚拟目录请求一个ISAPIRuntime的实例(如果这个实例还不存在的话).每个虚拟目录拥有它自己的应用程序域(AppDomain),当一个独立的应用(指一个ASP.NET程序)开始的时候ISAPIRuntime从启动过程就一直在应用程序域中存在.实例化(译注:应该是指ISAPIRuntime的实例化)似乎是通过COM来进行的,因为接口方法都被暴露为COM可调用的方法.
当第一个针对某虚拟目录的请求到来时,System.Web.Hosting.AppDomainFactory.Create()函数被调用来创建一个ISAPIRuntime的实例.这就开始了这个应用的启动进程.这个调用接收这个应用的类型,模块名称和虚拟目录信息,这些信息被ASP.NET用来创建应用程序域并启动此虚拟目录的ASP.NET程序.这个HttpRuntime实例(译注:原文为This HttpRuntime derived object,但HttpRuntime是一个sealed类,疑为原文错误)在一个新的应用程序域中被创建.每个虚拟目录(即一个ASP.NET应用程序寄)宿在一个独立的应用程序域中,而且他们也只有在特定的ASP.NET程序被请求到的时候才会被载入.ISAPI扩展管理这些HttpRuntime对象的实例,并根据请求的虚拟目录将内部的请求路由到正确的那个HttpRuntime对象上.
图4-ISAPI请求使用一些没有文档记载的类,接口并调用许多工厂方法传送到ASP.NET的HTTP管道的过程.每个Web程序/虚拟目录在它自己的应用程序域中运行,调用者(译注:指ISAPI DLL)保持一个IISAPIRuntime接口的引用来触发ASP.NET的请求处理.
HttpRuntime,HttpContext和HttpApplication
当一个请求到来时,它被路由到ISAPIRuntime.ProcessRequest()方法.这个方法调用HttpRuntime.ProcessRequest方法,它作一些重要的事情(用Reflector查看System.Web.HttpRuntime.ProcessRequestInternal方法):
- 为请求创建一个新的HttpContext实例
- 获取一个HttpApplication实例
- 调用HttpApplication.Init()方法来设置管道的事件
- Init()方法触发开始ASP.NET管道处理的HttpApplication.ResumeProcessing()方法
首先一个新的HttpContext对象被创建并用来传递ISAPIWorkerRequest(ISAPI ECB的包装器).这个上下文在整个请求的生命周期总都是可用的并总可以通过静态属性HttpContext.Currect来访问.正像名字所暗示的那样,HttpContext对象代表了当前活动请求的上下文因为他包含了在请求生命周期中所有典型的你需要访问的重要对象:Request,Response,Application,Server,Cache.在请求处理的任何时候HttpContext.Current给你访问所有这些的能力.
HttpContext对象也包含一个非常有用的Items集合,你可以用它来保存针对特定请求的数据.上下文对象在请求周期的开始时被创建,在请求结束时被释放,所有在Items集合中保存的数据只在这个特定的请求中可用.一个很好的使用的例子是请求日志机制,当你通过想通过在Global.asax中挂接Application_BeginRequest和Application_EndRequest方法记录请求的开始和结束时间(象在列表3中显示的那样).HttpContext对你就非常有用了-如果你在请求或页面处理的不同部分需要数据,你自由的使用它.
列表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);
}
}
一旦上下文被设置好,ASP.NET需要通过HttpApplication对象将收到的请求路由到适合的应用程序/虚拟目录.每个ASP.NET应用程序必须被设置到一个虚拟目录(或者Web根目录)而且每个”应用程序”是被单独的处理的.
HttpApplication类似仪式的主人-它是处理动作开始的地方
域的主人:HttpApplication
每个请求都被路由到一个HttpApplication对象上.HttpApplicationFactory类根据应用程序的负载为你的ASP.NET应用创建一个HttpApplication对象池并为每个请求分发HttpApplication对象的引用.对象池的大小受machine.config文件中ProcessModel键中的MaxWorkerThreads设置限制,默认是20个(译注:此处可能有误,根据Reflector反编译的代码,池的大小应该是100个,如果池大小小于100,HttpApplicationFactory会创建满100个,但是考虑到会有多个线程同时创建HttpApplication的情况,实际情况下有可能会超过100个).
对象池以一个更小的数字开始;通常是一个然后增长到和同时发生的需要被处理的请求数量一样.对象池被监视,这样在大负载下它可能会增加到最大的实例数量,当负载降低时会变回一个更小的数字.
HttpApplication是你的Web程序的外部包装器,而且它被映射到在Global.asax里面定义的类上.它是进入HttpRuntime的第一个入口点.如果你查看Global.asax(或者对应的代码类)你会发现这个类直接继承自HttpApplication:
public class Global : System.Web.HttpApplication
HttpApplication的主要职责是作为Http管道的事件控制器,所以它的接口主要包含的是事件.事件挂接是非常广泛的,包括以下这些:
l BeginRequest
l AuthenticateRequest
l AuthorizeRequest
l ResolveRequestCache
l AquireRequestState
l PreRequestHandlerExecute
l …Handler Execution…
l PostRequestHandlerExecute
l ReleaseRequestState
l UpdateRequestCache
l EndRequest
每个事件在Global.assx文件中以Application_前缀开头的空事件作为实现.例如, Application_BeginRequest(), Application_AuthorizeRequest()..这些处理器为了便于使用而提供因为它们是在程序中经常被使用的,这样你就不用显式的创建这些事件处理委托了.
理解每个ASP.NET虚拟目录在它自己的应用程序域中运行,而且在应用程序域中有多个从ASP.NET管理的池中返回的HttpApplication实例同时运行,是非常重要的.这是多个请求可以被同时处理而不互相妨碍的原因.
查看列表4来获得应用程序域,线程和HttpApplication之间的关系.
列表4-显示应用程序域,线程和HttpApplication实例之间的关系
private void Page_Load(object sender, System.EventArgs e)
{
// Put user code to initialize the page here
this.ApplicationId = ((HowAspNetWorks.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);
}
这是随sample提供的demo的一部分,运行的结果在图5中显示.运行两个浏览器,打开这个演示页面可以看到不同的ID.
图5-你可以通过同时运行多个浏览器来简单的查看应用程序域,应用程序池实例和请求线程是如何交互的.当多个请求同时发起,你可以看到线程ID和应用程序ID变化了,但是应用程序域还是同一个.
你可能注意到在大多数请求上,当线程和HttpApplication ID变化时应用程序域ID却保持不变,虽然它们也可能重复(指线程和HttpApplication ID).HttpApplication是从一个集合中取出,在随后到来的请求中可以被复用的,所以它的ID有时是会重复的.注意Application实例并不和特定的线程绑定-确切的说它们是被指定给当前请求的活动线程.
线程是由.NET的线程池管理的,默认是多线程套间(MTA)线程.你可以在ASP.NET的页面上通过指定@Page指令的属性ASPCOMPAT=”true”来覆盖套间属性.ASPCOMPAT意味着为COM组件提供一个安全的执行环境, 回到运行时
在这里我们有一个在ISAPI扩展中活动的,可调用的ISAPIRuntime对象的实例.每次运行时是启动的并运行着的时候(译注:相对的,如果运行时并没有启动,就需要象上一章所说的那样载入运行时),ISAPI的代码调用ISAPIRuntime.ProcessRequest()方法,这个方法是真正的进入ASP.NET管道的入口.这个流程在图4中显示.
记住ISAPI是多线程的,所以请求也会通过AppDomainFactory.Create()(译注:原文为ApplicationDomainFactory,疑有误)函数中返回的引用在多线程环境中被处理.列表1显示了ISAPIRuntime.ProcessRequest()方法中反编译后的代码,这个方法接收一个ISAPI ecb对象和服务类型(WorkerRequestType)作为参数.这个方法是线程安全的,所以多个ISAPI线程可以同时在这一个被返回的对象实例上安全的调用这个方法.
列表1:ProcessRequest方法接收一个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对象,此对象负责为当前请求创建创建请求上下文.在列表2中显示了这个过程.
System.Web.Hosting.ISAPIWorkerRequest类是HttpWorkerRequest类的一个抽象子类(译注:HttpWorkerRequest和ISAPIWorkerRequest都是抽象类,并且ISAPIWorkerRequest继承自HttpWorkerRequest),它的工作是构建一个作为Web应用输入的输入输出的抽象视角.注意这里有另一个工厂方法:CreateWorkerRequest,通过判断接受到的第二个参数来创建对应的WorkerRequest对象.有三个不同的版本:ISAPIWorkerRequestInProc,ISAPIWorkerRequestInProcForIIS6,ISAPIWorkerRequestOutOfProc.每次有请求进入,这个对象被创建并作为请求和响应对象的基础,它会接收它们的数据和由WorkerRequest提供的数据流.
抽象的HttpWorkerRequest类在低层接口上提供一个高层的抽象,这样就封装了数据是从哪里来的,可以是一个CGI Web服务器,Web浏览器控件或者是一些你用来给HTTP运行时”喂”数据的自定义的机制.关键是ASP.NET能用统一的方法来接收信息.
在使用IIS的情况下,这个抽象是建立在ISAPI ECB块周围.在我们的请求处理过程中,ISAPIWorkerRequest挂起ISAPI ECB并根据需要从它那里取出信息.列表2显示了请求字符串值(query string value)是如何被取出来的.
列表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实现了一个高层次的包装方法,它调用了低层的核心方法,负责真正的访问非托管APIs-或称为”服务级别的实现”(service level implementation).这些核心方法在特殊的ISAPIWorkerRequest子类中为它寄宿的环境提供特殊的实现.这实现了简单的扩展的(pluggable)环境,这样一来当以后新的Web服务器接口或其他平台成为了ASP.NET的目标时附加的实现类可以在被简单的提供出来.这里还有一个协助类(helper class)System.Web.UnsafeNativeMethods.里面许多对ISAPI ECB结构的操作实现了对ISAPI扩展的非托管操作.