1. 程式人生 > >ASP.NET Core 執行原理解剖[4]:進入HttpContext的世界

ASP.NET Core 執行原理解剖[4]:進入HttpContext的世界

原文: ASP.NET Core 執行原理解剖[4]:進入HttpContext的世界

HttpContext是ASP.NET中的核心物件,每一個請求都會建立一個對應的HttpContext物件,我們的應用程式便是通過HttpContext物件來獲取請求資訊,最終生成響應,寫回到HttpContext中,完成一次請求處理。在前面幾章中也都有提到HttpContext,本章就來一起探索一下HttpContext的世界,揭開它的神祕面紗。

目錄

本系列文章從原始碼分析的角度來探索 ASP.NET Core 的執行原理,分為以下幾個章節:

ASP.NET Core 執行原理解剖[1]:Hosting

ASP.NET Core 執行原理解剖[2]:Hosting補充之配置介紹

ASP.NET Core 執行原理解剖[3]:Middleware-請求管道的構成

ASP.NET Core 執行原理解剖[4]:進入HttpContext的世界(Current)

  1. IHttpContextFactory
  2. IFeatureCollection
  3. HttpContext
  4. IHttpContextAccessor

ASP.NET Core 執行原理解剖[5]:Authentication

IHttpContextFactory

第一章中,我們介紹到,WebHost 在啟動 IServer 時,會傳入一個 IHttpApplication<TContext> 型別的物件,Server 負責對請求的監聽,在接收到請求時,會呼叫該物件的 ProcessRequestAsync 方法將請求轉交給我們的應用程式。IHttpApplication<TContext> 的預設實現為 HostingApplication ,有如下定義:

public class HostingApplication : IHttpApplication<HostingApplication.Context>
{
    private readonly RequestDelegate _application;
    private readonly IHttpContextFactory _httpContextFactory;

    public Context CreateContext(IFeatureCollection contextFeatures)
    {
        var context = new Context();
        var httpContext = _httpContextFactory.Create(contextFeatures);

        _diagnostics.BeginRequest(httpContext, ref context);

        context.HttpContext = httpContext;
        return context;
    }

    public Task ProcessRequestAsync(Context context)
    {
        return _application(context.HttpContext);
    }

    public void DisposeContext(Context context, Exception exception)
    {
        var httpContext = context.HttpContext;
        _diagnostics.RequestEnd(httpContext, exception, context);
        _httpContextFactory.Dispose(httpContext);
        _diagnostics.ContextDisposed(context);
    }
}

首先使用 IHttpContextFactory 來建立 HttpContext 例項,然後在 ProcessRequestAsync 方法中呼叫上一章介紹的 RequestDelegate,由此進入到我們的應用程式當中。

IHttpContextFactory 負責對 HttpContext 的建立和釋放,分別對應著CreateDispose方法,它的預設實現類為HttpContextFactory,定義如下:

public class HttpContextFactory : IHttpContextFactory
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly FormOptions _formOptions;

    public HttpContext Create(IFeatureCollection featureCollection)
    {
        var httpContext = new DefaultHttpContext(featureCollection);
        if (_httpContextAccessor != null)
        {
            _httpContextAccessor.HttpContext = httpContext;
        }

        var formFeature = new FormFeature(httpContext.Request, _formOptions);
        featureCollection.Set<IFormFeature>(formFeature);

        return httpContext;
    }

    public void Dispose(HttpContext httpContext)
    {
        if (_httpContextAccessor != null)
        {
            _httpContextAccessor.HttpContext = null;
        }
    }
}

如上,HttpContextFactory 只是簡單的使用 new DefaultHttpContext(featureCollection) 來建立 HttpContext 的例項,而這裡涉及到一個 IFeatureCollection 物件,它是由 Server 根據原始請求建立而來的,下面就先介紹一下該物件。

IFeatureCollection

不過,在介紹 IFeatureCollection 之前,我們先需先回顧一下OWIN:

OWIN是 “Open Web Server Interface for .NET” 的首字母縮寫,它定義了一套Web Server和Web Application之間的標準介面,主要用於解除 ASP.NET 與 IIS 的緊密耦合。為此,OWIN 定義了四個核心元件:Host, Server, Middleware, Application,併為Server和Middleware的之間的互動提供了一個 Func<IDictionary<string,object>,Task> 型別的標準介面。

每一個OWIN中介軟體,都會接收到一個 IDictionary<string,object> 型別的變數,用來表示當前請求的相關資訊,也稱為環境字典。每一個支援OWIN標準的 Web Server 都會根據請求的原始上下文資訊,封裝成這個環境字典,然後在OWIN中介軟體之間傳遞,進而完成整個請求的處理。環境字典定義了一系列預先約定好的Key,比如:用 "owin.RequestBody" 來表示請求體,"owin.RequestHeaders" 來表示請求頭,"owin.RequestMethod" 來表示請求方法等。

OWIN是隨著ASP.NET MVC5進行到我們的視線中,在當時,ASP.NET WebAPI 2.0 也基於OWIN實現了自寄宿模式。再後來,提出了 ASP.NET 5 與 MVC6,完全是基於OWIN的模式來開發的,再到今天的 ASP.NET Core,OWIN的概念已被模糊化了,但是還是隨處可以見到OWIN的影子,並且也提供了對 OWIN 的擴充套件支援。

在 ASP.NET Core 中,提出了 IFeatureCollection 的概念,它本質上也是一個 IDictionary<string,object> 鍵值對,但是它具有面向物件的特點,相對於 IDictionary<string,object> 更加清晰,容易理解,並且Server構建成這樣一個物件也很容易,它有如下定義:

public interface IFeatureCollection : IEnumerable<KeyValuePair<Type, object>>
{
    bool IsReadOnly { get; }

    int Revision { get; }

    object this[Type key] { get; set; }

    TFeature Get<TFeature>();

    void Set<TFeature>(TFeature instance);
}

它的定義非常簡單,由一系列以鍵值對來表示的標準特性物件(TFeature)組成,可以通過一個索引以及 GetSet 方法來獲取或設定這些特性物件。

下面,我們看一下在 ASP.NET Core 中的對它的一個模擬實現:

public class FeatureCollection : IFeatureCollection
{
    private IDictionary<Type, object> _features;
    private readonly IFeatureCollection _defaults;
    private volatile int _containerRevision;

    public virtual int Revision
    {
        get { return _containerRevision + (_defaults?.Revision ?? 0); }
    }

    public object this[Type key]
    {
        get
        {
            object result;
            return _features != null && _features.TryGetValue(key, out result) ? result : _defaults?[key];
        }
        set
        {
            if (value == null)
            {
                if (_features != null && _features.Remove(key))
                {
                    _containerRevision++;
                }
                return;
            }

            if (_features == null)
            {
                _features = new Dictionary<Type, object>();
            }
            _features[key] = value;
            _containerRevision++;
        }
    }

    public TFeature Get<TFeature>()
    {
        return (TFeature)this[typeof(TFeature)];
    }

    public void Set<TFeature>(TFeature instance)
    {
        this[typeof(TFeature)] = instance;
    }    
}

如上,它的內部屬性 _features 便是OWIN中的標準環境字典,並且提供了更加方便的泛型 Get, Set 方法,以及一個索引器來訪問該環境字典。不過,如果只是這樣,那使用起來依然不夠方便,更為重要的是 ASP.NET Core 還提供了一系列的特性物件,並以這些特性物件的型別做為環境字典中的Key。

通過上面程式碼,還可以發現,每次對該環境字典的修改,都會使 Revision 屬性遞增1。

這裡為什麼說FeatureCollection是一個模擬的實現呢?具我觀察,FeatureCollection物件只在ASP.NET Core的測試程式碼中用到,而每個Server都有它自己的方式來構建IFeatureCollection,並不會使用FeatureCollection,關於Server中是如何建立IFeatureCollection例項的,可以參考KestrelHttpServer中的實現,這裡就不再深究。

那特性物件又是什麼呢?我們先看一下請求特性的定義:

public interface IHttpRequestFeature
{
    string Protocol { get; set; }
    string Scheme { get; set; }
    string Method { get; set; }
    string PathBase { get; set; }
    string Path { get; set; }
    string QueryString { get; set; }
    string RawTarget { get; set; }
    IHeaderDictionary Headers { get; set; }
    Stream Body { get; set; }
}

再看一下表單特性的定義:

public interface IFormFeature
{
    bool HasFormContentType { get; }
    IFormCollection Form { get; set; }
    IFormCollection ReadForm();
    Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken);
}

可以看到,這些特性物件與我們熟悉的 HttpContext 中的屬性非常相似,這也就大大簡化了在 IHttpRequestFeatureHttpContext 之間的轉換。我們可以通過這些特性介面定義的屬性來獲取到原始上下文中描述的資訊,並通過特性物件提供的方法來操作原始上下文,它就像Web Server與我們的應用程式之間的橋樑,完成抽象和具體之間的轉換。

ASP.NET Core 提供了一系列豐富的特性物件,如 Session, Cookies, Query, Form, WebSocket, Request, Response 等等, 更詳細的列表可以檢視 Microsoft.AspNetCore.Http.Features

HttpContext

HttpContext 物件我們應該都很熟悉了,它用來表示一個抽象的HTTP上下文,而HttpContext物件的核心又體現在用於描述請求的Request和描述響應的Response屬性上。除此之外,它還包含一些與當前請求相關的其他上下文資訊,如描述當前HTTP連線的ConnectionInfo物件,控制WebSocket的WebSocketManager,代表當前使用者的ClaimsPrincipal物件的Session,等等:

public abstract class HttpContext
{
    public abstract IFeatureCollection Features { get; }
    public abstract HttpRequest Request { get; }
    public abstract HttpResponse Response { get; }
    public abstract ConnectionInfo Connection { get; }
    public abstract WebSocketManager WebSockets { get; }
    public abstract ClaimsPrincipal User { get; set; }
    public abstract IDictionary<object, object> Items { get; set; }
    public abstract IServiceProvider RequestServices { get; set; }
    public abstract CancellationToken RequestAborted { get; set; }
    public abstract string TraceIdentifier { get; set; }
    public abstract ISession Session { get; set; }
    public abstract void Abort();
}

在我們處理請求時,如果希望終止該請求,可以通過 RequestAborted 屬性給請求管道傳送一個終止資訊。當需要對整個管道共享一些與當前上下文相關的資料,可以將它儲存在 Items 字典中。而在 ASP.NET Coer 1.x 中還包含一個管理認證的AuthenticationManager物件,但是在 2.0 中,將它移到了 AuthenticationHttpContextExtensions 中,因為使用者認證本來就一個相對複雜且獨立的模組,把它獨立出去會更加符合 ASP.NET Core 的簡潔模組化特性。

在上文中,我們瞭解到 HttpContext 的預設實現使用的是 DefaultHttpContext 型別 ,而 DefaultHttpContext 便是對上面介紹的 IFeatureCollection 物件的封裝:

public class DefaultHttpContext : HttpContext
{
    private FeatureReferences<FeatureInterfaces> _features;

    private HttpRequest _request;
    private HttpResponse _response;

    public DefaultHttpContext(IFeatureCollection features)
    {
        Initialize(features);
    }

    public virtual void Initialize(IFeatureCollection features)
    {
        _features = new FeatureReferences<FeatureInterfaces>(features);
        _request = InitializeHttpRequest();
        _response = InitializeHttpResponse();
    }

    protected virtual HttpRequest InitializeHttpRequest() => new DefaultHttpRequest(this);
}

如上,DefaultHttpContext通過 Initialize 來完成從 IFeatureCollection 到 HttpContext 的轉換,而各個屬性的轉換又交給了它們自己。

HttpRequest

HttpRequest 可以用來獲取到描述當前請求的各種相關資訊,比如請求的協議(HTTP或者HTTPS)、HTTP方法、地址,以及該請求的請求頭,請求體等:

public abstract class HttpRequest
{
    public abstract HttpContext HttpContext { get; }
    public abstract string Method { get; set; }
    public abstract string Scheme { get; set; }
    public abstract bool IsHttps { get; set; }
    public abstract HostString Host { get; set; }
    public abstract PathString PathBase { get; set; }
    public abstract PathString Path { get; set; }
    public abstract QueryString QueryString { get; set; }
    public abstract IQueryCollection Query { get; set; }
    public abstract string Protocol { get; set; }
    public abstract IHeaderDictionary Headers { get; }
    public abstract IRequestCookieCollection Cookies { get; set; }
    public abstract long? ContentLength { get; set; }
    public abstract string ContentType { get; set; }
    public abstract Stream Body { get; set; }
    public abstract bool HasFormContentType { get; }
    public abstract IFormCollection Form { get; set; }
    public abstract Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken = new CancellationToken());
}

HttpRequest是一個抽象類,它的預設實現是DefaultHttpRequest:

public class DefaultHttpRequest : HttpRequest
{
    private readonly static Func<IFeatureCollection, IHttpRequestFeature> _nullRequestFeature = f => null;
    private FeatureReferences<FeatureInterfaces> _features;
    
    public DefaultHttpRequest(HttpContext context)
    {
        Initialize(context);
    }

    public virtual void Initialize(HttpContext context)
    {
        _context = context;
        _features = new FeatureReferences<FeatureInterfaces>(context.Features);
    }

    private IHttpRequestFeature HttpRequestFeature => _features.Fetch(ref _features.Cache.Request, _nullRequestFeature);

    public override string Method
    {
        get { return HttpRequestFeature.Method; }
        set { HttpRequestFeature.Method = value; }
    }

    public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken)
    {
        return FormFeature.ReadFormAsync(cancellationToken);
    }
}

在 DefaultHttpRequest 中,並沒有額外的功能,它只是簡單的與 IHttpRequestFeature 中的同名屬性和方法做了一個對映,而 IHttpRequestFeature 物件的獲取又涉及到一個 FeatureReferences<FeatureInterfaces> 型別, 從字面意思來說,就是對Feature物件的一個引用,用來儲存對應的Feature例項,並在上文介紹的 Revision 屬性發生變化時,清空Feature例項的快取:

public struct FeatureReferences<TCache>
{
    public IFeatureCollection Collection { get; private set; }
    public int Revision { get; private set; }

    public TCache Cache;

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public TFeature Fetch<TFeature, TState>(ref TFeature cached, TState state, Func<TState, TFeature> factory) where TFeature : class
    {
        var flush = false;
        var revision = Collection.Revision;
        if (Revision != revision)
        {
            cached = null;
            flush = true;
        }
        return cached ?? UpdateCached(ref cached, state, factory, revision, flush);
    }

    private TFeature UpdateCached<TFeature, TState>(ref TFeature cached, TState state, Func<TState, TFeature> factory, int revision, bool flush) where TFeature : class
    {
        if (flush)
        {
            Cache = default(TCache);
        }
        cached = Collection.Get<TFeature>();
        if (cached == null)
        {
            cached = factory(state);
            Collection.Set(cached);
            Revision = Collection.Revision;
        }
        else if (flush)
        {
            Revision = revision;
        }
        return cached;
    }

    public TFeature Fetch<TFeature>(ref TFeature cached, Func<IFeatureCollection, TFeature> factory)
        where TFeature : class => Fetch(ref cached, Collection, factory);
}

如上,當 Revision 生成變化時,會將 Cache 設定為 null , 然後重新從 IFeatureCollection 中獲取,最後更新 Revision 為最新版本,相當於一個快取工廠。

Fetch方法使用了[MethodImpl(MethodImplOptions.AggressiveInlining)]特性,表示該方法會盡可能的使用內聯方式來執行。而內聯是一種很重要的優化方式, 它允許編譯器在方法呼叫開銷比方法本身更大的情況下消除對方法呼叫的開銷,即直接將該方法體嵌入到呼叫者中。

HttpResponse

在瞭解了表示請求的抽象類 HttpRequest 之後,我們再來認識一下與它對應的,用來描述響應的 HttpResponse 型別:

public abstract class HttpResponse
{
    private static readonly Func<object, Task> _callbackDelegate = callback => ((Func<Task>)callback)();
    private static readonly Func<object, Task> _disposeDelegate = disposable =>
    {
        ((IDisposable)disposable).Dispose();
        return Task.CompletedTask;
    };

    public abstract HttpContext HttpContext { get; }
    public abstract int StatusCode { get; set; }
    public abstract IHeaderDictionary Headers { get; }
    public abstract Stream Body { get; set; }
    public abstract long? ContentLength { get; set; }
    public abstract string ContentType { get; set; }
    public abstract IResponseCookies Cookies { get; }
    public abstract bool HasStarted { get; }
    public abstract void OnStarting(Func<object, Task> callback, object state);
    public virtual void OnStarting(Func<Task> callback) => OnStarting(_callbackDelegate, callback);
    public abstract void OnCompleted(Func<object, Task> callback, object state);
    public virtual void RegisterForDispose(IDisposable disposable) => OnCompleted(_disposeDelegate, disposable);
    public virtual void OnCompleted(Func<Task> callback) => OnCompleted(_callbackDelegate, callback);
    public virtual void Redirect(string location) => Redirect(location, permanent: false);
    public abstract void Redirect(string location, bool permanent);
}

HttpResponse也是一個抽象類,我們使用它來輸出對請求的響應,如設定HTTP狀態碼,Cookies,HTTP響應報文頭,響應主體等,以及提供了一些將響應傳送到客戶端時的相關事件。

HasStarted 屬性用來表示響應是否已開始發往客戶端,在我們第一次呼叫 response.Body.WriteAsync 方法時,該屬性便會被設定為 True。需要注意的是,一旦 HasStarted 設定為 true 後,便不能再修改響應頭,否則將會丟擲 InvalidOperationException 異常,也建議我們在HasStarted設定為true後,不要再對 Response 進行寫入,因為此時 content-length 的值已經確定,繼續寫入可能會造成協議衝突。

HttpResponse 的預設實現為 DefaultHttpResponse ,它與 DefaultHttpRequest 類似,只是對 IHttpResponseFeature 的封裝,不過 ASP.NET Core 也為我們提供了一些擴充套件方法,如:我們在寫入響應時,通常使用的是 Response 的擴充套件方法 WriteAsync

public static class HttpResponseWritingExtensions
{
    public static Task WriteAsync(this HttpResponse response, string text, CancellationToken cancellationToken = default(CancellationToken))
    {
        return response.WriteAsync(text, Encoding.UTF8, cancellationToken);
    }

    public static Task WriteAsync(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken))
    {
        byte[] data = encoding.GetBytes(text);
        return response.Body.WriteAsync(data, 0, data.Length, cancellationToken);
    }
}

ASP.NET Core 還為 Response 提供了用來一個清空響應頭和響應體的擴充套件方法:

public static class ResponseExtensions
{
    public static void Clear(this HttpResponse response)
    {
        if (response.HasStarted)
        {
            throw new InvalidOperationException("The response cannot be cleared, it has already started sending.");
        }
        response.StatusCode = 200;
        response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = null;
        response.Headers.Clear();
        if (response.Body.CanSeek)
        {
            response.Body.SetLength(0);
        }
    }
}

還有比較常用的傳送檔案的擴充套件方法:SendFileAsync ,獲取響應頭的擴充套件方法:GetTypedHeaders 等等,就不再細說。

IHttpContextAccessor

在 ASP.NET 4.x 我們經常會通過 HttpContext.Current 來獲取當前請求的 HttpContext 物件,而在 ASP.NET Core 中,HttpContext 不再有 Current 屬性,並且在 ASP.NET Core 中一切皆注入,更加推薦使用注入的方式來獲取例項,而非使用靜態變數。因此,ASP.NET Core 提供了一個 IHttpContextAccessor 介面,用來統一獲取當前請求的 HttpContext 例項的方式:

public interface IHttpContextAccessor
{
    HttpContext HttpContext { get; set; }
}

它的定義非常簡單,就只有一個 HttpContext 屬性,它在ASP.NET Core 中還有一個內建的實現類:HttpContextAccessor

public class HttpContextAccessor : IHttpContextAccessor
{
    private static AsyncLocal<HttpContext> _httpContextCurrent = new AsyncLocal<HttpContext>();

    public HttpContext HttpContext
    {
        get
        {
            return _httpContextCurrent.Value;
        }
        set
        {
            _httpContextCurrent.Value = value;
        }
    }
}

這裡使用了一個 AsyncLocal<T> 型別來儲存 HttpContext 物件,可能很多人對 AsyncLocal 不太瞭解,這裡就來介紹一下:

在.NET 4.5 中引用了 async await 等關鍵字,使我們可以像編寫同步方法一樣方便的來執行非同步操作,因此我們的大部分程式碼都會使用非同步。以往我們所使用的 ThreadLocal 在同步方法中沒有問題,但是在 await 後有可能會建立新實的例(await 之後可能還交給之前的執行緒執行,也有可能是一個新的執行緒來執行),而不再適合用來儲存執行緒內的唯一例項,因此在 .NET 4.6 中引用了 AsyncLocal<T> 型別,它類似於 ThreadLocal,但是在 await 之後就算切換執行緒也仍然可以保持同一例項。我們知道在 ASP.NET 4.x 中,HttpContext的 Current 例項是通過 CallContext 物件來儲存的,但是 ASP.NET Core 中不再支援CallContext,故使用 AsyncLocal<T> 來保證執行緒內的唯一例項。

不過,ASP.NET Core 預設並沒有注入 IHttpContextAccessor 物件,如果我們想在應用程式中使用它,則需要手動來註冊:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}

在上面介紹的HttpContextFactory類的建構函式中會注入IHttpContextAccessor例項,併為其HttpContext屬性賦值,並在Dispose方法中將其設定為null。

總結

在ASP.NET 4.x 中,我們就對 HttpContext 非常熟悉了,而在 ASP.NET Core 中,它的變化並不大,只是做了一些簡化,因此本文較為簡單,主要描述了一下 HttpContext 是如何建立的,以及它的構成,最後則介紹了一下在每個請求中獲取 HttpContext 唯一例項的方式,而在 ASP.NET Core 2.0 中 HttpContext 的 AuthenticationManager 物件已標記為過時,添加了一些擴充套件方法來實現AuthenticationManager中的功能,下一章就來介紹一下 ASP.NET Core 中的認證系統。