从Asp .net到Asp core (第二篇)《Asp Core 的生命周期》
前面一篇文章简单回顾了Asp .net的生命周期,也简单提到了Asp .net与Asp Core 的区别,我们说Asp Core不在使用Asp.netRuntime,所以它也没有了web程序生命周期中一步步事件,更没有了Http Module和HttpHandler,那么在全新的Asp Core程序中,一个请求会有怎样的生命周期呢?
学习一门技术或者框架没有什么是一个Hello World!程序理解不了的,如果理解不了,那就两个Hello World! 所以我们先来写一个简单的Hello World!“”例子
项目介绍:1)我们先新建的项目写一个“Hello World! ”程序,2)然后直接部署到linux系统的服务器中去,做一个ASP.NET Core的一个最简单的跨平台部署实践
1.1 创建并发布一个Asp Core “Hello World!”程序
我们新建一个空的Asp Core项目(此处用的是.Net Core 2.1.3),结构如下:
我们通过上一篇文章已经知道原来在Asp .net中,对于一个合法的请求进入IIS服务器后,首先会由HTTP.SYS将这个请求交给 IIS 工作者进程。但是这一点在Asp Core发生了一些变化。Asp Core不再依赖HTTP.SYS,而是默认将Kestrel配置为默认的服务器(当然我们可以在Asp Core中任然配置为HTTP.SYS,但是只能用于Windows环境 而且无法与 IIS 或 IIS Express 结合使用)
可能很多人还不知道Kestrel是啥?这里先简单介绍一下Kestrel:它是一个跨平台的适用于 ASP.NET Core 的 Web 服务器,划重点:Kestrel是服务器,也就是在Asp Core中我们直接我们写的代码打包到服务器上面不依赖其他任何的宿主服务器即可马上访问我们的网站,即如下的访问模式
代码很简单如下:
我们用Use方法模拟使用中间件处理逻辑代码,Run方法最后运行和输出“Hello World!”
需要注意的是我为了方便服务器上面观察请求,安装了Microsoft.DotNet.Watcher.Tools包,它可以记录日志输出到我们的控制台程序,即下面代码中的logger.LogInformation
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args).UseUrls("http://*:8086") .UseStartup<Startup>(); }
public class Startup { public IConfiguration configuration; public Startup(IConfiguration _configuration) { configuration = _configuration; } // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env,ILoggerFactory loggerFactory) { loggerFactory.AddConsole(LogLevel.Debug); var logger = loggerFactory.CreateLogger("MyLogger"); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.Use(async (context,next) => { var timer = Stopwatch.StartNew(); logger.LogInformation($"=> begin the request in{env.EnvironmentName}"); await next(); logger.LogInformation($"=> completed request {timer.ElapsedMilliseconds}"); }); app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } }
我们然后将这个程序直接打包发布到linux服务器上面去
首先要在所发布的linux服务器上面安装.net core运行时,然后将打包好的文件直接复制到服务器上面去(这里用的服务器终端连接程序是MobaXterm,很好用),如图:
我们定位到文件目录然后输了命令:dotnet 项目名称.dll 运行项目
运行成功后我们浏览器输入服务器ip:端口号访问结果如图
我们于此同时查看服务器终端也会看到我们代码中记录的日志
这个“Hello World!”程序就大功告成了(这里需要注意的是我们发布的这个应用程序一旦终端断开,或者程序自己抛出异常等情况,这个程序就会停止导致网站无法访问,如果要程序一直运行我就需要用一些项目容器程序去管理这个程序,这里先不做过多讨论),虽然只是运行一个“Hello World!”到网页当中,但是其实在Main函数的那段调用代码中,Asp Core框架已经帮我们做了很多事情,具体做了哪些事情呢?
1.2 简单解析Asp Core程序入口调用过程
我们最先建立项目的时候Program目录下会有如下代码
public class Program { public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>(); }
从上面的代码我们可以看到,新的Asp Core和其他应用程序一样也是以Main函数作为程序的入口,然后通过Main函数调用启动运行整个web程序,这和原来的Asp .net也是一个全新的改变
这里额外提一下,我们可以单独使用 Kestrel,当然我们也可以用IIS,Nginx等服务器作为项目的代理服务器,具体例子我们此处就不演示了,流程如下:
我们回到上面Main调用的代码,上面这段代码调用其实就包含了整个Asp Core的生命周期,我们做如下的一个简单解析,依次调用了如下四个方法:
- 执行WebHost.CreateDefaultBuilder(args)静态方法,创建IWebHostBuilder对象。
- 执行IWebHostBuilder.UseStartup<Startup>()。
- 执行IWebHostBuilder.Build(),创建一个IWebHost对象。
- 执行IWebHost.Run()。
逐一简单讲解这四个方法分别做了哪些事情:
方法一:CreateDefaultBuilder(args)——其实顾名思义这个方法,创建了一个默认配置的WebHostBuilder,源码里面New了一个WebHostBuilder对象,然后对该对象读取了一些默认配置(指定Web主机要使用的内容根目录,在Web主机上使用给定的配置设置),同时也写入了一些配置性的委托(环境变量,将Kestrel配置为默认服务器,日志对象,配置默认服务提供程序等)
源码链接:https://github.com/aspnet/MetaPackages/blob/master/src/Microsoft.AspNetCore/WebHost.cs#L148
方法二:然后是IWebHostBuilder对象的UseStartup<Startup>()方法,这个方法的逻辑很简单,将Starup类注册到容器中 ,Startup类中包含了配置服务与Middleware的两个方法,我们后面还将继续介绍这两个方法。
方法三:WebHostBuilder对象的Build()方法,这个方法创建了WebHost对象,并将之前配置所需的一系列对象注入到WebHost中,包括:已创建的服务对象集合,容器对象,配置对象等,然后将初始化后的WebHost对象返回。
方法四:IWebHost.Run()方法,里面只有一行代码,执行了host.StartAsync( )启动web程序并,这个函数返回Task对象,并等待Task返回结果。
注:上面介绍到的代码中有两个WebHost容易把人搞晕,我们需要知道的是一开始调用CreateDefaultBuilder方法的静态类WebHost和我们后面通过WebHostBuilder创建的非静态WebHost类完全不是同一个类,可以理解为前面的这个静态的WebHost类只是一个封装好用来创建WebHostBuilder以便于后面代码的注册和配置的工具类,而后面通过WebHostBuilder创建并配置的WebHost类才是真正的web程序宿主服务类
第一个WebHost类的源码地址:https://github.com/aspnet/MetaPackages/blob/master/src/Microsoft.AspNetCore/WebHost.cs#L148
第二个WebHost类的源码地址:https://github.com/aspnet/AspNetCore/blob/master/src/Hosting/Hosting/src/Internal/WebHost.cs
基础流程图:
详细流程图:
1.3 管道核心概念:服务(Services)与中间件(Middleware)
这里我们简单的通过对源码梳理,已经对Asp Core的整个调用过程有了一定的了解。对于一般的开发人员来说,可能并不会去过多的接触这些源码,日后的大部分配置工作基本都是在Starup类的两个方法中完成的,那下面我们就来详细介绍Starup类的基础原理与运用
我们上面梳理流程的时候简单提到了在方法二的步骤中会将Starup类注册到容器中,Starup类有如下两个方法
- ConfigureServices:可以用来配置和注册服务
- Configure:可以用来将中间件添加到管道
在理解服务和中间件之前我们还需要先知道一个概念:DI容器
什么是DI容器?它是ASP.NET Core内置的一个依赖注入容器,它的作用就管理注册的服务对象,可以把服务对象注入到我们所需的类中去,如果之前接触过依赖注入框架的人应该很容易理解这一概念
这是两篇关于介绍依赖注入的文章:
依赖注入微软官方文档:https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1
这是我之前一篇关于依赖注入的文章:https://www.cnblogs.com/ruanraun/p/Dependency.html
什么是服务?服务是一个提供应用功能的可重用组件。 在 ConfigureServices 中配置配置(也称为“注册”)并通过依存关系注入 (DI) 或 ApplicationServices 在整个应用中使用
举个栗子:比如说我们想在我们的Asp Core 程序中使用EF数据框架,我们就可以将EF上下文对象dbcontext作为服务注册的程序中,然后我们在我们的程序中就可以通过构造函数的依赖注入的方式获取我们的EF上下文对象
1):注册上下文对象EntityContext对象
public void ConfigureServices(IServiceCollection services) { //注册上下文对象EntityContext对象 services.AddDbContext<EntityContext>(options => options.UseSqlServer(Configuration["ConnectionStrings:DefaultConnection"])); }
2):MVC控制器中,获取EntityContext对象
public class ApiBaseController : Controller { public ApiBaseController(EntityContext _context) { this.dbcontext = _context; } }
什么是中间件?中间件是一种装配到应用管道以处理请求和响应的软件。 简单来说就是我们可以用中间件处理或过滤请求,对于当前中间件处理好的请求,我们可以选择终止请求,也可以选择传递到下一个中间件
可能单纯看概念可能你对注册服务和将中间件添加到管道还是很陌生,那我们不妨先结合一个实际例子去实现这两个概念,然后通过例子去理解它们
示例基本功能介绍:我们要实现的例子很简单,我们写一个Person类,这个类有姓名(name)性别(sex)年龄(age)三个属性,然后这个Person类的属性数据读取自配置文件,随后把这个类作为服务注册到我们的Asp Core程序中,然后自定义一个中间件MyMiddleWare去使用我们注册的Person类
(1)我们在之前的空的Asp Core项目中新建一个Person类,同时在appsettings.json配置文件中配置一个person的json数据,然后在Person类的构造函数中读取配置文件
Person code:
using Microsoft.Extensions.Configuration; namespace MyCore { public class Person { public Person(IConfiguration configuration) { Name = configuration["person:name"]; Age = configuration["person:age"]; Sex = configuration["person:sex"]; } public string Name { get; set; } public string Age { get; set; } public string Sex { get; set; } } }
appsettings.json code:
{ "person": { "name": "zhangsan", "age": 18, "sex": "boy" } }
(2)然后我们新建一个MyMiddleware类,然后在该类写如下代码:
MyMiddleware code:
public class MyMiddleware { private readonly RequestDelegate _Next; private readonly IHostingEnvironment _Env; private readonly Person _Person; public MyMiddleware(RequestDelegate next,IHostingEnvironment env,Person person) { _Next = next; _Env = env; _Person = person; } public async Task Invoke(HttpContext context) { var timer = Stopwatch.StartNew(); context.Response.Headers.Add("MyHeader1", new[] { _Env.EnvironmentName }); context.Response.Headers.Add("MyHeader2", new[] { _Person.Name }); await _Next(context); if (_Env.IsDevelopment()&&context.Response.ContentType!=null&&context.Response.ContentType=="text/html") { await context.Response.WriteAsync($"=> completed request {timer.ElapsedMilliseconds}"); } } }
我自定义了一个中间件MyMiddleware,然后在请求返回时为请求新增了两个请求头MyHeader1和MyHeader2
(3)然后我们在Startup类中去注册服务和使用中间件
public class Startup { public IConfiguration configuration; public Startup(IConfiguration _configuration) { configuration = _configuration; } // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.UserMyServices(configuration); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UserMyMiddleware(); app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } } public static class MiddlewareHelper { public static IApplicationBuilder UserMyMiddleware(this IApplicationBuilder app) { return app.UseMiddleware<MyMiddleware>(); } } public static class ServiceCollectionHelper { public static IServiceCollection UserMyServices(this IServiceCollection services,IConfiguration configuration) { Person ps = new Person(configuration); services.AddSingleton(ps); return services.AddSingleton(ps); } }
然后我们运行项目network查看请求结果,可以看到我们的请求头多了两个请求头MyHeader1和MyHeader2
上面这个简单的例子我们就完成了注册自定义服务和使用自定义中间件,其实我们以后在Asp core用到的第三方库或者框架都是通过上面类似的方法去实现的,比如新建一个Asp Core Mvc项目的话会看的ConfigureServices方法里面有services.AddMvc();虽然只有简单的一句代码,但是其实AddMvc方法已经把所有的与Mvc框架有关的服务注册到了框架项目当中,比如Rasor视图服务,授权服务,映射关系服务等等,然后这些服务就可以供Configure和项目中的其他类使用
我们来梳理一下这个例子核心代码,
《1》在扩展方法UserMyServices中将实例化的Person对象通过AddSingleton方法注入到IServiceCollection集合中,IServiceCollection集合中的所有对象或者对象的引用最终会进入DI容器,通过依赖注入的方式供其他类使用,这就是我们为啥可以直接在MyMiddleware类中直接可以拿到实例化后的Person对象以及一些如IHostingEnvironment一样的系统对象
与AddSingleton功能类似的还有AddTransient和AddScoped,这三者的区别如下
- AddTransient:暂时生存期服务是每次从服务容器进行请求时创建的。 这种生存期适合轻量级、 无状态的服务。也就是每次请求容器都会的实例都会是一个新的对象
- AddScoped:作用域生存期服务以每个客户端请求(连接)一次的方式创建。即同一客户端的所请求的实例都相同(但是要是当前连接),不同客户端则不同
- AddSingleton:生命周期服务是在第一次请求时创建的(或者当你在指定实例时运行
ConfigureServices
时),然后每个后续请求都将使用相同的实例。 也就是在当前程序生命周期中,所有客户端的以后每次请求访问的都会是同一个对象
《2》在扩展方法UserMyMiddleware调用我们自定义的中间件MyMiddleware,系统会根据Configure中的管道调用先后找到MyMiddleware的Invoke方法并去调用(自定义中间件必须命名为Invoke)
《3》在自定义中间件中我们得到了一个RequestDelegate委托 ,我们可以在自定义中间件中直接终止请求,当然跟多是我们可以通过这个委托将请求传递给下一个中间件,而且最有意思的是我们可以灵活决定RequestDelegate委托的调用代码位置顺序来决定在当前中间到调用下一中间件前后所需执行那些代码,前面这句话有点拗口,可以通过官方文档的这张图片去理解
1.4 总结
先简单的梳理了一下Asp core的调用周期,然后又简单说了一下比较核心的两个概念:服务和中间件,当然关于Asp core框架的基础和技术细节其实还有很多,无法通过一篇文章就能说清楚,这篇只是通过我之前的开发和学习,简单梳理了里面我认为对于一般开发人员来说最核心的几个概念,希望是你学习Asp core一块敲门砖。