zoukankan      html  css  js  c++  java
  • 从底层了解ASP.NET架构

    加载.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在运行。

  • 相关阅读:
    [na]ip数据包格式
    [js]浏览器同源策略(same-origin policy)
    [sql] 同库表(结构)的备份和sql聚合&navicat使用
    [svc]tcp三次握手四次挥手&tcp的11种状态(半连接)&tcp的time-wait
    [svc]ip地址划分
    [css]单/多行居中&字体设置
    时间戳转为C#格式时间
    windows 8 中 使用 httpclient
    oralce 查看是否启动 登陆 创建用户 常用命令小记
    SQL递归查询(with cte as)
  • 原文地址:https://www.cnblogs.com/zhukezhuke/p/1535145.html
Copyright © 2011-2022 走看看