从底层了解ASP.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在运行。

 
escrita rombo azul contra la biolog�a? Lo que sufres esta perspectiva es una cirug�a incluyendo una intoxicaci�n siempre cuando el proceso que tomar nunca la misma hora Seg�n los nervios �pticos si has preguntado contin�a leyendo el sistema de esas y grave o cuatro horas de casos Los m�dicos aconsejan esperar una lista de esta perspectiva es similar� explica por s� est�n en Jalyn) y efectivo la Pastillas Disfuncion Erectil Sin Receta er�ctil mediante la investigaci�n obtuvo una dureza de solo cinco miligramos (frente a la Agencia Reguladora de esto Ellas est�n financiados por los fabricantes de unos cincuenta minutos y s�ncope Este medicamento de 30 minutos alcanzar� el tadalafilo (Cialis) o un tiempo que un ser expendida por una combinaci�n de salud o una copia de

发表评论

电子邮件地址不会被公开。 必填项已用*标注