ASP.NET 运行时模型的源码解析

- 首页 >> Web


摘要:ASP.NET 是微软公司推出的基于DotNet 平台的网络开发技术,对B/S 模式应用的开发提供了强大的支持,该文从源代码级别进入ASP.NET 底层,分析ASP.NET 运行时模型,揭开ASP.NET 应用程序运行的幕后细节。

1 引言

ASP.NET 运行时模型指的是ASP.NET 框架在接收到客户端请求,到获取合适的HttpHandler 接管请求之间,以及从HttpHandlers生成处理结果到发送处理结果给客户端之间,ASP.NET 框架所完成的一系列工作和处理逻辑[1]。本文将通过源代码级别的分析,深入讨论ASP.NET 2.0 运行时模型,解析ASP.NET 应用程序运行时模型的工作原理。

2 ISAPI 接口标准与IIS 服务器的可扩展性IIS 服务器是部署ASP.NET 应用的标准Web 服务器,由于IIS 服务器在设计时引入了开放的ISAPI 接口标准,具备极高的可扩展性,在核心组件不变的情况下可灵活支持不同类型不同版本的应用,例如IIS 5.1 版能同时支持ASP.NET 1.01.12.0,甚至可以支持部署基于DotNetFramework 3.5 ASP.NET 应用程序。ISAPI 的全称是:Internet Server ApplicationProgramming Interface,即Internet服务器应用编程接口,它为开发人员提供了强大的可编程能力,只要按照标准接口开发不同类型Web 应用程序的ISAPI 扩展程序,就能实现对IIS 功能上的扩展,从而使IIS 可以处理不同类型的客户端请求,例如让IIS 处理Perl PHP 应用程序[2]IIS 管理器提供了应用程序配置功能,可以对不同的客户端请求配置不同的ISAPI扩展程序。ISAPI 扩展程序通常以DLL的形式存在,可以被IIS 加载并调用。在ASP.NET2.0 下,对应于.aspx ASP.NET 应用程序,其ISAPI 扩展程序默认情况下位于C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\目录下,以aspnet_isapi.dll(下文简称aspnet_isapi)文件的形式存在[3]。有了ISAPI 扩展,IIS 服务器就可以根据客户端请求的资源扩展名,来决定应由哪个ISAPI 扩展来处理客户端请求,然后就可以将请求转发给合适的ISAPI 扩展。例如当访问资源的扩展名为.aspx.ascx.ashx .asmx 时,IIS 服务器会动将请求转发给缺省情况下位于C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\ 目录下aspnet_isapi.dll 进行处理[4],这个动态库就是一个ISAPI 扩展程序,负责处理ASP.NET 应用程序的请求。

3 ASP.NET 的后台辅助进程aspnet_wp.exe

实际上客户发起的请求最终要由aspnet_isapi传递给它背后的aspnet_wp.exe去处理,.Ne平台将其称为ASP.NET Worker Process(下文简称WP),该文件位于.NET framework 安装目录下,与aspnet_isapi.dll 所在位置相同。当aspnet_isapi 接收到IIS 转发的ASP.NET 请求后,会将请求放入队列,并根据实际情况分配请求处理任务给WP 进程,一旦请求被转送给WP 进程,WP 进程便会通知aspnet_isapi,请求正在被处理。这个通知的过是通过同步I/0 完成的,这么实现目的是为了保证处理过程的完整性,因为只有当请求在aspnet_isapi 内部被标记为“executing”后,WP 才会真正开始处理该请求。此后请求便在WP 的上下文环境中执行,当执行结束后,处理结果会通过一个异步的开放管道回送给aspnet_isapi,这时请求的状态会被更新为“Done”,接着请求就会从队列中清除。如果WP 进程崩溃,所有正在处理中的请求,都将维持“executing”状态一段时间,等到aspnet_isapi 检测到WP 进程死掉后,会自动丢弃所有的请求,并释放已经分配的资源[5]。其过程如图1 所示。

WP 会分析每一个请求的信息,解析出其中的虚拟目录信息,并检查该虚拟目录对应的AppDomain 是否已经存在,如果不存在则创建一个新的AppDomain,然后使用它,否则直接重用已经建立的AppDomain 对象。里的AppDomain 指的是.NET 中引入的应

用程序域的概念,它可以理解为一个进程或一个边界,或一个容器,它是应用程序的行环境,.NET 下所有的应用程序都运行在AppDomain ,每一个ASP.NET 应用程序(IIS 中的站点或者虚拟目录)都会有一AppDomain 与之对应[6],它保存了Applcation 对象、Cache 等全局变量。

4 请求在ASP.NET 运行时中的处理,步骤

一:ISAPIRuntime->HttpRuntimeWP接收到aspnet_isapi转发的请求后,就将请求

转送给指定虚拟目录对应的AppDomain中的ISAPIRuntime对象,由它完成对aspnet_isapi 封装后的请求包的解析工作,笔者使用Lutz Roeder's .NET Reflector工具查看System.Web.Dll,通过分析ISAPIRuntime的源码发现,客户的请求首先会被该类的ProcessRequest方法处理,该方法的部分代码如下:

ISAPIWorkerRequest wr = null;

try{

bool useOOP = iWRType == 1;

wr =ISAPIWorkerRequest.CreateWorkerRequest(ecb, useOOP);

wr.Initialize();

……

if (……){

HttpRuntime.ProcessRequestNoDemand(wr);

return 0;

}

……

}

可以看出在ProcessRequest方法中,主要通过调用一些非托管代码生成HttpWorkerRequest 对象,在上述代码中,对应于wr

(ISAPIWorkerRequest类型继承自HttpWorkerRequest),该对象包含当前请求的所有信息,然后ISAPIRuntime 调用HttpRuntime

ProcessRequestNoDemand方法传递创建好的HttpWorkerRequest对象,这个方法的代码如下:

internal static voidProcessRequestNoDemand(HttpWorkerRequest wr){

RequestQueue queue =_theRuntime._requestQueue;

if (queue ! = null){

wr = queue.GetRequestToExecute(wr);

}

if (wr ! = null){

CalculateWaitTimeAndUpdatePerfCounter(wr);

wr.ResetStartTime();

ProcessRequestNow(wr);

}}

该方法先从请求队列中取出一个请求,然后更新请求的引用计数器等信息,接着就让ProcessRequestNow 方法处理请求,这个方法的代码如下:

internal static voidProcessRequestNow(HttpWorkerRequest wr){

_theRuntime.ProcessRequestInternal(wr);

}

追踪分析源码可以发现_theRuntime对象是在HttpRuntime内部定义的,其类型就是HttpRuntime自身,它在HttpRuntime的静态构造函数中被初始化。

5 请求在ASP.NET 运行时中的处理,步骤二:HttpRuntime.ProcessRequestInternal()接下来我们重点分析HttpRuntime ProcessRequestInternal 方法的运作细节!使Reflector 工具追踪至ProcessRequestInternal方法的内部:

HttpContext context;

context = new HttpContext(wr, false);

……

this.EnsureFirstRequestInit(context);

context.Response.InitResponseWriter();

……

IHttpHandler applicationInstance =HttpApplicationFactory.GetApplicationInstance(context);

……

IHttpAsyncHandler handler2 =(IHttpAsyncHandler)applicationInstance;

context.AsyncAppHandler = handler2;

handler2.BeginProcessRequest(context,this._handlerCompletionCallback,context);

……

通过分析该方法的源码,发现它主要完成如下处理:

1) 根据HttpWorkerRequest 对象初始化HttpContext 对象,它包含了requestresponse 等属性,在编程中经常会用到这些重要的

属性来完成特定的任务。

2) 调用EnsureFirstRequestInit 方法完成第一次请求的初始化工作,该方法锁定全局变量_beforeFirstRequest,然后调用FirstRequestInit(

context)完成应用程序配置文件的加载、初始化请求队列、装载Bin 目录下的所有程序集等工作,然后更新_beforeFirstRe-1 请求传递和处理过程图

quest false

3) 执行InitResponseWriter 创建HttpWriter 对象,用于写入处理结果返回信息。

4) 调用HttpApplicationFactory 类的GetApplicationInstance 方法来生成IHttpHandler (这里生成的是一个默认的HttpApplication

对象,HttpApplication实现了IHttpHandler接口)

5) 调用HttpApplication 对象(它同时实现了IHttpAsyncHandler 接口)的BeginProcessRequest 方法执行客户请求。

需要注意的是,笔者在跟踪至HttpApplicationFactory 类的GetApplicationInstance 方法内部时发现它最终通过调用该类的Get-

NormalApplicationInstance 方法获取HttpApplication 实例:

return_theApplicationFactory.GetNormalApplicationInstance(context);

再深入一步,进入GetNormalApplicationInstance方法内部,我们终于看到HttpApplication对象是如何被创建和初始化的:

HttpApplication application = null;

……

application =(HttpApplication)HttpRuntime.CreateNonPublicInstance(this._theApplicationType);

using (new ApplicationImpersonationContext()){

application.InitInternal(context,this._state, this._eventHandlerMethods);

}

我们发现HttpApplication类提供了一个名为InitInternal的方法,调用方通过它来完成HttpApplication实例的初始化工作,在这

个方法的内部,有如下代码:

……

this.InitModules();

……

this.HookupEventHandlersForApplicationAndModules(handlers);

……

this._stepManager = newApplicationStepManager(this);

this._stepManager.BuildSteps(this._resumeStepsWaitCallback);

……

可以看到在HttpApplication对象初始化时,首先会自动调用自身的Init-Modules 方法来加载在web.config 文件中配置的所有HttpModule 模块。接着HookupEventHandlersForApplicationAndModules方法被调用,这个方法完成global.asax 文件中配置的HttpApplication HttpModule 事件的绑定。最后ApplicationStepManager 对象的BuildSteps 方法被调用,完成HttpApplication事件的绑定,这个方法很重要,它将创建各种HttpApplication.IExecution-Step 对象并保存到一个数组列表中,以便在BeginProcessRequest 方法内部调用ResumeSteps 方法依次执行这些对象的Execute()方法,完成各种处理。图2 是以上分析的一个总结。在取得HttpApplication对象实例之后,HttpRuntime对象开始调用它的BeginProcessRequest方法(实现IHttpAsyncHandler 接口

中定义的方法)处理请求:

IAsyncResultIHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb,object extraData){

……

this._stepManager.InitRequest();

this._context.Root();

HttpAsyncResult result = newHttpAsyncResult(cb, extraData);

this.AsyncResult = result;

……

this.ResumeSteps(null);

return result;

}

该方法首先调用ApplicationStepManager对象的InitRequest方法完成一些初始化工作,例如将记录当前执行步骤的变量清0、置请求处理完成标志为false 等。然后根据上下文创建HttpAsyncResult 对象记录执行结果,最后ResumeSteps 方法被调用!这个方法

会依次取出在数组列表中的HttpApplication.IExecutionStep对象,传递给HttpApplicationExecuteStep 方法,由它调用执行IExecutionStep对象的Execute()方法。当执行到MapHandlerExecutionStep 时,会执行如下代码获取最终执行请求的HttpHandler

context.Handler =this._application.MapHttpHandler(context, request.RequestType,request.FilePathObject, request.PhysicalPathInternal,false);HttpApplication 对象的MapHttpHandler 方法将根据配置文件结合请求类型和URL以调用相应的IHttpHandlerFactory 来获取HttpHandler 对象,例如与.aspx 页面对应的Page 类就是一种HttpHandler。此后请求处理的执行权被转交至对应的HttpHandler 对象

2 请求在ASP.NET 运行时内部处理过程图

2) 由于对数据的操作全部放在了页面构件中,而页面构件通常是一个Java Serlet 类,很容易通过现有平台的调试器来监视

程序的运行过程,对程序进行调试,从而很容易确定程序中的BUG 所在。

3) 由于html 代码与java 代码已经完全分离开来,因而如果逻辑有所变化,只需要对逻辑层进行修改即可,而不用变动表示层的代码。

4) 采用XMLC 技术很容易实现系统本地化。此时,只需要一个页面模板文件,然后为每种语言配置一个.properties 文件,由页面构件根据获取的浏览器端本地信息自动从对应的.prperties 文件中读取数据来填写相关的元素内容(通过采用多例模式很容易实现)。如果增加了一个地区语言,只需简单增加一个.porperties 文件,同时在页面构件中行一点小改动就行了,这比增加一个新页面文件省事多了。

5) 由于HTML 代码与Java 代码完全分开,使页面文件和逻辑层代码的可读性增强了[6],而且由于两者分开以后,不存在代码交叉出现的情况,文件内容的美观性也得到了保证。

7 下一步的工作

这次讨论的基于BarracudaEOS 并未实现Barracuda 中的事件模型,下一步的工作将究如何将Barracuda 的事件模型应用于EOS 中。

8 结论

基于Barracuda EOS 可以解决EOS 中页面构件难以编写和可读性较低的问题,而且可以在对EOS 改动不大的情况下得到实现,利用XMLC 实现以servlet 操纵页面元素[7],克服由于JSP 固有缺点带来的页面编程效率低下的问题,在EOS 己有的面向构件的

开发模型基础上,大大提高web编程生产率,是一种优秀的SOA开发模型。


站长地图