• 设为首页
  • 点击收藏
  • 手机版
    手机扫一扫访问
    迪恩网络手机版
  • 关注官方公众号
    微信扫一扫关注
    公众号

用Asp.net写自己的服务框架

原作者: [db:作者] 来自: [db:来源] 收藏 邀请

我的上篇博客【我心目中的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这二类对象的功能以及如何使用它们。

管线(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 类的开发人员尤其需要注意这些事件。
1. 对请求进行验证,将检查浏览器发送的信息,并确定其是否包含潜在恶意标记。 有关更多信息,请参见 ValidateRequest 和脚本侵入概述。
2. 如果已在 Web.config 文件的 UrlMappingsSection 节中配置了任何 URL,则执行 URL 映射。
3. 引发 BeginRequest 事件。
4. 引发 AuthenticateRequest 事件。
5. 引发 PostAuthenticateRequest 事件。
6. 引发 AuthorizeRequest 事件。
7. 引发 PostAuthorizeRequest 事件。
8. 引发 ResolveRequestCache 事件。
9. 引发 PostResolveRequestCache 事件。
10. 根据所请求资源的文件扩展名(在应用程序的配置文件中映射),选择实现 IHttpHandler 的类,对请求进行处理。 如果该请求针对从 Page 类派生的对象(页),并且需要对该页进行编译,则 ASP.NET 会在创建该页的实例之前对其进行编译。
11. 引发 PostMapRequestHandler 事件。
12. 引发 AcquireRequestState 事件。
13. 引发 PostAcquireRequestState 事件。
14. 引发 PreRequestHandlerExecute 事件。
15. 为该请求调用合适的 IHttpHandler 类的 ProcessRequest 方法(或异步版 IHttpAsyncHandler.BeginProcessRequest)。 例如,如果该请求针对某页,则当前的页实例将处理该请求。
16. 引发 PostRequestHandlerExecute 事件。
17. 引发 ReleaseRequestState 事件。
18. 引发 PostReleaseRequestState 事件。
19. 如果定义了 Filter 属性,则执行响应筛选。
20. 引发 UpdateRequestCache 事件。
21. 引发 PostUpdateRequestCache 事件。
22. 引发 EndRequest 事件。
23. 引发 PreSendRequestHeaders 事件。
24. 引发 PreSendRequestContent 事件。

如果是IIS7,第10个事件也就是MapRequestHandler事件,而且在EndRequest 事件前,还增加了另二个事件:LogRequest 和 PostLogRequest 事件。
只有当应用程序在 IIS 7.0 集成模式下运行,并且与 .NET Framework 3.0 或更高版本一起运行时,才会支持 MapRequestHandler、LogRequest 和 PostLogRequest 事件。

这里要补充一下:从BeginRequest开始的事件,并不是每个事件都会被触发,因为在整个处理过程中,随时可以调用Response.End() 或者有未处理的异常发生而提前结束整个过程。在那些"知名"的事件中,也只有EndRequest事件是肯定会触发的, (部分Module的)BeginRequest有可能也不会被触发。

对于这些管线事件,我只想提醒2个非常重要的地方:
1. 每个请求都将会映射到一个HttpHandler,通常也是处理请求的主要对象。
2. 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中,我做了三简单的事:
1. 读取输入数据: 从Request.Form中。
2. 执行特定的业务逻辑: 一个简单的判断。
3. 返回结果给客户端: 调用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能做更多的事情。

关于Content-Encoding的解释

2012年2月1日补充内容:

很多人在看了前面那段示例代码,都有一个疑问:Fish Li,你是不是搞错了,Content-Encoding并不是个请求头啊?

对于这个疑问,有人在博客园中给我发过消息,也有给我发邮件。在一次次地解释后,我还是打算在这里说说我选这个标头的理由。

首先,我要说的是:我也知道 Content-Encoding 不是一个【标准】的请求头。
对于浏览器来说,也没必要有这个头,因为浏览器在发送请求时,总是不压缩的。 因此在HTTP的标准中没有为请求头定义一个表示请求体已编码过的标头。 但是,如果因为HTTP标准没有定义这个头,我就应该不去实现这个功能吗??

要知道并非只有浏览器才能发送HTTP请求,我用C#写点代码也可以发送HTTP请求啊!
你说我要不要实现这个压缩请求体的功能??

我们自己去实现发送HTTP请求的客户端时,为了能让服务端知道:请求体的内容是压缩过的,是不是必须有个头去告诉服务端? 然而,Accept-Encoding只是告诉服务端:客户端能接受什么样的编码,并不能表示某次请求体的内容是压缩过的,你说有什么好的办法??

所以,我就在请求头中就使用了Content-Encoding。 事实上,我就算用"XXXXXXXXXXXX"表示请求体是压缩过的,也可以的,只要我在服务端去检测这个头就可以了。 但为了自己容易理解,我宁可选择这个标头。

选 HttpHandler 还是 HttpModule ?

HttpHandler是每个请求的主要处理对象,而HttpModule可以选择请求交给哪个HttpHandler来处理, 甚至,它还可以选择它自己来处理请求。
下面我给个示例代码来说明HttpModule也能直接请求:

/// <summary>
/// 此Module示范了直接使用Module也能处理客户端的请求。
/// 建议:除非要很好的理由,否则不建议使用这种方法。
/// </summary>
internal class DirectProcessRequestMoudle : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.PostAuthorizeRequest += new EventHandler(app_PostAuthorizeRequest);
    }

    void app_PostAuthorizeRequest(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        ServiceInfo info = GetServiceInfo(app.Context);
        if( info == null )
            return;

        ServiceExecutor.ProcessRequest(app.Context, info);
        app.Response.End();
    }

为了更好的回答本节的这个问题,我再给段等效的代码,不过,请求是经过HttpHandler来处理

internal class MyServiceHandler : IHttpHandler
{
    internal ServiceInfo ServiceInfo { get; set; }

    public void ProcessRequest(HttpContext context)
    {
        ServiceInfo info = this.ServiceInfo ?? GetServiceInfo(context);

        ServiceExecutor.ProcessRequest(context, info);
    }

HttpHandler和HttpModule都能处理请求,我该选哪个??

对于此类情况,我的答案是:视情况而定,正如我在注释中描述的那样,除非要很好的理由,否则不建议使用HttpModule处理请求。 用HttpModule在某些时候可能会快点,关键点在于处理完成时要调用Response.End();这会让后面的事件全都短路, 其它的HttpModule就没有机会执行。如果您的框架或者项目设计很依赖于管线中的事件处理,那么调用Response.End();无疑会破坏这个规则, 也将会导致不能得到正确的结果。选择HttpHandler就不会有这种事情发生。

不过,也没有绝对的事情:在请求处理期间,您可以在任何地方调用Response.End(); 结果也是一样的。

幸好,短路的情况并不经常发生,因此选择HttpHandler会让整个Asp.net的管线都能发挥作用,因此,我建议优先选择HttpHandler。
尤其是在HttpHandler能很好的完成工作的前提下,就应该选HttpHandler,因为选HttpModule会给其它请求带来不必要的性能损失, 具体细节请继续阅读。

其实,我们还可以从另一个角度来看这个问题。
首先,请仔细地阅读前面的示例代码,您是否发现它们在实现方式上非常类似?
现在应该找到答案了吧:把具体的处理操作分离到HttpHandler,HttpModule之外的地方。 那么,此时这个问题也就不是问题了,您也可以提供多种方案供使用者选择。 比如:我就为【我的服务框架】提供了5种方式让使用者可以轻松地将一个C#方法公开为一个服务方法, 该如何选择这个问题,由使用者来决定,这个问题不会让我为难。

我的观点:在没有太多技术难度的前提下,提供多种解决办法应该是对的,您将会避开很多麻烦事情。

看不见的性能问题

前面我介绍了HttpModule的重要优点:高重用性。只要写好一个HttpModule可以放在任何Asp.net项目中使用,非常方便。

不过,再好的东西也不能滥用。HttpModule也可以对性能产生负面影响。 原因也很简单:对于每个Asp.net请求,每个HttpModule都会在它们所订阅的事件中, 去执行一些操作逻辑。这些操作逻辑或许对一些请求是无意义的,但仍会执行。 因此,计算机将会白白浪费一些资源去执行一些无意义的代码。

知道了原因,解决办法也就很清楚了:
1. 去掉不需要的HttpModule
2. 在每个HttpModule的事件处理器中,首先要确定是不是自己所需要处理的请求。对一些不相关的请求,应该立即退出。

在我们创建一个Asp.net项目时,如果不做任何修改,微软已经为我们加载了好多HttpModule 。请看以下代码:

protected void Page_Load(object sender, EventArgs e)
{
    HttpApplication app = HttpContext.Current.ApplicationInstance;        
    StringBuilder sb = new StringBuilder();

    foreach( string module in app.Modules.AllKeys ) 
        sb.AppendFormat(module).Append("<br />");

    Response.Write(sb.ToString());
}

输出结果如下:

总共有14个。
哎,大多数是我肯定不会用到的,但它们却被加载了,因此,在它们所订阅的事件中,它们的代码将会检查所有的请求, 那些无意义的代码将有机会执行。如果您不想视而不见,那么请在web.config中做类似的修改,将不需要的Module移除。

<httpModules>
    <remove name="Session"/>
    <remove name="RoleManager"/>
</httpModules>

HttpModule的第2个需要注意的地方是:HttpModule对所有的请求有效,如果HttpModule不能处理所有的请求, 那么请先判断当前请求是否需要处理,对于不需要处理的请求,应该立即退出。请看以下示例代码:

/// <summary>
/// 【演示用】让Aspx页的请求支持gzip压缩输出
/// </summary>
public class FishGzipModule : IHttpModule
{
    public void Init(HttpApplication app)
    {
        app.BeginRequest += new EventHandler(app_BeginRequest);
    }

    void app_BeginRequest(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        // 这里做个简单的演示,只处理aspx页面的输出压缩。
        // 当然了,IIS也提供压缩功能,这里也仅当演示用,或许可适用于一些特殊场合。
        if( app.Request.AppRelativeCurrentExecutionFilePath.EndsWith(
                            "aspx", StringComparison.OrdinalIgnoreCase) == false )
            // 注意:先判断是不是要处理的请求,如果不是,直接退出。
            //        而不是:先执行了后面的判断,再发现不是aspx时才退出。
            return;


        string flag = app.Request.Headers["Accept-Encoding"];
        if( string.IsNullOrEmpty(flag) == false && flag.ToLower().IndexOf("gzip") >= 0 ) {
            app.Response.Filter = new GZipStream(app.Response.Filter, CompressionMode.Compress);
            app.Response.AppendHeader("Content-Encoding", "gzip");
        }
    }

更多实战介绍

本文从这里起,将不再过多的叙述一些理论文字,而是将以实战的形式展示Asp.net的强大管线功能, 这些实战展示了一些很经典的应用场景, 其中大部分示例代码将做为【我的服务框架】的关键部分。因此请注意理解这些代码。

实战代码大量使用了上篇博客【我心目中的Asp.net核心对象】所介绍的绝大多数对象, 也算是再次展示那些核心对象的重要性,因此请务必先了解那些核心对象。

上篇博客仅展示了那些强大对象的功能,单独使用它们,也是不现实的, 今天,我将演示它们与HttpHandler, HttpModule一起并肩工作所能完成的各种任务。

故事未讲完,传奇在继续。更多精彩即将上演!

实战演示 - 模拟更多的HttpMethod

近几年又有一种被称为RESTful Web服务的概念进入开发人员的视野,它提倡使用HTTP协议提供的GET、POST、PUT和DELETE方法来操作网络资源。 不过,目前的浏览器只支持GET、POST这二种方法,因此就有人想到采用HTTP头,表单值,或者查询字符串的形式来模拟这些浏览器不支持的HTTP方法。 每种支持RESTful Web服务的框架都有它们自己的实现方式,今天我将使用HttpModule也来模拟这个操作。 最终的结果是可以直接访问HttpRequest.HttpMethod获取这些操作的方法名字。

实现原理:订阅管线中的BeginRequest事件,检查当前请求是否需要修改HttpMethod,如果是,则修改HttpMethod属性。
所以选择BeginRequest这个事件,是因为这个事件比较早,可以让请求的后续阶段都能读到新的结果。

/// <summary>
/// 【演示用】实现了模拟更多 HttpMethod 的Module
/// </summary>
internal class XHttpMethodModule : IHttpModule
{
    private FieldInfo _field;

    public void Init(HttpApplication context)
    {
        // 订阅这个较早的事件,可以让请求的后续阶段都能读到新的结果。
        context.BeginRequest += new EventHandler(context_BeginRequest);
        _field = typeof(HttpRequest).GetField("_httpMethod", BindingFlags.Instance | BindingFlags.NonPublic);
    }

    void context_BeginRequest(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        // 这里仅检查是否为POST操作,如果您的应用中需要使用GET来模拟的,请修改这里。
        if( string.Equals(app.Request.HttpMethod, "POST", StringComparison.OrdinalIgnoreCase) ) {

            // 这里为了简单,我只检查请求头,如果还需要检查表单值或者查询字符串,请修改这里。
            string headerOverrideValue = app.Request.Headers["X-HTTP-Method-Override"];

            if( string.IsNullOrEmpty(headerOverrideValue) == false ) {
                if( string.Equals(headerOverrideValue, "GET", StringComparison.OrdinalIgnoreCase) == false &&
                    string.Equals(headerOverrideValue, "POST", StringComparison.OrdinalIgnoreCase) == false ) {

                    // HttpRequest.HttpMethod属性其实就是访问_httpMethod这个私有字段,我将直接修改它。
                    // 这样修改后,最原始的HTTP方法就丢失,通常这或许也是可以接受的。
                    _field.SetValue(app.Request, headerOverrideValue.ToUpper());
                }
            }
        }
    }

我认为采用HttpModule来处理这个问题是个不错的选择。它至少有2个好处:
1. 这个HttpModule能继续给其它的网站项目使用,因此提高了代码的重用性。
2. 我可以随时决定要不要支持模拟,不需要模拟时,从web.config中不加载它就可以了,因此切换很灵活,且不需要修改现有代码。

来看一下页面及调用结果吧

protected void Page_Load(object sender, EventArgs e)
{
    Response.Write(Request.HttpMethod);
}

调用结果如下:

实战演示 - URL重写

使用HttpModule来实现URL重写。这个功能应该是HttpModule非常经典的应用了。

通常情况下,这种应用常用的方式是将一个URL: /product/12 重写为 /product.aspx?id=12 , 此时product.aspx应该是一个已经已存在的页面。显然重写后的地址更友好。URL重写的目的就是能让URL更友好。

实现原理:订阅管线的PostAuthorizeRequest事件,检查URL是不是期望修改的模式,如果是,则调用Context.RewritePath()完成URL的重写操作。 在管线的后续处理中,最终会使用新的URL来映射到一个合适的HttpHandler。 说明:选择的事件只要在【第10个事件】之前就可以了,因为在第10个事件前重写URL,才能保证到将请求映射到合适的处理器来执行。 就这么简单,请参考以下代码:

public class MyServiceUrlRewriteModule : IHttpModule
{
    // 为了演示简单,直接写死地址。
    // 注意:MyService.axd 必须在web.config中注册,以保证它能成功映射。
    public static string RewriteUrlPattern = "/MyService.axd?sc={1}&op={1}";

    public void Init(HttpApplication app)
    {
        app.PostAuthorizeRequest += new EventHandler(app_PostAuthorizeRequest);
    }

    void app_PostAuthorizeRequest(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        // 这里将检查URL是否为需要重写的模式,比如:
        //   http://localhost:11647/service/OrderService/QueryOrder
        NamesPair pair = FrameworkRules.ParseNamesPair(app.Request);
        if( pair == null )
            return;

        // 开始重写URL,最后将会映射到MyServiceHandler
        int p = app.Request.Path.IndexOf('?');
        if( p > 0 )
            app.Context.RewritePath(string.Format(RewriteUrlPattern, pair.ServiceName, pair.MethodName)
                + "&" + app.Request.Path.Substring(p + 1)
                );
        else
            app.Context.RewritePath(string.Format(RewriteUrlPattern, pair.ServiceName, pair.MethodName));
    }

重写发生了什么?
对于一个传入请求:http://localhost:11647/service/FormDemoService/ShowUrlInfo
它将被重写为:http://localhost:11647/MyService.axd?sc=FormDemoService&op=ShowUrlInfo
由于在web.config中,对MyService.axd已做过注册,因此Asp.net会将请求转交给注册的处理器来处理它。

注意:URL重写,会影响某些变量的值。请参考以下代码,我将写个服务方法来检测这个现象:

[MyServiceMethod]
public string ShowUrlInfo(int a)
{
    System.Web.HttpRequest request = System.Web.HttpContext.Current.Request;

    System.Text.StringBuilder sb = new System.Text.StringBuilder();
    sb.AppendFormat("Path: {0}\r\n", request.Path);            
    sb.AppendFormat("RawUrl: {0}\r\n", request.RawUrl);
    sb.AppendFormat("Url.PathAndQuery: {0}\r\n", request.Url.PathAndQuery);

    return sb.ToString();
}

输出结果:

实战演示 - URL路由

使用HttpModule来实现URL路由。这个功能随着Asp.net MVC框架的出现也逐渐流行起来了。

URL路由的目标也是为了使用URL更友好,与URL重写类似。

实现原理:订阅管线的PostResolveRequestCache事件,检查URL是不是期望的路由模式,如果是, 则要根据请求中所包含的信息找到一个合适的处理器,并临时保存这个处理器,重写URL到一个Asp.net能映射处理器的地址。 在管线的PostMapRequestHandler中,检查前面有没有临时保存的处理器,如果有,则重新给Context.Handler赋值,并重写URL到原始地址。 在管线的后续处理中,最终会使用Context.Handler的HttpHandler。 就这么简单,请参考以下代码:

public class MyServiceUrlRoutingModule : IHttpModule
{
    private static readonly object s_dataKey = new object();

    public void Init(HttpApplication app)
    {
        app.PostResolveRequestCache += new EventHandler(app_PostResolveRequestCache);
        app.PostMapRequestHandler += new EventHandler(app_PostMapRequestHandler);
    }

    private void app_PostResolveRequestCache(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        // 获取合适的处理器,注意这是与URL重写的根本差别。
        // 即:根据当前请求【主动】寻找一个处理器,而不是使用RewritePath让Asp.net替我们去找。
        MyServiceHandler handler = GetHandler(app.Context);
        if( handler == null )
            return;

        // 临时保存前面获取到的处理器,这个值将在PostMapRequestHandler事件中再取出来。
        app.Context.Items[s_dataKey] = handler;

        // 进入正常的MapRequestHandler事件,随便映射到一个处理器就行了。
        app.Context.RewritePath("~/MyServiceUrlRoutingModule.axd");
    }

    private void app_PostMapRequestHandler(object sender, EventArgs e)
    {
        HttpApplication app = (HttpApplication)sender;

        // 取出在PostResolveRequestCache事件中获得的处理器
        MyServiceHandler handler = (MyServiceHandler)app.Context.Items[s_dataKey];
        if( handler != null ) {
            // 还原URL请求地址。注意这里和URL重写的差别。
            app.Context.RewritePath(app.Request.RawUrl);

            // 还原根据GetHandler(app.Context)调用得到的处理器。
            // 因为此时app.Context.Handler是由"~/MyServiceUrlRoutingModule.axd"映射得到的。
            app.Context.Handler = handler;
        }
    }

注意:在MyServiceUrlRoutingModule中,我将请求【路由】到一个MyServiceHandler的实例,而不是让Asp.net根据URL来替我选择。

这段代码还个简化的版本,有兴趣的可阅读我的博客 【细说 HttpHandler 的映射过程】

在URL重写的演示中,有些URL相关的属性发生了改变,我们再来看一下URL路由是个什么结果:

实现自己的服务框架

本篇博客在开头说过:将在本次博客中改进上次的服务实现,让它成为一个真正能用的服务框架。
前面在讲述Asp.net管线时,给出了很多示例代码,这些示例代码都可以在博客的结尾处下载到。 这些代码来源于【我的服务框架】中的部分源代码,下面我将重点介绍【我的服


鲜花

握手

雷人

路过

鸡蛋
该文章已有0人参与评论

请发表评论

全部评论

专题导读
热门推荐
热门话题
阅读排行榜

扫描微信二维码

查看手机版网站

随时了解更新最新资讯

139-2527-9053

在线客服(服务时间 9:00~18:00)

在线QQ客服
地址:深圳市南山区西丽大学城创智工业园
电邮:jeky_zhao#qq.com
移动电话:139-2527-9053

Powered by 互联科技 X3.4© 2001-2213 极客世界.|Sitemap