作為ASP.NET Core請求處理管道的“龍頭”的服務器負責監聽和接收請求并最終完成對請求的響應。它將原始的請求上下文描述為相應的特性(Feature),并以此將HttpContext上下文創建出來,中間件針對HttpContext上下文的所有操作將借助于這些特性轉移到原始的請求上下文上。學習ASP.NET Core框架最有效的方式就是按照它的原理“再造”一個框架,了解服務器的本質最好的手段就是試著自定義一個服務器。現在我們自定義一個真正的服務器。在此之前,我們再來回顧一下表示服務器的IServer接口。[本文節選《ASP.NET Core 6框架揭秘》第18章]
一、IServer
二、請求和響應特性
三、StreamBodyFeature
四、HttpListenerServer
一、IServer
作為服務器的IServer對象利用如下所示的Features屬性提供了與自身相關的特性。除了利用StartAsync<TContext>和StopAsync方法啟動和關閉服務器之外,它還實現了IDisposable接口,資源的釋放工作可以通過實現的Dispose方法來完成。StartAsync<TContext>方法將IHttpApplication<TContext>類型的參數作為處理請求的“應用”,該對象是對中間件管道的封裝。從這個意義上講,服務器就是傳輸層和這個IHttpApplication<TContext>對象之間的“中介”。
public?interface?IServer?:?IDisposable
{IFeatureCollection?Features?{?get;?}Task?StartAsync<TContext>(IHttpApplication<TContext>?application,?CancellationToken?cancellationToken)?where?TContext?:?notnull;Task?StopAsync(CancellationToken?cancellationToken);
}
雖然不同服務器類型的定義方式千差萬別,但是背后的模式基本上與下面這個以偽代碼定義的服務器類型一致。如下這個Server利用IListener對象來監聽和接收請求,該對象是利用構造函數中注入的IListenerFactory工廠根據指定的監聽地址創建出來的。StartAsync<TContext>方法從Features特性集合中提取出IServerAddressesFeature特性,并針對它提供的每個監聽地址創建一個IListener對象。該方法為每個IListener對象開啟一個“接收和處理請求”的循環,循環中的每次迭代都會調用IListener對象的AcceptAsync方法來接收請求,我們利用RequestContext對象來表示請求上下文。
public?class?Server?:?IServer
{private?readonly?IListenerFactory?_listenerFactory;private?readonly?List<IListener>?_listeners?=?new();public?IFeatureCollection?Features?{?get;?}?=?new?FeatureCollection();public?Server(IListenerFactory?listenerFactory)?=>?_listenerFactory?=?listenerFactory;public?async?Task?StartAsync<TContext>(IHttpApplication<TContext>?application,?CancellationToken?cancellationToken)?where?TContext?:?notnull{var?addressFeature?=?Features.Get<IServerAddressesFeature>()!;foreach?(var?address?in?addressFeature.Addresses){var?listener?=?await?_listenerFactory.BindAsync(address);_listeners.Add(listener);_?=?StartAcceptLoopAsync(listener);}async?Task?StartAcceptLoopAsync(IListener?listener){while?(true){var?requestContext?=?await?listener.AcceptAsync();_?=?ProcessRequestAsync(requestContext);}}async?Task?ProcessRequestAsync(RequestContext?requestContext){var?feature?=?new?RequestContextFeature(requestContext);var?contextFeatures?=?new?FeatureCollection();contextFeatures.Set<IHttpRequestFeature>(feature);contextFeatures.Set<IHttpResponseFeature>(feature);contextFeatures.Set<IHttpResponseBodyFeature>(feature);var?context?=?application.CreateContext(contextFeatures);Exception??exception?=?null;try{await?application.ProcessRequestAsync(context);}catch?(Exception?ex){exception?=?ex;}finally{application.DisposeContext(context,?exception);}}}public?Task?StopAsync(CancellationToken?cancellationToken)?=>?Task.WhenAll(_listeners.Select(listener?=>?listener.StopAsync()));public?void?Dispose()?=>?_listeners.ForEach(listener?=>?listener.Dispose());
}public?interface?IListenerFactory
{Task<IListener>?BindAsync(string?listenAddress);
}public?interface?IListener?:?IDisposable
{Task<RequestContext>?AcceptAsync();Task?StopAsync();
}public?class?RequestContext
{...
}public?class?RequestContextFeature?:?IHttpRequestFeature,?IHttpResponseFeature,?IHttpResponseBodyFeature
{public?RequestContextFeature(RequestContext?requestContext);...
}
StartAsync<TContext>方法接下來利用此RequestContext上下文將RequestContextFeature特性創建出來。RequestContextFeature特性類型同時實現了IHttpRequestFeature, IHttpResponseFeature和 IHttpResponseBodyFeature這三個核心接口,我們特性針對這三個接口將特性對象添加到創建的FeatureCollection集合中。特性集合隨后作為參數調用IHttpApplication<TContext>的CreateContext方法將TContext上下文創建出來,后者將進一步作為參數調用另一個ProcessRequestAsync方法將請求分發給中間件管道進行處理。待處理結束,IHttpApplication<TContext>對象的DisposeContext方法被調用,創建的TContext上下文承載的資源得以釋放。
二、請求和響應特性
接下來我們將采用類似的模式來定義一個基于HttpListener的服務器。提供的HttpListenerServer的思路就是利用自定義特性來封裝表示原始請求上下文的HttpListenerContext對象,我們使用HttpRequestFeature和HttpResponseFeature這個兩個現成特性。
public?class?HttpRequestFeature?:?IHttpRequestFeature
{public?string?Protocol?{?get;?set;?}public?string?Scheme?{?get;?set;?}public?string?Method?{?get;?set;?}public?string?PathBase?{?get;?set;?}public?string?Path?{?get;?set;?}public?string?QueryString?{?get;?set;?}public?string?RawTarget?{?get;?set;?}public?IHeaderDictionary?Headers?{?get;?set;?}public?Stream?Body?{?get;?set;?}
}
public?class?HttpResponseFeature?:?IHttpResponseFeature
{public?int?StatusCode?{?get;?set;?}public?string??ReasonPhrase?{?get;?set;?}public?IHeaderDictionary?Headers?{?get;?set;?}public?Stream?Body?{?get;?set;?}public?virtual?bool?HasStarted?=>?false;public?HttpResponseFeature(){StatusCode?=?200;Headers?=?new?HeaderDictionary();Body?=?Stream.Null;}public?virtual?void?OnStarting(Func<object,?Task>?callback,?object?state)?{?}public?virtual?void?OnCompleted(Func<object,?Task>?callback,?object?state)?{?}
}
如果我們使用HttpRequestFeature來描述請求,意味著HttpListener在接受到請求之后需要將請求信息從HttpListenerContext上下文轉移到該特性上。如果使用HttpResponseFeature來描述響應,待中間件管道在完成針對請求的處理后,我們還需要將該特性承載的響應數據應用到HttpListenerContext上下文上。
三、StreamBodyFeature
現在我們有了描述請求和響應的兩個特性,還需要一個描述響應主體的特性,為此我們定義了如下這個StreamBodyFeature特性類型。StreamBodyFeature直接使用構造函數提供的Stream對象作為響應主體的輸出流,并根據該對象創建出Writer屬性返回的PipeWriter對象。本著“一切從簡”的原則,我們并沒有實現用來發送文件的SendFileAsync方法,其他成員也采用最簡單的方式進行了實現。
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;
}
四、HttpListenerServer
在如下這個自定義的HttpListenerServer服務器類型中,與傳輸層交互的HttpListener體現在_listener字段上。服務器在初始化過程中,它的Features屬性返回的IFeatureCollection對象中添加了一個ServerAddressesFeature特性,因為我們需要用它來存放注冊的監聽地址。實現StartAsync<TContext>方法將監聽地址從這個特性中取出來應用到HttpListener對象上。
public?class?HttpListenerServer?:?IServer
{private?readonly?HttpListener?_listener?=?new();public?IFeatureCollection?Features?{?get;?}=?new?FeatureCollection();public?HttpListenerServer()?=>?Features.Set<IServerAddressesFeature>(new?ServerAddressesFeature());public?Task?StartAsync<TContext>(IHttpApplication<TContext>?application,CancellationToken?cancellationToken)?where?TContext?:?notnull{var?pathbases?=?new?HashSet<string>(StringComparer.OrdinalIgnoreCase);var?addressesFeature?=?Features ? ? ? ? ? ? .Get<IServerAddressesFeature>()!;foreach?(string?address?in?addressesFeature.Addresses){_listener.Prefixes.Add(address.TrimEnd('/')?+?"/");pathbases.Add(new?Uri(address).AbsolutePath.TrimEnd('/'));}_listener.Start();while?(true){var?listenerContext?=?_listener.GetContext();_?=?ProcessRequestAsync(listenerContext);}async?Task?ProcessRequestAsync(?HttpListenerContext?listenerContext){FeatureCollection?features?=?new();var?requestFeature?=?CreateRequestFeature(pathbases,?listenerContext);var?responseFeature?=?new?HttpResponseFeature();var?body?=?new?MemoryStream();var?bodyFeature?=?new?StreamBodyFeature(body);features.Set<IHttpRequestFeature>(requestFeature);features.Set<IHttpResponseFeature>(responseFeature);features.Set<IHttpResponseBodyFeature>(bodyFeature);var?context?=?application.CreateContext(features);Exception??exception?=?null;try{await?application.ProcessRequestAsync(context);var?response?=?listenerContext.Response;response.StatusCode?=?responseFeature.StatusCode;if?(responseFeature.ReasonPhrase?is?not?null){response.StatusDescription?=?responseFeature.ReasonPhrase;}foreach?(var?kv?in?responseFeature.Headers){response.AddHeader(kv.Key,?kv.Value);}body.Position?=?0;await?body.CopyToAsync(listenerContext.Response.OutputStream);}catch?(Exception?ex){exception?=?ex;}finally{body.Dispose();application.DisposeContext(context,?exception);listenerContext.Response.Close();}}}public?void?Dispose()?=>?_listener.Stop();private?static?HttpRequestFeature?CreateRequestFeature(HashSet<string>?pathbases,HttpListenerContext?listenerContext){var?request?=?listenerContext.Request;var?url?=?request.Url!;var?absolutePath?=?url.AbsolutePath;var?protocolVersion?=?request.ProtocolVersion;var?requestHeaders?=?new?HeaderDictionary();foreach?(string?key?in?request.Headers){requestHeaders.Add(key,?request.Headers.GetValues(key));}var?requestFeature?=?new?HttpRequestFeature{Body?=?request.InputStream,Headers?=?requestHeaders,Method?=?request.HttpMethod,QueryString?=?url.Query,Scheme?=?url.Scheme,Protocol?=?$"{url.Scheme.ToUpper()}/{protocolVersion.Major}.{protocolVersion.Minor}"};var?pathBase?=?pathbases.First(it?=>?absolutePath.StartsWith(it,?StringComparison.OrdinalIgnoreCase));requestFeature.Path?=?absolutePath[pathBase.Length..];requestFeature.PathBase?=?pathBase;return?requestFeature;}public?Task?StopAsync( CancellationToken?cancellationToken){_listener.Stop();return?Task.CompletedTask;}
}
在調用Start方法將HttpListener啟動后,StartAsync<TContext>方法開始“請求接收處理”循環。接收到的請求上下文被封裝成HttpListenerContext上下文,其承載的請求信息利用CreateRequestFeature方法轉移到創建的HttpRequestFeature特性上。StartAsync<TContext>方法創建的“空”HttpResponseFeature對象來描述響應,另一個描述響應主體的StreamBodyFeature特性則根據創建的MemoryStream對象構建而成,意味著中間件管道寫入的響應主體的內容將暫存到這個內存流中。我們將這三個特性注冊到創建的FeatureCollection集合上,并將后者作為參數調用了IHttpApplication<TContext>對象的CreateContext方法將TContext上下文創建出來。此上下文進一步作為參數調用了IHttpApplication<TContext>對象的ProcessRequestAsync方法,中間件管道得以接管請求。
待中間件管道的處理工作完成后,響應的內容還暫存在兩個特性中,我們還需要將它們應用到代表原始HttpListenerContext上下文上。StartAsync<TContext>方法從HttpResponseFeature特性提取出響應狀態碼和響應報頭轉移到HttpListenerContext上下文上,然后上述這個MemoryStream對象“拷貝”到HttpListenerContext上下文承載的響應主體輸出流中。
using?App;
using?Microsoft.AspNetCore.Hosting.Server;
using?Microsoft.Extensions.DependencyInjection.Extensions;var?builder?=?WebApplication.CreateBuilder(args);
builder.Services.Replace(ServiceDescriptor.Singleton<IServer,?HttpListenerServer>());
var?app?=?builder.Build();
app.Run(context?=>?context.Response.WriteAsync("Hello?World!"));
app.Run("http://localhost:5000/foobar/");
我們采用上面的演示程序來檢測HttpListenerServer能否正常工作。我們為HttpListenerServer類型創建了一個ServiceDescriptor對象將現有的服務器的服務注冊替換掉。在調用WebApplication對象的Run方法時顯式指定了具有PathBase(“/foobar”)的監聽地址“http://localhost:5000/foobar/”,如圖1所示的瀏覽器以此地址訪問應用,會得到我們希望的結果。
圖1 HttpListenerServer返回的結果