一 前言
Artech 分享了 200行程式碼,7個物件——讓你瞭解ASP.NET Core框架的本質 。 用一個極簡的模擬框架闡述了ASP.NET Core框架最為核心的部分。
這裡一步步來完成這個迷你框架。
二 先來一段簡單的程式碼
這段程式碼非常簡單,啟動伺服器並監聽本地5000埠和處理請求。
static async Task Main(string[] args)
{
HttpListener httpListener = new HttpListener();
httpListener.Prefixes.Add("http://localhost:5000/");
httpListener.Start();
while (true)
{
var context = await httpListener.GetContextAsync();
await context.Response.OutputStream.WriteAsync(Encoding.UTF8.GetBytes("hello world"));
context.Response.Close();
}
}
現在要分離伺服器(Server) 和 請求處理(handle),那麼一個簡單設計架構就出來了 :
Pipeline =Server + HttpHandler
三 處理器的抽象
處理器要從請求(Request)中獲取資料,和定製響應(Response)的資料。
可以想到我們的處理器的處理方法應該是這樣的:
Task Handle(/*HttpRequest HttpResponse*/);
它可以處理請求和響應,由於處理可以是同步或者非同步的,所以返回Task。
很容易想到要封裝http請求和響應,封裝成一個上下文(Context) 供處理器使用(這樣的好處,處理器需要的其他資料也可以封裝在這裡,統一使用),所以要開始封裝HttpContext。
封裝HttpContext
public class HttpRequest
{
public Uri Url { get; }
public NameValueCollection Headers { get; }
public Stream Body { get; }
}
public class HttpResponse
{
public NameValueCollection Headers { get; }
public Stream Body { get; }
public int StatusCode { get; set; }
}
public class HttpContext
{
public HttpRequest Request { get; set; }
public HttpResponse Response { get; set; }
}
要支援不同的伺服器,則不同的伺服器都要提供HttpContext,這樣有了新的難題:伺服器和HttpContext之間的適配 。
現階段的HttpContext包含HttpRequest和HttpResponse,請求和響應的資料都是要伺服器(Server)提供的。
可以定義介面,讓不同的伺服器提供實現介面的例項:
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之間的適配,定義一個功能的集合,通過型別可以找到伺服器提供的例項
public interface IFeatureCollection:IDictionary<Type,object>
{
}
public static partial class Extensions
{
public static T Get<T>(this IFeatureCollection features)
{
return features.TryGetValue(typeof(T), out var value) ? (T)value : default;
}
public static IFeatureCollection Set<T>(this IFeatureCollection features,T feature)
{
features[typeof(T)] = feature;
return features;
}
}
public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection { }
接下來修改HttpContext,完成適配
public class HttpContext
{
public HttpContext(IFeatureCollection features)
{
Request = new HttpRequest(features);
Response = new HttpResponse(features);
}
public HttpRequest Request { get; set; }
public HttpResponse Response { get; set; }
}
public class HttpRequest
{
private readonly IHttpRequestFeature _httpRequestFeature;
public HttpRequest(IFeatureCollection features)
{
_httpRequestFeature = features.Get<IHttpRequestFeature>();
}
public Uri Url => _httpRequestFeature.Url;
public NameValueCollection Headers => _httpRequestFeature.Headers;
public Stream Body => _httpRequestFeature.Body;
}
public class HttpResponse
{
private readonly IHttpResponseFeature _httpResponseFeature;
public HttpResponse(IFeatureCollection features)
{
_httpResponseFeature = features.Get<IHttpResponseFeature>();
}
public int StatusCode
{
get => _httpResponseFeature.StatusCode;
set => _httpResponseFeature.StatusCode = value;
}
public NameValueCollection Headers => _httpResponseFeature.Headers;
public Stream Body => _httpResponseFeature.Body;
}
public static partial class Extensions
{
public static Task WriteAsync(this HttpResponse response,string content)
{
var buffer = Encoding.UTF8.GetBytes(content);
return response.Body.WriteAsync(buffer, 0, buffer.Length);
}
}
定義處理器
封裝好了HttpContext,終於可以回過頭來看看處理器。
處理器的處理方法現在應該是這樣:
Task Handle(HttpContext context);
接下來就是怎麼定義這個處理器了。
起碼有兩種方式:
1、定義一個介面:
public interface IHttpHandler
{
Task Handle(HttpContext context);
}
2、定義一個委託型別
public delegate Task RequestDelegate(HttpContext context);
兩種方式,本質上沒啥區別,委託程式碼方式更靈活,不用實現一個介面,還符合鴨子模型。
處理器就選用委託型別。
定義了處理器,接下來看看伺服器
四 伺服器的抽象
伺服器應該有一個開始方法,傳入處理器,並執行。
伺服器抽象如下:
public interface IServer
{
Task StartAsync(RequestDelegate handler);
}
定義一個HttpListener的伺服器來實現IServer,由於HttpListener的伺服器需要提供HttpContext所需的資料,所以先定義HttpListenerFeature
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;
}
}
定義HttpListener伺服器
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();
Console.WriteLine($"伺服器{typeof(HttpListenerServer).Name} 開啟,開始監聽:{string.Join(";", _urls)}");
while (true)
{
var listtenerContext = await _httpListener.GetContextAsync();
var feature = new HttpListenerFeature(listtenerContext);
var features = new FeatureCollection()
.Set<IHttpRequestFeature>(feature)
.Set<IHttpResponseFeature>(feature);
var httpContext = new HttpContext(features);
await handler(httpContext);
listtenerContext.Response.Close();
}
}
}
修改Main方法執行測試
static async Task Main(string[] args)
{
IServer server = new HttpListenerServer();
async Task FooBar(HttpContext httpContext)
{
await httpContext.Response.WriteAsync("fooBar");
}
await server.StartAsync(FooBar);
}
執行結果如下:
至此,完成了伺服器和處理器的抽象。
接下來單看處理器,所有的處理邏輯都集合在一個方法中,理想的方式是有多個處理器進行處理,比如處理器A處理完,則接著B處理器進行處理……
那麼就要管理多個處理器之間的連線方式。
五 中介軟體
中介軟體的定義
假設有三個處理器A,B,C
框架要實現:A處理器開始處理,A處理完成之後,B處理器開始處理,B處理完成之後,C處理器開始處理。
引入中介軟體來完成處理器的連線。
中介軟體的要實現的功能很簡單:
- 傳入下一個要執行的處理器;
- 在中介軟體中的處理器裡,記住下一個要執行的處理器;
- 返回中介軟體中的處理器,供其他中介軟體使用。
所以中介軟體應該是這樣的:
//虛擬碼
處理器 Middleware(傳入下一個要執行的處理器)
{
return 處理器
{
//處理器的邏輯
下一個要執行的處理器在這裡執行
}
}
舉個例子,現在有三個中介軟體FooMiddleware,BarMiddleware,BazMiddleware,分別對應的處理器為A,B,C
要保證 處理器的處理順序為 A->B->C
則先要執行 最後一個BazMiddleware,傳入“完成處理器” 返回 處理器C
然後把處理器C 傳入 BarMiddleware ,返回處理器B,依次類推。
//虛擬碼
var middlewares=new []{FooMiddleware,BarMiddleware,BazMiddleware};
middlewares.Reverse();
var next=完成的處理器;
foreach(var middleware in middlewares)
{
next= middleware(next);
}
//最後的next,就是最終要傳入IServer 中的處理器
模擬執行時的虛擬碼:
//傳入完成處理器,返回處理器C
處理器 BazMiddleware(完成處理器)
{
return 處理器C
{
//處理器C的處理程式碼
完成處理器
};
}
//傳入處理器C,返回處理器B
處理器 BarMiddleware(處理器C)
{
return 處理器B
{
//處理器B的處理程式碼
執行處理器C
};
}
//傳入處理器B,返回處理器A
處理器 FooMiddleware(處理器B)
{
return 處理器A
{
//處理器A的處理程式碼
執行處理器B
};
}
這樣當處理器A執行的時候,會先執行自身的程式碼,然後執行處理器B,處理器B執行的時候,先執行自身的程式碼,然後執行處理器C,依次類推。
所以,中介軟體的方法應該是下面這樣的:
RequestDelegate DoMiddleware(RequestDelegate next);
中介軟體的管理
要管理中介軟體,就要提供註冊中介軟體的方法和最終構建出RequestDelegate的方法。
定義註冊中介軟體和構建處理器的介面: IApplicationBuilder
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 IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
_middlewares.Add(middleware);
return this;
}
public RequestDelegate Build()
{
_middlewares.Reverse();
RequestDelegate next = context => { context.Response.StatusCode = 404; return Task.CompletedTask; };
foreach (var middleware in _middlewares)
{
next = middleware(next);
}
return next;
}
}
定義中介軟體測試
在Program 類裡定義三個中介軟體:
static RequestDelegate FooMiddleware(RequestDelegate next)
{
return async context =>
{
await context.Response.WriteAsync("foo=>");
await next(context);
};
}
static RequestDelegate BarMiddleware(RequestDelegate next)
{
return async context =>
{
await context.Response.WriteAsync("bar=>");
await next(context);
};
}
static RequestDelegate BazMiddleware(RequestDelegate next)
{
return async context =>
{
await context.Response.WriteAsync("baz=>");
await next(context);
};
}
修改Main方法測試執行
static async Task Main(string[] args)
{
IServer server = new HttpListenerServer();
var handler = new ApplicationBuilder()
.Use(FooMiddleware)
.Use(BarMiddleware)
.Use(BazMiddleware)
.Build();
await server.StartAsync(handler);
}
執行結果如下:
六 管理伺服器和處理器
為了管理伺服器和處理器之間的關係 抽象出web宿主
如下:
public interface IWebHost
{
Task StartAsync();
}
public class WebHost : IWebHost
{
private readonly IServer _server;
private readonly RequestDelegate _handler;
public WebHost(IServer server,RequestDelegate handler)
{
_server = server;
_handler = handler;
}
public Task StartAsync()
{
return _server.StartAsync(_handler);
}
}
Main方法可以改一下測試
static async Task Main(string[] args)
{
IServer server = new HttpListenerServer();
var handler = new ApplicationBuilder()
.Use(FooMiddleware)
.Use(BarMiddleware)
.Use(BazMiddleware)
.Build();
IWebHost webHost = new WebHost(server, handler);
await webHost.StartAsync();
}
要構建WebHost,需要知道用哪個伺服器,和配置了哪些中介軟體,最後可以構建出WebHost
程式碼如下:
public interface IWebHostBuilder
{
IWebHostBuilder UseServer(IServer server);
IWebHostBuilder Configure(Action<IApplicationBuilder> configure);
IWebHost Build();
}
public class WebHostBuilder : IWebHostBuilder
{
private readonly List<Action<IApplicationBuilder>> _configures = new List<Action<IApplicationBuilder>>();
private IServer _server;
public IWebHost Build()
{
//所有的中介軟體都註冊在builder上
var builder = new ApplicationBuilder();
foreach (var config in _configures)
{
config(builder);
}
return new WebHost(_server, builder.Build());
}
public IWebHostBuilder Configure(Action<IApplicationBuilder> configure)
{
_configures.Add(configure);
return this;
}
public IWebHostBuilder UseServer(IServer server)
{
_server = server;
return this;
}
}
給IWebHostBuilder加一個擴充套件方法,用來使用HttpListenerServer 伺服器
public static partial class Extensions
{
public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls)
{
return builder.UseServer(new HttpListenerServer(urls));
}
}
修改Mian方法
static async Task Main(string[] args)
{
await new WebHostBuilder()
.UseHttpListener()
.Configure(app=>
app.Use(FooMiddleware)
.Use(BarMiddleware)
.Use(BazMiddleware))
.Build()
.StartAsync();
}
完成。
七 新增一個UseMiddleware 擴充套件 玩玩
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder application, Type type)
{
//省略實現
}
public static IApplicationBuilder UseMiddleware<T>(this IApplicationBuilder application) where T : class
{
return application.UseMiddleware(typeof(T));
}
新增一箇中介軟體
public class QuxMiddleware
{
private readonly RequestDelegate _next;
public QuxMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
await context.Response.WriteAsync("qux=>");
await _next(context);
}
}
public static partial class Extensions
{
public static IApplicationBuilder UseQux(this IApplicationBuilder builder)
{
return builder.UseMiddleware<QuxMiddleware>();
}
}
使用中介軟體
class Program
{
static async Task Main(string[] args)
{
await new WebHostBuilder()
.UseHttpListener()
.Configure(app=>
app.Use(FooMiddleware)
.Use(BarMiddleware)
.Use(BazMiddleware)
.UseQux())
.Build()
.StartAsync();
}
執行結果
最後,期待Artech 新書。