您的位置:首页 > 理论基础 > 计算机网络

从底层了解ASP.NET架构(引自:http://tech.it168.com/msoft/2007-12-24/200712241034626.shtml)

2009-08-16 22:26 381 查看
加载.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通道真正的登录点。图1展示了这里的流程。



图1把ISAPI的请求转到ASP.NET通道需要调用很多没有正式文档的类和接口,以及几个工厂方法。每一个Web程序/虚拟目录都运行在属于自己的
AppDomain里。调用者将维护一个IISAPIRuntime接口的代理引用,负责触发ASP.NET的请求处理。记住:ISAPI是多线程的,因
此请求可以以多线程的方式穿过AppDomainFactory.Create()返回的对象引用。列表1展现了从
IsapiRuntime.ProcessRequest方法反编译得到的代码。这个方法接收一个ISAPI
ecb对象和一个服务器类型参数(这个参数用于指定创建何种版本的ISAPIWorkerRequest),这个方法是线程安全的,因此多个ISAPI线
程可以同时安全的调用单个返回对象的实例。

列表 1: ProcessRequest请求进入 .NET的登录点

public int ProcessRequest(IntPtr ecb, int iWRType)
{
// ISAPIWorkerRequest从HttpWorkerRequest 继承,这里创建的是
// ISAPIWorkerRequest派生类的一个实例
HttpWorkerRequest request1 =
ISAPIWorkerRequest.CreateWorkerRequest(ecb,iWRType);
//得到请求的物理路径
string text1 = request1.GetAppPathTranslated();
//得到AppDomain的物理路径
string text2 = HttpRuntime.AppDomainAppPathInternal;
if (((text2 == null) || text1.Equals(".")) ||
(string.Compare(text1, text2, true,
CultureInfo.InvariantCulture) == 0))
{
HttpRuntime.ProcessRequest(request1);
return 0;
}
//如果外部请求的AppDomain物理路径和原来AppDomain的路径不同,说明ISAPI维持
//的AppDomain的引用已经失效了,所以,需要把原来的程序关闭,当有新的请求时,会
//再次启动程序。
HttpRuntime.ShutdownAppDomain("Physical path changed from " +
text2 + " to " + text1);
return 1;
}

需要提醒的是,这里的代码是通过反编译.NET框架内的代码得到的,我们永远也不会和这些代码打交道,而且这些代码以后可能会有所变动。这里的用意是揭示
ASP.NET在底层发生了什么。ProcessRequest接收了非托管参数ecb的引用,然后把它传给了ISAPIWorkerRequest对
象,这个对象负责创建当前请求的内容。如列表2所示。

列表2: 一个ISAPIWorkerRequest 的方法

// *** 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;
}

// *** 再派生于ISAPIWorkerRequest的类ISAPIWorkerRequestInProcIIS6的实现// *** 代码
// *** 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);
}


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可以看到)。

为请求创建了一个新的HttpContext实例

获取一个HttpApplication实例
调用HttpApplication.Init()初始化管道事件
nit()触发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在请求或者页面处理的不同阶段,如果需要相关数据都可以使用它获取。

列表 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根目录),并且它们都是独立处理请求的。

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管道的事件控制器,因此,它的接口主要有事件组成,这些事件包括:
BeginRequest
AuthenticateRequest
AuthorizeRequest
ResolveRequestCache
[此处创建处理程序(即与请求 URL 对应的页)。]
AcquireRequestState
PreRequestHandlerExecute
[执行处理程序。]
PostRequestHandlerExecute
ReleaseRequestState
[响应筛选器(如果有的话),筛选输出。]
UpdateRequestCache
EndRequest

这里的每一个事件都在Global.asax文件中以Application_为前缀,无实现代码的方法出现。举个例子,如
Application_BeginRequest()和Application_AuthorizeRequest()。由于它们在程序中会经常用到,
所以出于方便的考虑,这些事件的处理器都已经被提供了,这样你就不必再显式的创建这些事件处理器的委托了。

每一个ASP.NET
Web程序运行在各自的AppDomain里,在AppDomain里同时运行着多个HttpApplication的实例,这些实例存放在
ASP.NET管理的一个HttpApplication对象池里,认识到这一点,是非常重要的。这就是为什么可以同时处理多个请求,而这些请求不会互相
干扰的原因。

使用列表4的代码,可以进一步了解AppDomain,线程,HttpApplication之间的关系。

列表 4: AppDomain, Threads and HttpApplication instances之间的关系

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: " +
Thread.CurrentThread.IsThreadPoolThread.ToString() +
"<br>Thread Apartment: " +
Thread.CurrentThread.ApartmentState.ToString();

// *** 为了可以同时看到多个请求一起到达,故意放慢速度
Thread.Sleep(3000);
}


这是样例程序的一部分,运行的结果如图5所示。为了检验结果,你应该打开两个浏览器,输入相同的地址,观察那些不同的ID的值。



图2同时运行几个浏览器,你会很容易的看到AppDomains,application对象以及处理请求的线程之间内在的关系。当多个请求触发时会看到线程和application的ID在改变,而AppDomain的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在运行。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: