在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
转自:http://www.cnblogs.com/rijing2004/archive/2007/09/14/howaspnetwork.html (翻译)从底层了解ASP.NET体系结构前言
1. ASP.NET是什么? 2. 从浏览器到ASP.NET 3. ISAPI连接 5. 进入.NET运行时 7. 回到运行时 8. HttpRuntime,HttpContext以及HttpApplication 10. 穿过ASP.NET管道 11. HttpContext,HttpModules和HttpHandlers 12. HttpModules 13. HttpHandlers 14. 是否已经提供了足够的底层知识? 摘要:ASP.NET是一个用于构建Web程序的强大平台,提供了巨大的弹性和能力以至于它可以构建任意的Web程序。许多人仅仅对处于ASP.NET高层次的框架如:WebForms和WebServices比较熟悉,因此,在这篇文章里,我将会阐述有关ASP.NET比较底层的知识,并且将会解释,如何将请求从Web Server移交给ASP.NET运行时,然后通过ASP.NET HTTP管道处理这些请求。 对于我来说,了解一个平台的内部工作机制总是会让我感到一些满足和安慰,如同洞察,可以帮助我写出更好的程序。知道了工具有什么用途,以及它们如何组装成复杂框架的一部分,这些将会使你很容易的找到问题的解决方案,以及在你修改和调试错误时,都显得非常重要。这篇文章的目的就是从底层了解ASP.NET以及帮助你理解请求如何流入ASP.NET处理管道里。同时,你将会了解ASP.NET引擎的核心,以及一个Web请求如何在这里结束。这里讲到的许多知识都是你日常工作中没必要知道的,但是,如果你理解了ASP.NET如何把请求路由到应用程序的代码里(通常比较高层次的),这将对你非常有用。 注:整个ASP.NET引擎完全构建在托管代码里,其所有的扩展性都是通过托管代码去构建。
使用ASP.NET的大多数都比较熟悉WebForms和WebServices。这些高层次的实现,使得构建Web程序变得非常容易。ASP.NET被设计为驱动引擎,它把底层的接口提供给Web Server,为高层次Web应用程序的前端和末端提供了路由服务。WebForms和WebServices是建立在ASP.NET框架之上,有关HTTP处理的两种最常用的方式。 其实,在较低的层次上,ASP.NET也提供了足够多的灵活性。HTTP运行时和请求管道提供了同样的能力,可以构建类似于WebForms和WebServices的实现,当然,这些已经使用.NET托管代码实现了。如果你需要构建一个自定义HTTP处理平台,而这个平台要比WebForms所处的层次低一点,那么你就会用到所有这些类似的功能。 构建大多的Web界面,使用WebForms无疑是最容易的方法,但是,如果你想自定义一个内容处理器,或者需要对流入和流出的内容做特殊的处理,或者需要为一个应用程序定制一个应用服务器接口,那么使用这些低层次的处理或者模块将会得到更好的性能,以及可以在真正的请求处理中获得更多的控制权。尽管那些高层次的实现,如:WebForms和WebServices已提供了类似的功能,但由于它们针对请求添加了太多的控制(导致性能下降)。所以你完全可以另辟佳境,在较低层次上处理这些请求。
ASP.NET是什么?
让我们从最简单的定义开始,ASP.NET是什么?我通常喜欢用如下语句来描述ASP.NET。 ASP.NET是完全使用托管代码处理Web请求的一个成熟引擎平台。它不仅仅只是WebForms和WebServices。 ASP.NET是一个请求处理引擎。它获取客户端请求,然后通过它内置的管道,把请求传到一个终点,在这个终点,开发者可以添加处理这个请求的逻辑代码。实际上这个引擎和HTTP或者Web Server是完全分开的。事实上,HTTP运行时是一个组件,你可以把它宿主在IIS之外的应用程序上。甚至完全可以和其它的服务组合在一起。例如,你可以把HTTP运行时宿主在Windows桌面应用程序里(详细的内容请查看:http://www.west-wind.com/presentations/aspnetruntime/aspnetruntime.aspx)。 通过使用内置的管道路由请求,HTTP运行时提供了一套复杂的,但却很优雅的机制。在处理请求的每一个层面都牵涉到许多对象,但大多数对象都可以通过派生或者事件接口来扩展。所以,此框架具有非常高的可扩展性。通过这一套机制,可以进入较低层次的接口如:缓存,身份验证,授权等是有可能的。你可以在处理请求之前或之后过滤内容,或者仅仅把匹配指定签名的客户端请求直接路由到你的代码里或转向其它的URL。针对同一件事情,可以通过不同的处理方法完成,而且实现代码都非常的直观。除此之外,在容易开发和性能之间,HTTP运行时还提供了最佳的灵活性。 整个ASP.NET引擎完全构建在托管代码里,所有的扩展性功能都是通过托管代码的扩展提供。对于功能强大的.NET框架而言,使用自己的东西,构建一个成熟的、高性能的引擎体系结构已经成为一个遗嘱。尽管如此,但重要的是,ASP.NET给人印象最深的是高瞻远瞩的设计,这使得在其之上的工作变得非常容易,并且提供了几乎可以钩住请求处理当中任意部分的能力。 使用ASP.NET可以完成一些任务,之前这些任务是使用IIS上的ISAPI扩展和过滤来完成的。尽管还有一些限制,但与ASP相比,已经有了很大的进步。ISAPI是底层Win32样式的API,仅它的接口就有1兆,这对于大型的程序开发是非常困难的。由于ISAPI是底层的接口,因此它的速度也是非常的快。但对于企业级的程序开发是相当的难于管理的。所以,在一定的时间内,ISAPI主要充当其它应用程序或平台的桥接口。但是无论如何,ISAPI没有被废弃。事实上,微软平台上的ASP.NET和IIS的接口是通过宿主在.NET里的ISAPI扩展来通信的,然后直达ASP.NET运行时。ISAPI提供了与Web Server通信的核心接口,然后ASP.NET使用非托管代码获取请求以及对客户端请求发出响应。ISAPI提供的内容经由公共对象类似于HttpRequest和HttpResponse,通过一个设计优良的、可访问的接口,以托管对象的方式暴露非托管数据。
让我们从一个典型的ASP.NET Web请求的生命周期的起点开始。用户通过在浏览器中键入一个URL,点击一个超链接,提交一个HTML表单(一个post请求),或者一个客户端程序调用基于ASP.NET的WebService(通过ASP.NET提供服务)。在服务器端,IIS5或者IIS6将会收到这个请求。ASP.NET的底层通过ISAPI扩展与IIS通信,然后,通过ASP.NET,这个请求通常被路由到一个带有.aspx扩展名的页面。但是,这个处理过程如何工作,则完全依赖于HTTP处理器(handler)的执行。这个处理器将被安装用于处理指定的扩展。在IIS中,.aspx经由“应用程序扩展”被映射到ASP.NET ISAPI的dll文件:aspnet_isapi.dll。每一个触发ASP.NET的请求,都必须经由一个已经注册的,并且指向aspnet_isapi.dll的扩展名来标识。 注:ISAPI是自定义Web请求处理中第一个并且具有最高性能的IIS入口点。
依靠扩展名,ASP.NET把一个请求路由到一个恰当的处理器,该处理器则负责处理这个请求。举个例子,WebServices的扩展名.asmx不会把一个请求路由到磁盘上的某一个页面,而是会路由到在定义中附加了指定特性(WebMethodAttribute)的类,此特性会把它标识成一个Web Services的实现。许多其它的处理器将随着ASP.NET一起被安装。当然也可以定义你自己的处理器。在IIS里所有的HttpHandler被映射并指向ASP.NET ISAPI扩展,并且这些HttpHandler也都在web.config里配置,用于把请求路由到指定的HTTP处理器里执行。每一个处理器都是一个.NET类,用于处理指定的扩展。而这些处理器可以处理简单到只有几行代码的Hello World,也可以处理复杂到类似ASP.NET的页面以及执行WebService。就目前而言,仅仅需要理解扩展就是一种基本的映射机制,ASP.NET用它可以从ISAPI里获取一个请求,然后把请求路由到指定处理该请求的处理器中。 ISAPI连接
ISAPI是底层非托管的Win32 API。它定义的接口非常的单一并且性能最优。用这些接口处理原始指针(raw pointer),而函数指针列表(function pointer tables)则用于回调。ISAPI提供了最低层的、高性能的接口,开发者和工具厂商可以使用这些接口深入到IIS里。由于ISAPI是非常低层的,所以不太适合使用它构建应用级的程序。ISAPI趋向于被当作桥接口使用,用于给高层次的工具提供应用服务类型的功能。例如,ASP和ASP.NET都是被当作冷聚变(cold fusion)构建于ISAPI之上。大多Perl、PHP和JSP的执行如同许多第三方解决方案一样,可以在IIS运行。ISAPI是个非常好的工具,它给高层次的应用程序提供了高性能垂直访问接口。这使得那些高层次的应用程序需要的信息可以从ISAPI提供的信息中提炼。在ASP和ASP.NET里,引擎可以提炼ISAPI接口提供的表单里的对象如:Request和Response,这些对象可以从ISAPI请求的信息中读取它们各自的内容。 作为约定,ISAPI支持ISAPI扩展(extensions)和ISAPI过滤(filters)。扩展是请求处理接口,提供了跟Web Server输入和输出相关的逻辑处理。从本质上来说,它是一个事务接口。ASP和ASP.NET都被看作ISAPI扩展的实现。ISAPI是钩子接口,它允许你查看进入IIS的每一个请求并且可以修改请求的内容(包括输入和输出)或者改变模块(如:身份验证等)的行为。顺便提一下,ASP.NET通过两个方面的内容:HTTP处理器(对应ISAPI扩展)和HTTP 模块(对应ISAPI过滤)映射到ISAPI。这些相关的内容我将会在后面详细描述。 ISAPI是代码的初始点,标识ASP.NET一个请求的开始。ASP.NET映射了不同的扩展到它的ISAPI扩展,ISAPI扩展位于.NET Framework目录: <.NET FrameworkDir>"aspnet_isapi.dll 你可以在IIS服务管理器里看到这些映射,如图1所示。打开Web站点的根目录的属性,选择主目录选项卡,然后查看 配置|应用程序映射。
尽管.NET需要很多扩展名,但不必手工设置它们,你可以使用aspnet_regiis.exe实用工具确保所有的脚本映射都被注册。
IIS5和IIS6的不同之处
当一个请求进来的时候,IIS会检查脚本映射,然后把请求路由到aspnet_isapi.dll。接下来这个DLL文件的操作是什么呢?在IIS5和IIS6里,这个请求又是如何到达ASP.NET运行时的呢?它们两者的处理方式有没有重大变化呢?图2展示了一个大致的流程。
IIS5直接把aspnet_isapi.dll寄宿在inetinfo.exe进程里,或者它们中的一个将会与工作进程隔离,如果你拥有隔离权限,那么可以把Web站点和虚拟目录隔离的级别设置为中等或者高级。当第一个ASP.NET请求进来的时候,DLL将会在另一个EXE-aspnet_wp.exe里分配一个新的进程,然后把相关信息路由到这个新分配的进程里。接着这个新的进程会依次加载和寄宿.NET运行时。每一个进入到ISAPI DLL的请求,都将通过调用命名管道路由给这个工作者进程。 注:IIS6与之前的Web Server不同,已经对ASP.NET进行了优化处理。IIS6中使用了应用程序池。
值得注意的是,IIS6改变了这个处理模型,IIS不再直接寄宿像ISAPI扩展的任何外部可执行代码。代替的是,IIS总会保持一个单独的工作进程:应用程序池。所有的处理都发生在这个进程里,包括ISAPI dll的执行。对于IIS6而言,应用程序池是一个重大的改进,因为它们允许以更小的粒度控制一个指定进程的执行。你可以为每一个虚拟目录或者整个Web站点配置应用程序池,这可以使你很容易的把每一个应用程序隔离到各自的进程里,这样就可以把它与运行在同一台机器上其他程序完全隔离。从Web处理的角度看,如果一个进程死掉,至少它不会影响到其它的进程。 另外,应用程序池是高度可配置的。通过设置应用程序池的执行许可,可以配置它们的执行安全环境。并且可以为指定的应用程序按照相同的粒度定制这些配置。对于ASP.NET而言,IIS6最大的改进是使用应用程序池代替了machine.config里的ProcessModel实体的大部分功能。在IIS5里,这个实体是很难管理的,因为它的设置是全局的,而且不能够在指定Web程序的web.config里覆盖这些设置。当IIS6运行的时候,ProcessModel里的大部分配置将被忽略,取而代之的是读取应用程序池的配置。注意我这里说的是大部分,另外的一些配置,像线程池的大小和IO线程数目等仍然需要通过这个节点配置,这是因为,在服务器的应用程序池里没有提供类似功能的配置。 由于应用程序池是在外部执行的,所以这些执行可以很容易的被监控和管理。IIS6还提供了许多性能计数器,可以对重启和超时选项进行跟踪。在许多情况下,这可以帮助应用程序纠正问题。最后,IIS6的应用程序池的实现不依赖COM+,正如IIS5隔离进程一样,这提高了程序的性能和稳定性,特别是那些需要在内部使用COM对象的程序。 IIS6应用程序池也包含了ASP.NET固有的东西,ASP.NET可以和新的底层API通信,这些API允许直接访问HTTP缓冲存储器的API,而HTTP缓冲存储器的API可以直接进入Web Server的缓冲存储器,卸载ASP.NET级别的缓存。 在IIS6里,ISAPI扩展运行在应用程序池的工作进程里。.而NET运行时也运行在这个进程里,所以ISAPI扩展和.NET运行时的通信是发生在进程内的。这就使得比必须使用命名管道接口的IIS5具有更高的性能。尽管IIS的宿主模型不同,但是真正进入托管代码的接口是类似的,仅仅在获取被路由的请求时有一些变动。 进入.NET运行时
进入.NET运行时的真正登录点发生在一些没有正式文档的类和接口之间。在微软外面的世界,这些接口鲜为人知。微软的民间也不太热衷于讨论这些细节,可能是因为他们认为这些,对于使用ASP.NET构建程序的开发者没有太多的影响。 工作进程aspnet_wp.exe(IIS5)和w3wp.exe(IIS6)宿主在.NET运行时里。ISAPI DLL通过底层的COM调用一小撮非托管类型的接口,其实,最终调用的是ISAPIRuntime派生类的实例。进入运行时的第一个登录点是未归档ISAPIRuntime类,它通过COM把接口IISAPIRuntime暴露给调用者。这些COM 接口是底层的IUnknown,基于这些接口,就意味着从ISAPI扩展到ASP.NET之间的调用属于内部调用。图3是使用有名的反射工具Refector(http://www.aisto.com/roeder/dotnet/)看到的IISAPIRuntime接口的签名。Refector是一个可以查看和反编译程序集的工具,使用它可以很容易的查看元数据、反编译代码,就像图3中看到的那样。使用它一步一步地探究处理的过程,这是个非常不错的方法。
IISAPIRuntime接口担当着来自于ISAPI扩展(在IIS6里是直接通信的,在IIS5里间接的通过命名管道通信的)的非托管代码和托管代码之间的桥梁。如果你留意一下这个类,你会发现ProcessRequest方法的签名像下面的样子: [return: MarshalAs(UnmanagedType.I4)] int ProcessRequest([In] IntPtr ecb, [In, MarshalAs(UnmanagedType.I4)] int useProcessModel); ecb参数是ISAPI扩展控制块(extension control block),它被作为非托管资源传给ProcessRequest方法。此方法将获取ECB,然后把它作为基本的输入和输出接口,用于Request和Response对象。ISAPI ECB包含着所有底层的请求信息,这其中包括服务器变量,用于表单变量(form variables)的输入流,以及用于写数据并把数据发送到客户端的输出流中。一个单独的ECB引用基本上提供了一个ISAPI请求可以访问的所有功能。ProcessRequest既是登录点也是登出点,在这里非托管资源最先与托管代码相联系。 ISAPI扩展以异步的方式处理请求。所以,当ISAPI扩展调用了工作进程或者IIS的线程后,会立即返回,但会为当前有效的请求保留ECB。因此,ECB需要包含这样的机制,即当请求结束的时候通知ISAPI(通过ecb.ServerSupportFunction实现),然后ISAPI扩展释放ECB资源。接着以异步的方式立即释放ISAPI工作线程,和卸载由ASP.NET托管的那个隔离的处理线程。 ASP.NET得到ecb引用后,会在内部使用它来获取当前请求的相关信息,如服务器变量,POST的数据以及返回输出到客户端的数据。Ecb将继续存活直到这个请求结束或者IIS超时,在这之前,ASP.NET将会与ecb继续保持通信。当请求结束的时候,输出的内容会写进ISAPI的输出流里(通过ecb.WriteClient()实现)。然后ISAPI扩展会被通知请求已经结束,让它知道ECB可以被释放了。这个执行过程是非常高效的,这是因为,.NET类本质上只是担当着一个相当瘦小的包装器,而它包装的内容就是具有高性能的非托管ISAPI ECB。 加载.NET—稍微有点神秘
让我们回到之前略过的一个话题:当请求到达时,.NET运行时是如何被加载的。具体在哪里加载的,这是比较模糊的。关于这个处理过程,我没有找到相关的文档,由于我们现在讨论的是本地代码,所以通过反编译ISAPI DLL文件并把它描述出来显得不太容易。 我的最佳猜测是,在ISAPI扩展里,当第一个请求命中一个ASP.NET的映射扩展时,工作线程就会引导.NET运行时启动。一旦运行时存在了,非托管代码就可以为指定的虚拟目录请求一个ISAPIRuntime对象的实例,当然前提条件是,这个实例还不存在。每一个虚拟目录都会拥有一个AppDomain,在ISAPIRuntime存在的AppDomain里,它将引导一个单独的程序启动。由于接口被作为COM可调用的方法暴露,所以实例化操作将发生在COM之上。 为了创建ISAPIRuntime的实例,当指定虚拟目录的第一个请求到达时,System.Web.Hosting.AppDomainFactory.Create()方法将被调用。这将会启动程序的引导过程。这个方法接收的参数为:类型,模块名以及应用程序的虚拟路径,这些都将被ASP.NET用于创建AppDomain,接着会启动指定虚拟目录的ASP.NET程序。HttpRuntime的根对象将会在一个新的AppDomain里创建。每一个虚拟目录或者ASP.NET程序将寄宿在属于自己的AppDomain里。它们仅仅在有请求到达时启动。ISAPI扩展管理这些HttpRuntime对象的实例,然后基于请求的虚拟路径,把请求路由到正确的应用程序里。 回到运行时
这个时候,你已经拥有了一个ISAPIRuntime的活动实例,并且可以在ISAPI扩展里调用。一旦运行时启动并运行起来,ISAPI扩展就可以调用ISAPIRuntime.ProcessRequest()方法了,而这个方法就是进入ASP.NET通道真正的登录点。图4展示了这里的流程。
记住,ISAPI是多线程的,因此请求可以以多线程的方式穿过AppDomainFactory.Create()返回的对象引用。列表1展现了从IsapiRuntime.ProcessRequest方法反编译得到的代码。这个方法接收一个ISAPI ecb对象和一个服务器类型参数(这个参数用于指定创建何种版本的ISAPIWorkerRequest),这个方法是线程安全的,因此多个ISAPI线程可以同时安全的调用单个返回对象的实例。
这里实际的代码并不重要,需要提醒的是,这里的代码是通过反编译.NET框架内的代码得到的,你永远也不会和这些代码打交道,而且这些代码以后可能会有所变动。这里的用意是揭示ASP.NET在底层发生了什么。ProcessRequest接收了非托管参数ecb的引用,然后把它传给了ISAPIWorkerRequest对象,这个对象负责创建当前请求的内容。如列表2所示。
System.Web.Hosting.ISAPIWorkerRequest继承于抽象类HttpWorkerRequest,它的职责是创建一个抽象的输入和输出视图,为Web程序的输入提供服务。注意这里的另外一个工厂方法CreateWorkerRequest,它的第二个参数用于指定创建什么样的工作请求对象(即ISAPIWorkerRequest的派生类)。这里有3个不同的版本:ISAPIWorkerRequestInProc,ISAPIWorkerRequestInProcForIIS6,ISAPIWorkerRequestOutOfProc。当请求到来时,这个对象(指ISAPIWorkerRequest对象)将被创建,用于给Request和Response对象提供基础服务,而这两个对象将从数据的提供者WorkerRequest接收数据流。 抽象类HttpWorkerRequest围绕着底层的接口提供了高层的抽象(译注:抽象的目的是要把数据的处理与数据的来源解藕)。这样,就不用考虑数据的来源,无论它是一个CGI Web Server,Web浏览器控件还是你自定义的机制(用于把数据流入HTTP运行时),ASP.NET都可以以同样的方式从中获取数据。 有关IIS的抽象主要集中在ISAPI ECB块。在我们的请求处理当中,ISAPIWorkerRequest依赖于ISAPI ECB,当有需要的时候,会从中读取数据。列表2展示了如何从ECB里获取查询字符串的值的例子。 ISAPIWorkerRequest实现了一个高层次包装器方法(wrapper method),它调用了低层次的核心方法,而这些方法负责实际调用非托管API或者说是“服务层的实现”。核心的方法在ISAPIWorkerRequest的派生类里得以实现。这样可以针对它宿主的环境提供特定的实现。为以后增加一个额外环境的实现类作为新的Web Server接口提供了便利。同样使ASP.NET运行在其它平台上成为可能。另外这里还有一个帮助类:System.Web.UnsafeNativeMethods。它的许多方法是对ISAPI ECB进行操作,用于执行关于ISAPI扩展的非托管操作。 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 全部评论
专题导读
热门推荐
热门话题
阅读排行榜
|
请发表评论