深入浅出ASP.NET MVC5系列之一

前言

  为避免看官乏味,本系列博客限定在较新的.Net framework 4.5.1,Asp.net MVC5,IIS 7.X集成模式。

  对于微软应用层的技术。我向来不舍得花太多时间学习。但又由于公司现有的开发建立于Asp.net MVC5上,实在是有必要深入理解一下ASP.NET MVC5,故安排了约2-3周的时间摸摸脉,这个系列会是一边学习一边总结的产物。

从ASP.NET处理流程说起

   如果自己去写一个Web服务器,最简单的方法就是:

  1 监听请求-->2 获取请求信息-->3 处理请求-->4 发送响应

     Asp.net大体也是做的,它干了这么点事:

  1 HTTP.SYS 位于内核模式,监听请求。

  2 一旦监听到请求,转给W3SRV。

  3 W3SRV将请求转给WAS。

  4 WAS创建并管理应用程序池进程W3WP.EXE。

  5 W3WP.EXE创建运行时,处理请求。

  6 发送响应。

拓展:

  1 为什么需要WAS?

     WAS在IIS7之前是没有的,之所以抽象出这一层,是因为WAS不再参与具体的应用层协议的解析。这么讲:IIS支持Http协议,同时也支持WCF寄宿,咋干的?我们知道WCF的协议和HTTP协议当然是不一样的,但端口只有一个,肿么办?计算机世界里有这么一句话:计算机世界的绝大部分问题都可以通过分层的方法来解决。在协议适配和数据处理之间增加一个虚拟层即可。具体的,让每一个协议都有它自己的协议解析模块,将解析出来的数据都转给WAS,最后让WAS包装成WorkerRequest封送入ASP.NET的请求管道。这里,与IIS6中不同,W3SRV不再和ASP.NET直接接洽,它只负责接受HTTP.SYS监听的HTTP协议的请求,解析成请求数据,转给WAS。那么WCF请求呢?

    好的,了解得多一点的同学就知道windows上有这么个服务:NetTcpPortSharing,即端口共享服务。这么一说,好像一个端口可以被多应用程序监听一样。妈蛋,说好的世界观呢?多个进程可以共享一个端口,尼玛编程给我看看?

    对Socket熟悉的同学可以自己编程试试。此处略去一千字。

    好吧,先引入原文:

    Internet Information Services (IIS) has a listener to share a port between multiple HTTP applications. IIS listens on the port directly and forwards messages to the appropriate application based on information inside the message stream. This allows multiple HTTP applications to use the same port number without having to compete to reserve the port for receiving messages.

这么回事。IIS有一个共享端口,这个端口在接收到基于HTTP协议请求之后,会将消息再转一份给其它同样需要处理这个请求的HTTP应用程序。我们知道WCF是有HTTPBinding嘛,那么接下来,画张图来看看:

  

具体细节可能不会是这样,这是我大概的一个猜测而已,但是思路定当如此。

ASP.NET处理管道

 如果应用程序是第一次被访问,那么IIS在加载CLR之后,会创建应用程序域。一个特殊的运行时IsapiRuntime被加载,同时请求报文被封装为IsApiWorkerRequest.看过上篇的同学,应该知道怎么弄源码了。这里简单将源代码列出来。

public sealed class ISAPIRuntime
{
           public int ProcessRequest(IntPtr ecb, int iWRType) {
            IntPtr pHttpCompletion = IntPtr.Zero;
            if (iWRType == WORKER_REQUEST_TYPE_IN_PROC_VERSION_2) {
                pHttpCompletion = ecb;
                ecb = UnsafeNativeMethods.GetEcb(pHttpCompletion);
            } 
            ISAPIWorkerRequest wr = null;
            try {
                bool useOOP = (iWRType == WORKER_REQUEST_TYPE_OOP);
                wr = ISAPIWorkerRequest.CreateWorkerRequest(ecb, useOOP);
                wr.Initialize();

                // check if app path matches (need to restart app domain?)                
                String wrPath = wr.GetAppPathTranslated();
                String adPath = HttpRuntime.AppDomainAppPathInternal;                
                
                if (adPath == null ||
                    StringUtil.EqualsIgnoreCase(wrPath, adPath)) {
                    
                    HttpRuntime.ProcessRequestNoDemand(wr);
                    return 0;
                }
                else {
                    // need to restart app domain
                    HttpRuntime.ShutdownAppDomain(ApplicationShutdownReason.PhysicalApplicationPathChanged,
                                                  SR.GetString(SR.Hosting_Phys_Path_Changed,
                                                                                   adPath,
                                                                                   wrPath));
                    return 1;
                }
            }
            catch(Exception e) {
                try {
                    WebBaseEvent.RaiseRuntimeError(e, this);
                } catch {}
                
                // Have we called HSE_REQ_DONE_WITH_SESSION?  If so, don't re-throw.
                if (wr != null && wr.Ecb == IntPtr.Zero) {
                    if (pHttpCompletion != IntPtr.Zero) {
                        UnsafeNativeMethods.SetDoneWithSessionCalled(pHttpCompletion);
                    }
                    // if this is a thread abort exception, cancel the abort
                    if (e is ThreadAbortException) {
                        Thread.ResetAbort();
                    }                    
                    // IMPORTANT: if this thread is being aborted because of an AppDomain.Unload,
                    // the CLR will still throw an AppDomainUnloadedException. The native caller
                    // must special case COR_E_APPDOMAINUNLOADED(0x80131014) and not
                    // call HSE_REQ_DONE_WITH_SESSION more than once.
                    return 0;
                }
                
                // re-throw if we have not called HSE_REQ_DONE_WITH_SESSION
                throw;
            }
        }
}

当请求进入HttpRuntime的时候,我们所ASP.net的处理流程正式开始。接下来便是大家熟知的了:

HttpRuntime根据httpWorkerRequest创建HttpContext,接下来利用HttpApplicationFactory创建HttpApplication对象,接下来哒哒哒哒几百行代码,二十个左右的事件处理完,Asp.net的请求就结束了。这二十个事件干了以下事:

1 权限验证

2 缓存处理

3 Map Handler

4 获取状态上下文

5 处理请求

6 保存状态上下文

7 更新缓存

8 记录日志

9 发送响应。

说好的MVC呢 怎么光Asp.net?

   千呼万呼始出来,犹抱琵琶半遮面。

   Asp.net中诸多的对象中,有这么两个对象尤为重要,一是Httmodule,一个是Httphandler。

  Httpmodule用于订阅HttpApplication的管道事件,这赋予了Asp.net巨大的灵活性。HttpHandler用于处理请求,发送响应。当然Httpmodule也是可以处理请求的。这个区别和联系,就不在这里发散了,有兴趣的朋友可以去看选择HttpHandler还是HttpModule

  让我们来看看ASP.NET MVC是怎么玩的。

1  定义了一个UrlRoutingModule模块,订阅了上面20个事件中的的第2类事件,在这个事件中,使用 httpContext.RemapHandler 方法,将Handler定位到MvcHandler,让MvcHandler来处理请求。

2  MvcHandler根据路由表(这里等会发散),譬如

 routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Default", action = "Index", id = UrlParameter.Optional }
            );

如果我们的URL是http://localhost,那么它变将请求映射为http://localhost/Default/Index。即DefaultController中的Index方法。接下来,用反射,处理请求。

完毕。

你可能不知道的路由表

 在 routes.MapRoute方法中,我们看到有以下重载:

public static Route MapRoute(string name, string url, object defaults, object constraints, string[] namespaces)

前三个参数大家都很熟了,那么第四个和第五个呢?

 在这个.NET开源的时代,一切都不是问题,进去喵喵:

 public Route MapPageRoute(string routeName, string routeUrl, string physicalFile, bool checkPhysicalUrlAccess, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens) {
            if (routeUrl == null) {
                throw new ArgumentNullException("routeUrl");
            }
            Route route = new Route(routeUrl, defaults, constraints, dataTokens, new PageRouteHandler(physicalFile, checkPhysicalUrlAccess));
            Add(routeName, route);
            return route;
        }

我艹,这个源码为毛是从Object变为了RouteValueDictionnary呢,先不管了,看这生成了一个Route对象。继续瞄:

 public Route(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler) {
            Url = url;
            Defaults = defaults;
            Constraints = constraints;
            DataTokens = dataTokens;
            RouteHandler = routeHandler;
        }

将其赋值给了内部的Constraints,接下来看它在哪儿用的:

 private bool ProcessConstraints(HttpContextBase httpContext, RouteValueDictionary values, RouteDirection routeDirection) {
            if (Constraints != null) {
                foreach (var constraintsItem in Constraints) {
                    if (!ProcessConstraint(httpContext, constraintsItem.Value, constraintsItem.Key, values, routeDirection)) {
                        return false;
                    }
                }
            }
            return true;
        }
ProcessConstraint方法又是什么呢?
 protected virtual bool ProcessConstraint(HttpContextBase httpContext, object constraint, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
            IRouteConstraint customConstraint = constraint as IRouteConstraint;
            if (customConstraint != null) {
                return customConstraint.Match(httpContext, this, parameterName, values, routeDirection);
            }

            // If there was no custom constraint, then treat the constraint as a string which represents a Regex.
            string constraintsRule = constraint as string;
            if (constraintsRule == null) {
                throw new InvalidOperationException(String.Format(
                    CultureInfo.CurrentUICulture,
                    SR.GetString(SR.Route_ValidationMustBeStringOrCustomConstraint),
                    parameterName,
                    Url));
            }

            object parameterValue;
            values.TryGetValue(parameterName, out parameterValue);
            string parameterValueString = Convert.ToString(parameterValue, CultureInfo.InvariantCulture);
            string constraintsRegEx = "^(" + constraintsRule + ")$";
            return Regex.IsMatch(parameterValueString, constraintsRegEx,
                RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
        }

写到这儿我觉得不能讲太复杂了,简单的说,这里就是一个对URL做验证的过程,简单的说,你符合模版的同时,还得符合我设定的规则:

routes.MapRoute(
    "Product",
    "Product/{productId}",
    new {controller="Product", action="Details"},
    new {productId = @"\d+" }
 );

原来的RouteValueDictionnary对象其实如下:

public class RouteValueDictionary : IDictionary<string, object>

是一个对 IDictionary<string, object>接口的实现。看了一下,我发现我微软还没有把最新的源码放上去,我现在看到的源码还不是最新的:

[TypeForwardedFrom("System.Web.Routing, Version=3.5.0.0, Culture=Neutral, PublicKeyToken=31bf3856ad364e35")]
    public class RouteValueDictionary : IDictionary<string, object> 

而最新的是4.0.好吧,RouteValueDictionnary对象的作用搞明白了,那么将RouteValueDictionnary换成Object是为么干嘛?其实微软在这儿将好好的一个RouteValueDictionnary换成了一个匿名对象,如上的

new {productId = @"\d+" }

如果是原来的,就必须写成字典形式:

Key :ProductId,

Value:@"\d+"。

 

最后运用反射将属性从匿名对象中取出来,既然是反射,第五个参数命名空间,就有作用了。

一开始我只是对这两个参数感觉很奇怪而已,殊不知一下就翻出这么多东西。罗里吧嗦了这么多,其实就是想说,在.NET开源的这个时代,大家不应该只停留在眼上,多多看看源代码,提升.NET社区的整体水准,才是.NET社区的真正福音。

打造更强的.NET电商

  找钢网武汉研发中心招聘中高级.NET工程师。扎根于钢铁行业互联网电商,已完成N轮融资,估值超十亿美金。十三薪,福利丰厚,全员持股。

这可能会是你遇到的,最好的机会

有意向的朋友可以邮箱联系我:582105721@qq.com。最好能附上您的简历。我会第一时间转给HR。

 

 

posted @ 2015-02-09 08:58  灬后知后觉  阅读(4131)  评论(5编辑  收藏  举报