KestrelServer詳解[3]: 自定義一個迷你版的KestrelServer

和所有的服務器一樣,KestrelServer最終需要解決的是網絡傳輸的問題。在《網絡連接的創建》,我們介紹了KestrelServer如何利用連接接聽器的建立網絡連接,並再次基礎上演示瞭如何直接利用建立的連接接收請求和回覆響應。本篇更進一步,我們根據其總體設計,定義了迷你版的KestrelServer讓讀者看看這個重要的服務器大體是如何實現的。本文提供的示例演示已經同步到《ASP.NET Core 6框架揭祕-實例演示版》)

一、ConnectionDelegate
二、IConnectionBuilder
三、HTTP 1.x/HTTP 2.x V.S. HTTP 3
四、MiniKestrelServer

一、ConnectionDelegate

ASP.NET CORE在“應用”層將針對請求的處理抽象成由中間件構建的管道,實際上KestrelServer面向“傳輸”層的連接也採用了這樣的設計。當代表連接的ConnectionContext上下文創建出來之後,後續的處理將交給由連接中間件構建的管道進行處理。我們可以根據需要註冊任意的中間件來處理連接,比如可以將併發連結的控制實現在專門的連接中間件中。ASP.NET CORE管道利用RequestDelegate委託來表示請求處理器,連接管道同樣定義瞭如下這個ConnectionDelegate委託。

public delegate Task ConnectionDelegate(ConnectionContext connection);

二、IConnectionBuilder

ASP.NET CORE管道中的中間件體現爲一個Func<RequestDelegate, RequestDelegate>委託,連接管道的中間件同樣可以利用Func<ConnectionDelegate, ConnectionDelegate>委託來表示。ASP.NET CORE管道中的中間件註冊到IApplicationBuilder對象上並利用它將管道構建出來。連接管道依然具有如下這個IConnectionBuilder接口,ConnectionBuilder實現了該接口。

public interface IConnectionBuilder
{
    IServiceProvider ApplicationServices { get; }
    IConnectionBuilder Use(Func<ConnectionDelegate, ConnectionDelegate> middleware);
    ConnectionDelegate Build();
}

public class ConnectionBuilder : IConnectionBuilder
{
    public IServiceProvider ApplicationServices { get; }
    public ConnectionDelegate Build();
    public IConnectionBuilder Use(Func<ConnectionDelegate, ConnectionDelegate> middleware);
}

IConnectionBuilder接口還定義瞭如下三個擴展方法來註冊連接中間件。第一個Use方法使用Func<ConnectionContext, Func<Task>, Task>委託來表示中間件。其餘兩個方法用來註冊管道末端的中間件,這樣的中間件本質上就是一個ConnectionDelegate委託,我們可以將其定義成一個派生於ConnectionHandler的類型。

public static class ConnectionBuilderExtensions
{
    public static IConnectionBuilder Use(this IConnectionBuilder connectionBuilder,Func<ConnectionContext, Func<Task>, Task> middleware);
    public static IConnectionBuilder Run(this IConnectionBuilder connectionBuilder,Func<ConnectionContext, Task> middleware);
    public static IConnectionBuilder UseConnectionHandler<TConnectionHandler>(this IConnectionBuilder connectionBuilder) where TConnectionHandler : ConnectionHandler;
}

public abstract class ConnectionHandler
{
    public abstract Task OnConnectedAsync(ConnectionContext connection);
}

三、HTTP 1.x/HTTP 2.x V.S. HTTP 3

KestrelServer針對HTTP 1.X/2和HTTP 3的設計和實現基本上獨立的,這一點從監聽器的定義就可以看出來。就連接管道來說,基於HTTP 3的多路複用連接通過MultiplexedConnectionContext表示,它也具有“配套”的MultiplexedConnectionDelegate委託和IMultiplexedConnectionBuilder接口。ListenOptions類型同時實現了IConnectionBuilder和IMultiplexedConnectionBuilder接口,意味着我們在註冊終結點的時候還可以註冊任意中間件。

public delegate Task MultiplexedConnectionDelegate(MultiplexedConnectionContext connection);

public interface IMultiplexedConnectionBuilder
{
    IServiceProvider ApplicationServices { get; }
    IMultiplexedConnectionBuilder Use(Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate> middleware);
    MultiplexedConnectionDelegate Build();
}

public class MultiplexedConnectionBuilder : IMultiplexedConnectionBuilder
{
    public IServiceProvider ApplicationServices { get; }
    public IMultiplexedConnectionBuilder Use(Func<MultiplexedConnectionDelegate, MultiplexedConnectionDelegate> middleware);
    public MultiplexedConnectionDelegate Build();
}

public class ListenOptions : IConnectionBuilder, IMultiplexedConnectionBuilder

四、MiniKestrelServer

在瞭解了KestrelServer的連接管道後,我們來簡單模擬一下這種服務器類型的實現,爲此我們定義了一個名爲MiniKestrelServer的服務器類型。簡單起見,MiniKestrelServer只提供針對HTTP 1.1的支持。對於任何一個服務來說,它需要將請求交付給一個IHttpApplication<TContext>對象進行處理,MiniKestrelServer將這項工作實現在如下這個HostedApplication<TContext>類型中。

public class HostedApplication<TContext> : ConnectionHandler where TContext : notnull
{
    private readonly IHttpApplication<TContext> _application;
    public HostedApplication(IHttpApplication<TContext> application) => _application = application;

    public override async Task OnConnectedAsync(ConnectionContext connection)
    {
        var reader = connection!.Transport.Input;
        while (true)
        {
            var result = await reader.ReadAsync();
            using (var body = new MemoryStream())
            {
                var (features, request, response) = CreateFeatures(result, body);
                var closeConnection = request.Headers.TryGetValue("Connection", out var vallue) && vallue == "Close";
                reader.AdvanceTo(result.Buffer.End);

                var context = _application.CreateContext(features);
                Exception? exception = null;
                try
                {
                    await _application.ProcessRequestAsync(context);
                    await ApplyResponseAsync(connection, response, body);
                }
                catch (Exception ex)
                {
                    exception = ex;
                }
                finally
                {
                    _application.DisposeContext(context, exception);
                }
                if (closeConnection)
                {
                    await connection.DisposeAsync();
                    return;
                }
            }
            if (result.IsCompleted)
            {
                break;
            }
        }

        static (IFeatureCollection, IHttpRequestFeature, IHttpResponseFeature) CreateFeatures(ReadResult result, Stream body)
        {
            var handler = new HttpParserHandler();
            var parserHandler = new HttpParser(handler);
            var length = (int)result.Buffer.Length;
            var array = ArrayPool<byte>.Shared.Rent(length);
            try
            {
                result.Buffer.CopyTo(array);
                parserHandler.Execute(new ArraySegment<byte>(array, 0, length));
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(array);
            }
            var bodyFeature = new StreamBodyFeature(body);

            var features = new FeatureCollection();
            var responseFeature = new HttpResponseFeature();
            features.Set<IHttpRequestFeature>(handler.Request);
            features.Set<IHttpResponseFeature>(responseFeature);
            features.Set<IHttpResponseBodyFeature>(bodyFeature);

            return (features, handler.Request, responseFeature);
        }

        static async Task ApplyResponseAsync(ConnectionContext connection, IHttpResponseFeature response, Stream body)
        {
            var builder = new StringBuilder();
            builder.AppendLine($"HTTP/1.1 {response.StatusCode} {response.ReasonPhrase}");
            foreach (var kv in response.Headers)
            {
                builder.AppendLine($"{kv.Key}: {kv.Value}");
            }
            builder.AppendLine($"Content-Length: {body.Length}");
            builder.AppendLine();
            var bytes = Encoding.UTF8.GetBytes(builder.ToString());

            var writer = connection.Transport.Output;
            await writer.WriteAsync(bytes);
            body.Position = 0;
            await body.CopyToAsync(writer);
        }
    }
}

HostedApplication<TContext>是對一個IHttpApplication<TContext>對象的封裝。它派生於抽象類ConnectionHandler,重寫的OnConnectedAsync方法將針對請求的讀取和處理置於一個無限循環中。爲了將讀取的請求轉交給IHostedApplication<TContext>對象進行處理,它需要根據特性集合將TContext上下文創建出來。這裏提供的特性集合只包含三種核心的特性,一個是描述請求的HttpRequestFeature特性,它是利用HttpParser解析請求荷載內容得到的。另一個是描述響應的HttpResponseFeature特性,至於提供響應主體的特性由如下所示的StreamBodyFeature對象來表示。這三個特性的創建實現在CreateFeatures方法中。

public class StreamBodyFeature : IHttpResponseBodyFeature
{
    public Stream 	Stream { get; }
    public PipeWriter 	Writer { get; }

    public StreamBodyFeature(Stream stream)
    {
        Stream = stream;
        Writer = PipeWriter.Create(Stream);
    }

    public Task CompleteAsync() => Task.CompletedTask;
    public void DisableBuffering() { }
    public Task SendFileAsync(string path, long offset, long? count,
    CancellationToken cancellationToken = default)=> throw new NotImplementedException();
    public Task StartAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
}

包含三大特性的集合隨後作爲參數調用了IHostedApplication<TContext>對象的CreateContext方法將TContext上下文創建出來,此上下文作爲參數傳入了同一對象的ProcessRequestAsync方法,此時中間件管道接管請求。待中間件管道完成處理後, ApplyResponseAsync方法被調用以完成最終的響應工作。ApplyResponseAsync方法將響應狀態從HttpResponseFeature特性中提取並生成首行響應內容(“HTTP/1.1 {StatusCode} {ReasonPhrase}”),然後再從這個特性中將響應報頭提取出來並生成相應的文本。響應報文的首行內容和報頭文本按照UTF-8編碼生成二進制數組後利用ConnectionContext上下文的Transport屬性返回的IDuplexPipe對象發送出去後,它再將StreamBodyFeature特性收集到的響應主體輸出流“拷貝”到這個IDuplexPipe對象中,進而完成了針對響應主體內容的輸出。

如下所示的是MiniKestrelServer類型的完整定義。該類型的構造函數中注入了用於提供配置選項的IOptions<KestrelServerOptions>特性和IConnectionListenerFactory工廠,並且創建了一個ServerAddressesFeature對象並註冊到Features屬性返回的特性集合中。

public class MiniKestrelServer : IServer
{
    private readonly KestrelServerOptions _options;
    private readonly IConnectionListenerFactory _factory;
    private readonly List<IConnectionListener> _listeners = new();

    public IFeatureCollection Features { get; } = new FeatureCollection();

    public MiniKestrelServer(IOptions<KestrelServerOptions> optionsAccessor, IConnectionListenerFactory factory)
    {
        _factory = factory;
        _options = optionsAccessor.Value;
        Features.Set<IServerAddressesFeature>(new ServerAddressesFeature());
    }

    public void Dispose() => StopAsync(CancellationToken.None).GetAwaiter().GetResult();
    public Task StartAsync<TContext>(IHttpApplication<TContext> application, CancellationToken cancellationToken) where TContext : notnull
    {
        var feature = Features.Get<IServerAddressesFeature>()!;
        IEnumerable<ListenOptions> listenOptions;
        if (feature.PreferHostingUrls)
        {
            listenOptions = BuildListenOptions(feature);
        }
        else
        {
            listenOptions = _options.GetListenOptions();
            if (!listenOptions.Any())
            {
                listenOptions = BuildListenOptions(feature);
            }
        }

        foreach (var options in listenOptions)
        {
            _ = StartAsync(options);
        }
        return Task.CompletedTask;

        async Task StartAsync(ListenOptions litenOptions)
        {
            var listener = await _factory.BindAsync(litenOptions.EndPoint,cancellationToken);
            _listeners.Add(listener!);
            var hostedApplication = new HostedApplication<TContext>(application);
            var pipeline = litenOptions.Use(next => context => hostedApplication.OnConnectedAsync(context)).Build();
            while (true)
            {
                var connection = await listener.AcceptAsync();
                if (connection != null)
                {
                    _ = pipeline(connection);
                }
            }
        }

        IEnumerable<ListenOptions> BuildListenOptions(IServerAddressesFeature feature)
        {
            var options = new KestrelServerOptions();
            foreach (var address in feature.Addresses)
            {
                var url = new Uri(address);
                if (string.Compare("localhost", url.Host, true) == 0)
                {
                    options.ListenLocalhost(url.Port);
                }
                else
                {
                    options.Listen(IPAddress.Parse(url.Host), url.Port);
                }

            }
            return options.GetListenOptions();
        }
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(_listeners.Select(it => it.DisposeAsync().AsTask()));
}

實現的StartAsync<TContext>方法先將IServerAddressesFeature特性提取出來,並利用其PreferHostingUrls屬性決定應該使用直接註冊到KestrelOptions配置選項上的終結點還是使用註冊在該特定上的監聽地址。如果使用後者,註冊的監聽地址會利用BuildListenOptions方法轉換成對應的ListenOptions列表,否則直接從KestrelOptions對象的ListenOptions屬性提取所有的ListenOptions列表,由於這是一個內部屬性,不得不利用如下這個擴展方法以反射的方式獲取這個列表。

public static class KestrelServerOptionsExtensions
{
    public static IEnumerable<ListenOptions> GetListenOptions(this KestrelServerOptions options)
    {
        var property = typeof(KestrelServerOptions).GetProperty("ListenOptions",BindingFlags.NonPublic | BindingFlags.Instance);
        return (IEnumerable<ListenOptions>)property!.GetValue(options)!;
    }
}

對於每一個表示註冊終結點的ListenOptions配置選項,StartAsync<TContext>方法利用IConnectionListenerFactory工廠將對應的IConnectionListener監聽器創建出來,並綁定到指定的終結點上監聽連接請求。表示連接的ConnectionContext上下文一旦被創建出來後,該方法便會利用構建的連接管道對它進行處理。在調用ListenOptions配置選項的Build方法構建連接管道前,StartAsync<TContext>方法將HostedApplication<TContext>對象創建出來並作爲中間件進行了註冊。所以針對連接的處理將被這個HostedApplication<TContext>對象接管。

using App;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.Extensions.DependencyInjection.Extensions;

var builder = WebApplication.CreateBuilder();
builder.WebHost.UseKestrel(kestrel => kestrel.ListenLocalhost(5000));
builder.Services.Replace(ServiceDescriptor.Singleton<IServer, MiniKestrelServer>());
var app = builder.Build();
app.Run(context => context.Response.WriteAsync("Hello World!"));
app.Run();

如上所示的演示程序將替換了針對IServer的服務註冊,意味着默認的KestrelServer將被替換成自定義的MiniKestrelServer。啓動該程序後,由瀏覽器發送的HTTP請求(不支持HTTPS)同樣會被正常處理,並得到如圖18-6所示的響應內容。需要強調一下,MiniKestrelServer僅僅用來模擬KestrelServer的實現原理,不要覺得真實的實現會如此簡單。

clip_image002
圖1 由MiniKestrelServer回覆的響應內容

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章