KestrelServer詳解[2]: 網路連結的建立
《 註冊監聽終結點(Endpoint) 》已經詳細講述瞭如何使用KestrelServer,現在我們來簡單聊聊這種處理器的總體設計和實現原理。當KestrelServer啟動的時候,註冊的每個終結點將轉換成對應的“連線監聽器”,後者在監聽到初始請求時會建立“連線”,請求的接收和響應的回覆都在這個連線中完成。本文提供的示例演示已經同步到《 ASP.NET Core 6框架揭祕-例項演示版 》)
一、連線上下文(ConnectionContext )
監聽器建立的連線時一個抽象的概念,我們可以將其視為客戶端和服務端完成訊息交換而構建的“上下文”,該上下文通過如下這個ConnectionContext型別表示。ConnectionContext派生於抽象基類BaseConnectionContext,後者實現了IAsyncDisposable介面。每個連線具有一個通過ConnectionId屬性表示的ID,它的LocalEndPoint和RemoteEndPoint屬性返回本地(服務端)和遠端(客戶端)終結點。伺服器提供的特性集合體現在它的Features屬性上,另一個Items提供了一個存放任意屬性的字典。ConnectionClosed屬性提供的CancellationToken可以用來接收連線關閉的通知。Abort方法可以中斷當前連線,這兩個方法在ConnectionContext被重寫。ConnectionContext型別的Transport屬性提供的IDuplexPipe物件是用來對請求和響應進行讀寫的雙向管道。
public abstract class ConnectionContext : BaseConnectionContext { public abstract IDuplexPipe Transport { get; set; } public override void Abort(ConnectionAbortedException abortReason); public override void Abort(); } public abstract class BaseConnectionContext : IAsyncDisposable { public virtual EndPoint? LocalEndPoint { get; set; } public virtual EndPoint? RemoteEndPoint { get; set; } public abstract string ConnectionId { get; set; } public abstract IFeatureCollection Features { get; } public abstract IDictionary<object, object?> Items { get; set; } public virtual CancellationToken ConnectionClosed { get; set; } public abstract void Abort(); public abstract void Abort(ConnectionAbortedException abortReason); public virtual ValueTask DisposeAsync(); }
如果採用HTTP 1.X和HTTP 2協議,KestrelServer會採用TCP套接字(Socket)進行通訊,對應的連線體現為一個SocketConnection物件。如果採用的是HTTP 3,會採用基於UDP的QUIC協議進行通訊,對應的連線體現為一個QuicStreamContext物件。如下面的程式碼片段所示,這兩個型別都派生於TransportConnection,後者派生於ConnectionContext。
internal abstract class TransportConnection : ConnectionContext internal sealed class SocketConnection : TransportConnection internal sealed class QuicStreamContext : TransportConnection
二、連線監聽器(IConnectionListener )
KestrelServer同時支援三個版本的HTTP協議,HTTP 1.X和HTTP 2建立在TCP協議之上,針對這樣的終結點會轉換成通過如下這個IConnectionListener介面表示的監聽器。它的EndPoint屬性表示監聽器繫結的終結點,當AcceptAsync方法被呼叫時,監聽器便開始了網路監聽工作。當來自某個客戶端端的初始請求抵達後,它會將建立代表連線的ConnectionContext上下文創建出來。另一個UnbindAsync方法用來解除終結點繫結,並停止監聽。
public interface IConnectionListener : IAsyncDisposable { EndPoint EndPoint { get; } ValueTask<ConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default(CancellationToken)); ValueTask UnbindAsync(CancellationToken cancellationToken = default(CancellationToken)); }
QUIC利用傳輸層的UDP協議實現了真正意義上的“多路複用”,所以它將對應的連線監聽器介面命名為IMultiplexedConnectionListener。它的AcceptAsync方法建立的是代表多路複用連線的MultiplexedConnectionContext物件,後者的AcceptAsync會將ConnectionContext上下文創建出來。QuicConnectionContext 型別是對MultiplexedConnectionContext的具體實現,它的AcceptAsync方法建立的就是上述的QuicStreamContext物件,該型別派生於抽象類TransportMultiplexedConnection。
public interface IMultiplexedConnectionListener : IAsyncDisposable { EndPoint EndPoint { get; } ValueTask<MultiplexedConnectionContext?> AcceptAsync(IFeatureCollection? features = null,CancellationToken cancellationToken = default(CancellationToken)); ValueTask UnbindAsync(CancellationToken cancellationToken = default(CancellationToken)); } public abstract class MultiplexedConnectionContext : BaseConnectionContext { public abstract ValueTask<ConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default(CancellationToken)); public abstract ValueTask<ConnectionContext> ConnectAsync(IFeatureCollection? features = null,CancellationToken cancellationToken = default(CancellationToken)); } internal abstract class TransportMultiplexedConnection : MultiplexedConnectionContext internal sealed class QuicConnectionContext : TransportMultiplexedConnection
KestrelServer使用的連線監聽器均由對應的工廠來構建。如下所示的IConnectionListenerFactory介面代表用來構建IConnectionListener監聽器的工廠,IMultiplexedConnectionListenerFactory工廠則用來構建IMultiplexedConnectionListener監聽器。
public interface IConnectionListenerFactory { ValueTask<IConnectionListener> BindAsync(EndPoint endpoint,CancellationToken cancellationToken = default(CancellationToken)); } public interface IMultiplexedConnectionListenerFactory { ValueTask<IMultiplexedConnectionListener> BindAsync(EndPoint endpoint, IFeatureCollection? features = null,CancellationToken cancellationToken = default(CancellationToken)); }
三、總體設計
上面圍繞著“連線”介紹了一系列介面和型別,它們之間的關係體現在如圖1所示的UML中。KestrelServer啟動時會根據每個終結點支援的HTTP協議利用IConnectionListenerFactory或者IMultiplexedConnectionListenerFactory工廠來建立代表連線監聽器的IConnectionListener或者IMultiplexedConnectionListener物件。IConnectionListener監聽器會直接將代表連線的ConnectionContext上下文創建出來,IMultiplexedConnectionListener監聽器建立的則是一個MultiplexedConnectionContext上下文,代表具體連線的ConnectionContext上下文會進一步由該物件進行建立。
圖1 “連線”相關的介面和型別
四、利用連線接收請求和回覆響應
下面演示的例項直接利用IConnectionListenerFactory工廠建立的IConnectionListener監聽器來監聽連線請求,並利用建立的連線來接收請求和回覆響應。由於表示連線的ConnectionContext上下文直接面向傳輸層,接受的請求和回覆的響應都體現為二進位制流,解析二進位制資料得到請求資訊是一件繁瑣的事情。這裡我們借用了“HttpMachine”NuGet包提供的HttpParser元件來完成這個任務,為此我們為它定義瞭如下這個HttpParserHandler型別。如果將這個HttpParserHandler物件傳遞給HttpParser物件,後者在請求解析過程中會呼叫前者相應的方法,我們利用這些方法利用讀取的內容將描述請求的HttpRequestFeature特性構建出來。原始碼可以從 這裡 檢視。
public class HttpParserHandler : IHttpParserHandler { private string? headerName = null; public HttpRequestFeature Request { get; } = new HttpRequestFeature(); public void OnBody(HttpParser parser, ArraySegment<byte> data) => Request.Body = new MemoryStream(data.Array!, data.Offset, data.Count); public void OnFragment(HttpParser parser, string fragment) { } public void OnHeaderName(HttpParser parser, string name) => headerName = name; public void OnHeadersEnd(HttpParser parser) { } public void OnHeaderValue(HttpParser parser, string value) => Request.Headers[headerName!] = value; public void OnMessageBegin(HttpParser parser) { } public void OnMessageEnd(HttpParser parser) { } public void OnMethod(HttpParser parser, string method) => Request.Method = method; public void OnQueryString(HttpParser parser, string queryString) => Request.QueryString = queryString; public void OnRequestUri(HttpParser parser, string requestUri) => Request.Path = requestUri; }
如下所示的演示程式利用WebApplication物件的Services屬性提供的IServicePovider物件來提供IConnectionListenerFactory工廠。我們呼叫該工廠的BindAsync方法建立了一個連線監聽器並將其繫結到採用5000埠本地終結點。在一個無限迴圈中,我們呼叫監聽器的AcceptAsync方法開始監聽連線請求,並最終將代表連線的ConnectionContext上下文創建出來。
using App; using HttpMachine; using Microsoft.AspNetCore.Connections; using Microsoft.AspNetCore.Http.Features; using System.Buffers; using System.IO.Pipelines; using System.Net; using System.Text; var factory = WebApplication.Create().Services.GetRequiredService<IConnectionListenerFactory>(); var listener = await factory.BindAsync(new IPEndPoint(IPAddress.Any, 5000)); while (true) { var context = await listener.AcceptAsync(); _ = HandleAsync(context!); static async Task HandleAsync(ConnectionContext connection) { var reader = connection!.Transport.Input; while (true) { var result = await reader.ReadAsync(); var request = ParseRequest(result); reader.AdvanceTo(result.Buffer.End); Console.WriteLine("[{0}]Receive request: {1} {2} Connection:{3}",connection.ConnectionId, request.Method, request.Path, request.Headers?["Connection"] ?? "N/A"); var response = @"HTTP/1.1 200 OK Content-Type: text/plain; charset=utf-8 Content-Length: 12 Hello World!"; await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes(response)); if (request.Headers.TryGetValue("Connection", out var value) && string.Compare(value, "close", true) == 0) { await connection.DisposeAsync(); return; } if (result.IsCompleted) { break; } } } static HttpRequestFeature ParseRequest(ReadResult result) { var handler = new HttpParserHandler(); var parserHandler = new HttpParser(handler); parserHandler.Execute(new ArraySegment<byte>(result.Buffer.ToArray())); return handler.Request; } }
針對連線的處理實現在HandleAsync方法中。HTTP 1.1預設會採用長連線,多個請求會使用同一個連線傳送過來,所以針對單個請求的接收和處理會放在一個迴圈中,直到連線被關閉。請求的接收利用ConnectionContext物件的Transport屬性返回的IDuplexPipe物件來完成。簡單起見,我們假設每個請求的讀取剛好能夠一次完成,所以每次讀取的二進位制剛好是一個完整的請求。讀取的二進位制內容利用ParseRequest方法藉助於HttpParser物件轉換成HttpRequestFeature物件後,我們直接生成一個表示響應報文的字串並採用UTF-8對其編碼,編碼後的響應利用上述的IDuplexPipe物件傳送出去。這份手工生成的“Hello World!”響應將以圖18-5的形式呈現在瀏覽器上。
圖2 面向“連線”程式設計
按照HTTP 1.1規範的約定,如果客戶端希望關閉預設開啟的長連線,可以在請求中新增“Connection:Close”報頭。HandleAsync方法在處理每個請求時會確定是否攜帶了此報頭,並在需要的時候呼叫ConnectionContext上下文的 DisposeAsync方法關閉並釋放當前連線。該方法在對請求進行處理時會將此報頭和連線的ID輸出到控制檯上。圖2所示的控制檯輸出是先後接收到三次請求的結果,後面兩次顯式添加了“Connection:Close”,可以看出前兩次複用同一個連線。
- ASP.NET Core 6框架揭祕例項演示[32]:錯誤頁面的集中呈現方式
- ASP.NET Core 6框架揭祕例項演示[31]:路由"高階"用法
- 沒有Kubernetes怎麼玩Dapr?
- 全新升級的AOP框架Dora.Interception[6]: 框架設計和實現原理
- KestrelServer詳解[2]: 網路連結的建立
- 一個簡單的模擬例項說明Task及其排程問題
- ASP.NET Core 6框架揭祕例項演示[28]:自定義一個伺服器
- ASP.NET Core 6 Minimal API的模擬實現
- ASP.NET Core 6框架揭祕例項演示[26]:跟蹤應用接收的每一次請求
- ASP.NET Core 6框架揭祕例項演示[25]:配置與承載環境的應用
- ASP.NET Core 6框架揭祕例項演示[24]:中介軟體的多種定義方式
- ASP.NET Core 6框架揭祕例項演示[22]:如何承載你的後臺服務[補充]
- ASP.NET Core 6框架揭祕例項演示[19]:資料加解密與雜湊
- ASP.NET Core 6框架揭祕例項演示[12]:診斷跟蹤的進階用法
- ASP.NET Core 6框架揭祕例項演示[08]:配置的基本程式設計模式
- ASP.NET Core 6框架揭祕例項演示[05]:依賴注入基本程式設計模式
- ASP.NET Core 6框架揭祕例項演示[04]:自定義依賴注入框架
- ASP.NET Core 6框架揭祕例項演示[03]:Dapr初體驗
- ASP.NET Core 6框架揭祕例項演示[02]:基於路由、MVC和gRPC的應用開發
- ASP.NET Core 6框架揭祕例項演示[01]: 程式設計初體驗