1. 前言
本文从基础知识开始系统的描述了.Net大文件上传解决方案,希望给有需要的人提供帮助,同时介绍了IIS、Http管道、Asp.Net管道一些相对较底层知识,个人能力有限,不足之处请及时指正。
2. HTTP管道
2.1. ISAPI
当用户在浏览器中键入一个URL、点击一个超链接或提交一个HTML表单,在服务器端,IIS5或IIS6将会收到这个请求,并根据请求URL的扩展名不同交由不同的ISAPI扩展来处理,如下图所示:
ISAPI是底层非托管的win32 API,它定义的接口非常的单一并且性能最优。开发者和第三方厂商可以使用这些接口深入到IIS里。由于ISAPI是非常低层的,所以不太适合使用它构建应用级的程序。ISAPI趋向于被当作桥接口使用,用于给高层次的工具提供应用服务类型的功能,例如运行于IIS的Apache、Tomcat就是构建于ISAPI之上。ISAPI是个非常好的工具,它给高层次的应用程序提供了高性能垂直访问接口,这使得那些高层次的应用程序需要的信息可以从ISAPI提供的信息中提炼。在ASP和ASP.NET里,引擎可以提炼ISAPI接口提供的表单里的对象如:Request和Response,这些对象可以从ISAPI请求的信息中读取它们各自的内容。可以说,ISAPI是自定义Web请求处理中第一个并且具有最高性能的IIS入口点。
2.2. ISAPI扩展
作为约定,ISAPI支持ISAPI扩展(extensions)和ISAPI过滤(filters)。扩展是请求处理接口,提供了跟Web Server输入和输出相关的逻辑处理。从本质上来说,它是一个事务接口,ASP和ASP.NET都被看作ISAPI扩展的实现。ISAPI是钩子接口,它允许你查看进入IIS的每一个请求并且可以修改请求的内容(包括输入和输出)。
ASP.NET的ISAPI扩展为aspnet_isapi.dll,如下图所示:
2.3. IIS5与IIS6
当一个aspx请求到达IIS时,会检查脚本映射,然后把请求路由到aspnet_isapi.dll。接下来这个DLL文件的操作是什么呢?在IIS5和IIS6里,这个请求又是如何到达ASP.NET运行时的呢?它们两者的处理方式有没有重大变化呢?
在IIS5中:
1、客户端的请求进入IIS,IIS根据请求的URL试图找到一个扩展映射,当发现URL是一个.aspx时,则将请求交由aspnet_isapi.dll
2、ISAPI DLL再通过一些已命名的管道与ASP.NET工作者进程连接
3、ASP.NET把请求传递给一个指定的AppDomain
4、在AppDomain中开始走ASP.NET管道流程,这个流程经过了一系列的步骤,最终结果是将请求传递给服务终点(Endpoint)----我们编写的.aspx.cs类,这个类从Page派生,并实现IHttpHandler接口
在IIS6中:
IIS不再直接寄宿像ISAPI扩展的任何外部可执行代码。代替的是,IIS总会保持一个单独的工作进程:应用程序池。所有的处理都发生在这个进程里,包括ISAPI dll的执行。对于IIS6而言,应用程序池是一个重大的改进,因为它们允许以更小的粒度控制一个指定进程的执行。你可以为每一个虚拟目录或者整个Web站点配置应用程序池,这可以使你很容易的把每一个应用程序隔离到各自的进程里,这样就可以把它与运行在同一台机器上其他程序完全隔离。从Web处理的角度看,如果一个进程死掉,至少它不会影响到其它的进程。
3. Asp.net管道
3.1 主要流程
从请求进入ASP.NET工作者进程,直至它到达最终的处理程序之前要经过一系列的步骤和过程,这个步骤和过程本文中将其称之为ASP.NET管道,关于ASP.NET管道我并未找到相关官方的标准文档,而是根据Fritz Onion的《ASP.NET基础教程》整理而来。
1、ASP.NET管道的第一步是创建HttpWorkerRequest对象,它包含与当前请求有关的所有信息,包括请求的URL、标题等等。
2、把请求传递给HttpRuntime类的静态ProcessRequest方法。
3、HttpRutime类执行的第一件事情是创建HttpContext对象,并用HttpWorkerRequest类进行初始化。HttpContext类是管道的“粘合剂”,因为它把与当前请求有关的所有信息保存在一个地方,把所有类合成一体。第一次创建HttpContext对象时,它分配HttpRequest和HttpResponse类的新实例并且字段存储它们。它还为应用程序和会话状态提供了专用的访问器。(关于HttpContext请参阅3.2节)
4、一旦创创建了HttpContext类,HttpRuntime类就通过调用HttpApplicationFactory类的静态GetApplicationInstance方法,为该应用程序请求HttpApplication派生类的一个实例。
5、GetApplicationInstance要么创建HttpApplication(或派生类)类的一个新实例,要么从应用程序对象池中拖出一个实例。(关于HttpApplication请参阅3.3节)
6、一旦创建或检索到HttpApplication类,就对它进行初始化,并在初始化期间分配为此应用程序定义的所有模块。模块是实现IHttpModule接口的类,它为进程前后的请求提供服务。
7、一旦创建了模块,HttpRuntime类通过调用它的BeginProcessRequest方法,要求最新检索到的HttpApplication类对当前请求提供服务。然后,为当前请求找到合适的处理程序工厂。
8、创建处理程序,传递当前HttpContext,一旦ProcessRequest方法返回,请求完成。
3.2. HttpContext
上下文(HttpContextAsp.net)是Asp.net管道中最重要的一个类,该类维护所有与请求有关的数据,并且可以为管道中的大多数元素所访问,在请求的生命周期里,上下文一直是有效的。并且可以通过静态的HttpContext.Current属性访问,正如它的名字暗示的那样,HttpContext对象表示当前活动请求的上下文,因为它包含了在请求生命周期里你会用到的所有必需对象的引用,如:Request,Response,Application,Server,Cache。在请求处理过程的任何时候,你都可以使用HttpContext.Current访问这些对象。记住HttpContext是你的朋友,在请求或者页面处理的不同阶段,如果你需要相关数据都可以使用它获取。
3.3. HttpApplication
HttpApplication主要用作是Asp.net管道的事件控制器,负责请求的传输,通过触发事件,通知应用程序正在发生的事情。HttpApplication本身并不知晓发送给Web程序的数据,它仅仅是个消息邮递者,只负责事件之间的通信。它触发事件,然后通过传递HttpContext对象,把信息发送给被调用的方法。Asp.net管道的事件按照顺序逐一触发事件,如下图所示:
HttpApplication向外界提供的事件
事件 | 激活原因 | 顺序 |
BeginRequest | 收到新的请求 | 1 |
AuthenticateRequest | 已经确立用户的安全身份 | 2 |
AuthorizeRequest | 已经验证用户授权 | 3 |
ResolveRequestCache | 在受权以后但是在调用处理程序之前,如果找到缓存入口,则被缓存模块用来绕过处理程序的执行。 | 4 |
AcquireRequestState | 加载会话状态 | 5 |
PreRequestHandlerExecute | 请求发送到处理程序之前 | 6 |
PostRequestHandlerExecute | 请求发送到处理程序之后 | 7 |
ReleaseRequestState | 所有请求处理程序完成以后,被状态模块用来保存状态数据 | 8 |
UpdateRequestCache | 处理程序执行以后,被缓存模块用来存储缓存中的响应 | 9 |
EndRequest | 请求被处理以后 | 10 |
Disposed | 正好在关闭应用程序之前 | |
Error | 出现一个未经处理过的应用程序错误时 | |
PreSendRequestContent | 内容发送到客户之前 | |
PreSendRequestHeaders | HTTP标题发送到客户之前 |
3.4. IHttpModule
模块(HttpModule)是功能最强大的扩展点,它们在第一次创建应用程序时被创建,并且在应用程序的生命期内一直存在,可以接进任何一个HttpApplication事件。通常用于执行请求的预处理和后加工,在许多方面类似于IIS中的ISAPI过滤器。ASP.NET本身就用模块实现了许多应用程序级功能,包括身份验证、授权、缓存、及进程外会话状态管理。
如下示例了一个HttpModule的实现:
using System;
using System.Web;
namespace Test
{
public class MyModule : IHttpModule
{
public void Dispose(){}
public void Init(HttpApplication app)
{
app.BeginRequest += new EventHandler( this .Application_BeginRequest);
app.EndRequest += new EventHandler( this .Application_EndRequest);
}
protected void Application_BeginRequest( object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
app.Context.Response.Write( " This Is MyModule Application_BeginRequest . " );
}
protected void Application_EndRequest( object sender, EventArgs e)
{
HttpApplication app = sender as HttpApplication;
app.Context.Response.Write( " This Is MyModule Application_EndRequest . " );
}
}
}
web.config
< httpModules >
< add name ="myModule" type ="Test.MyModule, Test" />
</ httpModules >
3.5. IHttpHandler
using System;
using System.Web;
namespace Test
{
public class MyHandler : IHttpHandler
{
public void ProcessRequest(HttpContext cont)
{
cont.Response.Write("This Is MyHandler ." + "<br />");
}
public bool IsReusable {
get { return true; }
}
}
}
web.config
< httpHandlers >
< add verb ="GET" path ="*.aspx" type ="Test.MyHandler, Test" />
</ httpHandlers >
从如上代码可以发现,实现一个HttpHandler看似非常简单,它仅仅包含一个ProcessRequest()方法和一个IsReusable属性。但真是如此简单吗?记住,WebForms和WebServices都是作为HttpHandler实现的,在这个看似简单的接口里,其实很多的实现过程被隐藏了,我们只是做了简单输出而以。但关键点是通过ProcessRequest()可以得到一个HttpContext对象的实例,并且明白这个单独的方法可以从开始到结束负责处理一个Web请求。
另外大家可能注意到了IHttpHandler接口支持一个名称为IsReusable的只读属性,用于指明是否可以安全的共享一个特定处理程序的实例。如果构建一个自定义处理程序,并且从此属性返回true,则在用处理程序的实例对请求提供服务时,Asp.net共享处理程序的实例。如果返回false,则每次服务请求时要创建处理程序的一个新实例。通常,处理程序共享与否并没有多大差别,因为CLR中的实例化机制和垃圾回收器是非常有效的,所以共享处理程序类并不会得到多大好处。需要考虑使用共享的一种情况是,需要耗费大量时间的处理程序,比如从数据库中进行检索。而Asp.net提供的标准处理程序从不使用处理程序共享。