200行代码实现Mini ASP.NET Core
前言
在学习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行代码,7个对象——让你了解ASP.NET Core框架的本质
原文:200行代码,7个对象--让你了解ASP.NET Core框架的本质 2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘&g ...
- 为什么你需要将代码迁移到ASP.NET Core 2.0?
随着 .NET Core 2.0 的发布,.NET 开源跨平台迎来了新的时代.开发者们可以选择使用命令行.个人喜好的文本编辑器.Visual Studio 2017 15.3 和 Visual Stu ...
- 深入研究 Mini ASP.NET Core(迷你 ASP.NET Core),看看 ASP.NET Core 内部到底是如何运行的
前言 几年前,Artech 老师写过一个 Mini MVC,用简单的代码告诉读者 ASP.NET MVC 内部到底是如何运行的.当时我研究完以后,受益匪浅,内心充满了对 Artech 老师的感激,然后 ...
- SpringBoot,用200行代码完成一个一二级分布式缓存
缓存系统的用来代替直接访问数据库,用来提升系统性能,减小数据库复杂.早期缓存跟系统在一个虚拟机里,这样内存访问,速度最快. 后来应用系统水平扩展,缓存作为一个独立系统存在,如redis,但是每次从缓存 ...
- 200行代码实现简版react🔥
200行代码实现简版react
- 不到 200 行代码,教你如何用 Keras 搭建生成对抗网络(GAN)【转】
本文转载自:https://www.leiphone.com/news/201703/Y5vnDSV9uIJIQzQm.html 生成对抗网络(Generative Adversarial Netwo ...
- 200 行代码实现基于 Paxos 的 KV 存储
前言 写完[paxos 的直观解释]之后,网友都说疗效甚好,但是也会对这篇教程中一些环节提出疑问(有疑问说明真的看懂了 ),例如怎么把只能确定一个值的 paxos 应用到实际场景中. 既然 Talk ...
- 200行代码,7个对象——让你了解ASP.NET Core框架的本质
2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘>的分享.在此次分享中,我按照ASP.NET Core自身的运行原理和设计 ...
- 200行代码,7个对象——让你了解ASP.NET Core框架的本质[3.x版]
2019年1月19日,微软技术(苏州)俱乐部成立,我受邀在成立大会上作了一个名为<ASP.NET Core框架揭秘>的分享.在此次分享中,我按照ASP.NET Core自身的运行原理和设计 ...
随机推荐
- Java 并发编程(一):摩拳擦掌
这篇文章的标题原本叫做——Java 并发编程(一):简介,作者名叫小二.但我在接到投稿时觉得这标题不够新颖,不够吸引读者的眼球,就在发文的时候强行修改了标题(也不咋滴). 小二是一名 Java 程序员 ...
- Leetcode之二分法专题-744. 寻找比目标字母大的最小字母(Find Smallest Letter Greater Than Target)
Leetcode之二分法专题-744. 寻找比目标字母大的最小字母(Find Smallest Letter Greater Than Target) 给定一个只包含小写字母的有序数组letters ...
- Leetcode之回溯法专题-216. 组合总和 III(Combination Sum III)
Leetcode之回溯法专题-216. 组合总和 III(Combination Sum III) 同类题目: Leetcode之回溯法专题-39. 组合总数(Combination Sum) Lee ...
- MSIL实用指南-比较运算
数值的比较就是大于.小于.等于.大于等于.小于等于.不等于,它们的运算结果都是布尔值.大于.小于.等于有直接对应的指令,分别是Cgt.Clt.Ceq.大于等于.小于等于.不等于没有直接对应的指令,它的 ...
- vue-cli3.x创建及运行项目
Node 版本要求 Vue CLI 需要 Node.js 8.9 或更高版本 (推荐 8.11.0+).如果你已经全局安装了旧版本的 vue-cli (1.x 或 2.x),你需要先通过 npm un ...
- 通过代码审计找出网站中的XSS漏洞实战(三)
一.背景 笔者此前录制了一套XSS的视频教程,在漏洞案例一节中讲解手工挖掘.工具挖掘.代码审计三部分内容,准备将内容用文章的形式再次写一此,前两篇已经写完,内容有一些关联性,其中手工XSS挖掘篇地址为 ...
- [翻译] .NET Core 3.0 Preview 9 发布
原文: Announcing .NET Core 3.0 Preview 9 今天,我们宣布推出 .NET Core 3.0 Preview 9.就像 Preview 8 一样,我们专注于打磨 .NE ...
- gym/101955/problem/E - The Kouga Ninja Scrolls 线段数 维护 切比雪夫距离 2018沈阳icpc
传送门 思路: 这道题要把给定的每个坐标利用切比雪夫坐标表示,这样两个点的距离就是max(dx,dy),而不是一开始的dx + dy,有利于线段树的维护,又由于询问的是区间的最大差值,限制是两个点是属 ...
- “玲珑杯”ACM比赛 Round #18 1147 - 最后你还是AK了(思维,边的贡献)
题目链接:http://www.ifrog.cc/acm/problem/1147 题解:这题很容易想到的是边的贡献也就是每条边最多被取到几次,和点的贡献类似,那些加边只要加在边贡献大的边上就行.然后 ...
- codeforces 789 C. Functions again(dp求区间和最大)
题目链接:http://codeforces.com/contest/789/problem/C 题意:就是给出一个公式 然后给出一串数求一个区间使得f(l,r)最大. 这题需要一个小小的处理 可以设 ...