1. 程式人生 > >200行程式碼實現Mini ASP.NET Core

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