Kestrel Serving Requests
01/15/2023
4 minutes
In this post I review the way Kestrel serves HTTP requests. Please note, that the architecture described here may change from release to release of ASP.NET Core. At the time of writing .NET 7 and the corresponding ASP.NET Core is a month away from release.
Accepting connection on HTTP2 and HTTP3
ASP.NET Core's Kestrel opens IConnectionListener<T>
s upon startup. For each ASPNETCORE_URLS
set, a connection listener is instantiated. A base IConnectionListener
implementation is a SocketConnectionListener
. Connection listeners implement the decorator pattern. For example, GenericConnectionListener
expects another connection listener as a constructor parameter.Connection listeners provide basic functionality to bind/unbind to a given port and to accept new connection requests.
ConnectionDispatcher
also initiated during startup. It starts to run an infinite while
loop in AcceptConnectionsAsync()
method to accept new connections on a given listener. When a client sends a request, a new connection in initialized. In this case a KestrelConnection<T>
, that also implements IThreadPoolWorkItem.Execute()
.
ConnectionDispatcher
dispatches the connection object to the thread pool, so the current thread accepting new requests becomes free again to handle new connection requests.
KestrelConnection<T>
provides a few connection level features, like connection lifetime notifications or heartbeat, etc. However, in its execute method it invokes a Func<T, Task>
delegate. This delegate comes from the ConnectionDispatcher
, and it points to a middleware pipeline.
Notice, that so far there was no mentioning of HTTP. This delegate though (in case of HTTP) can point to an HTTP middleware pipeline. For example, HttpMultiplexedConnectionMiddleware<TContext>
middleware is used in case of HTTP3 or HttpsConnectionMiddleware
, HttpConnectionMiddleware
used in case of HTTP2. From this point on, the goal is to handle HTTP connections. HttpConnectionMiddleware
has a reference to the application that represents the request delegates of the host application. This middleware also creates an HttpConnection
and instructs it to process the incoming HTTP connection. It passes the application as an argument to ProcessRequestsAsync()
method. HttpConnection
itself does not do much with a connection. It provides a common abstraction for http connections' initialization/abortion/timeout functionality. However, its main job is to delegate the request further to a request processor, which can either be:
Http1Connection
Http2Connection
Http3Connection
At this point, the execution stack of HTTP2 and HTTP3 requests diverts. I will track the way of handling h2 requests in the rest of this article. Note, that the above mentioned application is passed along to the downstream method calls as an argument.
HTTP2
Upon Http2Connection
creation, the new object opens a new internal Pipe and initiates a task that copies the data from the input of socket connection to its internal pipe.
However, the requests gets served by the ProcessRequestsAsync()
method, which runs a while
loop and reads data from the internal pipe. When data is available, it tries to parse it as an HTTP2 frame. HTTP2 defines a set of frames, for example HEADERS, DATA, SETTINGS, PING or GOAWAY, etc.
Http2Connection
does not block the connection handling loop in ProcessRequestsAsync()
. Instead, when it reads a frame, for example a complete HEADERS frame, it creates a new Http2Stream
object, and passes this object to the thread pool for execution. Http2Connection
is responsible to handle the frames and the connection.
Http2Stream
continues processing the request. Http2Stream
derives from HttpProtocol
which provides a shared functionality between HTTP1, HTTP2 and HTTP3. At this point, it tries to parse the request, creates a message body. Once that is successful, it delegates the rest of the work to the host application. Finally, the host application runs the middleware pipelines of the framework/users' code, which eventually ends in controllers or request delegates:
app.Run(async context => { await context.Response.WriteAsync("Hello " + context.Request.Protocol); });