前言

在学习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的更多相关文章

  1. 200行代码,7个对象——让你了解ASP.NET Core框架的本质

    原文:200行代码,7个对象--让你了解ASP.NET Core框架的本质 2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘&g ...

  2. 为什么你需要将代码迁移到ASP.NET Core 2.0?

    随着 .NET Core 2.0 的发布,.NET 开源跨平台迎来了新的时代.开发者们可以选择使用命令行.个人喜好的文本编辑器.Visual Studio 2017 15.3 和 Visual Stu ...

  3. 深入研究 Mini ASP.NET Core(迷你 ASP.NET Core),看看 ASP.NET Core 内部到底是如何运行的

    前言 几年前,Artech 老师写过一个 Mini MVC,用简单的代码告诉读者 ASP.NET MVC 内部到底是如何运行的.当时我研究完以后,受益匪浅,内心充满了对 Artech 老师的感激,然后 ...

  4. SpringBoot,用200行代码完成一个一二级分布式缓存

    缓存系统的用来代替直接访问数据库,用来提升系统性能,减小数据库复杂.早期缓存跟系统在一个虚拟机里,这样内存访问,速度最快. 后来应用系统水平扩展,缓存作为一个独立系统存在,如redis,但是每次从缓存 ...

  5. 200行代码实现简版react🔥

    200行代码实现简版react

  6. 不到 200 行代码,教你如何用 Keras 搭建生成对抗网络(GAN)【转】

    本文转载自:https://www.leiphone.com/news/201703/Y5vnDSV9uIJIQzQm.html 生成对抗网络(Generative Adversarial Netwo ...

  7. 200 行代码实现基于 Paxos 的 KV 存储

    前言 写完[paxos 的直观解释]之后,网友都说疗效甚好,但是也会对这篇教程中一些环节提出疑问(有疑问说明真的看懂了 ),例如怎么把只能确定一个值的 paxos 应用到实际场景中. 既然 Talk ...

  8. 200行代码,7个对象——让你了解ASP.NET Core框架的本质

    2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘>的分享.在此次分享中,我按照ASP.NET Core自身的运行原理和设计 ...

  9. 200行代码,7个对象——让你了解ASP.NET Core框架的本质[3.x版]

    2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘>的分享.在此次分享中,我按照ASP.NET Core自身的运行原理和设计 ...

随机推荐

  1. Java 学习笔记---Java double类型相加问题

    多个double类型的数直接相加的时候,可能存在精度误差.( 由于计算机算法以及硬件环境决定只能识别 0 1.计算机默认的计算结果在都在一个指定精度范围之内,想往深的了解,可以学习数值分析等) 在金融 ...

  2. Redis学习总结(五)--Redis集群创建

    在之前我们讲到了主从,但是对于大数据量的场景下我们就需要用到集群了,让我们来了解下集群吧. 为什么需要集群 单机内存太小 redis最高可以达到10万/s 请求,如果超过该频率呢? 数据分布方式 数据 ...

  3. python 19 包

    目录 1. 包 2. logging 日志 2.1 日志级别 2.2 配置日志格式: 2.3 logger 对象配置 1. 包 文件夹下具有__init__.py文件就是一个包 from bake.c ...

  4. ElasticSearch:组合查询或复合查询

    Bool查询 允许在单独的查询中组合任意数量的查询,指定的查询语句表名哪些部分是必须匹配(must).应该匹配(should)或不能匹配(must_not) Bool过滤器 和查询功能一致,但是同等情 ...

  5. python requests接口测试系列:连接mysql,获取mysql查询的值作为接口的入参

    主要思路: 连接mysql数据库,这里数据库需要使用Proxifier来设置代理,然后才能正常连接 获取mysql数据库中某一数据,作为接口的参数信息 将接口返回结果保存至csv数据表中 # -*- ...

  6. 一文读懂HashMap

    推荐 转载:https://www.jianshu.com/p/ee0de4c99f87

  7. poj2186Popular Cows+tarjan缩点+建图

    传送门: 题意: 给出m条关系,表示n个牛中的崇拜关系,这些关系满足传递性.问被所有牛崇拜的牛有几头: 思路: 先利用tarjan缩点,同一个点中的牛肯定就是等价的了,建立新的图,找出其中出度为0的点 ...

  8. CodeForces 639C Bear and Polynomials

    Bear and Polynomials 题解: 如果改变一个其中的一个数,那么需要知道的是,前面的数都可以进到当前位来,如果过不来的话,那么就会因为前面有数导致无法变成0. 所以我们将前面的数不断向 ...

  9. 杭电多校第十场 hdu6434 Count 欧拉函数打表 快速打表模板

    Problem I. Count Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 524288/524288 K (Java/Other ...

  10. 九度 题目1454:Piggy-Bank 完全背包

    题目1454:Piggy-Bank 时间限制:1 秒 内存限制:128 兆 特殊判题:否 提交:1584 解决:742 题目描述: Before ACM can do anything, a budg ...