KestrelServer详解[2]: 网络连接是如何创建的?

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了KestrelServer详解[2]: 网络连接是如何创建的?相关的知识,希望对你有一定的参考价值。

KestrelServer详解[1]:注册监听终结点(Endpoint)》已经详细讲述了如何使用KestrelServer,现在我们来简单聊聊这种服务器的总体设计和实现原理。当KestrelServer启动的时候,注册的每个终结点将转换成对应的“连接监听器”,后者在监听到初始请求时会创建“连接”,请求的接收和响应的回复都在这个连接中完成。[本文节选《ASP.NET Core 6框架揭秘》第18章]

一、连接上下文(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”,可以看出前两次复用同一个连接。

以上是关于KestrelServer详解[2]: 网络连接是如何创建的?的主要内容,如果未能解决你的问题,请参考以下文章

KestrelServer详解[1]:注册监听终结点(Endpoint)

KestrelServer

.NET 云原生架构师训练营(KestrelServer源码分析)--学习笔记

物理真实网络连接和虚拟机联网的方式详解

Altium Designer 网络连接方式Port和Net Label详解

网络连接详解