前言
中介軟體(Middleware)對於Asp.NetCore專案來說,不能說重要,而是不能缺少,因為Asp.NetCore的請求管道就是通過一系列的中介軟體組成的;在伺服器接收到請求之後,請求會經過請求管道進行相關的過濾或處理;
正文
那中介軟體是那路大神?
會經常聽說,需要註冊一下中介軟體,如圖:
所以說,中介軟體是針對請求進行某種功能需求封裝的元件,而這個元件可以控制是否繼續執行下一個中介軟體;如上圖中的app.UserStaticFiles()就是註冊靜態檔案處理的中介軟體,在請求管道中就會處理對應的請求,如果沒有靜態檔案中介軟體,那就處理不了靜態檔案(如html、css等);這也是Asp.NetCore與Asp.Net不一樣的地方,前者是根據需求新增對應的中介軟體,而後者是提前就全部準備好了,不管用不用,反正都要路過,這也是Asp.NetCore效能比較好的原因之一;
而對於中介軟體執行邏輯,官方有一個經典的圖:
如圖所示,請求管道由一個個中介軟體(Middleware)組成,每個中介軟體可以在請求和響應中進行相關的邏輯處理,在有需要的情況下,當前的中介軟體可以不傳遞到下一個中介軟體,從而實現斷路;如果這個不太好理解,如下圖:
每層外圈代表一箇中介軟體,黑圈代表最終的Action方法,當請求過來時,會依次經過中介軟體,Action處理完成後,返回響應時也依次經過對應的中介軟體,而執行的順序如箭頭所示;(這裡省去了一些其他邏輯,只說中介軟體)。
好了好了,理論說不好,擔心把看到的小夥伴繞進去了,就先到這吧,接下來從程式碼中看看中介軟體及請求管道是如何實現的;老規矩,找不到下手的地方,就先找能"摸"的到的地方,這裡就先扒靜態檔案的中介軟體:
namespace Microsoft.AspNetCore.Builder
{
public static class StaticFileExtensions
{
// 呼叫就是這個擴充套件方法
public static IApplicationBuilder UseStaticFiles(this IApplicationBuilder app)
{
if (app == null)
{
throw new ArgumentNullException(nameof(app));
}
// 這裡呼叫了 IApplicationBuilder 的擴充套件方法
return app.UseMiddleware<StaticFileMiddleware>();
}
// 這裡省略了兩個過載方法,是可以指定引數的
}
}
UseMiddleware方法實現
// 看著呼叫的方法
public static IApplicationBuilder UseMiddleware<TMiddleware>(this IApplicationBuilder app, params object[] args)
{
// 內部呼叫了以下方法
return app.UseMiddleware(typeof(TMiddleware), args);
}
// 其實這裡是對自定義中介軟體的註冊,這裡可以不用太深入瞭解
public static IApplicationBuilder UseMiddleware(this IApplicationBuilder app, Type middleware, params object[] args)
{
if (typeof(IMiddleware).GetTypeInfo().IsAssignableFrom(middleware.GetTypeInfo()))
{
// IMiddleware doesn't support passing args directly since it's
// activated from the container
if (args.Length > 0)
{
throw new NotSupportedException(Resources.FormatException_UseMiddlewareExplicitArgumentsNotSupported(typeof(IMiddleware)));
}
return UseMiddlewareInterface(app, middleware);
}
// 取得容器
var applicationServices = app.ApplicationServices;
// 反編譯進行包裝成註冊中介軟體的樣子(Func<ReuqestDelegate,RequestDelegate>),但可以看到本質使用IApplicationBuilder中Use方法
return app.Use(next =>
{
// 獲取指定型別中的方法列表
var methods = middleware.GetMethods(BindingFlags.Instance | BindingFlags.Public);
// 找出名字是Invoke或是InvokeAsync的方法
var invokeMethods = methods.Where(m =>
string.Equals(m.Name, InvokeMethodName, StringComparison.Ordinal)
|| string.Equals(m.Name, InvokeAsyncMethodName, StringComparison.Ordinal)
).ToArray();
// 如果有多個方法 ,就丟擲異常,這裡保證方法的唯一
if (invokeMethods.Length > 1)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddleMutlipleInvokes(InvokeMethodName, InvokeAsyncMethodName));
}
// 如果沒有找到,也就丟擲異常
if (invokeMethods.Length == 0)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoInvokeMethod(InvokeMethodName, InvokeAsyncMethodName, middleware));
}
// 取得唯一的方法Invoke或是InvokeAsync方法
var methodInfo = invokeMethods[0];
// 判斷型別是否返回Task,如果不是就丟擲異常,要求返回Task的目的是為了後續包裝RequestDelegate
if (!typeof(Task).IsAssignableFrom(methodInfo.ReturnType))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNonTaskReturnType(InvokeMethodName, InvokeAsyncMethodName, nameof(Task)));
}
// 判斷方法的引數,引數的第一個引數必須是HttpContext型別
var parameters = methodInfo.GetParameters();
if (parameters.Length == 0 || parameters[0].ParameterType != typeof(HttpContext))
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareNoParameters(InvokeMethodName, InvokeAsyncMethodName, nameof(HttpContext)));
}
// 開始構造RequestDelegate物件
var ctorArgs = new object[args.Length + 1];
ctorArgs[0] = next;
Array.Copy(args, 0, ctorArgs, 1, args.Length);
var instance = ActivatorUtilities.CreateInstance(app.ApplicationServices, middleware, ctorArgs);
// 如果引數只有一個HttpContext 就包裝成一個RequestDelegate返回
if (parameters.Length == 1)
{
return (RequestDelegate)methodInfo.CreateDelegate(typeof(RequestDelegate), instance);
}
// 如果引數有多個的情況就單獨處理,這裡不詳細進去了
var factory = Compile<object>(methodInfo, parameters);
return context =>
{
var serviceProvider = context.RequestServices ?? applicationServices;
if (serviceProvider == null)
{
throw new InvalidOperationException(Resources.FormatException_UseMiddlewareIServiceProviderNotAvailable(nameof(IServiceProvider)));
}
return factory(instance, context, serviceProvider);
};
});
}
以上程式碼其實現在拿出來有點早了,以上是對自定義中介軟體的註冊方式,為了扒程式碼的邏輯完整,拿出來了;這裡可以不用深究裡面內容,知道內部呼叫了IApplicationBuilder的Use方法即可;
由此可見,IApplicationBuilder就是構造請求管道的核心型別,如下:
namespace Microsoft.AspNetCore.Builder
{
public interface IApplicationBuilder
{
// 容器,用於依賴注入獲取物件的
IServiceProvider ApplicationServices
{
get;
set;
}
// 屬性集合,用於中介軟體共享資料
IDictionary<string, object> Properties
{
get;
}
// 針對伺服器的特性
IFeatureCollection ServerFeatures
{
get;
}
// 構建請求管道
RequestDelegate Build();
// 克隆例項的
IApplicationBuilder New();
// 註冊中介軟體
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware);
}
}
IApplicationBuilder的預設實現就是ApplicationBuilder,走起,一探究竟:
namespace Microsoft.AspNetCore.Builder
{ // 以下 刪除一些屬性和方法,具體可以私下看具體程式碼
public class ApplicationBuilder : IApplicationBuilder
{
// 儲存註冊中介軟體的連結串列
private readonly IList<Func<RequestDelegate, RequestDelegate>> _components = new List<Func<RequestDelegate, RequestDelegate>>();
// 註冊中介軟體
public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
// 將中介軟體加入到連結串列
_components.Add(middleware);
return this;
}
// 構造請求管道
public RequestDelegate Build()
{
// 構造一個404的中介軟體,這就是為什麼地址匹配不上時會報404的原因
RequestDelegate app = context =>
{
// 判斷是否有Endpoint中介軟體
var endpoint = context.GetEndpoint();
var endpointRequestDelegate = endpoint?.RequestDelegate;
if (endpointRequestDelegate != null)
{
var message =
$"The request reached the end of the pipeline without executing the endpoint: '{endpoint.DisplayName}'. " +
$"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
$"routing.";
throw new InvalidOperationException(message);
}
// 返回404 Code
context.Response.StatusCode = 404;
return Task.CompletedTask;
};
// 構建管道,首先將註冊的連結串列倒序一把,保證按照註冊順序執行
foreach (var component in _components.Reverse())
{
app = component(app);
}
// 最終返回
return app;
}
}
}
在註冊的程式碼中,可以看到所謂的中介軟體就是Func<RequestDelegate, RequestDelegate>,其中RequestDelegate就是一個委託,用於處理請求的,如下:
public delegate Task RequestDelegate(HttpContext context);
之所以用Func<RequestDelegate, RequestDelegate>的形式表示中介軟體,應該就是為了中介軟體間驅動方便,畢竟中介軟體不是單獨存在的,是需要多箇中介軟體結合使用的;
那請求管道構造完成了,那請求是如何到管道中呢?
應該都知道,Asp.NetCore內建了IServer(如Kestrel),負責監聽對應的請求,當請求過來時,會將請求給IHttpApplication
namespace Microsoft.AspNetCore.Hosting.Server
{
public interface IHttpApplication<TContext>
{
// 執行上下文建立
TContext CreateContext(IFeatureCollection contextFeatures);
// 執行上下文釋放
void DisposeContext(TContext context, Exception exception);
// 處理請求,這裡就使用了請求管道處理
Task ProcessRequestAsync(TContext context);
}
}
而對於IHttpApplication
namespace Microsoft.AspNetCore.Hosting
{
internal class HostingApplication : IHttpApplication<HostingApplication.Context>
{
// 構建出來的請求管道
private readonly RequestDelegate _application;
// 用於建立請求上下文的
private readonly IHttpContextFactory _httpContextFactory;
private readonly DefaultHttpContextFactory _defaultHttpContextFactory;
private HostingApplicationDiagnostics _diagnostics;
// 建構函式初始化變數
public HostingApplication(
RequestDelegate application,
ILogger logger,
DiagnosticListener diagnosticSource,
IHttpContextFactory httpContextFactory)
{
_application = application;
_diagnostics = new HostingApplicationDiagnostics(logger, diagnosticSource);
if (httpContextFactory is DefaultHttpContextFactory factory)
{
_defaultHttpContextFactory = factory;
}
else
{
_httpContextFactory = httpContextFactory;
}
}
// 建立對應的請求的上下文
public Context CreateContext(IFeatureCollection contextFeatures)
{
Context hostContext;
if (contextFeatures is IHostContextContainer<Context> container)
{
hostContext = container.HostContext;
if (hostContext is null)
{
hostContext = new Context();
container.HostContext = hostContext;
}
}
else
{
// Server doesn't support pooling, so create a new Context
hostContext = new Context();
}
HttpContext httpContext;
if (_defaultHttpContextFactory != null)
{
var defaultHttpContext = (DefaultHttpContext)hostContext.HttpContext;
if (defaultHttpContext is null)
{
httpContext = _defaultHttpContextFactory.Create(contextFeatures);
hostContext.HttpContext = httpContext;
}
else
{
_defaultHttpContextFactory.Initialize(defaultHttpContext, contextFeatures);
httpContext = defaultHttpContext;
}
}
else
{
httpContext = _httpContextFactory.Create(contextFeatures);
hostContext.HttpContext = httpContext;
}
_diagnostics.BeginRequest(httpContext, hostContext);
return hostContext;
}
// 將建立出來的請求上下文交給請求管道處理
public Task ProcessRequestAsync(Context context)
{
// 請求管道處理
return _application(context.HttpContext);
}
// 以下刪除了一些程式碼,具體可下面檢視....
}
}
這裡關於Server監聽到請求及將請求交給中間處理的具體過程沒有具體描述,可以結合啟動流程和以上內容在細扒一下流程吧(大傢俬下搞吧),這裡就簡單說說中介軟體及請求管道構建的過程;(後續有時間將整體流程走一遍);
總結
這節又是純程式碼來“忽悠”小夥伴了,對於理論概念可能表達的不夠清楚,歡迎交流溝通;其實這裡只是根據流程走了一遍原始碼,並沒有一行行解讀,所以小夥伴看此篇文章程式碼部分的時候,以除錯的思路去看,從註冊中介軟體那塊開始,到最後請求交給請求管道處理,注重這個流程即可;
下一節說說中介軟體的具體應用;
------------------------------------------------
一個被程式搞醜的帥小夥,關注"Code綜藝圈",識別關注跟我一起學~~~