1. 程式人生 > >動手寫一個簡版 asp.net core

動手寫一個簡版 asp.net core

# 動手寫一個簡版 asp.net core ## Intro 之前看到過蔣金楠老師的一篇 200 行程式碼帶你瞭解 asp.net core 框架,最近參考蔣老師和 Edison 的文章和程式碼,結合自己對 asp.net core 的理解 ,最近自己寫了一個 MiniAspNetCore ,寫篇文章總結一下。 ## HttpContext `HttpContext` 可能是最為常用的一個類了,`HttpContext` 是請求上下文,包含了所有的請求資訊以及響應資訊,以及一些自定義的用於在不同中介軟體中傳輸資料的資訊 來看一下 `HttpContext` 的定義: ``` csharp public class HttpContext { public IServiceProvider RequestServices { get; set; } public HttpRequest Request { get; set; } public HttpResponse Response { get; set; } public IFeatureCollection Features { get; set; } public HttpContext(IFeatureCollection featureCollection) { Features = featureCollection; Request = new HttpRequest(featureCollection); Response = new HttpResponse(featureCollection); } } ``` `HttpRequest` 即為請求資訊物件,包含了所有請求相關的資訊, `HttpResponse` 為響應資訊物件,包含了請求對應的響應資訊 `RequestServices` 為 asp.net core 裡的`RequestServices`,代表當前請求的服務提供者,可以使用它來獲取具體的服務例項 `Features` 為 asp.net core 裡引入的物件,可以用來在不同中介軟體中傳遞資訊和用來解耦合 ,下面我們就來看下 `HttpRequest` 和 `HttpResponse` 是怎麼實現的 HttpRequest: ``` csharp public class HttpRequest { private readonly IRequestFeature _requestFeature; public HttpRequest(IFeatureCollection featureCollection) { _requestFeature = featureCollection.Get(); } public Uri Url => _requestFeature.Url; public NameValueCollection Headers => _requestFeature.Headers; public string Method => _requestFeature.Method; public string Host => _requestFeature.Url.Host; public Stream Body => _requestFeature.Body; } ``` HttpResponse: ``` csharp public class HttpResponse { private readonly IResponseFeature _responseFeature; public HttpResponse(IFeatureCollection featureCollection) { _responseFeature = featureCollection.Get(); } public bool ResponseStarted => _responseFeature.Body.Length > 0; public int StatusCode { get => _responseFeature.StatusCode; set => _responseFeature.StatusCode = value; } public async Task WriteAsync(byte[] responseBytes) { if (_responseFeature.StatusCode <= 0) { _responseFeature.StatusCode = 200; } if (responseBytes != null && responseBytes.Length > 0) { await _responseFeature.Body.WriteAsync(responseBytes); } } } ``` ## Features 上面我們提供我們可以使用 `Features` 在不同中介軟體中傳遞資訊和解耦合 由上面 `HttpRequest`/`HttpResponse` 的程式碼我們可以看出來,`HttpRequest` 和 `HttpResponse` 其實就是在 `IRequestFeature` 和 `IResponseFeature` 的基礎上封裝了一層,真正的核心其實是 `IRequestFeature`/`IResponseFeature` ,而這裡使用介面就很好的實現瞭解耦,可以根據不同的 WebServer 使用不同的 `RequestFeature`/`ResponseFeature`,來看下 `IRequestFeature`/`IResponseFeature` 的實現 ``` csharp public interface IRequestFeature { Uri Url { get; } string Method { get; } NameValueCollection Headers { get; } Stream Body { get; } } public interface IResponseFeature { public int StatusCode { get; set; } NameValueCollection Headers { get; set; } public Stream Body { get; } } ``` > 這裡的實現和 asp.net core 的實際的實現方式應該不同,asp.net core 裡 Headers 同一個 Header 允許有多個值,asp.net core 裡是 StringValues 來實現的,這裡簡單處理了,使用了一個 `NameValueCollection` 物件 上面提到的 `Features` 是一個 `IFeatureCollection` 物件,相當於是一系列的 `Feature` 物件組成的,來看下 `FeatureCollection` 的定義: ``` csharp public interface IFeatureCollection : IDictionary { } public class FeatureCollection : Dictionary, IFeatureCollection { } ``` 這裡 `IFeatureCollection` 直接實現 `IDictionary` ,通過一個字典 Feature 型別為 Key,Feature 物件為 Value 的字典來儲存 為了方便使用,可以定義兩個擴充套件方法來方便的Get/Set ``` csharp public static class FeatureExtensions { public static IFeatureCollection Set(this IFeatureCollection featureCollection, TFeature feature) { featureCollection[typeof(TFeature)] = feature; return featureCollection; } public static TFeature Get(this IFeatureCollection featureCollection) { var featureType = typeof(TFeature); return featureCollection.ContainsKey(featureType) ? (TFeature)featureCollection[featureType] : default(TFeature); } } ``` ## Web伺服器 ![](https://img2018.cnblogs.com/blog/19327/201901/19327-20190128080856626-710206291.jpg) 上面我們已經提到了 Web 伺服器通過 `IRequestFeature`/`IResponseFeature` 來實現不同 web 伺服器和應用程式的解耦,web 伺服器只需要提供自己的 `RequestFeature`/`ResponseFeature` 即可 為了抽象不同的 Web 伺服器,我們需要定義一個 `IServer` 的抽象介面,定義如下: ``` csharp public interface IServer { Task StartAsync(Func requestHandler, CancellationToken cancellationToken = default); } ``` `IServer` 定義了一個 `StartAsync` 方法,用來啟動 Web伺服器, `StartAsync` 方法有兩個引數,一個是 requestHandler,是一個用來處理請求的委託,另一個是取消令牌用來停止 web 伺服器 示例使用了 `HttpListener` 來實現了一個簡單 Web 伺服器,`HttpListenerServer` 定義如下: ``` csharp public class HttpListenerServer : IServer { private readonly HttpListener _listener; private readonly IServiceProvider _serviceProvider; public HttpListenerServer(IServiceProvider serviceProvider, IConfiguration configuration) { _listener = new HttpListener(); var urls = configuration.GetAppSetting("ASPNETCORE_URLS")?.Split(';'); if (urls != null && urls.Length > 0) { foreach (var url in urls .Where(u => u.IsNotNullOrEmpty()) .Select(u => u.Trim()) .Distinct() ) { // Prefixes must end in a forward slash ("/") // https://stackoverflow.com/questions/26157475/use-of-httplistener _listener.Prefixes.Add(url.EndsWith("/") ? url : $"{url}/"); } } else { _listener.Prefixes.Add("http://localhost:5100/"); } _serviceProvider = serviceProvider; } public async Task StartAsync(Func requestHandler, CancellationToken cancellationToken = default) { _listener.Start(); if (_listener.IsListening) { Console.WriteLine("the server is listening on "); Console.WriteLine(_listener.Prefixes.StringJoin(",")); } while (!cancellationToken.IsCancellationRequested) { var listenerContext = await _listener.GetContextAsync(); var featureCollection = new FeatureCollection(); featureCollection.Set(listenerContext.GetRequestFeature()); featureCollection.Set(listenerContext.GetResponseFeature()); using (var scope = _serviceProvider.CreateScope()) { var httpContext = new HttpContext(featureCollection) { RequestServices = scope.ServiceProvider, }; await requestHandler(httpContext); } listenerContext.Response.Close(); } _listener.Stop(); } } ``` `HttpListenerServer` 實現的 `RequestFeature`/`ResponseFeatue` ``` csharp public class HttpListenerRequestFeature : IRequestFeature { private readonly HttpListenerRequest _request; public HttpListenerRequestFeature(HttpListenerContext listenerContext) { _request = listenerContext.Request; } public Uri Url => _request.Url; public string Method => _request.HttpMethod; public NameValueCollection Headers => _request.Headers; public Stream Body => _request.InputStream; } public class HttpListenerResponseFeature : IResponseFeature { private readonly HttpListenerResponse _response; public HttpListenerResponseFeature(HttpListenerContext httpListenerContext) { _response = httpListenerContext.Response; } public int StatusCode { get => _response.StatusCode; set => _response.StatusCode = value; } public NameValueCollection Headers { get => _response.Headers; set { _response.Headers = new WebHeaderCollection(); foreach (var key in value.AllKeys) _response.Headers.Add(key, value[key]); } } public Stream Body => _response.OutputStream; } ``` 為了方便使用,為 `HttpListenerContext` 定義了兩個擴充套件方法,就是上面 `HttpListenerServer` 中的 `GetRequestFeature`/`GetResponseFeature`: ``` csharp public static class HttpListenerContextExtensions { public static IRequestFeature GetRequestFeature(this HttpListenerContext context) { return new HttpListenerRequestFeature(context); } public static IResponseFeature GetResponseFeature(this HttpListenerContext context) { return new HttpListenerResponseFeature(context); } } ``` ## RequestDelegate 在上面的 `IServer` 定義裡有一個 requestHandler 的 物件,在 asp.net core 裡是一個名稱為 `RequestDelegate` 的物件,而用來構建這個委託的在 asp.net core 裡是 `IApplicationBuilder`,這些在蔣老師和 Edison 的文章和程式碼裡都可以看到,這裡我們只是簡單介紹下,我在 MiniAspNetCore 的示例中沒有使用這些物件,而是使用了自己抽象的 `PipelineBuilder` 和原始委託實現的 asp.net core 裡 `RequestDelegate` 定義: ``` csharp public delegate Task RequestDelegate(HttpContext context); ``` 其實和我們上面定義用的 `Func` 是等價的 `IApplicationBuilder` 定義: ``` csharp /// /// Defines a class that provides the mechanisms to configure an application's request pipeline. ///
public interface IApplicationBuilder { /// /// Gets or sets the that provides access to the application's service container. /// IServiceProvider ApplicationServices { get; set; } /// /// Gets the set of HTTP features the application's server provides. /// IFeatureCollection ServerFeatures { get; } /// /// Gets a key/value collection that can be used to share data between middleware. ///
IDictionary Properties { get; } /// /// Adds a middleware delegate to the application's request pipeline. /// /// The middleware delegate. /// The . IApplicationBuilder Use(Func middleware); /// /// Creates a new that shares the of this /// . /// /// The new . IApplicationBuilder New(); /// /// Builds the delegate used by this application to process HTTP requests. ///
/// The request handling delegate. RequestDelegate Build(); } ``` 我們這裡沒有定義 `IApplicationBuilder`,使用了簡化抽象的 `IAsyncPipelineBuilder`,定義如下: ``` csharp public interface IAsyncPipelineBuilder { IAsyncPipelineBuilder Use(Func, Func> middleware); Func Build(); IAsyncPipelineBuilder New(); } ``` 對於 asp.net core 的中介軟體來說 ,上面的 `TContext` 就是 `HttpContext`,替換之後也就是下面這樣的: ``` csharp public interface IAsyncPipelineBuilder { IAsyncPipelineBuilder Use(Func, Func> middleware); Func Build(); IAsyncPipelineBuilder New(); } ``` 是不是和 `IApplicationBuilder` 很像,如果不像可以進一步把 `Func` 使用 `RequestDelegate` 替換 ``` csharp public interface IAsyncPipelineBuilder { IAsyncPipelineBuilder Use(Func middleware); RequestDelegate Build(); IAsyncPipelineBuilder New(); } ``` 最後再將介面名稱替換一下: ``` csharp public interface IApplicationBuilder1 { IApplicationBuilder1 Use(Func middleware); RequestDelegate Build(); IApplicationBuilder1 New(); } ``` 至此,就完全可以看出來了,這 `IAsyncPipelineBuilder` 就是一個簡版的 `IApplicationBuilder` `IAsyncPipelineBuilder` 和 `IApplicationBuilder` 的作用是將註冊的多箇中間件構建成一個請求處理的委託 ![](https://img2020.cnblogs.com/blog/489462/202005/489462-20200522120639290-1183675935.png) 中介軟體處理流程: ![](https://img2020.cnblogs.com/blog/489462/202005/489462-20200522120936921-489107143.png) 更多關於 PipelineBuilder 構建中介軟體的資訊可以檢視 [讓 .NET 輕鬆構建中介軟體模式程式碼](https://www.cnblogs.com/weihanli/p/12700006.html) 瞭解更多 ## WebHost 通過除了 Web 伺服器之外,還有一個 Web Host 的概念,可以簡單的這樣理解,一個 Web 伺服器上可以有多個 Web Host,就像 IIS/nginx (Web Server) 可以 host 多個站點 可以說 WebHost 離我們的應用更近,所以我們還需要 `IHost` 來託管應用 ``` csharp public interface IHost { Task RunAsync(CancellationToken cancellationToken = default); } ``` `WebHost` 定義: ``` csharp public class WebHost : IHost { private readonly Func _requestDelegate; private readonly IServer _server; public WebHost(IServiceProvider serviceProvider, Func requestDelegate) { _requestDelegate = requestDelegate; _server = serviceProvider.GetRequiredService(); } public async Task RunAsync(CancellationToken cancellationToken = default) { await _server.StartAsync(_requestDelegate, cancellationToken).ConfigureAwait(false); } } ``` 為了方便的構建 `Host`物件,引入了 `HostBuilder` 來方便的構建一個 `Host`,定義如下: ``` csharp public interface IHostBuilder { IHostBuilder ConfigureConfiguration(Action configAction); IHostBuilder ConfigureServices(Action configureAction); IHostBuilder Initialize(Action initAction); IHostBuilder ConfigureApplication(Action> configureAction); IHost Build(); } ``` `WebHostBuilder`: ``` csharp public class WebHostBuilder : IHostBuilder { private readonly IConfigurationBuilder _configurationBuilder = new ConfigurationBuilder(); private readonly IServiceCollection _serviceCollection = new ServiceCollection(); private Action _initAction = null; private readonly IAsyncPipelineBuilder _requestPipeline = PipelineBuilder.CreateAsync(context => { context.Response.StatusCode = 404; return Task.CompletedTask; }); public IHostBuilder ConfigureConfiguration(Action configAction) { configAction?.Invoke(_configurationBuilder); return this; } public IHostBuilder ConfigureServices(Action configureAction) { if (null != configureAction) { var configuration = _configurationBuilder.Build(); configureAction.Invoke(configuration, _serviceCollection); } return this; } public IHostBuilder ConfigureApplication(Action> configureAction) { if (null != configureAction) { var configuration = _configurationBuilder.Build(); configureAction.Invoke(configuration, _requestPipeline); } return this; } public IHostBuilder Initialize(Action initAction) { if (null != initAction) { _initAction = initAction; } return this; } public IHost Build() { var configuration = _configurationBuilder.Build(); _serviceCollection.AddSingleton(configuration); var serviceProvider = _serviceCollection.BuildServiceProvider(); _initAction?.Invoke(configuration, serviceProvider); return new WebHost(serviceProvider, _requestPipeline.Build()); } public static WebHostBuilder CreateDefault(string[] args) { var webHostBuilder = new WebHostBuilder(); webHostBuilder .ConfigureConfiguration(builder => builder.AddJsonFile("appsettings.json", true, true)) .UseHttpListenerServer() ; return webHostBuilder; } } ``` > 這裡的示例我在 `IHostBuilder` 裡增加了一個 `Initialize` 的方法來做一些初始化的操作,我覺得有些資料初始化配置初始化等操作應該在這裡操作,而不應該在 `Startup` 的 `Configure` 方法裡處理,這樣 `Configure` 方法可以更純粹一些,只配置 asp.net core 的請求管道,這純屬個人意見,沒有對錯之分 > > 這裡 Host 的實現和 asp.net core 的實現不同,有需要的可以深究原始碼,在 asp.net core 2.x 的版本里是有一個 `IWebHost` 的,在 asp.net core 3.x 以及 .net 5 裡是沒有 `IWebHost` 的取而代之的是通用主機 `IHost`, 通過實現了一個 `IHostedService` 來實現 `WebHost` 的 ## Run 執行示例程式碼: ``` csharp public class Program { private static readonly CancellationTokenSource Cts = new CancellationTokenSource(); public static async Task Main(string[] args) { Console.CancelKeyPress += OnExit; var host = WebHostBuilder.CreateDefault(args) .ConfigureServices((configuration, services) => { }) .ConfigureApplication((configuration, app) => { app.When(context => context.Request.Url.PathAndQuery.StartsWith("/favicon.ico"), pipeline => { }); app.When(context => context.Request.Url.PathAndQuery.Contains("test"), p => { p.Run(context => context.Response.WriteAsync("test")); }); app .Use(async (context, next) => { await context.Response.WriteLineAsync($"middleware1, requestPath:{context.Request.Url.AbsolutePath}"); await next(); }) .Use(async (context, next) => { await context.Response.WriteLineAsync($"middleware2, requestPath:{context.Request.Url.AbsolutePath}"); await next(); }) .Use(async (context, next) => { await context.Response.WriteLineAsync($"middleware3, requestPath:{context.Request.Url.AbsolutePath}"); await next(); }) ; app.Run(context => context.Response.WriteAsync("Hello Mini Asp.Net Core")); }) .Initialize((configuration, services) => { }) .Build(); await host.RunAsync(Cts.Token); } private static void OnExit(object sender, EventArgs e) { Console.WriteLine("exiting ..."); Cts.Cancel(); } } ``` 在示例專案目錄下執行 `dotnet run`,並訪問 `http://localhost:5100/`: ![](https://img2020.cnblogs.com/blog/489462/202005/489462-20200522121759714-1340223664.png) 仔細觀察瀏覽器 `console` 或 `network` 的話,會發現還有一個請求,瀏覽器會預設請求 `/favicon.ico` 獲取網站的圖示 ![](https://img2020.cnblogs.com/blog/489462/202005/489462-20200522121925619-761432869.png) 因為我們針對這個請求沒有任何中介軟體的處理,所以直接返回了 404 在訪問 `/test`,可以看到和剛才的輸出完全不同,因為這個請求走了另外一個分支,相當於 asp.net core 裡 `Map`/`MapWhen` 的效果,另外 `Run` 代表裡中介軟體的中斷,不會執行後續的中介軟體 ![](https://img2020.cnblogs.com/blog/489462/202005/489462-20200522122208721-151942406.png) ## More 上面的實現只是我在嘗試寫一個簡版的 asp.net core 框架時的實現,和 asp.net core 的實現並不完全一樣,如果需要請參考原始碼,上面的實現僅供參考,上面實現的原始碼可以在 Github 上獲取 asp.net core 原始碼: ## Reference - - - - - -