200行代碼實現Mini ASP.NET Core


前言

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

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


ASP.NET Core框架揭秘

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

代碼結構:

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

本文從以下五個角度講述:

  • 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運行起來的流程,如下圖所示:

 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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM