日期:2013-11-07  浏览次数:20365 次

.NET 1.1中预编译ASP.NET页面实现原理浅析

MS在发布ASP.NET时的一大功能特性是,与ASP和PHP等脚本语言不同,ASP.NET实际上是一种编译型的快速网页开发环境。这使得ASP.NET在具有开发和修改的简便性的同时,不会负担效率方面的损失。实现上ASP.NET与JSP的思路类似,引擎在第一次使用一个页面之前,会将之编译成一个类,自动生成Assembly并载入执行。
而通过《在WinForm程序中嵌入ASP.NET》一文中我们可以了解到,ASP.NET引擎实际上是可以无需通过IIS等Web服务器调用而被使用的,这就使得手工预编译ASP.NET页面成为可能。实际上这个需求是普遍存在的,早在ASP时代就层有第三方产品支持将ASP页面编译成二进制程序,以提高执行效率和保障代码安全性,而将伴随Whidbey发布的ASP.NET 2.0更是直接内置了预编译ASP.NET页面的功能。

实际上网上早就有人讨论过在ASP.NET 1.1中模拟预编译特性的实现方法,例如以下两篇文章

Pre-Compiling ASP.NET Web Pages
Pre-Compile ASPX pages in .NET 1.1

其思路基本上都是遍历所有需要预编译的页面文件,然后通过模拟Web页面请求的方式,触发ASP.NET引擎的自动预编译机制。这样做的好处是完全模拟真实情况,无需了解ASP.NET引擎的实现原理;但同时也会受到诸多限制,如预编译结果不透明,无法脱离原始ASP.NET页面文件使用等等,而且无法使我们从原理上理解预编译特性的实现。

下面我将分三到四个小节,简要讨论 ASP.NET 自动编译机制的实现、ASP.NET 页面文件编译的实现以及如何在ASP.NET 1.1中实现手动预编译页面和相应分发机制。

[1] 自动预编译机制浅析

本节我们将详细分析讨论.NET 1.1中,ASP.NET引擎内部实现自动页面预编译的原理。

首先,我们所说的ASP.NET页面实际上主要分为四类:

1.Web 应用程序文件 Global.asax
2.Web 页面文件 *.aspx
3.用户自定义控件文件 *.ascx
4.Web 服务程序文件 *.asmx

Web 应用程序文件对于每个Web 应用程序来说是可选唯一的,用来处理ASP.NET应用程序一级的事件,并将被预编译为一个System.Web.HttpApplication类的子类;
Web 页面文件是普通的ASP.NET页面,处理特定页面的事件,将被预编译为一个System.Web.UI.Page类的子类;
用户自定义控件文件是特殊的ASP.NET页面,处理控件自身的事件,将被预编译为一个System.Web.UI.UserControl类的子类;
Web 服务程序文件则是与前三者不太相同的一种特殊页面文件,暂时不予讨论。

然后,前三种ASP.NET文件的编译时机也不完全相同。Web 应用程序文件在此 Web 应用程序文件第一次被使用时自动编译;Web 页面文件在此Web页面第一次被使用时自动编译,实际上是调用 HttpRuntime.ProcessRequest 函数触发预编译;用户自定义控件文件则在其第一次被 Web 页面使用的时候自动编译,实际上是调用 Page.LoadControl 函数触发预编译。

在了解了以上这些基本知识后,我们来详细分析一下自动预编译的实现机制。

HttpRuntime.ProcessRequest 函数是处理Web页面请求的调用发起者,伪代码如下:


以下为引用:

public static void HttpRuntime.ProcessRequest(HttpWorkerRequest wr)
{
// 检查当前调用者有没有作为ASP.NET宿主(Host)的权限
InternalSecurityPermissions.AspNetHostingPermissionLevelMedium.Demand();

if(wr == null)
{
throw new ArgumentNullException("custom");
}

RequestQueue queue = HttpRuntime._theRuntime._requestQueue;

if(queue != null)
{
// 将参数中的Web页面请求放入请求队列中
// 并从队列中使用FIFO策略获取一个页面请求
wr = queue.GetRequestToExecute(wr);
}

if(wr != null)
{
// 更新性能计数器
HttpRuntime.CalculateWaitTimeAndUpdatePerfCounter(wr);
// 实际完成页面请求工作
HttpRuntime.ProcessRequestNow(wr);
}
}




HttpRuntime.ProcessRequestNow函数则直接调用缺省HttpRuntime实例的ProcessRequestInternal函数完成实际页面请求工作,伪代码如下:

以下为引用:

internal static void HttpRuntime.ProcessRequestNow(HttpWorkerRequest wr)
{
HttpRuntime._theRuntime.ProcessRequestInternal(wr);
}




HttpRuntime.ProcessRequestInternal函数逻辑稍微复杂一些,大致可分为四个部分。

首先检查当前HttpRuntime实例是否第一次被调用,如果是第一次调用则通过FirstRequestInit函数初始化;
接着调用HttpResponse.InitResponseWriter函数初始化页面请求的返回对象HttpWorkerRequest.Response;
然后调用HttpApplicationFactory.GetApplicationInstance函数获取当前 Web 应用程序实例;
最后使用Web应用程序实例完成实际的页面请求工作。

伪代码如下:

以下为引用:

private void HttpRuntime.ProcessRequestInternal(HttpWorkerRequest wr)
{
// 构造 HTTP 调用上下文对象
HttpContext ctxt = new HttpContext(wr, 0);

// 设置发送结束异步回调函数
wr.SetEndOfSendNotification(this._asyncEndOfSendCallback, ctxt);

// 更新请求计数器
Interlocked.Increment(&(this._activeRequestCount));

try
{
// 检查当前HttpRuntime实例是否第一次被调用
if(this._beforeFirstRequest)
{
lock(this)
{
// 使用 Double-Checked 模式 避免冗余锁定
if(this._beforeFirstRequest)
{
this._firstRequestStartTime = DateTime.UtcNow;
this.FirstRequestInit(ctxt); // 初始化当前 HttpRuntime 运行时环境
this._beforeFirstRequest = false;
}
}
}

// 根据配置文件设置,扮演具有较高特权的角