我的上篇博客【我心目中的Asp.net核心对象】 讲述了一些我认为在Asp.net中比较重要的核心对象,以及演示了直接使用它们也能实现一个简单的服务响应。 今天,我将继续把我认为Asp.net的另一些重要的内容拿出来与大家一起分享, 同时将使用本次所讲述的内容改进上篇博客所演示的那个简单的服务,让它成为一个真正能用的服务框架。
在这篇博客中,不仅会继续演示上次提到的三个核心对象,我还会再引入另二个关键对象, 我将用更多实战代码来演示它们如何在一起协同工作,来完成许多常见任务, 展现它们的精彩故事,也最终会让它们来实现我的服务框架。 因此,这篇博客不仅仅是针对Asp.net的基础技术的讲解,而是更多地以实战形式展示这些核心对象所能发挥的强大功能, 以一个不到700行的轻量级服务框架来显示它们的核心价值,才是这篇博客的目标。
首先我要谈的话题是Asp.net的请求处理【管线】,我认为这是Asp.net中最重要的内容了, 所有到达Asp.net的请求都要经过管线来处理,不管是WebForms, MVC, WebService, WCF(Asp.net的承载方式), 还是其它微软的采用HTTP协议的框架。为什么这些框架都选择要Asp.net做为它们的运行平台呢?
我们可以考虑一下:如果让您从无到有设计一个服务框架,有哪些事件是必须要处理的?
我想有三个最根本的事件要做:1. 监听请求端口,2. 为每个传入的连接请求分配线程来执行具体的响应操作, 3. 要把请求的数据读出来,并负责将处理后的响应数据发送给调用者。
这其实是个比较复杂也很枯燥的过程,但每个服务器端程序都需要这些基本功能。 幸好IIS和Asp.net可以为我们做好这些事情,所以那些框架选择Asp.net平台就可以省去这些复杂的任务。 使用Asp.net平台不仅可以简化设计,它还有着良好的扩展性以满足更多的框架在这个平台上面继续开发, 而这个良好扩展性是离不开它的请求处理管线的。
Asp.net是一个功能完善的平台框架,它既提供一些高层次的框架供我们使用,比如:WebForms, MVC, WebService, 也提供一些低层次的机制让我们使用,以便于让我们开发有特殊要求的新框架,新解决方案。 这个低层次的机制就是请求处理管线,使用这个管线的有二类对象:HttpHandler, HttpModule, 控制这条管线工作的对象是:HttpApplication 。通常情况下,我们并不需要直接使用HttpApplication对象, 因此本文的主题将主要介绍HttpHandler, HttpModule这二类对象的功能以及如何使用它们。
理解Asp.net管线
管线(Pipeline)这个词也是很有点意思,这个词也形象地说明了每个Asp.net请求的处理过程: 请求是在一个管道中,要经过一系列的过程点,这些过程点连接起来也就形成一条线。 以上是我对于这个词的理解,如果有误,恳请给予指正。 这些一系列的过程点,其实就是由HttpApplication引发的一系列事件,通常可以由HttpModule来订阅, 也可以在Global.asax中订阅,这一系列的事件也就构成了一次请求的生命周期。
事件模式,也就是观察者模式。根据【C# 3.0 设计模式】一书中的定义:" 观察者模式定义了对象之间的一种联系,使得当一个对象改变状态时,所有其它的对象都可以相应地被通知到。" Asp.net的管线设计正是采用了这种方式, 在这个设计模式中,观察者就是许多HttpModule对象,被观察的对象就是每个"请求",它的状态是由HttpApplication 控制,用于描述当前请求的处理阶段,HttpApplication会根据一个特定的顺序修改这个状态,并在每个状态改变后引发相应的事件。 Asp.net会为每个请求分配一个HttpApplication对象来引发这些事件,因此可以让一大批观察者了解每个请求的状态, 每个观察者也可以在感兴趣的时候修改请求的一些数据。 这些与请求相关的数据的也就是我上篇博客中提到的HttpRequest, HttpResponse。 正是由于引入了事件机制,Asp.net框架也有了极强的扩展能力。再来看看管线处理请求的过程,我将直接引用MSDN中的原文【IIS 5.0 和 6.0 的 ASP.NET 应用程序生命周期概述】中的片段。
在处理该请求时将由 HttpApplication 类执行以下事件。 希望扩展 HttpApplication 类的开发人员尤其需要注意这些事件。
-
对请求进行验证,将检查浏览器发送的信息,并确定其是否包含潜在恶意标记。 有关更多信息,请参见 ValidateRequest 和脚本侵入概述。
-
如果已在 Web.config 文件的 UrlMappingsSection 节中配置了任何 URL,则执行 URL 映射。
-
引发 BeginRequest 事件。
-
引发 AuthenticateRequest 事件。
-
引发 PostAuthenticateRequest 事件。
-
引发 AuthorizeRequest 事件。
-
引发 PostAuthorizeRequest 事件。
-
引发 ResolveRequestCache 事件。
-
引发 PostResolveRequestCache 事件。
-
根据所请求资源的文件扩展名(在应用程序的配置文件中映射),选择实现 IHttpHandler 的类,对请求进行处理。 如果该请求针对从 Page 类派生的对象(页),并且需要对该页进行编译,则 ASP.NET 会在创建该页的实例之前对其进行编译。
-
引发 PostMapRequestHandler 事件。
-
引发 AcquireRequestState 事件。
-
引发 PostAcquireRequestState 事件。
-
引发 PreRequestHandlerExecute 事件。
-
为该请求调用合适的 IHttpHandler 类的 ProcessRequest 方法(或异步版 IHttpAsyncHandler.BeginProcessRequest)。 例如,如果该请求针对某页,则当前的页实例将处理该请求。
-
引发 PostRequestHandlerExecute 事件。
-
引发 ReleaseRequestState 事件。
-
引发 PostReleaseRequestState 事件。
-
如果定义了 Filter 属性,则执行响应筛选。
-
引发 UpdateRequestCache 事件。
-
引发 PostUpdateRequestCache 事件。
-
引发 EndRequest 事件。
-
引发 PreSendRequestHeaders 事件。
-
引发 PreSendRequestContent 事件。
如果是IIS7,第10个事件也就是MapRequestHandler事件,而且在EndRequest 事件前,还增加了另二个事件:LogRequest 和 PostLogRequest 事件。
只有当应用程序在 IIS 7.0 集成模式下运行,并且与 .NET Framework 3.0 或更高版本一起运行时,才会支持 MapRequestHandler、LogRequest 和 PostLogRequest 事件。
这里要补充一下:从BeginRequest开始的事件,并不是每个事件都会被触发,因为在整个处理过程中,随时可以调用Response.End() 或者有未处理的异常发生而提前结束整个过程。在那些"知名"的事件中,也只有EndRequest事件是肯定会触发的, (部分Module的)BeginRequest有可能也不会被触发。
对于这些管线事件,我只想提醒2个非常重要的地方:
-
每个请求都将会映射到一个HttpHandler,通常也是处理请求的主要对象。
-
HttpModule可以任意订阅这些事件,在事件处理器中也可以参与修改请求的操作。
这2点也决定了HttpHandler和HttpModule的工作方式。
我找了二张【老图片】,希望能更直观的说明Asp.net管线的处理过程。结合我前面讲述的内容,再品味一下老图片吧。


HttpHandler
HttpHandler通常是处理请求的核心对象。绝大多数的的请求都在【第10步】被映射到一个HttpHandler, 然后在【第15步】中执行处理过程,因此也常把这类对象称为处理器或者处理程序。我们熟知的Page就是一个处理器, 一个ashx文件也是一个处理器,不过ashx显示得更原始,我们还是来看一下ashx通常是个什么样子:
<%@ WebHandler Language="C#" Class="Login" %>
using System;
using System.Web;
public class Login : IHttpHandler {
public void ProcessRequest (HttpContext context) {
context.Response.ContentType = "text/plain";
string username = context.Request.Form["name"];
string password = context.Request.Form["password"];
if( password == "aaaa" ) {
System.Web.Security.FormsAuthentication.SetAuthCookie(username, false);
context.Response.Write("OK");
}
else {
context.Response.Write("用户名或密码不正确。");
}
}
public bool IsReusable {
get {
return false;
}
}
}
可以看到它仅仅是实现一个IHttpHandler接口而已,IHttpHandler接口也很简单:
// 定义 ASP.NET 为使用自定义 HTTP 处理程序同步处理 HTTP Web 请求而实现的协定。
public interface IHttpHandler
{
// 获取一个值,该值指示其他请求是否可以使用 System.Web.IHttpHandler 实例。
//
// 返回结果:
// 如果 System.Web.IHttpHandler 实例可再次使用,则为 true;否则为 false。
bool IsReusable { get; }
// 通过实现 System.Web.IHttpHandler 接口的自定义 HttpHandler 启用 HTTP Web 请求的处理。
void ProcessRequest(HttpContext context);
}
IsReusable属性上面有注释,我就不说了。接口中最重要的部分就是方法 void ProcessRequest(HttpContext context); 这个方法简单地不能再简单,只有一个参数,但这个参数的能量可不小,有了它几乎就有了一切 ,这就是我对它的评价。 关于HttpContext的更多详细介绍请参考我的博客【我心目中的Asp.net核心对象】。
在Login.ashx中,我做了三简单的事:
-
读取输入数据: 从Request.Form中。
-
执行特定的业务逻辑: 一个简单的判断。
-
返回结果给客户端: 调用Response.Write()
是的,就是这三个简单的操作,但也是绝大多数ashx文件的常规写法,它的确可以完成一次请求的处理过程。
**记住:**事实上任何HttpHandler都是这样处理请求的,只是有时会借助一些框架的包装而变了味道而已。
**我认为:**HttpHandler的强大离不开HttpContext,HttpHandler的重要性是因为管线会将每个请求都映射到一个HttpHandler。
通常,我们需要新的HttpHandler,创建一个ashx文件就可以了。 但也可以创建自己的HttpHandler,或者要将一类【特殊的路径/扩展名】交给某个处理器来处理, 那么就需要我们在web.config中注册那个处理器。
**注意:**如果是【特殊的扩展名】可能还需要在IIS中注册,原因很简单:IIS不将请求交给Asp.net,我们的代码根本没机会运行!
我们可以采用以下方式在web.config中注册一个自定义的处理器:
<httpHandlers>
<add path="/MyService.axd" verb="*" validate="false" type="MySimpleServiceFramework.MyServiceHandler"/>
</httpHandlers>
或者:(为了排版,我将一些代码做了换行处理)
<httpHandlers>
<remove verb="*" path="*.cs"/>
<add verb="*" path="*.cs" validate="false" type="FishWebLib.Ajax.AjaxMethodV2Handler,
FishWebLib, Version=3.0.0.0, Culture=neutral, PublicKeyToken=04db02423b9ebbb2"/>
<remove verb="*" path="*.ascx"/>
<add verb="*" path="*.ascx" validate="false" type="FishWebLib.Ajax.UserControlHandler,
FishWebLib, Version=3.0.0.0, Culture=neutral, PublicKeyToken=04db02423b9ebbb2"/>
</httpHandlers>
HttpModule
前面我已经提到过HttpModule的工作方式:订阅管线事件,并在事件处理器中执行所需的相关操作。
这个描述看起来很平淡,但是,它的工作方式给了它无限强大的处理能力。
它的无限强大的处理能力来源于可以订阅管线事件,因此,它有能力可以在许多阶段修改请求, 这些修改最终可能会影响请求的处理。
前面我说过:"HttpHandler是处理请求的主要对象",但HttpModule却可以随意指定将某个请求交给某个处理器来执行!
甚至,HttpModule也可以直接处理请求,完全不给HttpHandler工作的机会!
我们来看一下HttpModule的实现方式:
/// <summary>
/// 能支持双向GZIP压缩的Module,它会根据客户端是否启用GZIP来自动处理。
/// 对于服务来说,不用关心GZIP处理,服务只要处理输入输出就可以了。
/// </summary>
internal class DuplexGzipModule : IHttpModule
{
public void Init(HttpApplication app)
{
app.BeginRequest += new EventHandler(app_BeginRequest);
}
void app_BeginRequest(object sender, EventArgs e)
{
HttpApplication app = (HttpApplication)sender;
// 注意:这里不能使用"Accept-Encoding"这个头,二者的意义完全不同。
if( app.Request.Headers["Content-Encoding"] == "gzip" ) {
app.Request.Filter = new GZipStream(app.Request.Filter, CompressionMode.Decompress);
app.Response.Filter = new GZipStream(app.Response.Filter, CompressionMode.Compress);
app.Response.AppendHeader("Content-Encoding", "gzip");
}
}
public void Dispose()
{
}
}
每个HttpModule只需要实现IHttpModule接口就可以了。IHttpModule也是个简单的接口:
public interface IHttpModule
{
void Dispose();
void Init(HttpApplication app);
}
在这二个方法中,第一个方法通常可以保持为空。最重要的方法就是Init,它给了HttpModule能订阅管线事件的机会, 然后在相应的事件处理中,我们就可以执行它的具体操作了。
还记得我在博客【我心目中的Asp.net核心对象】 最后给出一个示例吗?在QueryOrderService.ashx中,为了支持gzip,需要直接调用GZipStream类,对于一二个ashx来说,或许不是问题, 如果这样的处理器变多了,每个处理器都那样写,您能受得了吗?反正我是受不了的,因此今天我把它改成使用Module来实现, 代码简单了许多。在本文末尾可以下载。
对于Asp.net项目来说:当您发现有很多处理输入输出的操作非常类似时, 那正是HttpModule可以发挥的舞台,请把这些重复的操作交给它吧。
让HttpModule工作也需要在web.config中注册:
<httpModules>
<add name="DuplexGzipModule" type="MySimpleServiceFramework.DuplexGzipModule"/>
</httpModules>
通常,我会把一些HttpModule放在类库中实现,然后在需要使用的项目的web.config中注册。
这也体现它的高重用性:写一次,许多项目就可以直接使用。
HttpModule的加载方式: 前面我说过"Asp.net会为每个请求分配一个HttpApplication对象",在每个HttpApplication对象的初始化操作中, 它会加载所有在web.config中注册的HttpModule。由于Asp.net并不是只创建一个HttpApplication对象,而是多个HttpApplication对象, 因此每个HttpModule的Init事件是有可能被多次调用的。 许多人喜欢在这里做各类初始化的操作,那么请注意在这里修改静态变量成员时的线程安全问题。 特别地,如果要执行程序初始化的操作,那么还是把代码放在Global.asax的Application_Start中去处理吧, HttpModule的Init事件并不合适。
**为HttpModule选择订阅合适的管线事件:**这是非常重要的,订阅不同的事件,产生的结果也会不一样。 原因也很简单,在Asp.net运行环境中,并不只有一个HttpModule,某个HttpModule的判断可能要依据其它HttpModule的输出结果, 而且在某些(晚期的)管线事件中,也不能再修改输出数据。 在后面的示例中,DirectProcessRequestMoudle订阅了PostAuthorizeRequest事件,如果订阅BeginRequest事件或许将得到更好的性能, 但是,在BeginRequest事件中,身份认证模块还没有工作,因此每个请求在这个事件阶段都属于"未登录"状态。
前面说了一大堆的HttpModule,事实上,在这个示例中,主角是另一个对象:Filter 。上篇博客我就提过它,最后为了演示它, 把它放在一个HttpHandler里【糟蹋了】,没办法,上篇的主题不是管线呀。今天只好和HttpModule一起出场了。 我认为Filter还是应该和HttpModule一起使用才能发挥它的独特价值。Filter的特点还真不合适在HttpHandler中使用, 如果您在HttpHandler里使用Filter,我认为有必要考虑一下是不是用错了。
借HttpModule的地盘我们来谈谈Filter。Filter很低调,低调到什么程度:可能很少有人关注过它,因此也少有人用过它。 事实也确实如此,一般情况下可以不用它,但用到它,你会发现它非常强大。前面我经常说到【输入输出流】, 请求的数据,除了请求头以外,基本上全放在流中,如果您希望对这些数据以流的方式进行处理,特别是希望对于所有请求, 或者某类请求,那么使用Filter是非常恰当的。前面的示例就是一个非常合理地使用,好好地品味它, 或许您还能发现Filter能做更多的事情。