和所有的服务器一样,KestrelServer最终需要解决的是网络传输的问题。在《网络连接的创建》,我们介绍了KestrelServer如何利用连接接听器的建立网络连接,并再次基础上演示了如何直接利用建立的连接接收请求和回复响应。本篇更进一步,我们根据其总体设计,定义了迷你版的KestrelServer让读者看看这个重要的服务器大体是如何实现的。本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)

一、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();
} public class ConnectionBuilder : IConnectionBuilder
{
public IServiceProvider ApplicationServices { get; }
public ConnectionDelegate Build();
public IConnectionBuilder Use(Func<ConnectionDelegate, ConnectionDelegate> middleware);
}

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()));
}

如上所示的演示程序将替换了针对IServer的服务注册,意味着默认的KestrelServer将被替换成自定义的MiniKestrelServer。启动该程序后,由浏览器发送的HTTP请求(不支持HTTPS)同样会被正常处理,并得到如图18-6所示的响应内容。需要强调一下,MiniKestrelServer仅仅用来模拟KestrelServer的实现原理,不要觉得真实的实现会如此简单。


图1 由MiniKestrelServer回复的响应内容

KestrelServer详解[3]: 自定义一个迷你版的KestrelServer的更多相关文章

  1. ASP.NET Core管道深度剖析(2):创建一个“迷你版”的管道来模拟真实管道请求处理流程

    从<ASP.NET Core管道深度剖析(1):采用管道处理HTTP请求>我们知道ASP.NET Core请求处理管道由一个服务器和一组有序的中间件组成,所以从总体设计来讲是非常简单的,但 ...

  2. 写一个迷你版Smarty模板引擎,对认识模板引擎原理非常好(附代码)

    前些时间在看创智博客韩顺平的Smarty模板引擎教程,再结合自己跟李炎恢第二季开发中CMS系统写的tpl模板引擎.今天就写一个迷你版的Smarty引擎,虽然说我并没有深入分析过Smarty的源码,但是 ...

  3. 人脸验证算法Joint Bayesian详解及实现(Python版)

    人脸验证算法Joint Bayesian详解及实现(Python版) Tags: JointBayesian DeepLearning Python 本博客仅为作者记录笔记之用,不免有很多细节不对之处 ...

  4. Cookie 详解以及实现一个 cookie 操作库

    Cookie 详解以及实现一个 cookie 操作库 cookie 在前端有着大量的应用,但有时我们对它还是一知半解.下面来看看它的一些具体的用法 Set-Cookie 服务器通过设置响应头来设置客户 ...

  5. 直播的本质(创业者应该要从商业模式的右边开始思考,你为用户创造了什么价值?找客户并不难,但要想办法让客户不离不弃;PC端功能的丰富很重要,因为手机版通常只是一个迷你版)

    我想稍微给直播这件事浇点冷水. 的确,直播现在越来越火,YouTube凭着良好的基础建设平台前段时间也做起了直播,Facebook Live最近也加入了变脸.预定直播时间和双人录制的功能,更不用说国内 ...

  6. Koa源码解析,带你实现一个迷你版的Koa

    前言 本文是我在阅读 Koa 源码后,并实现迷你版 Koa 的过程.如果你使用过 Koa 但不知道内部的原理,我想这篇文章应该能够帮助到你,实现一个迷你版的 Koa 不会很难. 本文会循序渐进的解析内 ...

  7. Net is as typeof 运行运算符详解 net 自定义泛型那点事

    Net is as typeof 运行运算符详解   概述 在了解运行运算符的前提我们需要了解什么是RTTI ,在任何一门面向对象的语言中,都有RTTI这个概念(即 运行时). RTTI(Run-Ti ...

  8. java中的注解详解和自定义注解

    一.java中的注解详解 1.什么是注解 用一个词就可以描述注解,那就是元数据,即一种描述数据的数据.所以,可以说注解就是源代码的元数据.比如,下面这段代码: @Override public Str ...

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

    具有跨平台能力的KestrelServer是最重要的服务器类型.针对KestrelServer的设置均体现在KestrelServerOptions配置选项上,注册的终结点是它承载的最重要的配置选项. ...

随机推荐

  1. Java全栈学习路线、学习资源和面试题一条龙

    肝了一个月,终于把Java学习路线.面试题资源和电子书资源都整理好了. Java 从基础到微服务的学习路线,其中还包括科班知识.学习建议.后续的学习引导和相应的学习资源(视频.书籍.网站),还整理了J ...

  2. 【故障公告】k8s 开船记:增加控制舱(control-plane)造成的翻船

    春节期间我们更换了 kubernetes 生产集群,旧集群的 kubernetes 版本是 1.17.0,新集群版本是 1.23.3,新集群上部署了 dapr,最近准备将更多独立部署的服务器部署到 k ...

  3. GitLab API使用小结

    GitLab API使用小结 背景描述 需求描述: 最近因为工作上的需求,需要对GitLab进行大批量的操作,又因为服务器不在境内,所以所有的操作都需要连接VPN来进行FQ访问.目前大概有6000多个 ...

  4. Ansible-DevOps

    Ansible-DevOps Ansible 搭建 CentOS7 yum install epel-release yum search ansible yum install ansible -y ...

  5. 【Kotlin】初识Kotlin之面向对象

    [Kotlin]初识Kotlin之面向对象 1.类 在Kotlin中,类用关键字class来定义 如果一个类具有类体,那么需要使用{ }来写类体内容,如果不需要类体,那么只需要定义类名就可以了 // ...

  6. python中随机生成整数

    1 #可以多运行几次,看看结果是不是随机生成的~ 2 3 import random 4 #调用random模块,与 5 a = random.randint(1,100) 6 # 随机生成1-100 ...

  7. 如何在3D场景中在模型上面绘制摄取点

    有些时候,我们在屏幕上面绘制一个摄取点,在单屏玩游戏的模式下,我们并不能觉得有什么不妥.但是最近VR的热火朝天,我们带上眼镜看双屏的时候,总觉得这个摄取点看着很不舒服. 这个问题该怎么解决?在这里我首 ...

  8. [杂记]如何在ppt里插入高亮代码

    在为c语言习题课准备课件的时候发现需要向ppt内插入代码,简单的文本粘贴没有高亮,格式也很混乱需要自己慢慢调,于是自己鼓捣了一下,实现了向ppt内插入高亮代码. [所需软件]Office PowerP ...

  9. 华为HCIP实验--OSPF单区域

    场景:你是公司的网络管理员.现在公司的网络中有三台ARG3路由器,通过以太网实现相互的连通.在以太网这样的广播式多路访问网络上,可能存在安全隐患,所有你选择采用OSPF区域认证的方法来避免恶意的路由攻 ...

  10. bool? int?等可为空的数值类型的运算 三值逻辑

    算术运算:(+,-,*,/)时,只要一个为null,则结果为null. 比较运算符: <.>.<= 和 >=,也是如此.如果一个或全部两个操作数都为 null,则结果为 fal ...