在这个特殊的春节,大家想必都在家出不了门,远看已经到了回城里上班的日子,但是因为一只蝙蝠的原因导致我们无法回到工作岗位,大家可能有的在家远程办公,有些在家躺着看书,有的是在家打游戏;在这个特殊无聊的日子,从无聊的被窝中 开启了流量共享wifi 来进行.net core 3.1 源代码的解读和学习,并且把学习到的东西分享给大家。
疑问
刚刚接触ASP.NET CORE 项目的同学可能会有如下疑问:
- ASP.NET CORE 项目的启动过程是怎么样的?
- 为什么ASP.NET CORE项目可以在控制台中运行启动后变成了一个网站程序?
现在我这里使用.NETCORE 3.1 最新稳定发布版本来进行以上问题的解析,带大家解决以上问题的疑惑,学习完大家会对ASP.NETCORE 项目会有一个不一样的理解和领悟.
启动过程
刚刚接触ASP.NET core 的同学们估计都会觉得和之前的ASP.NET 设计大不一样,代码风格也有很大的变化,以前的ASP.NET 是全家桶框架模式,里面包含了所有的实现,你用的到的用不到的都集成在里面;然而ASP.NET CORE 框架做了大的改变,以最小化抽象设计,通过扩展方法完成易用性扩展.
解读过源代码的同学们都可以发现大多api都是最小化单元抽象接口方式进行设计,其他复杂的方法api都是通过扩展方法进行扩展提供,这也是.NET Core 高效易扩展的一大优势原因.
对于ASP.NET Core应用程序来说,我们要记住非常重要的一点是:其本质上是一个独立的控制台应用,它并不是必需在IIS内部托管且并不需要IIS来启动运行(而这正是ASP.NET Core跨平台的基石)。ASP.NET Core应用程序拥有一个内置的Self-Hosted(自托管)的Web Server(Web服务器),用来处理外部请求。
不管是托管还是自托管,都离不开Host(宿主)。在ASP.NET Core应用中通过配置并启动一个Host来完成应用程序的启动和其生命周期的管理。而Host的主要的职责就是Web Server的配置和Pilpeline(请求处理管道)的构建。
我们现在来创建一个ASP.NETCORE WEB 项目 步骤如下
文件-> 新建 -> 项目 -> 选择ASP.Net Core Web应用程序 -> 选择.NETCORE 3.1 框架 如图:
创建项目后我们从Program 类中可以看到以下代码:
代码语言:javascript复制public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)//开启一个默认的通用主机Host建造者
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
查看以上代码可以发现 Main 方法中代码很简单 ,清晰可见
- CreateHostBuilder(args) :方法创建了一个IHostBuilder 抽象对象,创建过程包含CreateDefaultBuilder(args) :开启创建一个默认的通用宿主机Host建造者,再通过ConfigureWebHostDefaults()方法配置开启默认的Kestrel 为默认的Web服务器并对其进行默认配置,并集成对iis的集成
- Build() :负责创建IHost,看过源代码的同学可以发现Build的过程 会配置各种东西,本身通过管道模式进行了一系列的默认或者自定义的配置以及服务注册的构建(下面会详细讲解)
- Run() :启动Host
所以,ASP.NET Core应用的启动本质上是启动作为宿主的Host对象,
其主要涉及到两个关键对象IHostBuilder和IHost,它们的内部实现是ASP.NET Core应用的核心所在。下面我们就结合源码并梳理调用堆栈来一探究竟!
源代码详细图如下:
从上图中我们可以看出CreateDefaultBuilder()方法主要干了五件大事:
- UseContentRoot:指定Web host使用的content root(内容根目录),比如Views。默认为当前应用程序根目录。
- ConfigureHostConfiguration :启动时宿主机需要的环境变量等相关,支持命令行
- ConfigureAppConfiguration:设置当前应用程序配置。主要是读取 appsettinggs.json 配置文件、开发环境中配置的UserSecrets、添加环境变量和命令行参数 。
- ConfigureLogging:读取配置文件中的Logging节点,配置日志系统。
- UseDefaultServiceProvider:设置默认的依赖注入容器。
从图中可以看出CreateDefaultBuilder 后调用了ConfigureWebHostDefaults 方法,该方法默认主要做了以下几个事情
- UseStaticWebAssets:静态文件环境的配置启用
- UseKestrel:开启Kestrel为默认的web 服务器.
- ConfigureServices:服务中间件的注册,包含路由的中间件的注册
- UseIIS:对iis 集成的支持
- UseStartup:程序Startup 启动,该启动类中可以注册中间件、扩展第三方中间件,以及相关应用程序配置的处理等等操作
现在所有的配置都已经配置创建好了,接下来我们来看看Build 方法主要做了哪些不为人知的事情,先来看下源代码
代码语言:javascript复制 /// <summary>
/// Run the given actions to initialize the host. This can only be called once.
/// </summary>
/// <returns>An initialized <see cref="IHost"/></returns>
public IHost Build()
{
if (_hostBuilt)
{
throw new InvalidOperationException("Build can only be called once.");
}
_hostBuilt = true;
BuildHostConfiguration();
CreateHostingEnvironment();
CreateHostBuilderContext();
BuildAppConfiguration();
CreateServiceProvider();
return _appServices.GetRequiredService<IHost>();
}
从代码中可以发现有一个_hostBuilt 的变量,细心的同学可以发现该变量主要是用于控制是否build 过,所以这里可以大胆猜测只能build 一次该Host;现在看下源代码解析图:
经过查看源代码得到的执行结构如上,因此我把代码改造成如下结构。
代码语言:javascript复制public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)//开启一个默认的通用主机Host建造者
.ConfigureAppConfiguration(config => {
//注册应用程序内所使用的配置文件,比如数据库链接等等
Console.WriteLine("ConfigureAppConfiguration");
})
.ConfigureServices(service =>
{
//注册服务中间件等操作
Console.WriteLine("ConfigureServices");
})
.ConfigureHostConfiguration(builder => {
//启动时需要的组件配置等,比如监听的端口 url地址等
Console.WriteLine("ConfigureHostCOnfiguration");
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Startup 代码如下:
代码语言:javascript复制public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
Console.WriteLine("Startup ");
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
Console.WriteLine("ConfigureServices");
services.AddControllersWithViews();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
Console.WriteLine("Configure");
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
通过执行发现执行代码顺序正如我们源代码的执行顺序一致,执行顺序如下图:
为何通过控制台命令运行后自动启动了一个网站程序?
以前ASP.NET web项目是需要搭建在iis 中托管运行,但是ASP.NETCORE 项目可以直接通过命令行进行托管运行,运行后可以直接浏览器打开,你们有没有考虑过为什么?,细心的同学查看项目属性也会发现项目的输出类型也是控制台项目,如图:
查看这图,有没有发现很神奇,为什么输出类型竟然可以通过控制台命令行进行启动项目呢?
在上面的源代码分析过程中可以发现启动时会启动一个Kestrel 服务器(ConfigureWebHostDefaults方法中会调用UseKestrel),所以命令后启动一个控制台应用程序后相当于启动了一台web服务器;下面简要的概括下Kestrel 服务器的优势:
- Kestrel:Kestrel 是个精简高效的 HttpServer,以包形式提供,自身不能单独运行。内部封装了对 libuv 的调用,作为I/O底层,屏蔽各系统底层实现差异;有了Kestrel才能真正的实现跨平台.
好了,想必同学们到这里已经对上面 两个疑惑有了清晰的答案了。这里我抛出一个疑问,看了上面的代码解读,大家有没有发现ASP.NET CORE 和ASP.NET 有了很大的不同,这是什么样的设计改进呢?敬请期待下期我们一起来学习ASP.NET CORE 的牛逼的管道模型.