asp服务器_200行代码,7个对象——让你了解ASP.NET Core框架的本质「3.x版」
2019年1月19日,微軟技術(蘇州)俱樂部成立,我受邀在成立大會上作了一個名為《ASP.NET Core框架揭秘》的分享。在此次分享中,我按照ASP.NET Core自身的運行原理和設計思想創(chuàng)建了一個 “迷你版” 的ASP.NET Core框架,并且利用這個 “極簡” 的模擬框架闡述了ASP.NET Core框架最核心、最本質的東西。整個框架涉及到的核心代碼不會超過200行,涉及到7個核心的對象。由于ASP.NET Core 3.X采用了不同的應用承載方式,所以我們將這個模擬框架升級到3.x版本。[本篇內容節(jié)選自即將出版的《ASP.NET Core 3框架解密》,感興趣的朋友可以通過《“ASP.NET Core 3框架揭秘”讀者群,歡迎加入》加入本書讀者群,以便及時了解本書的動態(tài)。源代碼從這里下載。]
目錄一、中間件委托鏈 HttpContext 中間件 中間件管道的構建二、服務器 IServer 針對服務器的適配 HttpListenerServer三、承載服務 WebHostedService WebHostBuilder 應用構建
一、中間件委托鏈
通過本篇文章,我將管道最核心的部分提取出來構建一個“迷你版”的ASP.NET Core框架。較之真正的ASP.NET Core框架,雖然重建的模擬框架要簡單很多,但是它們采用完全一致的設計。為了能夠在真實框架中找到對應物,在定義接口或者類型時會采用真實的名稱,但是在API的定義上會做最大限度的簡化。
HttpContext
一個HttpContext對象表示針對當前請求的上下文。要理解HttpContext上下文的本質,需要從請求處理管道的層面來講。對于由一個服務器和多個中間件構成的管道來說,面向傳輸層的服務器負責請求的監(jiān)聽、接收和最終的響應,當它接收到客戶端發(fā)送的請求后,需要將請求分發(fā)給后續(xù)中間件進行處理。對于某個中間件來說,完成自身的請求處理任務之后,在大部分情況下需要將請求分發(fā)給后續(xù)的中間件。請求在服務器與中間件之間,以及在中間件之間的分發(fā)是通過共享上下文的方式實現(xiàn)的。
如下圖所示,當服務器接收到請求之后,會創(chuàng)建一個通過HttpContext表示的上下文對象,所有中間件都在這個上下文中完成針對請求的處理工作。那么一個HttpContext對象究竟會攜帶什么樣的上下文信息?一個HTTP事務(Transaction)具有非常清晰的界定,如果從服務器的角度來說就是始于請求的接收,而終于響應的回復,所以請求和響應是兩個基本的要素,也是HttpContext承載的最核心的上下文信息。
我們可以將請求和響應理解為一個Web應用的輸入與輸出,既然HttpContext上下文是針對請求和響應的封裝,那么應用程序就可以利用這個上下文對象得到當前請求所有的輸入信息,也可以利用它完成我們所需的所有輸出工作。所以,我們?yōu)锳SP.NET Core模擬框架定義了如下這個極簡版本的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; }}如上面的代碼片段所示,我們可以利用HttpRequest對象得到當前請求的地址、請求消息的報頭集合和主體內容。利用HttpResponse對象,我們不僅可以設置響應的狀態(tài)碼,還可以添加任意的響應報頭和寫入任意的主體內容。
中間件
HttpContext對象承載了所有與當前請求相關的上下文信息,應用程序針對請求的響應也利用它來完成,所以可以利用一個Action類型的委托對象來表示針對請求的處理,我們姑且將它稱為請求處理器(Handler)。但Action僅僅是請求處理器針對“同步”編程模式的表現(xiàn)形式,對于面向Task的異步編程模式,這個處理器應該表示成類型為Func的委托對象。
由于這個表示請求處理器的委托對象具有非常廣泛的應用,所以我們?yōu)樗鼘iT定義了如下這個RequestDelegate委托類型,可以看出它就是對Func委托的表達。一個RequestDelegate對象表示的是請求處理器,那么中間件在模型中應如何表達?
public delegate Task RequestDelegate(HttpContext context);作為請求處理管道核心組成部分的中間件可以表示成類型為Func的委托對象。換句話說,中間件的輸入與輸出都是一個RequestDelegate對象。我們可以這樣來理解:對于管道中的某個中間件(下圖所示的第一個中間件)來說,后續(xù)中間件組成的管道體現(xiàn)為一個RequestDelegate對象,由于當前中間件在完成了自身的請求處理任務之后,往往需要將請求分發(fā)給后續(xù)中間件進行處理,所以它需要將后續(xù)中間件構成的RequestDelegate對象作為輸入。
當代表當前中間件的委托對象執(zhí)行之后,如果將它自己“納入”這個管道,那么代表新管道的RequestDelegate對象就成為該委托對象執(zhí)行后的輸出結果,所以中間件自然就表示成輸入和輸出類型均為RequestDelegate的Func對象。
中間件管道的構建
從事軟件行業(yè)10多年來,筆者對架構設計越來越具有這樣的認識:好的設計一定是“簡單”的設計。所以在設計某個開發(fā)框架時筆者的目標是再簡單點。上面介紹的請求處理管道的設計就具有“簡單”的特質:Pipeline = Server + Middlewares。但是“再簡單點”其實是可以的,我們可以將多個中間件組成一個單一的請求處理器。請求處理器可以通過RequestDelegate對象來表示,所以整個請求處理管道將具有更加簡單的表達:Pipeline = Server + RequestDelegate(見下圖12)。
表示中間件的Func對象向表示請求處理器的RequestDelegate對象之間的轉換是通過IApplicationBuilder對象來完成的。從接口命名可以看出,IApplicationBuilder對象是用來構建“應用程序”(Application)的,實際上,由所有注冊中間件構建的RequestDelegate對象就是對應用程序的表達,因為應用程序的意圖完全是由注冊的中間件達成的。
public interface IApplicationBuilder{ RequestDelegate Build(); IApplicationBuilder Use(Func middleware);}如上所示的代碼片段是模擬框架對IApplicationBuilder接口的簡化定義。它的Use方法用來注冊中間件,而Build方法則將所有的中間件按照注冊的順序組裝成一個RequestDelegate對象。如下所示的代碼片段中ApplicationBuilder類型是對該接口的默認實現(xiàn)。我們給出的代碼片段還體現(xiàn)了這樣一個細節(jié):當我們將注冊的中間件轉換成一個表示請求處理器的RequestDelegate對象時,會在管道的尾端添加一個處理器用來響應一個狀態(tài)碼為404的響應。這個細節(jié)意味著如果沒有注冊任何的中間件或者所有注冊的中間件都將請求分發(fā)給后續(xù)管道,那么應用程序會回復一個狀態(tài)碼為404的響應。
public class ApplicationBuilder : IApplicationBuilder{ private readonly IList> _middlewares = new List>(); public RequestDelegate Build() { RequestDelegate next = context => { context.Response.StatusCode = 404; return Task.CompletedTask; }; foreach (var middleware in _middlewares.Reverse()) { next = middleware.Invoke(next); } return next; } public IApplicationBuilder Use(Func middleware) { _middlewares.Add(middleware); return this; }}二、服務器
服務器在管道中的職責非常明確:負責HTTP請求的監(jiān)聽、接收和最終的響應。具體來說,啟動后的服務器會綁定到指定的端口進行請求監(jiān)聽。一旦有請求抵達,服務器會根據(jù)該請求創(chuàng)建代表請求上下文的HttpContext對象,并將該上下文分發(fā)給注冊的中間件進行處理。當中間件管道完成了針對請求的處理之后,服務器會將最終生成的響應回復給客戶端。
IServer
在模擬的ASP.NET Core框架中,我們將服務器定義成一個極度簡化的IServer接口。在如下所示的代碼片段中,IServer接口具有唯一的StartAsync方法來啟動自身代表的服務器。服務器最終需要將接收的請求分發(fā)給注冊的中間件,而注冊的中間件最終會被IApplicationBuilder對象構建成一個代表請求處理器的RequestDelegate對象,StartAsync方法的參數(shù)handler代表的就是這樣一個對象。
public interface IServer{ Task StartAsync(RequestDelegate handler);}針對服務器的適配
面向應用層的HttpContext對象是對請求和響應的抽象與封裝,但是請求最初是由面向傳輸層的服務器接收的,最終的響應也會由服務器回復給客戶端。所有ASP.NET Core應用使用的都是同一個HttpContext類型,但是它們可以注冊不同類型的服務器,應如何解決兩者之間的適配問題?計算機領域有這樣一句話:“任何問題都可以通過添加一個抽象層的方式來解決,如果解決不了,那就再加一層。”同一個HttpContext類型與不同服務器類型之間的適配問題自然也可以通過添加一個抽象層來解決。我們將定義在該抽象層的對象稱為特性(Feature),特性可以視為對HttpContext某個方面的抽象化描述。
如上圖所示,我們可以定義一系列特性接口來為HttpContext提供某個方面的上下文信息,具體的服務器只需要實現(xiàn)這些Feature接口即可。對于所有用來定義特性的接口,最重要的是提供請求信息的IRequestFeature接口和完成響應的IResponseFeature接口。
下面闡述用來適配不同服務器類型的特性在代碼層面的定義。如下面的代碼片段所示,我們定義了一個IFeatureCollection接口來表示存放特性的集合。可以看出,這是一個以Type和Object作為Key和Value的字典,Key代表注冊Feature所采用的類型,而Value代表Feature對象本身,也就是說,我們提供的特性最終是以對應類型(一般為接口類型)進行注冊的。為了便于編程,我們定義了Set方法和Get方法來設置與獲取特性對象。
public interface IFeatureCollection : IDictionary { }public class FeatureCollection : Dictionary, IFeatureCollection { }public static partial class Extensions{ public static T Get(this IFeatureCollection features) => features.TryGetValue(typeof(T), out var value) ? (T)value : default(T); public static IFeatureCollection Set(this IFeatureCollection features, T feature) { features[typeof(T)] = feature; return features; }}最核心的兩種特性類型就是分別用來表示請求和響應的特性,我們可以采用如下兩個接口來表示。可以看出,IHttpRequestFeature接口和IHttpResponseFeature接口具有與抽象類型HttpRequest和HttpResponse完全一致的成員定義。
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; }}我們在前面給出了用于描述請求上下文的HttpContext類型的成員定義,下面介紹其具體實現(xiàn)。如下面的代碼片段所示,表示請求和響應的HttpRequest與HttpResponse分別是由對應的特性(IHttpRequestFeature對象和IHttpResponseFeature對象)創(chuàng)建的。HttpContext對象本身則是通過一個表示特性集合的IFeatureCollection 對象來創(chuàng)建的,它會在初始化過程中從這個集合中提取出對應的特性來創(chuàng)建HttpRequest對象和HttpResponse對象。
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();}public class HttpResponse{ private readonly IHttpResponseFeature _feature; public NameValueCollection Headers=> _feature.Headers; public Stream Body=> _feature.Body; public int StatusCode { get => _feature.StatusCode; set => _feature.StatusCode = value; } public HttpResponse(IFeatureCollection features)=> _feature = features.Get();}換句話說,我們利用HttpContext對象的Request屬性提取的請求信息最初來源于IHttpRequestFeature對象,利用它的Response屬性針對響應所做的任意操作最終都會作用到IHttpResponseFeature對象上。這兩個對象最初是由注冊的服務器提供的,這正是同一個ASP.NET Core應用可以自由地選擇不同服務器類型的根源所在。
HttpListenerServer
在對服務器的職責和它與HttpContext的適配原理有了清晰的認識之后,我們可以嘗試定義一個服務器。我們將接下來定義的服務器類型命名為HttpListenerServer,因為它對請求的監(jiān)聽、接收和響應是由一個HttpListener對象來實現(xiàn)的。由于服務器接收到請求之后需要借助“特性”的適配來構建統(tǒng)一的請求上下文(即HttpContext對象),這也是中間件的執(zhí)行上下文,所以提供針對性的特性實現(xiàn)是自定義服務類型的關鍵所在。
對HttpListener有所了解的讀者都知道,當它在接收到請求之后同樣會創(chuàng)建一個HttpListenerContext對象表示請求上下文。如果使用HttpListener對象作為ASP.NET Core應用的監(jiān)聽器,就意味著不僅所有的請求信息會來源于這個HttpListenerContext對象,我們針對請求的響應最終也需要利用這個上下文對象來完成。HttpListenerServer對應特性所起的作用實際上就是在HttpListenerContext和HttpContext這兩種上下文之間搭建起一座如下圖所示的橋梁。
上圖中用來在HttpListenerContext和HttpContext這兩個上下文類型之間完成適配的特性類型被命名為HttpListenerFeature。如下面的代碼片段所示,HttpListenerFeature類型同時實現(xiàn)了針對請求和響應的特性接口IHttpRequestFeature與IHttpResponseFeature。
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; }}創(chuàng)建HttpListenerFeature對象時需要提供一個HttpListenerContext對象,IHttpRequestFeature接口的實現(xiàn)成員所提供的請求信息全部來源于這個HttpListenerContext上下文,IHttpResponseFeature接口的實現(xiàn)成員針對響應的操作最終也轉移到這個HttpListenerContext上下文上。如下所示的代碼片段是針對HttpListener的服務器類型HttpListenerServer的完整定義。我們在創(chuàng)建HttpListenerServer對象的時候可以顯式提供一組監(jiān)聽地址,如果沒有提供,監(jiān)聽地址會默認設置“l(fā)ocalhost:5000”。在實現(xiàn)的StartAsync方法中,我們啟動了在構造函數(shù)中創(chuàng)建的HttpListenerServer對象,并且在一個無限循環(huán)中通過調用其GetContextAsync方法實現(xiàn)了針對請求的監(jiān)聽和接收。
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 string[] { "http://localhost:5000/" }; } public async Task StartAsync(RequestDelegate handler) { Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url)); _httpListener.Start(); while (true) { var listenerContext = await _httpListener.GetContextAsync(); var feature = new HttpListenerFeature(listenerContext); var features = new FeatureCollection() .Set(feature) .Set(feature); var httpContext = new HttpContext(features); await handler(httpContext); listenerContext.Response.Close(); } }}當HttpListener監(jiān)聽到抵達的請求后,我們會得到一個HttpListenerContext對象,此時只需要利用它創(chuàng)建一個HttpListenerFeature對象并且分別以IHttpRequestFeature接口和IHttpResponseFeature接口的形式注冊到創(chuàng)建的FeatureCollection集合上。我們最終利用這個FeatureCollection集合創(chuàng)建出代表請求上下文的HttpContext對象,當將它作為參數(shù)調用由所有注冊中間件共同構建的RequestDelegate對象時,中間件管道將接管并處理該請求。
三、承載服務
到目前為止,我們已經(jīng)了解構成ASP.NET Core請求處理管道的兩個核心要素(服務器和中間件),現(xiàn)在我們的目標是利用.NET Core承載服務系統(tǒng)來承載這一管道。毫無疑問,還需要通過實現(xiàn)IHostedService接口來定義對應的承載服務,為此我們定義了一個名為WebHostedService的承載服務。(關于.NET Core承載服務系統(tǒng),請參閱我的系列文章《服務承載系統(tǒng)》)
WebHostedService
由于服務器是整個請求處理管道的“龍頭”,所以從某種意義上來說,啟動一個ASP.NET Core應用就是為啟動服務器,所以可以將服務的啟動在WebHostedService承載服務中實現(xiàn)。如下面的代碼片段所示,創(chuàng)建一個WebHostedService對象時,需要提供服務器對象和由所有注冊中間件構建的RequestDelegate對象。在實現(xiàn)的StartAsync方法中,我們只需要調用服務器對象的StartAsync方法啟動它即可。
public class WebHostedService : IHostedService{ private readonly IServer _server; private readonly RequestDelegate _handler; public WebHostedService(IServer server, RequestDelegate handler) { _server = server; _handler = handler; } public Task StartAsync(CancellationToken cancellationToken) => _server.StartAsync(_handler); public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;}到目前為止,我們基本上已經(jīng)完成了所有核心的工作,如果能夠將一個WebHostedService實例注冊到.NET Core的承載系統(tǒng)中,它就能夠幫助我們啟動一個ASP.NET Core應用。為了使這個過程在編程上變得更加便利和“優(yōu)雅”,我們定義了一個輔助的WebHostBuilder類型。
WebHostBuilder
要創(chuàng)建一個WebHostedService對象,必需顯式地提供一個表示服務器的IServer對象,以及由所有注冊中間件構建而成的RequestDelegate對象,WebHostBuilder提供了更加便利和“優(yōu)雅”的服務器與中間件注冊方式。如下面的代碼片段所示,WebHostBuilder是對額外兩個Builder對象的封裝:一個是用來構建服務宿主的IHostBuilder對象,另一個是用來注冊中間件并最終幫助我們創(chuàng)建RequestDelegate對象的IApplicationBuilder對象。
public class WebHostBuilder{ public IHostBuilder HostBuilder { get; } public IApplicationBuilder ApplicationBuilder { get; } public WebHostBuilder(IHostBuilder hostBuilder, IApplicationBuilder applicationBuilder) { HostBuilder = hostBuilder; ApplicationBuilder = applicationBuilder; }}我們?yōu)閃ebHostBuilder定義了如下兩個擴展方法:UseHttpListenerServer方法完成了針對自定義的服務器類型HttpListenerServer的注冊;Configure方法提供了一個Action類型的參數(shù),利用該參數(shù)來注冊任意中間件。
public static partial class Extensions{ public static WebHostBuilder UseHttpListenerServer(this WebHostBuilder builder, params string[] urls) { builder.HostBuilder.ConfigureServices(svcs => svcs.AddSingleton(new HttpListenerServer(urls))); return builder; } public static WebHostBuilder Configure(this WebHostBuilder builder, Action configure) { configure?.Invoke(builder.ApplicationBuilder); return builder; }}代表ASP.NET Core應用的請求處理管道最終是利用承載服務WebHostedService注冊到.NET Core的承載系統(tǒng)中的,針對WebHostedService服務的創(chuàng)建和注冊體現(xiàn)在為IHostBuilder接口定義的ConfigureWebHost擴展方法上。如下面的代碼片段所示,ConfigureWebHost方法定義了一個Action類型的參數(shù),利用該參數(shù)可以注冊服務器、中間件及其他相關服務。
public static partial class Extensions{ public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action configure) { var webHostBuilder = new WebHostBuilder(builder, new ApplicationBuilder()); configure?.Invoke(webHostBuilder); builder.ConfigureServices(svcs => svcs.AddSingleton(provider => { var server = provider.GetRequiredService(); var handler = webHostBuilder.ApplicationBuilder.Build(); return new WebHostedService(server, handler); })); return builder; }}在ConfigureWebHost方法中,我們創(chuàng)建了一個ApplicationBuilder對象,并利用它和當前的IHostBuilder對象創(chuàng)建了一個WebHostBuilder對象,然后將這個WebHostBuilder對象作為參數(shù)調用了指定的Action委托對象。在此之后,我們調用IHostBuilder接口的ConfigureServices方法在依賴注入框架中注冊了一個用于創(chuàng)建WebHostedService服務的工廠。對于由該工廠創(chuàng)建的WebHostedService對象來說,服務器來源于注冊的服務,而作為請求處理器的RequestDelegate對象則由ApplicationBuilder對象根據(jù)注冊的中間件構建而成。
應用構建
到目前為止,這個用來模擬ASP.NET Core請求處理管道的“迷你版”框架已經(jīng)構建完成,下面嘗試在它上面開發(fā)一個簡單的應用。如下面的代碼片段所示,我們調用靜態(tài)類型Host的CreateDefaultBuilder方法創(chuàng)建了一個IHostBuilder對象,然后調用ConfigureWebHost方法并利用提供的Action對象注冊了HttpListenerServer服務器和3個中間件。在調用Build方法構建出作為服務宿主的IHost對象之后,我們調用其Run方法啟動所有承載的IHostedSerivce服務。
class Program{ static void Main() { Host.CreateDefaultBuilder() .ConfigureWebHost(builder => builder .UseHttpListenerServer() .Configure(app => app .Use(FooMiddleware) .Use(BarMiddleware) .Use(BazMiddleware))) .Build() .Run(); } 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");}由于中間件最終體現(xiàn)為一個類型為Func的委托對象,所以可以利用與之匹配的方法來定義中間件。演示實例中定義的3個中間件(FooMiddleware、BarMiddleware和BazMiddleware)對應的正是3個靜態(tài)方法,它們調用WriteAsync擴展方法在響應中寫了一段文字。
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); }}應用啟動之后,如果利用瀏覽器向應用程序采用的默認監(jiān)聽地址(“http://localhost:5000”)發(fā)送一個請求,得到的輸出結果如下圖所示。瀏覽器上呈現(xiàn)的文字正是注冊的3個中間件寫入的。
作者:蔣金楠
總結
以上是生活随笔為你收集整理的asp服务器_200行代码,7个对象——让你了解ASP.NET Core框架的本质「3.x版」的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 电脑分辨率设置工具_干货分享:PPT 导
- 下一篇: 安卓模拟器按键_安卓模拟机(夜神模拟器)