如何利用Asp.net构建个性化的服务框架?
- 内容介绍
- 文章标签
- 相关推荐
本文共计7399个文字,预计阅读时间需要30分钟。
我的上一篇博客【我心目中的Asp.net核心对象】讲述了在我看来Asp.net中比较重要的核心对象,以及演示了如何直接使用它们实现一个简单的服务响应。今天,我将继续分享我视为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中订阅,这一系列的事件也就构成了一次请求的生命周期。
事件模式,也就是观察者模式。根据一书中的定义:“ 观察者模式定义了对象之间的一种联系,使得当一个对象改变状态时,所有其它的对象都可以相应地被通知到。" Asp.net的管线设计正是采用了这种方式, 在这个设计模式中,观察者就是许多HttpModule对象,被观察的对象就是每个”请求“,它的状态是由HttpApplication 控制,用于描述当前请求的处理阶段,HttpApplication会根据一个特定的顺序修改这个状态,并在每个状态改变后引发相应的事件。 Asp.net会为每个请求分配一个HttpApplication对象来引发这些事件,因此可以让一大批观察者了解每个请求的状态, 每个观察者也可以在感兴趣的时候修改请求的一些数据。 这些与请求相关的数据的也就是我上篇博客中提到的HttpRequest, HttpResponse。 正是由于引入了事件机制,Asp.net框架也有了极强的扩展能力。再来看看管线处理请求的过程,我将直接引用MSDN中的原文中的片段。
在处理该请求时将由 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管线的处理过程。结合我前面讲述的内容,再品味一下老图片吧。
HttpHandlerHttpHandler通常是处理请求的核心对象。绝大多数的的请求都在被映射到一个HttpHandler, 然后在中执行处理过程,因此也常把这类对象称为处理器或者处理程序。我们熟知的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的更多详细介绍请参考我的博客。
在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中注册一个自定义的处理器:
<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)); }
重写发生了什么?
对于一个传入请求:localhost:11647/service/FormDemoService/ShowUrlInfo
它将被重写为: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来替我选择。
这段代码还个简化的版本,有兴趣的可阅读我的博客
在URL重写的演示中,有些URL相关的属性发生了改变,我们再来看一下URL路由是个什么结果:
实现自己的服务框架本篇博客在开头说过:将在本次博客中改进上次的服务实现,让它成为一个真正能用的服务框架。
前面在讲述Asp.net管线时,给出了很多示例代码,这些示例代码都可以在博客的结尾处下载到。
这些代码来源于中的部分源代码,下面我将重点介绍。
在中,一个类要想公开为服务类,并不需要继承某个类或者实现什么接口, 只需要在类上加一个特性就好了,方法也只需加一个特性,示例代码如下:
[MyService] public class OrderService { [MyServiceMethod] public static string Hello(string name) { return "Hello " + name; } [MyServiceMethod] public List<Order> QueryOrder(QueryOrderCondition query) { // 模拟查询过程,这里就直接返回一个列表。 List<Order> list = new List<Order>(); for( int i = 0; i < 10; i++ ) list.Add(DataFactory.CreateRandomOrder()); return list; } public string HiddenMethod(string aa) { // 这个方法应该是不能以服务方式被调用到的。 throw new NotImplementedException(); } }
如果某个方法需要只公开给登录用户或者指定的用户,还可以使用以下方式:
// 这是一个访问受限的服务类,只允许某些用户调用。 [Authorize] [MyService] public static class LimitService { [Authorize(Users="fish-li, cc")] [MyServiceMethod] public static string CalcPassword(string pwd) { // 这个方法只能由 fish-li, cc 二个用户来调用 if( pwd == null ) pwd = string.Empty; byte[] buffer = (new MD5CryptoServiceProvider()).ComputeHash(Encoding.Default.GetBytes(pwd)); return BitConverter.ToString(buffer).Replace("-", ""); } [MyServiceMethod] public static string CalcBase64(string str) { // 这个方法只能由已登录用户调用。 if( string.IsNullOrEmpty(str) ) return string.Empty; return Convert.ToBase64String(Encoding.UTF8.GetBytes(str)); } }
就这么简单,一个类,就可以成为一个服务。
说明:本框架并不要求将服务类在网站项目中实现,完全可以放在类库中实现。
还可以支持Session哦。
[MyService(SessionMode=SessionMode.Support)] public class SessionDemoService { [MyServiceMethod] public int Add(int a) { // 一个累加的方法,检验是否可以访问Session if( System.Web.HttpContext.Current.Session == null ) throw new InvalidOperationException("Session没有开启。"); object obj = System.Web.HttpContext.Current.Session["counter"]; int counter = (obj == null ? 0 : (int)obj); counter += a; System.Web.HttpContext.Current.Session["counter"] = counter; return counter; } }
SessionMode的定义如下:
public enum SessionMode { NotSupport, Support, ReadOnly } 支持的序列化的种类
在上篇博客中,我演示了使用JSON序列化的做法来实现一个服务响应。本来也是打算让框架仅支持JSON序列化的, 因为传输的数据量小嘛。没想到,做到后来,还是认为有必要把XML序列化也加进来,XML序列化快呀。 最后,居然想到既然是服务框架,Ajax调用也能算是服务吧,总不能不支持吧,后来干脆也能支持部分的Ajax调用了。
在中,服务端判断客户端发送的数据序列化方式是通过判断请求头"Serializer-Format"来实现的。 序列化的种类还允许继续自定义。只要实现以下接口:
public interface ISerializerProvider { object Deserialize(Type destType, HttpRequest request); void Serializer(object obj, HttpResponse response); }
然后调用以下方法就可以了:
public static class SerializerProviderFactory { public static void RegisterSerializerProvider(string name, Type type) { // ................................... }
判断客户端的序列化方式,由属性FrameworkRules.GetSerializerFormat来决定:
public static class FrameworkRules { private static string Internal_GetSerializerFormat(HttpRequest request){ string flag = request.Headers["Serializer-Format"]; return (string.IsNullOrEmpty(flag) ? "form" : flag); } private static Func<HttpRequest, string> _serializerFormatRule = Internal_GetSerializerFormat; /// <summary> /// 此委托用来判断客户端发起的请求中,数据是以什么方式序列化的。 /// 返回的结果将会交给SerializerProviderFactory.GetSerializerProvider()来获取序列化提供者 /// 默认的实现是检查请求头:"Serializer-Format" /// </summary> public static Func<HttpRequest, string> GetSerializerFormat { internal get { return _serializerFormatRule; } set { if( value == null ) throw new ArgumentNullException("value"); _serializerFormatRule = value; } }
只是一个委托,可以自己重新实现。
目前本框架提供了三个实现了接口ISerializerProvider的类供用户使用:JsonSerializerProvider, XmlSerializerProvider, FormSerializerProvider
这里只展示JsonSerializerProvider的实现:
internal class JsonSerializerProvider : ISerializerProvider { private static readonly MethodInfo s_JSSDeserializeMI = typeof(JavaScriptSerializer).GetMethod("Deserialize"); JavaScriptSerializer jss = new JavaScriptSerializer(); public object Deserialize(Type destType, HttpRequest request) { StreamReader sr = new StreamReader(request.InputStream, request.ContentEncoding); string input = sr.ReadToEnd(); MethodInfo deserialize = s_JSSDeserializeMI.MakeGenericMethod(destType); return deserialize.Invoke(jss, new object[] { input }); } public void Serializer(object obj, HttpResponse response) { if( obj == null ) return; response.ContentType = "application/json"; response.Write(jss.Serialize(obj)); }
注意:FormSerializerProvider的实现不够完善,因为再搞下去,就和就重复了。有兴趣的自己去完善吧。
这里再给自己的作品打个广告:
,
,
对于gzip的支持,我只想说:太简单了。
前面不是已给出DuplexGzipModule的实现代码嘛。是的,就是把它注册到web.config中就可以了。
你说简不简单? 完全不用写多余的代码,要不要gzip支持,也只是个配置问题!
说到这里,我想起前段时间Artech写的一篇博客通过WCF扩展实现消息压缩, 正如我在前篇博客的回复中说到的:“本来真没兴趣看的,不过,为了验证我的猜想,还是去看了一下,果然也没让我失望。”。
在此,有必要公开一下我的想法:绝对没有半点看不起Artech的意思,只是我对WCF没有兴趣了。理由也简单:不够简单。
还是接着说,Artech的博客展示了在WCF中压缩消息的方式,当然我相信Artech对于WCF的理解,他的方案或许应该是最简单的解决方案, 但是和对gzip的支持的易用性根本没法比。
WCF的粉丝们,当您看到这里,请先别忙着喷我。 听我说完:WCF的确很强大,我的这个不到700行的框架那也是根本不能和它相比的。
做这个比较仅仅是为了展示Asp.net是一个强大的平台,Asp.net有更高水准的扩展性。
利用发布服务的5种方式可以提供5种不同的方式,让您将一个类及方法公开成一个服务,供外界调用。
方法1:使用DirectProcessRequestMoudle,只需要配置web.config即可。
<localhost:11647/service/OrderService/QueryOrder
说明:URL模式是可以自由定义的,只要给FrameworkRules.ParseNamesPair赋值即可,它的定义如下:
public static Func<HttpRequest, NamesPair> ParseNamesPair
默认的实现方式:
internal static class UrlPatternHelper { // 为了演示简单,我只定义一个URL模式。 // 如果希望适用性更广,可以从配置文件中读取,并且可支持多组URL模式。 // URL中加了"/service/"只是为了能更好地区分其它请求,如果您的网站没有子目录,删除它也是可以的。 internal static readonly string ServicePattern = @"/service/(?<name>[^/]+)/(?<method>[^/]+)[/\?]?"; public static NamesPair ParseNamesPair(HttpRequest request) { if( request == null ) throw new ArgumentNullException("request"); Match match = Regex.Match(request.Path, ServicePattern); if( match.Success == false ) return null; return new NamesPair { ServiceName = match.Groups["name"].Value, MethodName = match.Groups["method"].Value }; }
客户端调用URL: localhost:11647/service/OrderService/QueryOrder
方法2:使用MyServiceUrlRoutingModule,只需要配置web.config即可。
<localhost:11647/service/OrderService/QueryOrder
说明:只有这种方式才能支持Session
方法3:使用MyServiceUrlRewriteModule,只需要配置web.config即可。
<localhost:11647/service/OrderService/QueryOrder
方法4:使用MyServiceHandler,只需要配置web.config即可。
<localhost:11647/MyService.axd?sc=OrderService&op=QueryOrder
方法5:创建一个ashx,不需要任何配置。
<%@ WebHandler Language="C#" Class="MyService" %> using System; using System.Web; using MySimpleServiceFramework; public class MyService : IHttpHandler { public void ProcessRequest (HttpContext context) { NamesPair pair = new NamesPair(); pair.ServiceName = context.Request.QueryString["sc"]; pair.MethodName = context.Request.QueryString["op"]; ServiceExecutor.ProcessRequest(context, pair); } public bool IsReusable { get { return false; } } }
客户端调用URL: localhost:11647/MyService.ashx?sc=OrderService&op=QueryOrder
注意:前三种方法,需要在IIS中做些额外的配置,因为URL中不包含文件扩展名了,IIS不知道把请求交给Asp.net来处理。
具体配置见下图,此处省略78个字。
虽然,我给出了5种发布方式,但是我还是想说说我个人的想法。
在这些方法中,使用URL重写,URL路由的方法,并不是我想推荐的,写它们是主要是为了展示HttpModule 。 不推荐它们是因为它们要判断URL是否符合指定模式,这个判断是有成本的。 至于成本有多高,特此,我专了做门的测试。 在示例代码压缩包中有个___TestRoutePerformance目录,结果如何,还是您自己去看吧,我也有点累了。
此外,我想问:对于服务来说,URL友好有多大意义?服务的URL会让用户来输入还是让Google的爬虫来访问?
如果以上二个问题都是否定的,那么,这二种方法就是在白白浪费机器的性能了。
当然了,如果您的站点访问量不大,那么这点性能也可以忽略不计了,就当我没说。
使用URL重写URL路由,还有个比较麻烦的事情:如果想通过URL多传递一个参数,那么,是不是又要修改URL模式?
对于使用DirectProcessRequestMoudle这种模式,我以前已经说过了:除非要很好的理由,否则不建议使用这种方法。
至于其它的二种方式,本质上是一样的,只是说:处理器谁来写的差别了。
不过,如果您要是选择手工创建一个处理器,除了不用修改web.config之外,
还可以自定义URL参数名,可以选择要不要支持Session
ReflectionHelper类用于根据类名及服务名定位到一个服务类型以及要调用的方法。
因此,它在框架中的作用也是非常关键的。
internal static class ReflectionHelper { private static List<TypeAndAttrInfo> s_typeList; static ReflectionHelper() { InitServiceTypes(); } /// <summary> /// 加载所有的服务类型,判断方式就是检查类型是否有MyServiceAttribute /// </summary> private static void InitServiceTypes() { s_typeList = new List<TypeAndAttrInfo>(256); ICollection assemblies = BuildManager.GetReferencedAssemblies(); foreach( Assembly assembly in assemblies ) { try { (from t in assembly.GetExportedTypes() let a = (MyServiceAttribute[])t.GetCustomAttributes(typeof(MyServiceAttribute), false) where a.Length > 0 select new TypeAndAttrInfo { ServiceType = t, Attr = a[0], AuthorizeAttr = t.GetClassAuthorizeAttribute() } ).ToList().ForEach(b => s_typeList.Add(b)); } catch { } } } private static AuthorizeAttribute GetClassAuthorizeAttribute(this Type t) { AuthorizeAttribute[] attrs = (AuthorizeAttribute[])t.GetCustomAttributes(typeof(AuthorizeAttribute), false); return (attrs.Length > 0 ? attrs[0] : null); } /// <summary> /// 根据一个名称获取对应的服务类型(从缓存中获取类型) /// </summary> /// <param name="typeName"></param> /// <returns></returns> private static TypeAndAttrInfo GetServiceType(string typeName) { if( string.IsNullOrEmpty(typeName) ) throw new ArgumentNullException("typeName"); // 查找类型的方式:如果有点号,则按全名来查找(包含命名空间),否则只看名字。 // 本框架对于多个匹配条件的类型,将返回第一个匹配项。 if( typeName.IndexOf('.') > 0 ) return s_typeList.FirstOrDefault(t => string.Compare(t.ServiceType.FullName, typeName, true) == 0); else return s_typeList.FirstOrDefault(t => string.Compare(t.ServiceType.Name, typeName, true) == 0); } private static Hashtable s_methodTable = Hashtable.Synchronized( new Hashtable(4096, StringComparer.OrdinalIgnoreCase)); /// <summary> /// 根据指定的类型以及方法名称,获取对应的方法信息 /// </summary> /// <param name="type"></param> /// <param name="methodName"></param> /// <returns></returns> private static MethodAndAttrInfo GetServiceMethod(Type type, string methodName) { if( type == null ) throw new ArgumentNullException("type"); if( string.IsNullOrEmpty(methodName)) throw new ArgumentNullException("methodName"); // 首先尝试从缓存中读取 string key = methodName + "@" + type.FullName; MethodAndAttrInfo mi = (MethodAndAttrInfo)s_methodTable[key]; if( mi == null ) { // 注意:这里不考虑方法的重载。 MethodInfo method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); if( method == null ) return null; MyServiceMethodAttribute[] attrs = (MyServiceMethodAttribute[]) method.GetCustomAttributes(typeof(MyServiceMethodAttribute), false); if( attrs.Length != 1 ) return null; // 由于服务方法的参数来源于反序列化,此时只可能包含一个参数。 ParameterInfo[] paraInfos = method.GetParameters(); if( paraInfos.Length != 1 ) throw new ArgumentNullException("指定的方法虽找到,但该方法的参数数量不是1"); AuthorizeAttribute[] auths = (AuthorizeAttribute[])method.GetCustomAttributes(typeof(AuthorizeAttribute), false); mi = new MethodAndAttrInfo { MethodInfo = method, ParamType = paraInfos[0].ParameterType, Attr = attrs[0], AuthorizeAttr = (auths.Length > 0 ? auths[0] : null) }; s_methodTable[key] = mi; } return mi; } /// <summary> /// 根据类型名称以及方法名称返回要调用的相关信息 /// </summary> /// <param name="pair">包含类型名称以及方法名称的对象</param> /// <returns></returns> public static InvokeInfo GetInvokeInfo(NamesPair pair) { if( pair == null ) throw new ArgumentNullException("pair"); InvokeInfo vkInfo = new InvokeInfo(); vkInfo.ServiceTypeInfo = GetServiceType(pair.ServiceName); if( vkInfo.ServiceTypeInfo == null ) return null; vkInfo.MethodAttrInfo = GetServiceMethod(vkInfo.ServiceTypeInfo.ServiceType, pair.MethodName); if( vkInfo.MethodAttrInfo == null ) return null; if( vkInfo.MethodAttrInfo.MethodInfo.IsStatic == false ) vkInfo.ServiceInstance = Activator.CreateInstance(vkInfo.ServiceTypeInfo.ServiceType); return vkInfo; } }
ServiceExecutor用于调用服务方法,前面所说的5种服务发布方式,最终都要经过这里。
/// <summary> /// 最终调用服务方法的工具类。 /// </summary> public static class ServiceExecutor { internal static void ProcessRequest(HttpContext context, ServiceInfo info) { if( context == null ) throw new ArgumentNullException("context"); if( info == null || info.InvokeInfo == null ) throw new ArgumentNullException("info"); //if( context.Request.InputStream.Length == 0 ) // throw new InvalidDataException("没有调用数据,请将调用数据以请求体的方式传入。"); if( info.InvokeInfo.AuthenticateRequest(context) == false ) ExceptionHelper.Throw403Exception(context); // 获取客户端的数据序列化格式。 // 默认实现方式:request.Headers["Serializer-Format"]; // 注意:这是我自定义的请求头名称,也可以不指定,默认为:form (表单) string serializerFormat = FrameworkRules.GetSerializerFormat(context.Request); ISerializerProvider serializerProvider = SerializerProviderFactory.GetSerializerProvider(serializerFormat); // 获取要调用方法的参数类型 Type destType = info.InvokeInfo.MethodAttrInfo.ParamType; // 获取要调用的参数 context.Request.InputStream.Position = 0; // 防止其它Module读取过,但没有归位。 object param = serializerProvider.Deserialize(destType, context.Request); // 调用服务方法 object result = info.InvokeInfo.MethodAttrInfo.MethodInfo.Invoke( info.InvokeInfo.ServiceInstance, new object[] { param }); // 写输出结果 if( result != null ) serializerProvider.Serializer(result, context.Response); } /// <summary> /// 用于根据服务的类名和方法名执行某个请求 /// </summary> /// <param name="context"></param> /// <param name="pair"></param> public static void ProcessRequest(HttpContext context, NamesPair pair) { if( pair == null ) throw new ArgumentNullException("pair"); if( string.IsNullOrEmpty(pair.ServiceName) || string.IsNullOrEmpty(pair.MethodName) ) ExceptionHelper.Throw404Exception(context); InvokeInfo vkInfo = ReflectionHelper.GetInvokeInfo(pair); if( vkInfo == null ) ExceptionHelper.Throw404Exception(context); ServiceInfo info = new ServiceInfo(pair, vkInfo); ProcessRequest(context, info); } } 关于写框架
以前曾在网上见过几次关于“重复造轮子”的说法。基本上,全是反对意见。
我今天也想谈谈这个话题:要不要自己写框架?
我的答案是:要。
自己写框架的好处不在于能将它做得多强大,多完美,而是从写框架的过程中,可以学到很多东西。
一个框架写完了,不在乎要给多少人使用,而是自己感觉有没有进步,这才是关键。
如果您不重复造轮子,可能永远不知道造轮子的过程,造轮子需要什么内容。
如果不重复造轮子,世界上将只有一个操作系统。
如果不重复造轮子,世界上将只有一个数据库。
如果不重复造轮子,世界上将只有一种编程语言。
这样单调会很好吗?
WebForms, MVC, Linq To SQL, Entity Framework, Remoting, Web Service, WCF
微软不也是一直在重复造轮子吗?
终于写完了这篇博客。这篇博客花了我最多的写作时间:整整二个周末还加上一些业余时间。
当然了,其中也包括实现框架的时间。这其实也是希望更好地展示Asp.net的核心内容。
写框架不是目标,展示Asp.net技术,写博客才是目标哦。
因此,这篇博客可谓是,只希望:
1. 能给大家分享Asp.net的核心内容。
2. 这篇博客的成绩能赶上。
我的写博热情也离不开您的肯定支持哦,如果您认为阅读此文有收获,别忘了点一下右下角的,。
Asp.net是个伟大的平台,能完成许多应用。这些应用的实现离不开这二次博客所提过的核心对象的支持,
本来还想再写点其它的示例,只是时间花得太多了。今天的示例已经很精彩了,到此为止吧,我也想休息了。
每个对象都是一个不朽的传奇,每个传奇背后都有一个精彩的故事。
我是Fish Li, 感谢大家阅读我的博客,请继续关注我的后续博客。
点击此处下载示例代码
如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的按钮。
如果,您希望更容易地发现我的新博客,不妨点击一下右下角的。
因为,我的写作热情也离不开您的肯定支持。
感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是Fish Li 。
本文共计7399个文字,预计阅读时间需要30分钟。
我的上一篇博客【我心目中的Asp.net核心对象】讲述了在我看来Asp.net中比较重要的核心对象,以及演示了如何直接使用它们实现一个简单的服务响应。今天,我将继续分享我视为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中订阅,这一系列的事件也就构成了一次请求的生命周期。
事件模式,也就是观察者模式。根据一书中的定义:“ 观察者模式定义了对象之间的一种联系,使得当一个对象改变状态时,所有其它的对象都可以相应地被通知到。" Asp.net的管线设计正是采用了这种方式, 在这个设计模式中,观察者就是许多HttpModule对象,被观察的对象就是每个”请求“,它的状态是由HttpApplication 控制,用于描述当前请求的处理阶段,HttpApplication会根据一个特定的顺序修改这个状态,并在每个状态改变后引发相应的事件。 Asp.net会为每个请求分配一个HttpApplication对象来引发这些事件,因此可以让一大批观察者了解每个请求的状态, 每个观察者也可以在感兴趣的时候修改请求的一些数据。 这些与请求相关的数据的也就是我上篇博客中提到的HttpRequest, HttpResponse。 正是由于引入了事件机制,Asp.net框架也有了极强的扩展能力。再来看看管线处理请求的过程,我将直接引用MSDN中的原文中的片段。
在处理该请求时将由 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管线的处理过程。结合我前面讲述的内容,再品味一下老图片吧。
HttpHandlerHttpHandler通常是处理请求的核心对象。绝大多数的的请求都在被映射到一个HttpHandler, 然后在中执行处理过程,因此也常把这类对象称为处理器或者处理程序。我们熟知的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的更多详细介绍请参考我的博客。
在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中注册一个自定义的处理器:
<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)); }
重写发生了什么?
对于一个传入请求:localhost:11647/service/FormDemoService/ShowUrlInfo
它将被重写为: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来替我选择。
这段代码还个简化的版本,有兴趣的可阅读我的博客
在URL重写的演示中,有些URL相关的属性发生了改变,我们再来看一下URL路由是个什么结果:
实现自己的服务框架本篇博客在开头说过:将在本次博客中改进上次的服务实现,让它成为一个真正能用的服务框架。
前面在讲述Asp.net管线时,给出了很多示例代码,这些示例代码都可以在博客的结尾处下载到。
这些代码来源于中的部分源代码,下面我将重点介绍。
在中,一个类要想公开为服务类,并不需要继承某个类或者实现什么接口, 只需要在类上加一个特性就好了,方法也只需加一个特性,示例代码如下:
[MyService] public class OrderService { [MyServiceMethod] public static string Hello(string name) { return "Hello " + name; } [MyServiceMethod] public List<Order> QueryOrder(QueryOrderCondition query) { // 模拟查询过程,这里就直接返回一个列表。 List<Order> list = new List<Order>(); for( int i = 0; i < 10; i++ ) list.Add(DataFactory.CreateRandomOrder()); return list; } public string HiddenMethod(string aa) { // 这个方法应该是不能以服务方式被调用到的。 throw new NotImplementedException(); } }
如果某个方法需要只公开给登录用户或者指定的用户,还可以使用以下方式:
// 这是一个访问受限的服务类,只允许某些用户调用。 [Authorize] [MyService] public static class LimitService { [Authorize(Users="fish-li, cc")] [MyServiceMethod] public static string CalcPassword(string pwd) { // 这个方法只能由 fish-li, cc 二个用户来调用 if( pwd == null ) pwd = string.Empty; byte[] buffer = (new MD5CryptoServiceProvider()).ComputeHash(Encoding.Default.GetBytes(pwd)); return BitConverter.ToString(buffer).Replace("-", ""); } [MyServiceMethod] public static string CalcBase64(string str) { // 这个方法只能由已登录用户调用。 if( string.IsNullOrEmpty(str) ) return string.Empty; return Convert.ToBase64String(Encoding.UTF8.GetBytes(str)); } }
就这么简单,一个类,就可以成为一个服务。
说明:本框架并不要求将服务类在网站项目中实现,完全可以放在类库中实现。
还可以支持Session哦。
[MyService(SessionMode=SessionMode.Support)] public class SessionDemoService { [MyServiceMethod] public int Add(int a) { // 一个累加的方法,检验是否可以访问Session if( System.Web.HttpContext.Current.Session == null ) throw new InvalidOperationException("Session没有开启。"); object obj = System.Web.HttpContext.Current.Session["counter"]; int counter = (obj == null ? 0 : (int)obj); counter += a; System.Web.HttpContext.Current.Session["counter"] = counter; return counter; } }
SessionMode的定义如下:
public enum SessionMode { NotSupport, Support, ReadOnly } 支持的序列化的种类
在上篇博客中,我演示了使用JSON序列化的做法来实现一个服务响应。本来也是打算让框架仅支持JSON序列化的, 因为传输的数据量小嘛。没想到,做到后来,还是认为有必要把XML序列化也加进来,XML序列化快呀。 最后,居然想到既然是服务框架,Ajax调用也能算是服务吧,总不能不支持吧,后来干脆也能支持部分的Ajax调用了。
在中,服务端判断客户端发送的数据序列化方式是通过判断请求头"Serializer-Format"来实现的。 序列化的种类还允许继续自定义。只要实现以下接口:
public interface ISerializerProvider { object Deserialize(Type destType, HttpRequest request); void Serializer(object obj, HttpResponse response); }
然后调用以下方法就可以了:
public static class SerializerProviderFactory { public static void RegisterSerializerProvider(string name, Type type) { // ................................... }
判断客户端的序列化方式,由属性FrameworkRules.GetSerializerFormat来决定:
public static class FrameworkRules { private static string Internal_GetSerializerFormat(HttpRequest request){ string flag = request.Headers["Serializer-Format"]; return (string.IsNullOrEmpty(flag) ? "form" : flag); } private static Func<HttpRequest, string> _serializerFormatRule = Internal_GetSerializerFormat; /// <summary> /// 此委托用来判断客户端发起的请求中,数据是以什么方式序列化的。 /// 返回的结果将会交给SerializerProviderFactory.GetSerializerProvider()来获取序列化提供者 /// 默认的实现是检查请求头:"Serializer-Format" /// </summary> public static Func<HttpRequest, string> GetSerializerFormat { internal get { return _serializerFormatRule; } set { if( value == null ) throw new ArgumentNullException("value"); _serializerFormatRule = value; } }
只是一个委托,可以自己重新实现。
目前本框架提供了三个实现了接口ISerializerProvider的类供用户使用:JsonSerializerProvider, XmlSerializerProvider, FormSerializerProvider
这里只展示JsonSerializerProvider的实现:
internal class JsonSerializerProvider : ISerializerProvider { private static readonly MethodInfo s_JSSDeserializeMI = typeof(JavaScriptSerializer).GetMethod("Deserialize"); JavaScriptSerializer jss = new JavaScriptSerializer(); public object Deserialize(Type destType, HttpRequest request) { StreamReader sr = new StreamReader(request.InputStream, request.ContentEncoding); string input = sr.ReadToEnd(); MethodInfo deserialize = s_JSSDeserializeMI.MakeGenericMethod(destType); return deserialize.Invoke(jss, new object[] { input }); } public void Serializer(object obj, HttpResponse response) { if( obj == null ) return; response.ContentType = "application/json"; response.Write(jss.Serialize(obj)); }
注意:FormSerializerProvider的实现不够完善,因为再搞下去,就和就重复了。有兴趣的自己去完善吧。
这里再给自己的作品打个广告:
,
,
对于gzip的支持,我只想说:太简单了。
前面不是已给出DuplexGzipModule的实现代码嘛。是的,就是把它注册到web.config中就可以了。
你说简不简单? 完全不用写多余的代码,要不要gzip支持,也只是个配置问题!
说到这里,我想起前段时间Artech写的一篇博客通过WCF扩展实现消息压缩, 正如我在前篇博客的回复中说到的:“本来真没兴趣看的,不过,为了验证我的猜想,还是去看了一下,果然也没让我失望。”。
在此,有必要公开一下我的想法:绝对没有半点看不起Artech的意思,只是我对WCF没有兴趣了。理由也简单:不够简单。
还是接着说,Artech的博客展示了在WCF中压缩消息的方式,当然我相信Artech对于WCF的理解,他的方案或许应该是最简单的解决方案, 但是和对gzip的支持的易用性根本没法比。
WCF的粉丝们,当您看到这里,请先别忙着喷我。 听我说完:WCF的确很强大,我的这个不到700行的框架那也是根本不能和它相比的。
做这个比较仅仅是为了展示Asp.net是一个强大的平台,Asp.net有更高水准的扩展性。
利用发布服务的5种方式可以提供5种不同的方式,让您将一个类及方法公开成一个服务,供外界调用。
方法1:使用DirectProcessRequestMoudle,只需要配置web.config即可。
<localhost:11647/service/OrderService/QueryOrder
说明:URL模式是可以自由定义的,只要给FrameworkRules.ParseNamesPair赋值即可,它的定义如下:
public static Func<HttpRequest, NamesPair> ParseNamesPair
默认的实现方式:
internal static class UrlPatternHelper { // 为了演示简单,我只定义一个URL模式。 // 如果希望适用性更广,可以从配置文件中读取,并且可支持多组URL模式。 // URL中加了"/service/"只是为了能更好地区分其它请求,如果您的网站没有子目录,删除它也是可以的。 internal static readonly string ServicePattern = @"/service/(?<name>[^/]+)/(?<method>[^/]+)[/\?]?"; public static NamesPair ParseNamesPair(HttpRequest request) { if( request == null ) throw new ArgumentNullException("request"); Match match = Regex.Match(request.Path, ServicePattern); if( match.Success == false ) return null; return new NamesPair { ServiceName = match.Groups["name"].Value, MethodName = match.Groups["method"].Value }; }
客户端调用URL: localhost:11647/service/OrderService/QueryOrder
方法2:使用MyServiceUrlRoutingModule,只需要配置web.config即可。
<localhost:11647/service/OrderService/QueryOrder
说明:只有这种方式才能支持Session
方法3:使用MyServiceUrlRewriteModule,只需要配置web.config即可。
<localhost:11647/service/OrderService/QueryOrder
方法4:使用MyServiceHandler,只需要配置web.config即可。
<localhost:11647/MyService.axd?sc=OrderService&op=QueryOrder
方法5:创建一个ashx,不需要任何配置。
<%@ WebHandler Language="C#" Class="MyService" %> using System; using System.Web; using MySimpleServiceFramework; public class MyService : IHttpHandler { public void ProcessRequest (HttpContext context) { NamesPair pair = new NamesPair(); pair.ServiceName = context.Request.QueryString["sc"]; pair.MethodName = context.Request.QueryString["op"]; ServiceExecutor.ProcessRequest(context, pair); } public bool IsReusable { get { return false; } } }
客户端调用URL: localhost:11647/MyService.ashx?sc=OrderService&op=QueryOrder
注意:前三种方法,需要在IIS中做些额外的配置,因为URL中不包含文件扩展名了,IIS不知道把请求交给Asp.net来处理。
具体配置见下图,此处省略78个字。
虽然,我给出了5种发布方式,但是我还是想说说我个人的想法。
在这些方法中,使用URL重写,URL路由的方法,并不是我想推荐的,写它们是主要是为了展示HttpModule 。 不推荐它们是因为它们要判断URL是否符合指定模式,这个判断是有成本的。 至于成本有多高,特此,我专了做门的测试。 在示例代码压缩包中有个___TestRoutePerformance目录,结果如何,还是您自己去看吧,我也有点累了。
此外,我想问:对于服务来说,URL友好有多大意义?服务的URL会让用户来输入还是让Google的爬虫来访问?
如果以上二个问题都是否定的,那么,这二种方法就是在白白浪费机器的性能了。
当然了,如果您的站点访问量不大,那么这点性能也可以忽略不计了,就当我没说。
使用URL重写URL路由,还有个比较麻烦的事情:如果想通过URL多传递一个参数,那么,是不是又要修改URL模式?
对于使用DirectProcessRequestMoudle这种模式,我以前已经说过了:除非要很好的理由,否则不建议使用这种方法。
至于其它的二种方式,本质上是一样的,只是说:处理器谁来写的差别了。
不过,如果您要是选择手工创建一个处理器,除了不用修改web.config之外,
还可以自定义URL参数名,可以选择要不要支持Session
ReflectionHelper类用于根据类名及服务名定位到一个服务类型以及要调用的方法。
因此,它在框架中的作用也是非常关键的。
internal static class ReflectionHelper { private static List<TypeAndAttrInfo> s_typeList; static ReflectionHelper() { InitServiceTypes(); } /// <summary> /// 加载所有的服务类型,判断方式就是检查类型是否有MyServiceAttribute /// </summary> private static void InitServiceTypes() { s_typeList = new List<TypeAndAttrInfo>(256); ICollection assemblies = BuildManager.GetReferencedAssemblies(); foreach( Assembly assembly in assemblies ) { try { (from t in assembly.GetExportedTypes() let a = (MyServiceAttribute[])t.GetCustomAttributes(typeof(MyServiceAttribute), false) where a.Length > 0 select new TypeAndAttrInfo { ServiceType = t, Attr = a[0], AuthorizeAttr = t.GetClassAuthorizeAttribute() } ).ToList().ForEach(b => s_typeList.Add(b)); } catch { } } } private static AuthorizeAttribute GetClassAuthorizeAttribute(this Type t) { AuthorizeAttribute[] attrs = (AuthorizeAttribute[])t.GetCustomAttributes(typeof(AuthorizeAttribute), false); return (attrs.Length > 0 ? attrs[0] : null); } /// <summary> /// 根据一个名称获取对应的服务类型(从缓存中获取类型) /// </summary> /// <param name="typeName"></param> /// <returns></returns> private static TypeAndAttrInfo GetServiceType(string typeName) { if( string.IsNullOrEmpty(typeName) ) throw new ArgumentNullException("typeName"); // 查找类型的方式:如果有点号,则按全名来查找(包含命名空间),否则只看名字。 // 本框架对于多个匹配条件的类型,将返回第一个匹配项。 if( typeName.IndexOf('.') > 0 ) return s_typeList.FirstOrDefault(t => string.Compare(t.ServiceType.FullName, typeName, true) == 0); else return s_typeList.FirstOrDefault(t => string.Compare(t.ServiceType.Name, typeName, true) == 0); } private static Hashtable s_methodTable = Hashtable.Synchronized( new Hashtable(4096, StringComparer.OrdinalIgnoreCase)); /// <summary> /// 根据指定的类型以及方法名称,获取对应的方法信息 /// </summary> /// <param name="type"></param> /// <param name="methodName"></param> /// <returns></returns> private static MethodAndAttrInfo GetServiceMethod(Type type, string methodName) { if( type == null ) throw new ArgumentNullException("type"); if( string.IsNullOrEmpty(methodName)) throw new ArgumentNullException("methodName"); // 首先尝试从缓存中读取 string key = methodName + "@" + type.FullName; MethodAndAttrInfo mi = (MethodAndAttrInfo)s_methodTable[key]; if( mi == null ) { // 注意:这里不考虑方法的重载。 MethodInfo method = type.GetMethod(methodName, BindingFlags.Static | BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase); if( method == null ) return null; MyServiceMethodAttribute[] attrs = (MyServiceMethodAttribute[]) method.GetCustomAttributes(typeof(MyServiceMethodAttribute), false); if( attrs.Length != 1 ) return null; // 由于服务方法的参数来源于反序列化,此时只可能包含一个参数。 ParameterInfo[] paraInfos = method.GetParameters(); if( paraInfos.Length != 1 ) throw new ArgumentNullException("指定的方法虽找到,但该方法的参数数量不是1"); AuthorizeAttribute[] auths = (AuthorizeAttribute[])method.GetCustomAttributes(typeof(AuthorizeAttribute), false); mi = new MethodAndAttrInfo { MethodInfo = method, ParamType = paraInfos[0].ParameterType, Attr = attrs[0], AuthorizeAttr = (auths.Length > 0 ? auths[0] : null) }; s_methodTable[key] = mi; } return mi; } /// <summary> /// 根据类型名称以及方法名称返回要调用的相关信息 /// </summary> /// <param name="pair">包含类型名称以及方法名称的对象</param> /// <returns></returns> public static InvokeInfo GetInvokeInfo(NamesPair pair) { if( pair == null ) throw new ArgumentNullException("pair"); InvokeInfo vkInfo = new InvokeInfo(); vkInfo.ServiceTypeInfo = GetServiceType(pair.ServiceName); if( vkInfo.ServiceTypeInfo == null ) return null; vkInfo.MethodAttrInfo = GetServiceMethod(vkInfo.ServiceTypeInfo.ServiceType, pair.MethodName); if( vkInfo.MethodAttrInfo == null ) return null; if( vkInfo.MethodAttrInfo.MethodInfo.IsStatic == false ) vkInfo.ServiceInstance = Activator.CreateInstance(vkInfo.ServiceTypeInfo.ServiceType); return vkInfo; } }
ServiceExecutor用于调用服务方法,前面所说的5种服务发布方式,最终都要经过这里。
/// <summary> /// 最终调用服务方法的工具类。 /// </summary> public static class ServiceExecutor { internal static void ProcessRequest(HttpContext context, ServiceInfo info) { if( context == null ) throw new ArgumentNullException("context"); if( info == null || info.InvokeInfo == null ) throw new ArgumentNullException("info"); //if( context.Request.InputStream.Length == 0 ) // throw new InvalidDataException("没有调用数据,请将调用数据以请求体的方式传入。"); if( info.InvokeInfo.AuthenticateRequest(context) == false ) ExceptionHelper.Throw403Exception(context); // 获取客户端的数据序列化格式。 // 默认实现方式:request.Headers["Serializer-Format"]; // 注意:这是我自定义的请求头名称,也可以不指定,默认为:form (表单) string serializerFormat = FrameworkRules.GetSerializerFormat(context.Request); ISerializerProvider serializerProvider = SerializerProviderFactory.GetSerializerProvider(serializerFormat); // 获取要调用方法的参数类型 Type destType = info.InvokeInfo.MethodAttrInfo.ParamType; // 获取要调用的参数 context.Request.InputStream.Position = 0; // 防止其它Module读取过,但没有归位。 object param = serializerProvider.Deserialize(destType, context.Request); // 调用服务方法 object result = info.InvokeInfo.MethodAttrInfo.MethodInfo.Invoke( info.InvokeInfo.ServiceInstance, new object[] { param }); // 写输出结果 if( result != null ) serializerProvider.Serializer(result, context.Response); } /// <summary> /// 用于根据服务的类名和方法名执行某个请求 /// </summary> /// <param name="context"></param> /// <param name="pair"></param> public static void ProcessRequest(HttpContext context, NamesPair pair) { if( pair == null ) throw new ArgumentNullException("pair"); if( string.IsNullOrEmpty(pair.ServiceName) || string.IsNullOrEmpty(pair.MethodName) ) ExceptionHelper.Throw404Exception(context); InvokeInfo vkInfo = ReflectionHelper.GetInvokeInfo(pair); if( vkInfo == null ) ExceptionHelper.Throw404Exception(context); ServiceInfo info = new ServiceInfo(pair, vkInfo); ProcessRequest(context, info); } } 关于写框架
以前曾在网上见过几次关于“重复造轮子”的说法。基本上,全是反对意见。
我今天也想谈谈这个话题:要不要自己写框架?
我的答案是:要。
自己写框架的好处不在于能将它做得多强大,多完美,而是从写框架的过程中,可以学到很多东西。
一个框架写完了,不在乎要给多少人使用,而是自己感觉有没有进步,这才是关键。
如果您不重复造轮子,可能永远不知道造轮子的过程,造轮子需要什么内容。
如果不重复造轮子,世界上将只有一个操作系统。
如果不重复造轮子,世界上将只有一个数据库。
如果不重复造轮子,世界上将只有一种编程语言。
这样单调会很好吗?
WebForms, MVC, Linq To SQL, Entity Framework, Remoting, Web Service, WCF
微软不也是一直在重复造轮子吗?
终于写完了这篇博客。这篇博客花了我最多的写作时间:整整二个周末还加上一些业余时间。
当然了,其中也包括实现框架的时间。这其实也是希望更好地展示Asp.net的核心内容。
写框架不是目标,展示Asp.net技术,写博客才是目标哦。
因此,这篇博客可谓是,只希望:
1. 能给大家分享Asp.net的核心内容。
2. 这篇博客的成绩能赶上。
我的写博热情也离不开您的肯定支持哦,如果您认为阅读此文有收获,别忘了点一下右下角的,。
Asp.net是个伟大的平台,能完成许多应用。这些应用的实现离不开这二次博客所提过的核心对象的支持,
本来还想再写点其它的示例,只是时间花得太多了。今天的示例已经很精彩了,到此为止吧,我也想休息了。
每个对象都是一个不朽的传奇,每个传奇背后都有一个精彩的故事。
我是Fish Li, 感谢大家阅读我的博客,请继续关注我的后续博客。
点击此处下载示例代码
如果,您认为阅读这篇博客让您有些收获,不妨点击一下右下角的按钮。
如果,您希望更容易地发现我的新博客,不妨点击一下右下角的。
因为,我的写作热情也离不开您的肯定支持。
感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,请继续关注我的后续博客,我是Fish Li 。

