阅读笔记 了解ASP.NET底层架构 之二

ASP.NET中的.NET runtime是如何加载起来的呢?

==============

这一点上, 微软没有公开的文档说明. 大致情况应该是这样的, 让我们从头开始:

1. 用户请求到来之前, IIS为每一个站点启动了相应的工作者进程(w3wp.exe)

2. 请求经过IIS基于网址将请求交给相应的站点, 找到站点对应的工作者进程.

3. 工作者进程基于请求的扩展名对请求进行分配, 发现是asp.net注册的扩展名, 在检查IIS的配置发现该扩展名应该使用ASP.NET作为ISAPI Extension来处理, 于是调用aspnet_isapi.dll中的HttpExtensionProc方法, 从而将控制转入到ASP.NET中.

这一步是我研究和猜想的, 不是参考资料上内容, 过程如下: 

1. 首先我使用.net reflector解析aspnet_isapi.dll. Reflector报告错误: dll does not contain a CLI header.

2. 这个信息说明aspnet_isapi.dll并不是一个.NET的程序集(后来自己想了想IIS的处理过程, 这里用reflector去解析aspnet_isapi.dll实在不聪明)

3. 于是使用dependency walker分析这个dll, 得到了这个dll暴露的方法列表, 见下图. 看这里就HttpExtensionProc方法比较有意思, 于是双击, dependendy walker自动调出Visual Studio的帮助文件, 并导航到这个方法的说明上.

4. 看说明, 这个方法是有应用程序自己定义的, 并且是Web服务器调用ISAPI Extension的主入口. Web服务器通过这份方法将控制传入到extension中. ISAPI extension使用这个方法向web服务器暴露自己处理请求的功能. 注意, msdn上说这个方法的名字并不是HttpExtensionProc, 它只是个占位符, 真正的名字由一个header定义.

5. 注意这个方法的原型的参数,就是我们前面详细描述过的ECB.

     DWORD WINAPI HttpExtensionProc( EXTENSION_CONTROL_BLOCK* pECB );

2009-11-20 10-21-57

4. 在aspnet_isapi.dll中从HttpExtensionProc开始, 通过一些非托管的接口和底层的COM对象, 调用到一些工厂方法, 其中System.Web.Hosting.AppDomainFactory.Create(string appId, string appPath)方法被调用, 该方法创建出System.Web.Hosting.ISAPIRuntime对象.

注意这个方法的参数, 它们是站点虚拟目录的路径信息还有module的名字, 这些参数被用来创建AppDomain和加载指定虚拟目录需要的ASP.NET应用程序.

5. 如前面一篇文章所说, ISAPIRuntime是非托管代码进入托管代码的接入点. 由底层的COM组件调用它的ProcessRequest方法, 从而进入了托管代码的世界, 也真真正正的进入了ASP.NET的数据处理管道.

6. 进入托管代码世界的第一步就是加载CLR引擎(如果CLR引擎还没有加载的话), 这一步详细介绍可以参考CLR是如何工作的一文中引擎初始化的部分. 我在这里啰嗦一点, 把这个过程也写在这里, 以便获得完整印象.

    System.Web.Hosting.ISAPIRuntime类存在于System.Web.dll中, 显然这是一个托管Assembly. 托管Assembly与mscoree.dll绑定. mscoree.dll加载之后读取托管assembly的metadata或app.config, 选择合适版本的CLR来加载.

    CLR加载起来之后, 就可以进行托管代码的执行了. 从这一步再深入下去请参考文章[翻译经典文章]深入.NET Framework内部, 看看CLR如何创建运行时对象的.

ProcessRequest如何使用ECB的?

===================

这里列出ProcessRequest的

   1:  public int ProcessRequest(IntPtr ecb, int iWRType)
   2:   
   3:  {
   4:        HttpWorkerRequest request1 = ISAPIWorkerRequest.CreateWorkerRequest(ecb, iWRType);
   5:        string text1 = request1.GetAppPathTranslated();
   6:        string text2 = HttpRuntime.AppDomainAppPathInternal;

7: if (((text2 == null) || text1.Equals(".")) ||

(string.Compare(text1, text2, true, CultureInfo.InvariantCulture) == 0))

   8:        {
   9:              HttpRuntime.ProcessRequest(request1);
  10:              return 0;
  11:        }
  12:        HttpRuntime.ShutdownAppDomain("Physical application path changed from " 
                                            +  text2 + " to " + text1);
  13:        return 1;
  14:  }

ProcessRequest接受到了非托管的ECB的引用, 把它传递给ISAPIWorkerRequest, 让它负责创建请求的上下文Context.

HttpWorkerRequest类的目的是提供一个围绕着底层接口的高层次的抽象视图, 这样才能屏蔽掉喂给HttpRuntime的数据来源的不同.

这里列出ISAPIRequest方法是如何调用非托管方法的:

   1:  public override byte[] GetQueryStringRawBytes()
   2:  {
   3:        byte[] buffer1 = new byte[this._queryStringLength];
   4:        if (this._queryStringLength > 0)
   5:        {
   6:              int num1 = this.GetQueryStringRawBytesCore(buffer1, this._queryStringLength);
   7:              if (num1 != 1)
   8:              {
   9:                    throw new HttpException( "Cannot_get_query_string_bytes");
  10:              }
  11:        }
  12:        return buffer1;
  13:  }
  14:   
  15:  // *** Implemented in a specific implementation class ISAPIWorkerRequestInProcIIS6
  16:  internal override int GetQueryStringCore(int encode, StringBuilder buffer, int size)
  17:  {
  18:        if (this._ecb == IntPtr.Zero)
  19:        {
  20:              return 0;
  21:        }
  22:        return UnsafeNativeMethods.EcbGetQueryString(this._ecb, encode, buffer, size);
  23:  }
HttpRuntime, HttpContext, 和HttpApplication都是干什么的? 有什么区别?

========================

当一个请求到达时, 请求被路由ISAPIRuntime.ProcessRequest()方法中, 这个方法接下来会调用HttpRuntime.ProcessRequest方法, 这个方法做了一些重要的事:

  • 为请求创建了一个新的HttpContext 实例
  • 获取了HttpApplication实例
  • 调用HttpApplication.Init() 方法来进行ASP.NET管道的事件设置
  • Init() 方法触发HttpApplication.ResumeProcessing() 方法, 该方法会开始ASP.NET的管道处理(即让请求经过一系列的事件函数)

HttpContext

这个对象在请求的整个生命期中都存在, 并且总是可以通过静态的HttpContext.Current属性而获得. 如同他的名字暗示的, HttpContext对象代表着当前请求的上下文, 因为它包含着你处理请求的生命期中要使用的所有重要对象的引用: Request, Response, Application, Server, Cache. 任何时候, 你都可以通过HttpContext.Current来获得这些对象.

HttpContext对象还包含着一个非常有用的Item Collection, 你使用它来存储请求相关的数据. Context对象在请求周期开始的时候就被创建出来, 在请求结束的时候才被释放掉. 所以Item Collection中存储的数据时针对当前请求的. 一个很好的使用范例是: 请求记录的机制, 可以帮助你记录下请求的起始时间的记录机制. 实现的方式是如下代码中所示的那样, 将Application_BeginRequest和Application_EndRequest方法勾连起来. HttpContext是你的朋友, 在请求的不同部分或者页面处理的不同部分使用它来帮助你获取数据吧.

   1:  protected void Application_BeginRequest(Object sender, EventArgs e)
   2:  {
   3:        //*** Request Logging
   4:        if (App.Configuration.LogWebRequests)
   5:              Context.Items.Add("WebLog_StartTime",DateTime.Now);
   6:  }
   7:   
   8:  protected void Application_EndRequest(Object sender, EventArgs e)
   9:  {
  10:        // *** Request Logging
  11:        if (App.Configuration.LogWebRequests)
  12:        {
  13:              try
  14:              {    
  15:                    TimeSpan Span = DateTime.Now.Subtract(
  16:                                  (DateTime) Context.Items["WebLog_StartTime"] );
  17:                    int MiliSecs = Span.TotalMilliseconds;
  18:   
  19:                    // do your logging
  20:                    WebRequestLog.Log(App.Configuration.ConnectionString,
  21:                                      true,MilliSecs);
  22:        }
  23:  }

HttpApplication

一旦Context对象被建立起来, ASP.NET需要路由你的请求到合适的虚拟目录上, 达到这个目的的方式是使用HttpApplication对象. 每个ASP.NET应用程序都必须建立一个虚拟目录, 并且这些"application"中的任何一个都是被独立的处理的.

HttpApplication如同庆祝活动的主席一样, 控制着处理请求的动作的进行.

每一个请求都要被路由到HttpApplication对象上. HttpApplicationFactory对象根据当前应用程序的负荷创建出一池子的HttpApplication对象, 然后将这些对象的引用分发给每一个请求. 池子的大小被machine.config文件中ProcessModel Key里的一个叫做MaxWorkerThreads的设置约束, 默认情况下值为20.

池中的线程的数目从1开始,随着并发到来的请求数量的增多而慢慢增多, 因为这些请求需要HttpApplication对象来处理. 池子中的对象的数目是受到监控的, 所以它不会超过规定的上限, 晚些时候, 这个数目会随着负荷的降低而下降.

HttpApplication是你具体Web应用程序的外部容器, 它映射了在Global.asax中定义的类. 它是你在你的应用程序中定期看到的进入HttpRuntime的入口点. 如果你查看一下你的Global.asax, 你会发现这个类是直接从HttpApplication继承来的.

public class Global : System.Web.HttpApplication

HttpApplication的主要目的就是在Http的处理管道中作为事件的控制器. 所以它的借口包括一些很主要的时间, 这些事件钩涉及的范围很广, 包括:

  • BeginRequest
  • AuthenticateRequest
  • AuthorizeRequest
  • ResolveRequestCache
  • AquireRequestState
  • PreRequestHandlerExecute
  • …Handler Execution…
  • PostRequestHandlerExecute
  • ReleaseRequestState
  • UpdateRequestCache
  • EndRequest

所有的这些事件都在Global.asax文件中也通过以Application_为前缀的空方法实现了, 比如说Application_BeginRequest(), Application_AuthorizeRequest().

这两个事件处理函数是处于方便的原因而提供的, 因为他们在应用程序中使用的频率很高, 这样做你就不用显示的再去创建这些事件处理函数的代理了.

理解下面的事实很重要:

每一个ASP.NET的虚拟应用程序都运行在他们自己的AppDomain中, 并且在AppDomain中, 多个HttpApplication实例并发的运行着, 这些HttpApplication是来自于ASP.NET管理的一个池. 正因为如此, 多个请求才能被同时处理, 并且不互相干扰.

为了弄清楚AppDomain, Threads 和HttpApplication之间的关系, 看看下面的代码吧:

   1:  private void Page_Load(object sender, System.EventArgs e)
   2:  {
   3:        // Put user code to initialize the page here
   4:        this.ApplicationId = ((HowAspNetWorks.Global)
   5:                    HttpContext.Current.ApplicationInstance).ApplicationId ;
   6:        this.ThreadId = AppDomain.GetCurrentThreadId();
   7:   
   8:        this.DomainId = AppDomain.CurrentDomain.FriendlyName;
   9:   
  10:        this.ThreadInfo = "ThreadPool Thread: " +
  11:              System.Threading.Thread.CurrentThread.IsThreadPoolThread.ToString() +
  12:                              "<br>Thread Apartment: " +
  13:              System.Threading.Thread.CurrentThread.ApartmentState.ToString();
  14:   
  15:        // *** Simulate a slow request so we can see multiple
  16:        //     requests side by side.
  17:        System.Threading.Thread.Sleep(3000);
  18:  }

另一个重要的事实是: 针对一个站点来说, HttpApplication对象都运行在相同的AppDomain中. 真因为如此, ASP.NET才可以保证web.config中的更改或者某个ASP.NET页面的修改会被整个AppDomain所识别. 因为在web.config中的修改会导致AppDomain被关闭, 然后重启. 这种方式保证了HttpApplication的所有实例都会看到那些修改, 因为AppDomain在重启的时候也重新从ASP.NET中加载了修改过的配置. 任何静态的引用都被重新加载, 所以如果说应用程序中再读取App configuration的配置时, 读取的已经是刷新过的值了.

事实上, Web application相当于"重启"了. 任何已经在pipeline处理中的请求会通过已经存在的pipeline继续执行下去, 同时新进来的请求会被路由到新的AppDomain上. 为了处理hung在那里的request, 即使在请求还在等待, ASP.NET还是会在请求超时的时候强制关掉AppDomain. 所以说呢, 对于同一个HttpApplication来说, 在某个特定的时间点上, 存在两个AppDomain是完全可能的. 因为老的正在被关闭, 而新的正在被自举上来. 两个AppDomain会持续地位自己的客户服务, 知道老的运行到结束掉等待的请求, 并被关闭, 只留下新的在运行.

原文地址:https://www.cnblogs.com/awpatp/p/1606951.html