|
|
|
|
HttpRuntime,HttpContext以及HttpApplication 当一个请求到来时,它将被路由到ISAPIRuntime.ProcessRequest()方法里。这个方法会接着调用HttpRuntime.ProcessRequest,在这个方法里,做了几件重要的事情(使用Refector反编译System.Web.HttpRuntime.ProcessRequestInternal可以看到)。 l 为请求创建了一个新的HttpContext实例 l 获取一个HttpApplication实例 l 调用HttpApplication.Init()初始化管道事件 l Init()触发HttpApplication.ResumeProcessing(),启动ASP.NET管道处理 首先,一个新的HttpContext对象被创建,并且给它传递一个封装了ISAPI ECB 的ISAPIWorkerRequest。在请求的生命周期里,这个上下文(context)一直是有效的。并且可以通过静态的HttpContext.Current属性访问。正如它的名字暗示的那样,HttpContext对象表示当前活动请求的上下文,因为它包含了在请求生命周期里你会用到的所有必需对象的引用,如:Request,Response,Application,Server,Cache。在请求处理过程的任何时候,你都可以使用HttpContext.Current访问这些对象。 HttpContext对象还包含了一个非常有用的列表集合,你可以使用它存储有关特定的请求需要的数据。上下文(context)对象创建于一个请求生命周期的开始,在请求结束时被释放。因此,保存在列表集合里的数据仅仅对当前的请求有效。一个很好的例子,就是记录请求的日志机制,在这里,通过使用Global.asax里的Application_BeginRequest和Application_EndRequest方法,你可以从请求的开始时间至结束时间段内,对请求进行跟踪。如列表3所示。记住HttpContext是你的朋友,在请求或者页面处理的不同阶段,如果你需要相关数据都可以使用它获取。
一旦请求的上下文对象被搭建起来,ASP.NET就需要通过一个HttpApplication对象,把你的请求路由到合适的程序/虚拟目录里。每一个ASP.NET程序都拥有各自的虚拟目录(Web根目录),并且它们都是独立处理请求的。 Web程序的主要部分:HttpApplication 每一个请求都将被路由到一个HttpApplication对象。HttpApplicationFactory类会为你的ASP.NET程序创建一个HttpApplication对象池,它负责加载程序和给每一个到来的请求分发HttpApplication的引用。这个HttpApplication对象池的大小可以通过machine.config里的ProcessModel节点中的MaxWorkerThreads选项配置,默认值是20。 HttpApplication对象池尽管以比较少的数目开始启动,通常是一个。但是当同时有多个请求需要处理时,池中的对象将会随之增加。而HttpApplication对象池,也将会被监控,目的是保持池中对象的数目不超过设置的最大值。当请求的数量减小时,池中的数目就会跌回一个较小的值。 对于你的Web程序而言,HttpApplication是一个外部容器,它对应到Global.asax文件里定义的类。基于标准的Web程序,它是你实际可以看到的进入HTTP运行时的第一个登录点。如果你查看Global.asax(后台代码),你就会看到这个类直接派生于HttpApplication。 public class Global : System.Web.HttpApplication HttpApplication主要用作HTTP管道的事件控制器,因此,它的接口主要有事件组成,这些事件包括: l BeginRequest l AuthenticateRequest l AuthorizeRequest l ResolveRequestCache l [此处创建处理程序(即与请求 URL 对应的页)。] l AcquireRequestState l PreRequestHandlerExecute l [执行处理程序。] l PostRequestHandlerExecute l ReleaseRequestState l [响应筛选器(如果有的话),筛选输出。] l UpdateRequestCache l EndRequest 这里的每一个事件都在Global.asax文件中以Application_为前缀,无实现代码的方法出现。举个例子,如Application_BeginRequest()和Application_AuthorizeRequest()。由于它们在程序中会经常用到,所以出于方便的考虑,这些事件的处理器都已经被提供了,这样你就不必再显式的创建这些事件处理器的委托了。 每一个ASP.NET Web程序运行在各自的AppDomain里,在AppDomain里同时运行着多个HttpApplication的实例,这些实例存放在ASP.NET管理的一个HttpApplication对象池里,认识到这一点,是非常重要的。这就是为什么可以同时处理多个请求,而这些请求不会互相干扰的原因。 使用列表4的代码,可以进一步了解AppDomain,线程,HttpApplication之间的关系。
这是样例程序的一部分,运行的结果如图5所示。为了检验结果,你应该打开两个浏览器,输入相同的地址,观察那些不同的ID的值。
你将会观察到AppDomain ID一直保持不变,而线程和HttpApplication的ID在请求多的时候会发生改变,尽管它们会出现重复。这是因为HttpApplications是在一个集合里面运行,下一个请求可能会再次使用同一个HttpApplication实例,所以有时候HttpApplication的ID会重复。注意,一个HttpApplication实例对象并不依赖于一个特定的线程,它们仅仅是被分配给处理当前请求的线程而已。 线程由.NET的ThreadPool提供服务,默认情况下,线程模型为多线程单元(MTA)。你可以通过在ASP.NET的页面的@Page指令里设置属性ASPCOMPAT="true"覆盖线程单元的状态。ASPCOMPAT意味着COM组件将在一个安全的环境下运行。ASPCOMPAT使用了单线程单元(STA)的线程为请求提供服务。STA线程在线程池里是单独设置的,这是因为它们需要特殊的处理方式。 实际上,这些HttpApplication对象运行在同一个AppDomain里是很重要的。这就是ASP.NET如何保证web.config的改变或者单独的ASP.NET页面得到验证可以贯穿整个AppDomain。改变web.config里的一个值,将导致AppDomain关闭并重新启动。这确保了所有的HttpApplication实例可以看到这些改变,这是因为当AppDomain重新加载的时候,来自ASP.NET的那些改变将会在AppDomain启动的时候重新读取。当AppDomain重新启动的时候任何静态的引用都将重新加载。这样,如果程序是从程序的配置文件读取的值,这些值将会被刷新。 在示例程序里可以看到这些,打开一个ApplicationPoolsAndThreads.aspx页面,注意观察AppDomain的ID。然后在web.config里做一些改动(增加一个空格,然后保存),重新加载这个页面(译注:由于缓存的影响可能会在原来的页面上刷新无效,需要先删除缓存再刷新即可),你就会看到一个新的AppDomain被创建了。 本质上,这些改变将引起Web程序重新启动。对于已经存在于处理管道的请求,将继续通过原来的管道处理。而对于那些新的请求,将被路由到新的AppDomain里。为了处理这些“挂起的请求”,在这些请求超时结束之后,ASP.NET将强制关闭AppDomain甚至某些请求还没有被处理。因此,在一个特定的时间点上,同一个HttpApplication实例在两个AppDomain里存在是有可能的,这个时间点就是旧的AppDomain正在关闭,而新的AppDomain正在启动。这两个AppDomain将继续为客户端请求提供服务,直到旧的AppDomain处理完所有的未处理的请求,然后关闭,这时候才会仅剩下新的AppDomain在运行。 穿过ASP.NET管道 HttpApplication负责请求的传输,通过触发事件,通知应用程序正在发生的事情。这个是作为HttpApplication.Init()方法的一部分实现的(使用Refector查看System.Web.HttpApplication.InitInternal和HttpApplication.ResumeSteps()的代码可以看到这一点)。在这个方法里,创建和启动了一系列事件,包括绑定事件处理器。Global.asax里的事件处理器会自动映射到对应的事件,它们也可以映射到额外添加的HTTPModules,这些HTTPModules本质上是HttpApplication已发布事件的一种扩展。 通过在web.config里注册,HTTPModules和HttpHandlers可以被动态的加载,并且可以添加到事件链条上。HTTPModules实际上就是事件处理器,它可以钩住指定HttpApplication的事件。而HttpHandlers就是一个端点,它可以被调用处理“应用程序级的请求处理”。 HTTPModules和HttpHandlers将被加载,然后添加到调用链上作为HttpApplication.Init()方法调用的一部分。图6展示了不同的事件,这些事件何时被触发以及通道上的哪些部分会受到它们的影响。
HttpContext,HttpModules和HttpHandlers HttpApplication本身并不知晓发送给Web程序的数据。它仅仅是个消息邮递者,只负责事件之间的通信。它触发事件,然后通过传递HttpContext对象,把信息发送给被调用的方法。在之前我们提到,当前请求的数据是在HttpContext对象里保存。它提供了请求从开始到结束需要的所有数据。图7展示了ASP.NET的管道之间的传输流程。注意,从请求的开始至结束,上下文对象(Context)都是你的伙伴,你可以在一个事件方法里使用它保存数据,然后在之后的事件方法里获取这些数据。
ASP.NET管道一旦启动,HttpApplication将逐一触发事件,如图6展示的那样。每一个事件都将被触发,如果事件绑定了事件处理器,那么这些事件处理器将被调用,执行它们的任务。这个过程的主要目的是通过调用HttpHandler处理指定的请求。对于ASP.NET请求而言,HttpHandler是处理请求机制的核心,在这里任意的应用程序级的代码被执行。记住,ASP.NET的页面和Web Service都是HttpHandler的具体实现,在这里,所有请求处理的核心功能被实现。HTTPModule则倾向于在分发给事件处理器之前或者之后对内容进行处理。在ASP.NET里典型的默认操作有:鉴定(Authentication),处理前的缓存操作以及各种处理后的编码操作机制。 关于HTTPModule和HttpHandler,这里有很多有用的信息,但为了保持这篇文章合理的尺度,我将仅仅讲述关于它们一些简短的、整体的看法。 HttpModules 伴随着HttpApplication触发的一系列事件,请求将会在管道之间穿梭。你已经看到了这些发布的事件,在Global.asax里都有对应事件的处理方法。这个方法(步骤)是程序(ASP.NET应用程序)携带的,尽管这个并不总是你需要的。如果你想构建一套通用的HttpApplication事件处理程序,并以插件的形式添加到任意的Web程序里。那么你可以使用HTTPModule,它是可重复使用的,不需要添加任何实现代码就可以在其它程序里使用,而你所做的仅仅在web.config里注册。 模块本质上是过滤器,在功能上类似于ASP.NET请求级别的ISAPI过滤。对于每一个穿过ASP.NET的HttpApplication对象的请求,模块都允许在HttpApplication对象触发的事件处理方法里截获这些请求。这些模块以类的形式存储在外部程序集里,可以在web.config里配置,当程序启动的时候加载。通过实现指定的接口和方法,模块就可以被添加到HttpApplication的事件链上。多个HttpModules可以钩住相同的事件,事件发生的顺序是它们在web.config里声明(配置)的顺序。如下就是在web.config里,一个模块的声明。 <configuration> 注意,在这里你需要指定一个完整的类型名和一个不带扩展名的程序集的名字。 模块允许你查看每一个传入的Web请求,基于触发的事件基础上执行操作。模块是非常有用的,它可以修改请求,输出响应的内容以及提供自定义的身份验证,另外还可以在特定的程序里,针对ASP.NET的每一个请求提供响应前处理和响应后处理。许多ASP.NET的特征像身份验证,会话引擎都是作为HTTP模块实现的。 HttpModules感觉有点类似于ISAPI过滤器,是由于它们查看进入ASP.NET程序的每一个请求,但它们局限性于仅可以查看映射到某一个ASP.NET程序或者虚拟目录的请求,和映射到ASP.NET的请求。因此,你仅可以查看所有的ASPX页面或者任意其它自定义的已经映射到这个程序的扩展名(译注:作者可能漏掉了ASMX,ASHX等,这里的意思应该是所有的ASP.NET默认的扩展名)。但是,你不能查看标准的.HTM或者图像文件,除非你通过添加这些扩展名,明确的把它们映射到ASP.NET的ISAPI DLL,如图一中那样。对于模块,一个常见的用处是过滤一个指定的文件夹的JPG图片内容,然后使用GDI+在每一张返回的图片上方添加“样图”字样。 实现一个HTTP模块是非常简单的:你必须实现一个IHttpModule接口,它包含两个方法:Init()和Dispose()。传递的事件参数中包含着一个HttpApplication对象的引用,接着,它会给你访问HttpContext对象的权限。在这两个方法里,你可以钩住HttpApplication的事件。举个例子,如果你想用一个模块钩住AuthenticateRequest事件,那么你需要做的会像列表5中展示的那样。
记住,你的模块已经有访问HttpContext对象的权限了。从这里到所有其它内置的ASP.NET管道对象如Response和Request,因此你可以获取输入的数据等等。但紧记,某些对象现在可能不能使用,只有到这条链的后面的环节才会有效, 在Init()方法里,你可以钩住多个事件,因此在一个模块里,可以管理多个不同功能的操作。但是,应该尽可能的把不同的逻辑代码放到不同的类里,这样可以确保这个模块是标准的组件。在许多情况下,你实现的功能可能需要钩住多个事件。举个例子,一个日志过滤器可能需要在BeginRequest里记录请求的开始时间,在EndRequest里记录请求完成的时间。 在HttpModules里使用HttpApplication的事件时,有一点是需要注意的,Response.End()和HttpApplication.CompleteRequest()方法会使ASP.NET跳过HttpApplication和模块的事件链。可以查看这两个方法的帮助文档获取更多的信息。 HttpHandlers 模块是相当低层次的,它针对每一个传入ASP.NET程序的请求触发。HTTP处理器则着重于处理一个指定的请求映射。通常一个页面扩展已经被映射到处理器了。 实现一个HTTP处理器所需要做的是非常基础的,但是通过访问HttpContext对象,就会有很多有用的功能。HTTP处理器通过一个简单IHttpHandler接口实现(或者它的异步版本IHttpAsyncHandler)。它仅仅有一个方法ProcessRequest()和一个属性IsReusable。这里的关键是ProcessRequest()会得到一个HttpContext对象的实例。这个单独的方法将从开始到结束负责处理一个Web请求。 单一的,简单的方法?可能太简单了,是吗?可是,一个简单的接口,但它的实现可能并不简单。记住,WebForms和WebServices都是作为HTTP处理器实现的。因此,在这个看似简单的接口里,过多的实现过程被隐藏了。关键是,事实上到现在,一个HTTP处理器可以访问所有为了开始处理请求而被组建和配置起来的ASP.NET的内置对象。关键是,HttpContext对象提供了所有与请求相关的功能,可以获取流入的数据和输出数据到Web服务器。 对于HTTP处理器而言,所有的操作都通过简单地调用ProcessRequest()方法执行。可以简单到如下的样子: public void ProcessRequest(HttpContext context) 一个完整的实现像WebForms页面引擎那样,可以根据HTML模版展现复杂的表单。所以,这里的关键点是你想用这个简单但功能强大的接口做什么。 因为对你而言,HttpContext对象是可以使用的,这样你就可以访问Request,Response,Session和Cache对象,因此你已经拥有了ASP.NET请求的所有特征,可以自己做主如何处理用户提交的信息,然后给客户端返回处理后产生的内容。记住,在一个ASP.NET请求的生命期内,上下文对象始终都是你的朋友。 处理器的关键操作通常是往Response对象里写输出数据,或者更确切的说,是往Response对象的OutputStream里写。这个就是真正返回到客户端的输出数据。在底层,由ISAPIWorkerRequest负责把OutputStream发回给ISAPI的ecb.WriteClient方法,因为ecb.WriteClient方法才是真正执行IIS产生输出数据的。 通过使用大量的处于高层的、基础的框架接口,WebForms实现了一个HTTP处理器。但最后,WebForm的Render()方法却简单的使用了一个HtmlTextWriter对象,把最终的输出发送给context.Response.OutputStream对象而结束。因此,相当的奇妙,最终甚至一个高层次的工具像WebForm,也仅仅是在Response和Request对象之上的一个高层次的抽象。 在这个时候,你可能想知道,是否需要从头实现一个完整的HTTP处理器?毕竟WebForms已经提供了一个易用的HTTP处理器的实现,因此为什么还要为这些大量底层的东西而烦恼,放弃这些已经提供的灵活性呢? WebForms是非常棒的,使用它可以生成复杂的HTML页面和处理业务层的逻辑而这些都需要图形化的设计和模版化的页面。除此以外,WebForms引擎还可以完成更多的,需要丰富的表现层的任务。如果所有你想做的是:从系统读取文件,由执行的代码把它返回。这样的任务,如果避开使用Web Forms页面框架,代替的是直接处理文件然后返回,相信后者才是更高效的方法。如果你做的事情仅仅是像从数据库里读取图片一样,那么完全没有必要使用页面框架来处理,因为这里的确没有Web UI需要你俘获离开图片的事件。 在这里找不到,组建一个页面对象和会话对象以及俘获页面层上事件的理由。对于你的任务而言,这里需要做的仅仅是执行代码,而这些与你手头上的任务毫不相干。 因此,这种情况下使用处理器会更加高效。处理器可以完成使用WebForms不可能完成的事情。比如:需要处理这样的请求,它们没有必要在磁盘上存在对应的物理文件,这些被请求的路径通常称为虚拟的URL。为了使这样的请求正常的工作,你需要确保已经在应用程序的扩展对话框(如图一所示)里关闭了“确认文件是否存在”的复选框。 对于内容提供者来说,这是通用的,如:动态的图像处理,XML服务,提供虚拟的URL使原来的URL重定向,下载管理等等,这些均不能使用WebForms实现。 是否已经提供了足够的底层知识? 哎呀!我们终于绕着请求的处理周期回到了原地。尽管我在这里没有探讨有关HTTP模块和HTTP处理器如何工作的更多细节,但仍旧提供了许多对你有帮助的底层信息。挖掘这些信息花费了我很多时间,通过了解ASP.NET在底层的工作模式,使我感到非常地满足,希望它也可以给你带来同样的感受。 在结束之前,还是让我们来回顾一下,我在这篇文章中讨论的从IIS到HTTP处理器的事件序列: l IIS得到一个请求 l 查询脚本映射扩展,然后把请求映射到aspnet_isapi.dll文件 l 代码进入工作者进程(IIS5里是aspnet_wp.exe;IIS6里是w3wp.exe) l .NET运行时被加载 l 非托管代码调用IsapiRuntime.ProcessRequest()方法 l 每一个请求调用一个IsapiWorkerRequest l 使用WorkerRequest调用HttpRuntime.ProcessRequest()方法 l 通过传递进来的WorkerRequest创建一个HttpContext对象 l 使用上下文对象调用HttpApplication.GetApplicationInstance(),从池中获取一个HttpApplication实例。 l 调用HttpApplication.Init(),启动管道事件序列,钩住模块和处理器 l 调用HttpApplicaton.ProcessRequest,开始处理请求 l 触发管道事件 l 调用HTTP处理器和ProcessRequest方法 l 把返回的数据输出到管道,触发处理请求后的事件 使用手边的例子将会更容易记住这些零碎的片断是如何组合起来的。为了记住它,我会不时地看一下它。现在应该回到工作中了,去做一些不太抽象的事情吧。 尽管讨论的这些是基于ASP.NET 1.1的。但这里描述的底层处理在ASP.NET 2.0好像并没有多大改变。 最后,非常感谢来自微软的Mike Volodarsky,是他校验了这篇文章,并且提出了一些宝贵的意见。还有Michele Leroux Bustamante,他为ASP.NET管道请求流程的幻灯片提供了依据。 |




