200行代码实现Mini ASP.NET Core

2023-06-12,,

前言

在学习ASP.NET Core源码过程中,偶然看见蒋金楠老师的ASP.NET Core框架揭秘,不到200行代码实现了ASP.NET Core Mini框架,针对框架本质进行了讲解,受益匪浅,本文结合ASP.NET Core Mini框架讲述ASP.NET Core核心。

微软官网关于ASP.NET Core的概念“ASP.NET Core是一个开源和跨平台的框架,用于构建基于Web的现代互联网连接应用程序,例如Web应用程序,IoT应用程序和移动后端。 ASP.NET Core应用程序可以在.NET Core或完整的.NET Framework上运行。 它的架构旨在为部署到云或在本地运行的应用程序提供优化的开发框架。 它由模块化组件组成,开销最小,因此您可以在构建解决方案时保持灵活性。 您可以在Windows,Mac和Linux上跨平台开发和运行ASP.NET核心应用程序”。可以从定义上看出ASP.NET Core框架具有跨平台、部署灵活、模块化等特点。


ASP.NET Core框架揭秘

ASP.NET Core Mini是200行代码实现的迷你版ASP.NET Core框架,有三大特点“简单”,“真实模拟”,“可执行”来让我们更加容易理解ASP.NET Core。

代码结构:

下图是项目运行页面输出的结果:

本文从以下五个角度讲述:

Program: 项目入口
Middleware:中间件
HttpContext:Http相关
WebHost:WebHost
Server:Server相关


Program

 using System.Threading.Tasks;
using App.Server;
using App.WebHost; namespace App
{
public static class Program
{
public static async Task Main(string[] args)
{
await CreateWebHostBuilder()
.Build()
.Run();
} private static IWebHostBuilder CreateWebHostBuilder()
{
return new WebHostBuilder()
.UseHttpListener()
.Configure(app => app
.Use(Middleware.Middleware.FooMiddleware)
.Use(Middleware.Middleware.BarMiddleware)
.Use(Middleware.Middleware.BazMiddleware));
}
}
}

可以看到项目的入口是Main方法,它只做了三件事,构造WebHostBuilder,然后Build方法构造WebHost,最后Run方法启动WebHost。我们可以简单的理解WebHostBuilder作用就是为了构造WebHost,他是WebHost的构造器,而WebHost是我们Web应用的宿主。

再看CreateWebHostBuilder的方法具体干了什么。首先创建了WebHostBuilder,然后UseHttpListener配置Server(比如ASP.NET Core中的Kestrel或IIS等等),一般包括地址和端口等,最后注册一系列的中间件。

从Program可以看出整个App运行起来的流程,如下图所示:

Middleware

在看HttpContext之前,我们先来看ASP.NET Core 的Http处理管道是什么样子,上图是官方给出的管道处理图,当我们的服务器接收到一个Http请求,管道进行处理,处理后再进行返回,可以看到,我们的Http请求经过多层中间件处理最后返回。

 using System.Threading.Tasks;

 namespace App.Middleware
{
public delegate Task RequestDelegate(HttpContext.HttpContext context);
}

首先来看RequestDelegate.cs,定义了一个参数类型是HttpContext,返回结果是Task的委托。

为什么会定义这个委托,可以想到一个Http请求会经过多层中间件处理,那么多层中间件处理可以想像成一个HttpHandler,他的参数就是HttpContext,返回结果是Task的委托。

 using App.HttpContext;

 namespace App.Middleware
{
public static class Middleware
{
public static RequestDelegate FooMiddleware(RequestDelegate next)
=> async context =>
{
await context.Response.WriteAsync("Foo=>");
await next(context);
}; public static RequestDelegate BarMiddleware(RequestDelegate next)
=> async context =>
{
await context.Response.WriteAsync("Bar=>");
await next(context);
}; public static RequestDelegate BazMiddleware(RequestDelegate next)
=> context => context.Response.WriteAsync("Baz");
}
}

Middleware中定义了三个简单的中间件,可以看到,中间件其实就是委托,将HttpContext一层一层进行处理。

Http请求进入管道,第一个中间件处理完,把自身作为结果传输到下一个中间件进行处理,那么参数是RequestDelegate,返回值是RequestDelegate的委托就是中间件,所以中间件其实就是Func<RequestDelegate,RequestDelegate>,简单来说,中间件就是RequestDelegate的加工工厂。

HttpContext

从Middleware了解到,HttpContext是RequestDelegate的参数,是每一个Middleware处理数据的来源。

我们可以这么理解,HttpContext就是我们整个Http请求中的共享资源,所以的中间件共享它,而每个中间件就是对它进行加工处理。

 using System;
using System.Collections.Specialized;
using System.IO;
using System.Text;
using System.Threading.Tasks; namespace App.HttpContext
{
public class HttpContext
{
public HttpRequest Request { get; }
public HttpResponse Response { get; } public HttpContext(IFeatureCollection features)
{
Request = new HttpRequest(features);
Response = new HttpResponse(features);
}
} public class HttpRequest
{
private readonly IHttpRequestFeature _feature; public Uri Url => _feature.Url; public NameValueCollection Headers => _feature.Headers; public Stream Body => _feature.Body; public HttpRequest(IFeatureCollection features) => _feature = features.Get<IHttpRequestFeature>();
} public class HttpResponse
{
private readonly IHttpResponseFeature _feature;
public HttpResponse(IFeatureCollection features) => _feature = features.Get<IHttpResponseFeature>(); public NameValueCollection Headers => _feature.Headers;
public Stream Body => _feature.Body; public int StatusCode
{
get => _feature.StatusCode;
set => _feature.StatusCode = value;
}
} public static partial class Extensions
{
public static Task WriteAsync(this HttpResponse response, string contents)
{
var buffer = Encoding.UTF8.GetBytes(contents);
return response.Body.WriteAsync(buffer, 0, buffer.Length);
}
}
}

代码结构可以看出request和reponse构成httpcontext,也反映出httpcontext的职责:Http请求的上下文。

但是,不同的Server和单一的HttpContext之间需要如何适配呢?因为我们可以注册多样的Server,可以是IIS也可以是Kestrel还可以是这里的HttpListenerServer。

所以我们需要定义统一的request和response接口,来适配不同的Server。如下图的IHttpRequestFeature和IHttpResponseFeature。

 using System;
using System.Collections.Specialized;
using System.IO; namespace App.HttpContext
{
public interface IHttpRequestFeature
{
Uri Url { get; } NameValueCollection Headers { get; } Stream Body { get; }
} public interface IHttpResponseFeature
{
int StatusCode { get; set; } NameValueCollection Headers { get; } Stream Body { get; }
}
}

在HttpListenerFeature.cs中实现request和response的接口,实现了适配不同的server。

 using System;
using System.Collections.Specialized;
using System.IO;
using System.Net; namespace App.HttpContext
{
public class HttpListenerFeature : IHttpRequestFeature, IHttpResponseFeature
{
private readonly HttpListenerContext _context; public HttpListenerFeature(HttpListenerContext context) => _context = context; Uri IHttpRequestFeature.Url => _context.Request.Url; NameValueCollection IHttpRequestFeature.Headers => _context.Request.Headers; NameValueCollection IHttpResponseFeature.Headers => _context.Response.Headers; Stream IHttpRequestFeature.Body => _context.Request.InputStream; Stream IHttpResponseFeature.Body => _context.Response.OutputStream; int IHttpResponseFeature.StatusCode
{
get => _context.Response.StatusCode;
set => _context.Response.StatusCode = value;
}
}
}

至于FeatureCollection.cs,它的作用就是将从httpListenerContext中获取的Http信息存储在FeatureCollection的Dictionary里,更加方便的对HttpRequestFeature和HttpResponseFeature进行操作。

扩展方法Get和Set的作用是方便操作FeatureCollection。

 using System;
using System.Collections.Generic; namespace App.HttpContext
{
public interface IFeatureCollection : IDictionary<Type, object>
{
} public class FeatureCollection : Dictionary<Type, object>, IFeatureCollection
{
} public static partial class Extensions
{
public static T Get<T>(this IFeatureCollection features) =>
features.TryGetValue(typeof(T), out var value) ? (T) value : default(T); public static IFeatureCollection Set<T>(this IFeatureCollection features, T feature)
{
features[typeof(T)] = feature;
return features;
}
}
}

WebHost

using System;
using System.Collections.Generic;
using App.Server; namespace App.WebHost
{
public interface IWebHostBuilder
{
IWebHostBuilder UseServer(IServer server); IWebHostBuilder Configure(Action<IApplicationBuilder> configure); IWebHost Build();
} public class WebHostBuilder : IWebHostBuilder
{
private IServer _server;
private readonly List<Action<IApplicationBuilder>> _configures = new List<Action<IApplicationBuilder>>(); public IWebHostBuilder Configure(Action<IApplicationBuilder> configure)
{
_configures.Add(configure);
return this;
} public IWebHostBuilder UseServer(IServer server)
{
_server = server;
return this;
} public IWebHost Build()
{
var builder = new ApplicationBuilder();
foreach (var configure in _configures)
{
configure(builder);
} return new WebHost(_server, builder.Build());
}
}
}

WebHost是我们App的宿主,通过WebHostBuild构造,代码里定义了三个方法,

UseServer: 配置server
Configure: 注册中间件
Build: 构造WebHost

 using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using App.Middleware; namespace App.WebHost
{
public interface IApplicationBuilder
{
IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware); RequestDelegate Build();
} public class ApplicationBuilder : IApplicationBuilder
{
private readonly List<Func<RequestDelegate, RequestDelegate>> _middlewares =
new List<Func<RequestDelegate, RequestDelegate>>(); public RequestDelegate Build()
{
_middlewares.Reverse();
return httpContext =>
{
RequestDelegate next = _ =>
{
_.Response.StatusCode = 404;
return Task.CompletedTask;
}; foreach (var middleware in _middlewares)
{
next = middleware(next);
} return next(httpContext);
};
} public IApplicationBuilder Use(Func<RequestDelegate, RequestDelegate> middleware)
{
_middlewares.Add(middleware);
return this;
}
}
}

ApplicationBuilder做了什么,Use方法我们把自定义的中间件放进集合里,而build方法就是构建webhost。首先把中间键集合顺序倒置,然后构造一个StatusCode为404的中间件,其次遍历中间件集合,最后返回构造好的管道。

如果中间件集合为空,我们返回Http 404错误。

至于为什么要Reverse(),是因为我们注册中间件的顺序与我们需要执行的顺序相反。

using System.Threading.Tasks;
using App.Middleware;
using App.Server; namespace App.WebHost
{
public interface IWebHost
{
Task Run();
} public class WebHost : IWebHost
{
private readonly IServer _server;
private readonly RequestDelegate _handler; public WebHost(IServer server, RequestDelegate handler)
{
_server = server;
_handler = handler;
} public Task Run() => _server.RunAsync(_handler);
}
}

WebHost只做了一件事,将我们构造的中间件管道处理器在指定Server运行起来。

 Server

我们自定义一个服务器,IServer定义统一接口,HttpListenerServer实现我们自定义的Server

using System;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using App.HttpContext;
using App.Middleware;
using App.WebHost; namespace App.Server
{
public class HttpListenerServer : IServer
{
private readonly HttpListener _httpListener;
private readonly string[] _urls; public HttpListenerServer(params string[] urls)
{
_httpListener = new HttpListener();
_urls = urls.Any() ? urls : new[] {"http://localhost:5000/"};
} public async Task RunAsync(RequestDelegate handler)
{
Array.ForEach(_urls, url => _httpListener.Prefixes.Add(url)); if (!_httpListener.IsListening)
{
_httpListener.Start();
} Console.WriteLine("Server started and is listening on: {0}", string.Join(';', _urls)); while (true)
{
var listenerContext = await _httpListener.GetContextAsync();
var feature = new HttpListenerFeature(listenerContext);
var features = new FeatureCollection()
.Set<IHttpRequestFeature>(feature)
.Set<IHttpResponseFeature>(feature);
var httpContext = new HttpContext.HttpContext(features); await handler(httpContext); listenerContext.Response.Close();
}
}
} public static class Extensions
{
public static IWebHostBuilder UseHttpListener(this IWebHostBuilder builder, params string[] urls)
=> builder.UseServer(new HttpListenerServer(urls));
}
}

使用UseHttpListener扩展方法,指定监听地址,默认为“http://localhost:5000/”。

RunAsync方法是我们WebHost的Run方法,循环中通过调用其GetContextAsync方法实现了针对请求的监听和接收。


总结

看完这篇文章应该对ASP.NET Core有一定对理解,核心就是中间件管道。不过ASP.NET Core源码远远不止这些,每个模块的实现较复杂,还有其他必不可少的模块(依赖注入、日志系统、异常处理等),需要更加深入的学习。我也会记录我的学习记录,最后来一张完整的Http请求管道图。

参考资料 :200行代码,7个对象——让你了解ASP.NET Core框架对本质

代码地址: GitHub

200行代码实现Mini ASP.NET Core的相关教程结束。

《200行代码实现Mini ASP.NET Core.doc》

下载本文的Word格式文档,以方便收藏与打印。