200行代碼實現Mini ASP.NET Core

前言

在學習ASP.NET Core源碼過程當中,偶然看見蔣金楠老師的ASP.NET Core框架揭祕,不到200行代碼實現了ASP.NET Core Mini框架,針對框架本質進行了講解,受益不淺,本文結合ASP.NET Core Mini框架講述ASP.NET Core核心。javascript

微軟官網關於ASP.NET Core的概念「ASP.NET Core是一個開源和跨平臺的框架,用於構建基於Web的現代互聯網鏈接應用程序,例如Web應用程序,IoT應用程序和移動後端。 ASP.NET Core應用程序能夠在.NET Core或完整的.NET Framework上運行。 它的架構旨在爲部署到雲或在本地運行的應用程序提供優化的開發框架。 它由模塊化組件組成,開銷最小,所以您能夠在構建解決方案時保持靈活性。 您能夠在Windows,Mac和Linux上跨平臺開發和運行ASP.NET核心應用程序」。能夠從定義上看出ASP.NET Core框架具備跨平臺、部署靈活、模塊化等特色。html


ASP.NET Core框架揭祕

ASP.NET Core Mini是200行代碼實現的迷你版ASP.NET Core框架,有三大特色「簡單」,「真實模擬」,「可執行」來讓咱們更加容易理解ASP.NET Core。java

代碼結構:git

下圖是項目運行頁面輸出的結果:github

本文從如下五個角度講述:web

  • Program: 項目入口
  • Middleware:中間件
  • HttpContext:Http相關
  • WebHost:WebHost
  • Server:Server相關

 Program後端

 using System.Threading.Tasks;
 using App.Server;
 using App.WebHost;

 namespace App
 {
     public static class Program
     {
         public static async Task Main(string[] args)
         {
             await CreateWebHostBuilder()
                 .Build()
                 .Run();
         }
 
         private static IWebHostBuilder CreateWebHostBuilder()
         {
             return new WebHostBuilder()
                 .UseHttpListener()
                 .Configure(app => app
                     .Use(Middleware.Middleware.FooMiddleware)
                     .Use(Middleware.Middleware.BarMiddleware)
                     .Use(Middleware.Middleware.BazMiddleware));
         }
     }
 }

能夠看到項目的入口是Main方法,它只作了三件事,構造WebHostBuilder,而後Build方法構造WebHost,最後Run方法啓動WebHost。咱們能夠簡單的理解WebHostBuilder做用就是爲了構造WebHost,他是WebHost的構造器,而WebHost是咱們Web應用的宿主。服務器

再看CreateWebHostBuilder的方法具體幹了什麼。首先建立了WebHostBuilder,而後UseHttpListener配置Server(好比ASP.NET Core中的Kestrel或IIS等等),通常包括地址和端口等,最後註冊一系列的中間件。架構

從Program能夠看出整個App運行起來的流程,以下圖所示:app

 Middleware 

在看HttpContext以前,咱們先來看ASP.NET Core 的Http處理管道是什麼樣子,上圖是官方給出的管道處理圖,當咱們的服務器接收到一個Http請求,管道進行處理,處理後再進行返回,能夠看到,咱們的Http請求通過多層中間件處理最後返回。

 using System.Threading.Tasks;
 
 namespace App.Middleware
 {
     public delegate Task RequestDelegate(HttpContext.HttpContext context);
 }

首先來看RequestDelegate.cs,定義了一個參數類型是HttpContext,返回結果是Task的委託。

爲何會定義這個委託,能夠想到一個Http請求會通過多層中間件處理,那麼多層中間件處理能夠想像成一個HttpHandler,他的參數就是HttpContext,返回結果是Task的委託。

 using App.HttpContext;
 
 namespace App.Middleware
 {
     public static class Middleware
     {
         public static RequestDelegate FooMiddleware(RequestDelegate next)
             => async context =>
             {
                 await context.Response.WriteAsync("Foo=>");
                 await next(context);
             };
 
         public static RequestDelegate BarMiddleware(RequestDelegate next)
             => async context =>
             {
                 await context.Response.WriteAsync("Bar=>");
                 await next(context);
             };
 
         public static RequestDelegate BazMiddleware(RequestDelegate next)
             => context => context.Response.WriteAsync("Baz");
     }
 }

Middleware中定義了三個簡單的中間件,能夠看到,中間件其實就是委託,將HttpContext一層一層進行處理。

Http請求進入管道,第一個中間件處理完,把自身做爲結果傳輸到下一個中間件進行處理,那麼參數是RequestDelegate,返回值是RequestDelegate的委託就是中間件,因此中間件其實就是Func<RequestDelegate,RequestDelegate>,簡單來講,中間件就是RequestDelegate的加工工廠。

 HttpContext

從Middleware瞭解到,HttpContext是RequestDelegate的參數,是每個Middleware處理數據的來源。

咱們能夠這麼理解,HttpContext就是咱們整個Http請求中的共享資源,因此的中間件共享它,而每一箇中間件就是對它進行加工處理。

 using System;
 using System.Collections.Specialized;
 using System.IO;
 using System.Text;
 using System.Threading.Tasks;
 
 namespace App.HttpContext
 {
     public class HttpContext
     {
         public HttpRequest Request { get; }
         public HttpResponse Response { get; }
 
         public HttpContext(IFeatureCollection features)
         {
             Request = new HttpRequest(features);
             Response = new HttpResponse(features);
         }
     }
 
     public class HttpRequest
     {
         private readonly IHttpRequestFeature _feature;
         
         public Uri Url => _feature.Url;
         
         public NameValueCollection Headers => _feature.Headers;
         
         public Stream Body => _feature.Body;
         
         public HttpRequest(IFeatureCollection features) => _feature = features.Get<IHttpRequestFeature>();
     }
 
     public class HttpResponse
     {
         private readonly IHttpResponseFeature _feature;
         public HttpResponse(IFeatureCollection features) => _feature = features.Get<IHttpResponseFeature>();
         
         public NameValueCollection Headers => _feature.Headers;
         public Stream Body => _feature.Body;
 
         public int StatusCode
         {
             get => _feature.StatusCode;
             set => _feature.StatusCode = value;
         }
     }
 
     public static partial class Extensions
     {
         public static Task WriteAsync(this HttpResponse response, string contents)
         {
             var buffer = Encoding.UTF8.GetBytes(contents);
             return response.Body.WriteAsync(buffer, 0, buffer.Length);
         }
     }
 }

代碼結構能夠看出request和reponse構成httpcontext,也反映出httpcontext的職責:Http請求的上下文。

可是,不一樣的Server和單一的HttpContext之間須要如何適配呢?由於咱們能夠註冊多樣的Server,能夠是IIS也能夠是Kestrel還能夠是這裏的HttpListenerServer。

因此咱們須要定義統一的request和response接口,來適配不一樣的Server。以下圖的IHttpRequestFeature和IHttpResponseFeature。

 using System;
 using System.Collections.Specialized;
 using System.IO;
 
 namespace App.HttpContext
 {
     public interface IHttpRequestFeature
     {
         Uri Url { get; }
         
         NameValueCollection Headers { get; }
         
         Stream Body { get; }
     }
 
     public interface IHttpResponseFeature
     {
         int StatusCode { get; set; }
         
         NameValueCollection Headers { get; }
         
         Stream Body { get; }
     }
 }

在HttpListenerFeature.cs中實現request和response的接口,實現了適配不一樣的server。

 using System;
 using System.Collections.Specialized;
 using System.IO;
 using System.Net;
 
 namespace App.HttpContext
 {
     public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature
     {
         private readonly HttpListenerContext _context;
 
         public HttpListenerFeature(HttpListenerContext context) => _context = context;
 
         Uri IHttpRequestFeature.Url => _context.Request.Url;
 
         NameValueCollection IHttpRequestFeature.Headers => _context.Request.Headers;
 
         NameValueCollection IHttpResponseFeature.Headers => _context.Response.Headers;
 
         Stream IHttpRequestFeature.Body => _context.Request.InputStream;
 
         Stream IHttpResponseFeature.Body => _context.Response.OutputStream;
 
         int IHttpResponseFeature.StatusCode
         {
             get => _context.Response.StatusCode;
             set => _context.Response.StatusCode = value;
         }
     }
 }

至於FeatureCollection.cs,它的做用就是將從httpListenerContext中獲取的Http信息存儲在FeatureCollection的Dictionary裏,更加方便的對HttpRequestFeature和HttpResponseFeature進行操做。

擴展方法Get和Set的做用是方便操做FeatureCollection。

 using System;
 using System.Collections.Generic;
 
 namespace App.HttpContext
 {
     public interface IFeatureCollection : IDictionary<Type, object>
     {
     }
 
     public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection
     {
     }
 
     public static partial class Extensions
     {
         public static T Get<T>(this IFeatureCollection features) =>
             features.TryGetValue(typeof(T), out var value) ? (T) value : default(T);
 
         public static IFeatureCollection Set<T>(this IFeatureCollection features, T feature)
         {
             features[typeof(T)] = feature;
             return features;
         }
     }
 }

 WebHost

using System;
 using System.Collections.Generic;
 using App.Server;
 
 namespace App.WebHost
 {
     public interface IWebHostBuilder
     {
         IWebHostBuilder UseServer(IServer server);
         
         IWebHostBuilder Configure(Action<IApplicationBuilder> configure);
         
         IWebHost Build();
     }
 
     public class WebHostBuilder : IWebHostBuilder
     {
         private IServer _server;
         private readonly List<Action<IApplicationBuilder>> _configures = new List<Action<IApplicationBuilder>>();
 
         public IWebHostBuilder Configure(Action<IApplicationBuilder> configure)
         {
             _configures.Add(configure);
             return this;
         }
 
         public IWebHostBuilder UseServer(IServer server)
         {
             _server = server;
             return this;
         }
 
         public IWebHost Build()
         {
             var builder = new ApplicationBuilder();
             foreach (var configure in _configures)
             {
                 configure(builder);
             }
 
             return new WebHost(_server, builder.Build());
         }
     }
 }

WebHost是咱們App的宿主,經過WebHostBuild構造,代碼裏定義了三個方法,

  • UseServer: 配置server
  • Configure: 註冊中間件
  • Build: 構造WebHost
 using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using App.Middleware;
 
 namespace App.WebHost
 {
     public interface IApplicationBuilder
     {
         IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
 
         RequestDelegate Build();
     }
 
     public class ApplicationBuilder : IApplicationBuilder
     {
         private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares =
             new List<Func<RequestDelegate, RequestDelegate>>();
 
         public RequestDelegate Build()
         {
             _middlewares.Reverse();
             return httpContext =>
             {
                 RequestDelegate next = _ =>
                 {
                     _.Response.StatusCode = 404;
                     return Task.CompletedTask;
                 };
 
                 foreach (var middleware in _middlewares)
                 {
                     next = middleware(next);
                 }
 
                 return next(httpContext);
             };
         }
 
         public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
         {
             _middlewares.Add(middleware);
             return this;
         }
     }
 }

ApplicationBuilder作了什麼,Use方法咱們把自定義的中間件放進集合裏,而build方法就是構建webhost。首先把中間鍵集合順序倒置,而後構造一個StatusCode爲404的中間件,其次遍歷中間件集合,最後返回構造好的管道。

若是中間件集合爲空,咱們返回Http 404錯誤。

至於爲何要Reverse(),是由於咱們註冊中間件的順序與咱們須要執行的順序相反。

using System.Threading.Tasks;
 using App.Middleware;
 using App.Server;
 
 namespace App.WebHost
 {
     public interface IWebHost
     {
         Task Run();
     }
 
     public class WebHost : IWebHost
     {
         private readonly IServer _server;
         private readonly RequestDelegate _handler;
 
         public WebHost(IServer server, RequestDelegate handler)
         {
             _server = server;
             _handler = handler;
         }
 
         public Task Run() => _server.RunAsync(_handler);
     }
 }

WebHost只作了一件事,將咱們構造的中間件管道處理器在指定Server運行起來。

 Server

咱們自定義一個服務器,IServer定義統一接口,HttpListenerServer實現咱們自定義的Server

using System;
 using System.Linq;
 using System.Net;
 using System.Threading.Tasks;
 using App.HttpContext;
 using App.Middleware;
 using App.WebHost;
 
 namespace App.Server
 {
     public class HttpListenerServer : IServer
     {
         private readonly HttpListener _httpListener;
         private readonly string[] _urls;
 
         public HttpListenerServer(params string[] urls)
         {
             _httpListener = new HttpListener();
             _urls = urls.Any() ? urls : new[] {"http://localhost:5000/"};
         }
 
         public async Task RunAsync(RequestDelegate handler)
         {
             Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url));
             
             if (!_httpListener.IsListening)
             {
                 _httpListener.Start();
             }
 
             Console.WriteLine("Server started and is listening on: {0}", string.Join(';', _urls));
 
             while (true)
             {
                  var listenerContext = await _httpListener.GetContextAsync();
                 var feature = new HttpListenerFeature(listenerContext);
                 var features = new FeatureCollection()
                     .Set<IHttpRequestFeature>(feature)
                     .Set<IHttpResponseFeature>(feature);
                 var httpContext = new HttpContext.HttpContext(features);
                 
                 await handler(httpContext);
                 
                 listenerContext.Response.Close();
             }
         }
     }
 
     public static class Extensions
     {
         public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls)
             => builder.UseServer(new HttpListenerServer(urls));
     }
 }

使用UseHttpListener擴展方法,指定監聽地址,默認爲「http://localhost:5000/」。

RunAsync方法是咱們WebHost的Run方法,循環中經過調用其GetContextAsync方法實現了針對請求的監聽和接收。


總結 

看完這篇文章應該對ASP.NET Core有必定對理解,核心就是中間件管道。不過ASP.NET Core源碼遠遠不止這些,每一個模塊的實現較複雜,還有其餘必不可少的模塊(依賴注入、日誌系統、異常處理等),須要更加深刻的學習。我也會記錄個人學習記錄,最後來一張完整的Http請求管道圖。

 

 

參考資料 :200行代碼,7個對象——讓你瞭解ASP.NET Core框架對本質

代碼地址: GitHub

相關文章
相關標籤/搜索