和所有的服務器一樣,KestrelServer最終需要解決的是網絡傳輸的問題。在《KestrelServer詳解[2]: 網絡連接是如何創建的?》,我們介紹了KestrelServer如何利用連接接聽器的建立網絡連接,并再次基礎上演示了如何直接利用建立的連接接收請求和回復響應。本篇更進一步,我們根據其總體設計,定義了迷你版的KestrelServer讓讀者看看這個重要的服務器大體是如何實現的。[本文節選《ASP.NET Core 6框架揭秘》第18章]
一、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();
}
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)同樣會被正常處理,并得到如圖1所示的響應內容。需要強調一下,MiniKestrelServer僅僅用來模擬KestrelServer的實現原理,不要覺得真實的實現會如此簡單。
圖1 由MiniKestrelServer回復的響應內容