声明:本博客上所有文章均为EagleFish在cnblogs上的原创,欢迎转载,但请注明出处。
写在前面的话:网上讲Asp.net运行模式的好文章已经很多了,笔者本不用多此一举,另成一文。但从笔者自己的学习经验看,如果学到的这些知识不能对应到类库中的源代码,印象总归不够深刻,大有隔靴搔痒之感。只好自己写上一篇,对这方面的知识做个小小的总结。文中所有内容都是笔者在看了网上很多文章后,结合自己的开发经验得出的一些理解,难免有错误的地方,欢迎批评指出。另外,由于笔者能力所限,很多地方并未说透(真正对应到代码),也盼高手能够给予补充。
一.进入Asp.net运行时之前
虽然本文的重点是对托管代码的解析,但为了整个知识点的完整性,这里简单介绍一下IIS处理请求的一些基本情况。在一个IIS服务器上,你可以设置多个应用程序池(每个应用程序池可以单独设置允许使用的最大内存数量、CPU使用率、回收工作进程的时间间隔等参数,而且一个应用程序池里面只能使用一个版本的.NET Framework),然后把自己的Web应用分别部署到这些应用程序池中。在默认情况下,每个应用池会有一个工作进程w3wp.exe来维护(如果开通了Web园功能,也可以设置多个工作进程)。每个应用程序(虚拟目录)在池中都有自己的应用程序域,这些应用程序域都处于这个应用程序池的工作进程的进程空间内。
IIS是通过各种ISAPI的扩展来处理各种类型的应用的。当我们从客户端提交一个请求过来之后,IIS会根据请求的页面或者服务的类型,把请求映射到指定的ISAPI扩展。比方说,如果我们需要让IIS支持perl这样的服务器端程序(当然,这个移植工作早就有人做过了),我们就需要编写一个专门处理对perl页面进行的请求的ISAPI扩展。根据ISAPI的定义(符合这个定义的ISAPI扩展才能和IIS正常交互),在你的扩展中可以包括ISAPI Extension和ISAPI Filter两大部分。ISAPI Extension是对请求的处理程序,完成和web服务器之间的输入输出;而ISAPI Filter则是一些回调接口,你可以通过实现这些接口来介入到整个请求处理的每一步骤,对Authentication,RevolveCache等环节进行控制。另外,ISAPI本身就是在工作进程里运行的,而asp.net运行时也是在工作进程里运行的,所以两者的交互非常有效率。
对于.aspx页面,这个扩展就是aspnet_isapi.dll。因为这些ISAPI都是非托管的Win32应用,直接对它们进行改动是比较困难的。所以,为了增强Asp.net运行时的可扩展性,aspnet_isapi.dll本身的功能非常少,我们可以把aspnet_isapi.dll简单理解为请求信息的路由器,负责把请求从IIS传送到asp.net运行时。而后面我们将要讲到的HttpHandle和HttpModule则分别担负起了ISAPI Extension和ISAPI Filter的功能,幸运的是,HttpHandle和HttpModule可以由纯的托管代码来实现。
二.从非托管代码到托管代码
前面说了,aspnet_isapi.dll是非托管代码,而asp.net运行时是托管代码,他们都运行在w3wp.exe工作进程里面,那么两者之间的调用点发生在什么地方呢?在介绍接下来的内容之前必须先介绍一个概念:ECB。ECB的全称是Extension Control Block,它是一个非托管资源包,具有对ISAPI接口完整的访问能力,包含了所有和一个传入请求有关的底层信息,如提交的标单中的数据等等。所以说,asp.net中的托管代码想要访问aspnet_isapi.dll对外提供的接口,就需要通过ECB。其实更准确的来说,是托管代码公布了一个IUnknown类型的接口供aspnet_isapi.dll调用,而aspnet_isapi.dll在调用的时候会把自己的ecb地址传进去。
明白了ECB的概念,下面我们要介绍一个接口和一个接口的实现类(位于System.Web.Hosting名字空间下),请读者注意笔者在代码中的注释(本文的主要目的就是和大家一起从代码实现的角度来认识整个Asp.net运行时,所以代码里的注释是笔者添加的关键性说明,后面的所有代码段都是这样):
1/**//*InterfaceType(ComInterfaceType.InterfaceIsIUnknown)指明了这个接口将作为 IUnknown 派生接口向 COM 公开,这就使得isapi.dll可以以COM方式调用此接口。*/
2[ComImport, Guid("08a2c56f-7c16-41c1-a8be-432917a1a2d1"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
3public interface IISAPIRuntime
4{
5 void StartProcessing();
6 void StopProcessing();
7 /**//*ProcessRequest方法就是整个处理流程中托管代码和非托管代码的分界点,可以看到里面是以一个IntPtr结构传入了调用方(也就是isapi.dll)的ECB地址*/
8 [return: MarshalAs(UnmanagedType.I4)]
9 int ProcessRequest([In] IntPtr ecb, [In, MarshalAs(UnmanagedType.I4)] int useProcessModel);
10 void DoGCCollect();
11}
12
13/**//*这个类实现了IISAPIRuntime接口。它的实例对象存在于每一个AppDomain中,作为整个Asp.net运行时的入口。*/
14public sealed class ISAPIRuntime : MarshalByRefObject, IISAPIRuntime, IRegisteredObject
15{
16 // Fields
17 private static int _isThisAppDomainRemovedFromUnmanagedTable;
18 private static string s_thisAppDomainsIsapiAppId;
19
20 // Methods
21 [AspNetHostingPermission(SecurityAction.Demand, Level=AspNetHostingPermissionLevel.Minimal), SecurityPermission(SecurityAction.Demand, Unrestricted=true)]
22 public ISAPIRuntime();
23 public void DoGCCollect();
24 public override object InitializeLifetimeService();
25 /**//*处理请求的入口点方法,由isapi.dll以COM方式调用*/
26 public int ProcessRequest(IntPtr ecb, int iWRType);
27 internal static void RemoveThisAppDomainFromUnmanagedTable();
28 internal void SetThisAppDomainsIsapiAppId(string appId);
29 public void StartProcessing();
30 public void StopProcessing();
31 void IRegisteredObject.Stop(bool immediate);
32}
所以,一切都是从aspnet_isapi.dll以COM方式调用了一个ISAPIRuntime对象的ProcessRequest方法开始的。可以多提一句的是,这种调用是异步的,也就是说,aspnet_isapi.dll在调用后会立即返回,但ECB会一直保留下来,直到整个请求被处理完毕之后再释放。
好,现在我们知道了ISAPIRuntime对象是托管代码的入口点,那么这个对象是什么时候产生的呢?换句话说,w3wp也是一个非托管代码写出的程序,它是在什么时候把.net运行时加载进来的呢?(如果好奇心再强一点,还可以问一问一个工作进程是什么时刻产生并开始运行的,它和应用程序池有着怎样的交互。)完全解释清楚这些问题已经超过了笔者目前的能力范围, 还望高人补充或提供资料线索。但目前我们从.net的代码中应该可以推断出,ISAPIRuntime对象和应用程序域是对应的,.net在创建应用程序域的时候,就会创建ISAPIRuntime对象,见下面的创建应用程序域的代码:
1/**//*这是System.Web.Hosting.AppDomainFactory类型的Create方法,它调用的是实际工厂的Create方法。*/
2[return: MarshalAs(UnmanagedType.Interface)]
3public object Create(string module, string typeName, string appId, string appPath, string strUrlOfAppOrigin, int iZone)
4{
5 /**//*实际工厂是一个AppManagerAppDomainFactory类型的对象。*/
6 return this._realFactory.Create(appId, appPath);
7}
8
9/**//*AppManagerAppDomainFactory.Create方法,请看代码内的注释。*/
10[return: MarshalAs(UnmanagedType.Interface)]
11public object Create(string appId, string appPath)
12{
13 object obj2;
14 try
15 {
16 if (appPath[0] == '.')
17 {
18 FileInfo info = new FileInfo(appPath);
19 appPath = info.FullName;
20 }
21 if (!StringUtil.StringEndsWith(appPath, '\\'))
22 {
23 appPath = appPath + @"\";
24 }
25 ISAPIApplicationHost appHost = new ISAPIApplicationHost(appId, appPath, false);
26 /**//*这个方法内部的调用链非常复杂,它一方面创建了一个应用程序域,一方面返回一个ISAPIRuntime对象。具体这个方法究竟是如何创建AppDomain对象的,大家可以用
27 JetBrain来跟踪其调用栈。关于这部分内容更详尽的信息,可参见ASP.NET Internals - The bridge between ISAPI and Application Domains一文。
28 另外,如果您使用JetBrain来调试系统程序集的话,有可能会因为缺少相应pdb文件而不能查看完整调试信息,这里提供一个根据已有程序集,先反汇编成中间码,
29 再重新以调试模式生成dll和pdb文件的方法:
30 1)生成IL文件: ildasm /tok /byt system.web.dll /out=system.web.il
31 2)重新生成PDB/DLL: ilasm system.web.il /DEBUG /DLL /OUTPUT=System.Web.dll*/
32 ISAPIRuntime o = (ISAPIRuntime) this._appManager.CreateObjectInternal(appId, typeof(ISAPIRuntime), appHost, false, null);
33 o.SetThisAppDomainsIsapiAppId(appId);
34 o.StartProcessing();
35 obj2 = new ObjectHandle(o);
36 }
37 catch (Exception)
38 {
39 throw;
40 }
41 return obj2;
42}
三.Asp.net运行时,我们等待已久的纯托管代码环境
好,经过上面长久的铺垫,我们终于进入了托管代码的领域。经过前面的内容,我们知道,在托管代码中首先被执行的是一个ISAPIRuntime对象的ProcessRequest方法,那么下面我们就来看一看这个方法主要做了些什么:
1/**//*ISAPIRuntime的方法,处理请求的入口。*/
2public int ProcessRequest(IntPtr ecb, int iWRType)
3{
4 try
5 {
6 /**//*这里ecb被作为参数传入,返回一个HttpWorkerRequest类型的对象,作为对一个请求的数据的封装。但HttpWorkerRequest
7 *只是一个抽象基类,CreateWorkerRequest作为一个工厂方法,返回的实际类型是ISAPIWorkerRequestInProc,
8 *ISAPIWorkerRequestInProcForIIS6或ISAPIWorkerRequestOutOfProc。这些类型里面提供的方法,其实大多
9 *围绕着如何从ecb中去获取数据,所以都包含了很多对System.Web.UnsafeNativeMethods类型中静态方法的调用。
10 **/
11 HttpWorkerRequest wr = ISAPIWorkerRequest.CreateWorkerRequest(ecb, iWRType);
12 string appPathTranslated = wr.GetAppPathTranslated();
13 string appDomainAppPathInternal = HttpRuntime.AppDomainAppPathInternal;
14 if ((appDomainAppPathInternal == null) || StringUtil.EqualsIgnoreCase(appPathTranslated, appDomainAppPathInternal))
15 {
16 /**//*从这里开始,对请求的处理流程就交给了HttpRuntime。需要注意的是,ISAPI是多线程的,而且对ProcessRequest的调用是异步的,
17 *这就要求HttpRuntime.ProcessRequest方法是线程安全的。看一看HttpRuntime.ProcessRequestNoDemand里的代码大家就清楚,
18 *所有的请求会被排成一个队列,顺次执行,保证了并发安全。
19 *最终,HttpRuntime.ProcessRequestInternal方法会被调用,我们接下来就去看看那个方法。
20 **/
21 HttpRuntime.ProcessRequestNoDemand(wr);
22 return 0;
23 }
24 HttpRuntime.ShutdownAppDomain(ApplicationShutdownReason.PhysicalApplicationPathChanged, SR.GetString("Hosting_Phys_Path_Changed", new object[] { appDomainAppPathInternal, appPathTranslated }));
25 }
26 catch (Exception exception)
27 {
28 Misc.ReportUnhandledException(exception, new string[] { SR.GetString("Failed_to_process_request") });
29 throw;
30 }
31 return 1;
32}
上面的代码段最主要的作用就是调用了HttpRumtime.ProcessRequestInternal方法,下面我们就一起来看看这个方法的实现:
1/**//*在HttpRuntime.ProcessRequestInternal()方法里,有如下几个重要的对象被创建出来:
2 *(1)HttpContext(包括其中的HttpRequest,HttpResponse)
3 *(2)HttpApplication
4 *同时,会执行HttpApplication对象的ProcessRequest方法,
5 */
6private void ProcessRequestInternal(HttpWorkerRequest wr)
7{
8 /**//*HttpContext对象在这里被创建。HttpWorkerRequest做为构造参数,而HttpWorkerRequest本身
9 *又围绕着对ecb的处理建立了一群高层的方法,它的实例会被HttpContext传给HttpRequest和HttpResponese
10 *做为他们的构造参数。所以,这里也能更清楚地看出HttpWorkerRequest作为ecb的托管环境封装器的实质。
11 *另外,这里也能清楚地反映出,每一个请求都有一个自己的HttpContext对象(而每一个HttpContext对象都管理着
12 *一个HttpSession对象--参见HttpContext的Session属性,这也就保证了每个访问者有自己的session对象。),你可以
13 *使用HttpContext.Current来访问到这个对象。
14 */
15 HttpContext extraData = new HttpContext(wr, false);
16 wr.SetEndOfSendNotification(this._asyncEndOfSendCallback, extraData);
17 Interlocked.Increment(ref this._activeRequestCount);
18 HostingEnvironment.IncrementBusyCount();
19 try
20 {
21 try
22 {
23 this.EnsureFirstRequestInit(extraData);
24 }
25 catch
26 {
27 if (!extraData.Request.IsDebuggingRequest)
28 {
29 throw;
30 }
31 }
32 extraData.Response.InitResponseWriter();
33 /**//*用应用程序工厂返回一个HttpApplication对象。
34 *和线程池对线程的管理相似,HttpApplicationFactory中以stack维护了一个HttpApplication的列表(参见HttpApplicationFactory
35 *的_freeList变量)。在这句方法调用的最后,实际是调用了 _theApplicationFactory.GetNormalApplicationInstance(context),
36 *里面就是从_freeList的栈顶pop出一个已经构造的HttpApplication实例。
37 *所以,对于每一个请求,由HttpContext作为上下文,由一个HttpApplication对象来控制整个应用处理的pipeline,整个
38 *处理过程是在由工作进程管理的线程池中的某个线程内完成的。
39 *另外,在一个应用程序域内,由于可以同时处理多个请求,所以就有多个HttpApplication实例和多个活动线程(您可以使用windbg的sos
40 *扩展来观察它们之间的关系,本文就不继续深入了)。
41 *还有,对所有HttpModules的加载就是发生在HttpApplication对象的创建过程之中(包括系统已经提供的Authentication等模块儿和我们
42 *的自定义模块)。我们可以在Web.config里声明自己的自定义模块。这些模块的作用就是在整个HttpApplication处理管线的相关事件点上,
43 *挂上自己的处理。注意一下IHttpModule接口的Init()方法的声明,这个方法的传入参数就是要被创建的HttpApplication对象,所以,如果
44 *你自己的模块想在缓存读取上加入一些自定义操作,你只需进行如下处理即可:
45 public class YourCustomModule : IHttpModule
46 {
47 public void Init(HttpApplication application)
48 {
49 application.ResolveRequestCache += new EventHandler(this.YourCustomResolveRequestCache);
50 }
51 }
52 *另外,通过对HttpApplicationFactory.GetApplicationInstance方法内部实现方式的阅读,你会发现在每一个HttpApplication对象被创建
53 *之后,会立刻调用这个对象的InitInternal方法,而这个方法里面做了很多重要的初始化操作,内容较多,我们将在下文中单独介绍。
54 *
55 */
56 IHttpHandler applicationInstance = HttpApplicationFactory.GetApplicationInstance(extraData);
57 if (applicationInstance == null)
58 {
59 throw new HttpException(SR.GetString("Unable_create_app_object"));
60 }
61 if (EtwTrace.IsTraceEnabled(5, 1))
62 {
63 EtwTrace.Trace(EtwTraceType.ETW_TYPE_START_HANDLER, extraData.WorkerRequest, applicationInstance.GetType().FullName, "Start");
64 }
65 /**//*看一下System.Web.HttpApplication的类型声明
66 *public class HttpApplication : IHttpAsyncHandler, IHttpHandler, IComponent, IDisposable
67 *你会发现它同时实现了同步和异步的IHandler,所以在默认情况下,Asp.net对请求的处理是异步的。
68 */
69 if (applicationInstance is IHttpAsyncHandler)
70 {
71 IHttpAsyncHandler handler2 = (IHttpAsyncHandler) applicationInstance;
72 extraData.AsyncAppHandler = handler2;
73 /**//*BeginProcessRequest会调用HttpApplication的ResumeSteps()方法,在ResumeSteps()中完成了整个应用程序周期的所有操作,
74 *包括对所有事件的触发、执行,对Handler的调用等。后文会有专门的小节进行介绍。*/
75 handler2.BeginProcessRequest(extraData, this._handlerCompletionCallback, extraData);
76 }
77 else
78 {
79 applicationInstance.ProcessRequest(extraData);
80 this.FinishRequest(extraData.WorkerRequest, extraData, null);
81 }
82 }
83 catch (Exception exception)
84 {
85 extraData.Response.InitResponseWriter();
86 this.FinishRequest(wr, extraData, exception);
87 }
88}
总之,通过machine.config及web.config档中对
Page page = BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(Page), context, true, true) as Page;
上面这句代码是被PageHandleFactory的GetHandle方法间接调用的(大家可以从HttpApplication.MapHttpHandler方法),调用返回的page对象是一个非常非常关键的实例(具体的方法调用过程中应当包括了对PageParser和PageBuilder的调用,望高手补充),因为它就是我们普通的aspx页面处理流程中,那个扮演着IHttpHandler的角色!也正因为此,我们在HttpRuntime.ProcessRequestInternal()方法里看到的applicationInstance.ProcessRequest(extraData)调用,实际上是调用的一个System.Web.UI.Page类型实例的ProcessRequest方法,整个执行流也因此进入了Page.ProcessRequestMain()的里面。我们平常所说的一个页面的生命周期的若干事件,您只要好好看看这个方法的实现,就都能明白了。由于这个方法估计是大家平时看的比较多的,对之也比较熟悉,本文这里就不多解释了。
四.HttpApplication的事件机制
到上面介绍的内容为止,整个处理流程基本上就讲完了。但如果只介绍到这里,恐怕大家对HttpApplication, IHttpModule, IHttpHandler三者的关系还是不太清楚。一个请求过程的所有事件(如BeginRequest、AuthenticateRequest等)是如何被触发的?如何通过自己的自定义Module来处理这些事件?Handler的处理又是在什么位置切入的?其实,这一切都是以HttpApplication内置的事件机制为核心的,下面就让我们来一步步揭示出它的实现方式(对.net中事件机制不够熟悉的读者可以先参看笔者另一篇文章: Part I of Events in Asp.Net: Events in .Net)。这里面涉及的事件很多,我们就以BeginRequest这个事件为例来说明:
(1)首先当然是要对事件本身进行定义
public event EventHandler BeginRequest
(2)由于有不止一个事件,为了方便对所有事件的管理,给每一个事件定义了一个唯一key,用作在事件容器中查找指定事件的标志
private static readonly object EventBeginRequest;
(3)把对一个个事件触发定义为一个个“执行步骤的执行”,下面是对“执行步骤”这个接口的定义
internal interface IExecutionStep
{
// 每一个“执行步骤”的执行命令
void Execute();
// Properties
bool CompletedSynchronously { get; }
bool IsCancellable { get; }
}
这里为什么要看似多此一举把对事件的触发再做一层封装,封装为所谓“执行步骤”,在后文介绍。
(4)用一个数组来保存所有的执行步骤。可以想象,真正到执行的时候,从数组里取出每一项IExecutionStep,再执行其Execute()方法即可。而Execute里面,肯定是对事件委托链的调用无疑
private IExecutionStep[] _execSteps;
(5)HttpModule在它初始化的时候完成对事件的注册。在创建HttpApplication对象的时候,会调用这个对象的InitInternal方法,这个方法内部会调用InitModules() ,它用来初始化和这个应用有关的所有HttpModule,而在这个方法里面,最重要的就是调用了每个Module的Init()方法。那么,如果我们有一个自定义的HttpModule,并希望这个Module去响应BeginRequest事件,我们应该这样定义自己的Module的Init()方法:
public void Init(HttpApplication application)
{
application.BeginRequest += new EventHandler(this.YourCustomMethodForBeginRequestEvent);
}
这样就完成了注册。当然,此时事件还没有执行,你也还没有看到事件和执行步骤的关系,HttpHandler也尚未登场。
(6)前面提到过所谓“执行步骤”,让我们先来看两个在HttpApplication中被定义的IExecutionStep,因为这两个执行步骤中所完成的,一个是对HttpHandler的解析和实例化,一个是调用HttpHandler的ProcessRequest方法,他们分别是MapHandlerExecutionStep和CallHandlerExecutionStep。读者可以自己去读一下这两个类的Execute()方法的代码,当您第一次从CallHandlerExecutionStep.Execute()中看到handler.ProcessRequest(context)和handler2.BeginProcessRequest(context, this._completionCallback, null)这样的调用语句,从MapHandlerExecutionStep.Execute()中看到context.Handler = this._application.MapHttpHandler(context, request.RequestType, request.FilePathObject, request.PhysicalPathInternal, false);这样的调用语句,一定会有一种“哎呀,原来你们在这里”的快感,呵呵。
(7)对“执行步骤数组”的初始化操作,就放在HttpApplication.InitInternal()方法里,具体就是下面这段语句:
1this.CreateEventExecutionSteps(EventBeginRequest, steps);
2this.CreateEventExecutionSteps(EventAuthenticateRequest, steps);
3this.CreateEventExecutionSteps(EventDefaultAuthentication, steps);
4this.CreateEventExecutionSteps(EventPostAuthenticateRequest, steps);
5this.CreateEventExecutionSteps(EventAuthorizeRequest, steps);
6this.CreateEventExecutionSteps(EventPostAuthorizeRequest, steps);
7this.CreateEventExecutionSteps(EventResolveRequestCache, steps);
8this.CreateEventExecutionSteps(EventPostResolveRequestCache, steps);
9steps.Add(new MapHandlerExecutionStep(this));
10this.CreateEventExecutionSteps(EventPostMapRequestHandler, steps);
11this.CreateEventExecutionSteps(EventAcquireRequestState, steps);
12this.CreateEventExecutionSteps(EventPostAcquireRequestState, steps);
13this.CreateEventExecutionSteps(EventPreRequestHandlerExecute, steps);
14steps.Add(new CallHandlerExecutionStep(this));//从这里,您可以很容易看到Handler对页面的解析处理在整个请求的处理的什么位置
15 //有哪些应用程序事件在它之前,哪些在它之后
16this.CreateEventExecutionSteps(EventPostRequestHandlerExecute, steps);
17this.CreateEventExecutionSteps(EventReleaseRequestState, steps);
18this.CreateEventExecutionSteps(EventPostReleaseRequestState, steps);
19steps.Add(new CallFilterExecutionStep(this));
20this.CreateEventExecutionSteps(EventUpdateRequestCache, steps);
21this.CreateEventExecutionSteps(EventPostUpdateRequestCache, steps);
22this._endRequestStepIndex = steps.Count;
23this.CreateEventExecutionSteps(EventEndRequest, steps);
24steps.Add(new NoopExecutionStep());
25this._execSteps = new IExecutionStep[steps.Count];
26steps.CopyTo(this._execSteps);//把整个数组拷贝给HttpApplication的私有变量_execSteps
steps.Add(new MapHandlerExecutionStep(this));
这种执行步骤是和事件无关的,只是在整个事件流中的特定位置执行一些特定的操作(实例化Handler之类的)。
而另一种是要把相关事件的处理方法列表添加到步骤中,每一个步骤其实是对一个事件的处理:
this.CreateEventExecutionSteps(EventBeginRequest, steps);
(8)执行步骤数组的执行。
其实在前文中已经提到过了,整个执行步骤数组的执行,是在HttpApplication.ResumeSteps()中调用的。恐怕就算不看这个方法的代码,大家也能想象得出,它是遍历整个执行步骤数组,然后调用其中每一项的Execute方法。这里大家大概也就清楚了,为什么会有一个执行步骤的概念。以笔者看来,首先它在概念上很好理解,完全贴合整个应用处理的管道模型pipeline;第二它屏蔽了引发事件的应用执行步骤和普通的内置执行步骤之间的差别;第三是比较容易在以后对整个流程进行改进和扩展。
本文结束,但对Asp.net运行时和.Net框架的探究其实只是刚刚开了个头。