.NET 雲原生架構師訓練營(KestrelServer原始碼分析)--學習筆記

MingsonZheng發表於2022-01-17

目錄

  • 目標
  • 原始碼

目標

理解 KestrelServer 如何接收網路請求,網路請求如何轉換成 http request context(C# 可識別)

原始碼

https://github.com/dotnet/aspnetcore/

在目錄 aspnetcore\src\Servers\Kestrel\Core\src\Internal 下有一個 KestrelServerImpl

internal class KestrelServerImpl : IServer

在 host 啟動的時候呼叫了 server 的 startup 方法,可以從這個入口開始

public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull

StartAsync 方法主要分為以下三步

async Task OnBind(ListenOptions options, CancellationToken onBindCancellationToken)
{
    ...
}

AddressBindContext = new AddressBindContext(_serverAddresses, Options, Trace, OnBind);

await BindAsync(cancellationToken).ConfigureAwait(false);

BindAsync 方法中使用 AddressBindContext 進行繫結

await AddressBinder.BindAsync(Options.ListenOptions, AddressBindContext!, cancellationToken).ConfigureAwait(false);

在 AddressBinder 的 BindAsync 方法中建立了多種策略進行繫結

var strategy = CreateStrategy(
    listenOptions.ToArray(),
    context.Addresses.ToArray(),
    context.ServerAddressesFeature.PreferHostingUrls);

...

await strategy.BindAsync(context, cancellationToken).ConfigureAwait(false);

例如 AddressesStrategy,它有自己的一個繫結方法

private class AddressesStrategy : IStrategy
{
    protected readonly IReadOnlyCollection<string> _addresses;

    public AddressesStrategy(IReadOnlyCollection<string> addresses)
    {
        _addresses = addresses;
    }

    public virtual async Task BindAsync(AddressBindContext context, CancellationToken cancellationToken)
    {
        foreach (var address in _addresses)
        {
            var options = ParseAddress(address, out var https);
            context.ServerOptions.ApplyEndpointDefaults(options);

            if (https && !options.IsTls)
            {
                options.UseHttps();
            }

            await options.BindAsync(context, cancellationToken).ConfigureAwait(false);
        }
    }
}

options 來自 IConnectionBuilder 的 ListenOptions 的繫結

public class ListenOptions : IConnectionBuilder, IMultiplexedConnectionBuilder

這一路走下來發現找不到重點,所以需要換一個方向從 OnBind 方法入手,它是一個委託,需要找到呼叫的地方

async Task OnBind(ListenOptions options, CancellationToken onBindCancellationToken)

可以看到 OnBind 方法傳入到 AddressBindContext 中

AddressBindContext = new AddressBindContext(_serverAddresses, Options, Trace, OnBind);

在 AddressBindContext 中它是一個 CreateBinding

public Func<ListenOptions, CancellationToken, Task> CreateBinding { get; }

全域性搜尋 CreateBinding

可以找到在 AddressBinder 的 BindEndpointAsync 方法中被呼叫

internal static async Task BindEndpointAsync(ListenOptions endpoint, AddressBindContext context, CancellationToken cancellationToken)
{
    try
    {
        await context.CreateBinding(endpoint, cancellationToken).ConfigureAwait(false);
    }
    catch (AddressInUseException ex)
    {
        throw new IOException(CoreStrings.FormatEndpointAlreadyInUse(endpoint), ex);
    }

    context.ServerOptions.OptionsInUse.Add(endpoint);
}

而 BindEndpointAsync 方法被 ListenOptions 的 BindAsync 方法呼叫,也就是上面提到的 StartAsync 的第三步 BindAsync 走到的 ListenOptions

internal virtual async Task BindAsync(AddressBindContext context, CancellationToken cancellationToken)
{
    await AddressBinder.BindEndpointAsync(this, context, cancellationToken).ConfigureAwait(false);
    context.Addresses.Add(GetDisplayName());
}

在第三步 BindAsync 方法中載入配置,載入之後才呼叫真正的繫結方法

Options.ConfigurationLoader?.Load();

await AddressBinder.BindAsync(Options.ListenOptions, AddressBindContext!, cancellationToken).ConfigureAwait(false);

所以整個過程的重點在第一步的 OnBind,而 OnBind 的重點在於 TransportManager 的 BindAsync 方法

options.EndPoint = await _transportManager.BindAsync(options.EndPoint, multiplexedConnectionDelegate, options, onBindCancellationToken).ConfigureAwait(false);

進入 TransportManager 中可以看到在 BindAsync 方法中開始接收

StartAcceptLoop(new GenericConnectionListener(transport), c => connectionDelegate(c), endpointConfig);

在 StartAcceptLoop 方法中呼叫了 StartAcceptingConnections 方法

var acceptLoopTask = connectionDispatcher.StartAcceptingConnections(connectionListener);

在 StartAcceptingConnections 方法中將需要被執行的方法新增到佇列中

ThreadPool.UnsafeQueueUserWorkItem(StartAcceptingConnectionsCore, listener, preferLocal: false);

在 StartAcceptingConnectionsCore 裡面開始監聽接收,這就是關鍵,這裡執行了 kestrelConnection,而 kestrelConnection 又包含 _connectionDelegate

var connection = await listener.AcceptAsync();

var kestrelConnection = new KestrelConnection<T>(
    id, _serviceContext, _transportConnectionManager, _connectionDelegate, connection, Log);

_transportConnectionManager.AddConnection(id, kestrelConnection);

ThreadPool.UnsafeQueueUserWorkItem(kestrelConnection, preferLocal: false);

在 kestrelConnection 中可以看到整個 ExecuteAsync 方法裡面只執行了 _connectionDelegate

await _connectionDelegate(connectionContext);

意識到 _connectionDelegate 的重要性之後再往回找是怎麼傳進來的,可以找到是在 KestrelServerImpl 中通過 ListenOptions 構建出來的

var connectionDelegate = options.Build();

在 Build 方法裡面可以看到它是一個管道

ConnectionDelegate app = context =>
{
    return Task.CompletedTask;
};

for (var i = _middleware.Count - 1; i >= 0; i--)
{
    var component = _middleware[i];
    app = component(app);
}

return app;

通過 _middleware 的 Use 方法的引用找不到有價值的資訊

public IConnectionBuilder Use(Func<ConnectionDelegate, ConnectionDelegate> middleware)
{
    _middleware.Add(middleware);
    return this;
}

於是回到 KestrelServerImpl 中,檢視 UseHttpServer 方法

options.UseHttpServer(ServiceContext, application, options.Protocols, addAltSvcHeader);

可以看到這個方法構建了一個 HttpConnectionMiddleware

var middleware = new HttpConnectionMiddleware<TContext>(serviceContext, application, protocols, addAltSvcHeader);
return builder.Use(next =>
{
    return middleware.OnConnectionAsync;
});

進入 HttpConnectionMiddleware 可以看到一個核心方法 OnConnectionAsync,建立了一個 HttpConnection,然後呼叫 ProcessRequestsAsync

var connection = new HttpConnection(httpConnectionContext);

return connection.ProcessRequestsAsync(_application);

在 ProcessRequestsAsync 方法中可以看到 KestrelServer 的核心邏輯,根據不同的協議,執行不同的邏輯;同時可以看到它是如何處理請求的,通過 requestProcessor 處理請求

switch (SelectProtocol())
{
    case HttpProtocols.Http1:
        requestProcessor = _http1Connection = new Http1Connection<TContext>((HttpConnectionContext)_context);
        _protocolSelectionState = ProtocolSelectionState.Selected;
        break;
    case HttpProtocols.Http2:
        requestProcessor = new Http2Connection((HttpConnectionContext)_context);
        _protocolSelectionState = ProtocolSelectionState.Selected;
        break;
    case HttpProtocols.Http3:
        requestProcessor = new Http3Connection((HttpMultiplexedConnectionContext)_context);
        _protocolSelectionState = ProtocolSelectionState.Selected;
        break;
}

await requestProcessor.ProcessRequestsAsync(httpApplication);

requestProcessor 是一個 IRequestProcessor 介面,它有多個實現,以 Http2Connection 為例

internal partial class Http2Connection : IHttp2StreamLifetimeHandler, IHttpStreamHeadersHandler, IRequestProcessor

在 Http2Connection 的 ProcessRequestsAsync 方法中讀取流,解析轉換,處理

await _frameWriter.WriteWindowUpdateAsync(0, diff);

var result = await Input.ReadAsync();

await ProcessFrameAsync(application, framePayload);

當 HttpConnectionMiddleware 的 OnConnectionAsync 處理完之後,如何與應用層程式碼拼接,這裡只是 Kestrel 的處理

可以通過 IRequestProcessor 介面的 ProcessRequestsAsync 方法的實現找到 HttpProtocol 的 ProcessRequestsAsync 方法,可以看到它執行了一個 ProcessRequests 方法

await ProcessRequests(application);

在 ProcessRequests 方法中將從 Body 裡面獲取的內容封裝到一個 context,這個才是真正的 HttpContext,然後再執行應用層程式碼,之前都是 Kestrel 的解析邏輯,這裡才是串聯到我們構建的管道 application

InitializeBodyControl(messageBody);

var context = application.CreateContext(this);

// Run the application code for this request
await application.ProcessRequestAsync(context);

接下來看一下 application 是如何傳過來的,一直找到 HttpConnection,HttpConnectionMiddleware,HttpConnectionBuilderExtensions,KestrelServerImpl

public async Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull

可以看到是 Host 呼叫 Server 的 StartAsync 傳進來的,這裡體現了職責分離的原則,對於應用層的管道,定義了一個 IHttpApplication application,這就是 requestDelegate

從 Host 傳到 Server,Server 完成了網路埠的繫結,網路的監聽接收,網路二進位制轉換成具體的 c# 可識別的 HTTPContext 之後,呼叫了 Host 那邊封裝好的一個 application 應用層的管道,這是 Host 在 Startup 裡面定義的,這就是一個完整的過程

文件:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/?view=aspnetcore-6.0&tabs=windows#kestrel

課程連結

https://appsqsyiqlk5791.h5.xiaoeknow.com/v1/course/video/v_5f39bdb8e4b01187873136cf?type=2

知識共享許可協議

本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。

歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

如有任何疑問,請與我聯絡 (MingsonZheng@outlook.com) 。

相關文章