NET Core中基于Generic Host来实现后台任务

https://www.cnblogs.com/catcher1994/p/9961228.html

目录

前言

什么是Generic Host

后台任务示例

控制台形式

消费MQ消息的后台任务

Web形式

部署

IHostedService和BackgroundService的区别

IHostBuilder的扩展写法

总结

前言

很多时候,后台任务对我们来说是一个利器,帮我们在后面处理了成千上万的事情。

在.NET Framework时代,我们可能比较多的就是一个项目,会有一到多个对应的Windows服务,这些Windows服务就可以当作是我们所说的后台任务了。

我喜欢将后台任务分为两大类,一类是不停的跑,好比MQ的消费者,RPC的服务端。另一类是定时的跑,好比定时任务。

那么在.NET Core时代是不是有一些不同的解决方案呢?答案是肯定的。

Generic Host就是其中一种方案,也是本文的主角。

什么是Generic Host

Generic Host是ASP.NET Core 2.1中的新增功能,它的目的是将HTTP管道从Web Host的API中分离出来,从而启用更多的Host方案。

这样可以让基于Generic Host的一些特性延用一些基础的功能。如:如配置、依赖关系注入和日志等。

Generic Host更倾向于通用性,换句话就是说,我们即可以在Web项目中使用,也可以在非Web项目中使用!

虽然有时候后台任务混杂在Web项目中并不是一个太好的选择,但也并不失是一个解决方案。尤其是在资源并不充足的时候。

比较好的做法还是让其独立出来,让它的职责更加单一。

下面就先来看看如何创建后台任务吧。

后台任务示例

我们先来写两个后台任务(一个一直跑,一个定时跑),体验一下这些后台任务要怎么上手,同样也是我们后面要使用到的。

这两个任务统一继承BackgroundService这个抽象类,而不是IHostedService这个接口。后面会说到两者的区别。

一直跑的后台任务

先上代码

public class PrinterHostedService2 : BackgroundService

{

private readonly ILogger _logger;

private readonly AppSettings _settings;

public PrinterHostedService2(ILoggerFactory loggerFactory, IOptionsSnapshot<AppSettings> options)
{
this._logger = loggerFactory.CreateLogger<PrinterHostedService2>();
this._settings = options.Value;
} public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Printer2 is stopped");
return Task.CompletedTask;
} protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_logger.LogInformation($"Printer2 is working. {_settings.PrinterDelaySecond}");
await Task.Delay(TimeSpan.FromSeconds(_settings.PrinterDelaySecond), stoppingToken);
}
}

}

来看看里面的细节。

我们的这个服务继承了BackgroundService,就一定要实现里面的ExecuteAsync,至于StartAsync和StopAsync等方法可以选择性的override。

我们ExecuteAsync在里面就是输出了一下日志,然后休眠在配置文件中指定的秒数。

这个任务可以说是最简单的例子了,其中还用到了依赖注入,如果想在任务中注入数据仓储之类的,应该就不需要再多说了。

同样的方式再写一个定时的。

定时跑的后台任务

这里借助了Timer来完成定时跑的功能,同样的还可以结合Quartz来完成。

public class TimerHostedService : BackgroundService

{

//other ...

private Timer _timer;

protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(_settings.TimerPeriod));
return Task.CompletedTask;
} private void DoWork(object state)
{
_logger.LogInformation("Timer is working");
} public override Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogInformation("Timer is stopping");
_timer?.Change(Timeout.Infinite, 0);
return base.StopAsync(cancellationToken);
} public override void Dispose()
{
_timer?.Dispose();
base.Dispose();
}

}

和第一个后台任务相比,没有太大的差异。

下面我们先来看看如何用控制台的形式来启动这两个任务。

控制台形式

这里会同时引入NLog来记录任务跑的日志,方便我们观察。

Main函数的代码如下:

class Program

{

static async Task Main(string[] args)

{

var builder = new HostBuilder()

//logging

.ConfigureLogging(factory =>

{

//use nlog

factory.AddNLog(new NLogProviderOptions { CaptureMessageTemplates = true, CaptureMessageProperties = true });

NLog.LogManager.LoadConfiguration("nlog.config");

})

//host config

.ConfigureHostConfiguration(config =>

{

//command line

if (args != null)

{

config.AddCommandLine(args);

}

})

//app config

.ConfigureAppConfiguration((hostContext, config) =>

{

var env = hostContext.HostingEnvironment;

config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)

.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);

            config.AddEnvironmentVariables();

            if (args != null)
{
config.AddCommandLine(args);
}
})
//service
.ConfigureServices((hostContext, services) =>
{
services.AddOptions();
services.Configure<AppSettings>(hostContext.Configuration.GetSection("AppSettings")); //basic usage
services.AddHostedService<PrinterHostedService2>();
services.AddHostedService<TimerHostedService>();
}) ; //console
await builder.RunConsoleAsync(); ////start and wait for shutdown
//var host = builder.Build();
//using (host)
//{
// await host.StartAsync(); // await host.WaitForShutdownAsync();
//}
}

}

对于控制台的方式,需要我们对HostBuilder有一定的了解,虽说它和WebHostBuild有相似的地方。可能大部分时候,我们是直接使用了WebHost.CreateDefaultBuilder(args)来构造的,如果对CreateDefaultBuilder里面的内容没有了解,那么对上面的代码可能就不会太清晰。

上述代码的大致流程如下:

new一个HostBuilder对象

配置日志,主要是接入了NLog

Host的配置,这里主要是引入了CommandLine,因为需要传递参数给程序

应用的配置,指定了配置文件,和引入CommandLine

Service的配置,这个就和我们在Startup里面写的差不多了,最主要的是我们的后台服务要在这里注入

启动

其中,

2-5的顺序可以按个人习惯来写,里面的内容也和我们写Startup大同小异。

第6步,启动的时候,有多种方式,这里列出了两种行为等价的方式。

a. 通过RunConsoleAsync的方式来启动

b. 先StartAsync然后再WaitForShutdownAsync

RunConsoleAsync的奥秘,我觉得还是直接看下面的代码比较容易懂。

///



/// Listens for Ctrl+C or SIGTERM and calls to start the shutdown process.

/// This will unblock extensions like RunAsync and WaitForShutdownAsync.

///

/// The to configure.

/// The same instance of the for chaining.

public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder)

{

return hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton<IHostLifetime, ConsoleLifetime>());

}

///



/// Enables console support, builds and starts the host, and waits for Ctrl+C or SIGTERM to shut down.

///

/// The to configure.

///

///

public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default)

{

return hostBuilder.UseConsoleLifetime().Build().RunAsync(cancellationToken);

}

这里涉及到了一个比较重要的IHostLifetime,Host的生命周期,ConsoleLifeTime是默认的一个,可以理解成当接收到ctrl+c这样的指令时,它就会触发停止。

接下来,写一下nlog的配置文件

这个时候已经可以通过命令启动我们的应用了。

dotnet run -- --environment Staging

这里指定了运行环境为Staging,而不是默认的Production。

在构造HostBuilder的时候,可以通过UseEnvironment或ConfigureHostConfiguration直接指定运行环境,但是个人更加倾向于在启动命令中去指定,避免一些不可控因素。

这个时候大致效果如下:

虽然效果已经出来了,不过大家可能会觉得这个有点小打小闹,下面来个略微复杂一点的后台任务,用来监听并消费RabbitMQ的消息。

消费MQ消息的后台任务

public class ComsumeRabbitMQHostedService : BackgroundService

{

private readonly ILogger _logger;

private readonly AppSettings _settings;

private IConnection _connection;

private IModel _channel;

public ComsumeRabbitMQHostedService(ILoggerFactory loggerFactory, IOptionsSnapshot<AppSettings> options)
{
this._logger = loggerFactory.CreateLogger<ComsumeRabbitMQHostedService>();
this._settings = options.Value;
InitRabbitMQ(this._settings);
} private void InitRabbitMQ(AppSettings settings)
{
var factory = new ConnectionFactory { HostName = settings.HostName, };
_connection = factory.CreateConnection();
_channel = _connection.CreateModel(); _channel.ExchangeDeclare(_settings.ExchangeName, ExchangeType.Topic);
_channel.QueueDeclare(_settings.QueueName, false, false, false, null);
_channel.QueueBind(_settings.QueueName, _settings.ExchangeName, _settings.RoutingKey, null);
_channel.BasicQos(0, 1, false); _connection.ConnectionShutdown += RabbitMQ_ConnectionShutdown;
} protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
stoppingToken.ThrowIfCancellationRequested(); var consumer = new EventingBasicConsumer(_channel);
consumer.Received += (ch, ea) =>
{
var content = System.Text.Encoding.UTF8.GetString(ea.Body);
HandleMessage(content);
_channel.BasicAck(ea.DeliveryTag, false);
}; consumer.Shutdown += OnConsumerShutdown;
consumer.Registered += OnConsumerRegistered;
consumer.Unregistered += OnConsumerUnregistered;
consumer.ConsumerCancelled += OnConsumerConsumerCancelled; _channel.BasicConsume(_settings.QueueName, false, consumer);
return Task.CompletedTask;
} private void HandleMessage(string content)
{
_logger.LogInformation($"consumer received {content}");
} private void OnConsumerConsumerCancelled(object sender, ConsumerEventArgs e) { ... }
private void OnConsumerUnregistered(object sender, ConsumerEventArgs e) { ... }
private void OnConsumerRegistered(object sender, ConsumerEventArgs e) { ... }
private void OnConsumerShutdown(object sender, ShutdownEventArgs e) { ... }
private void RabbitMQ_ConnectionShutdown(object sender, ShutdownEventArgs e) { ... } public override void Dispose()
{
_channel.Close();
_connection.Close();
base.Dispose();
}

}

代码细节就不需要多说了,下面就启动MQ发送程序来模拟消息的发送

同时看我们任务的日志输出

由启动到停止,效果都是符合我们预期的。

下面再来看看Web形式的后台任务是怎么处理的。

Web形式

这种模式下的后台任务,其实就是十分简单的了。

我们只要在Startup的ConfigureServices方法里面注册我们的几个后台任务就可以了。

public void ConfigureServices(IServiceCollection services)

{

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

services.AddHostedService();

services.AddHostedService();

services.AddHostedService();

}

启动Web站点后,我们发了20条MQ消息,再访问了一下Web站点的首页,最后是停止站点。

下面是日志结果,都是符合我们的预期。

可能大家会比较好奇,这三个后台任务是怎么混合在Web项目里面启动的。

答案就在下面的两个链接里。

https://github.com/aspnet/Hosting/blob/2.1.1/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs#L153

https://github.com/aspnet/Hosting/blob/2.1.1/src/Microsoft.AspNetCore.Hosting/Internal/HostedServiceExecutor.cs

上面说了那么多,都是在本地直接运行的,可能大家会比较关注这个要怎样部署,下面我们就不看看怎么部署。

部署

部署的话,针对不同的情形(web和非web)都有不同的选择。

正常来说,如果本身就是web程序,那么平时我们怎么部署的,就和平时那样部署即可。

花点时间讲讲部署非web的情形。

其实这里的部署等价于让程序在后台运行。

在Linux下面让程序在后台运行方式有好多好多,Supervisor、Screen、pm2、systemctl等。

这里主要介绍一下systemctl,同时用上面的例子来进行部署,由于个人服务器没有MQ环境,所以没有启用消费MQ的后台任务。

先创建一个 service 文件

vim /etc/systemd/system/ghostdemo.service

内容如下:

[Unit]

Description=Generic Host Demo

[Service]

WorkingDirectory=/var/www/ghost

ExecStart=/usr/bin/dotnet /var/www/ghost/ConsoleGHost.dll --environment Staging

KillSignal=SIGINT

SyslogIdentifier=ghost-example

[Install]

WantedBy=multi-user.target

其中,各项配置的含义可以自行查找,这里不作说明。

然后可以通过下面的命令来启动和停止这个服务

service ghostdemo start

service ghostdemo stop

测试无误之后,就可以设为自启动了。

systemctl enable ghostdemo.service

下面来看看运行的效果

我们先启动服务,然后去查看实时日志,可以看到应用的日志不停的输出。

当我们停了服务,再看实时日志,就会发现我们的两个后台任务已经停止了,也没有日志再进来了。

再去看看服务系统日志

sudo journalctl -fu ghostdemo.service

发现它确实也是停了。

在这里,我们还可以看到服务的当前环境和根路径。

IHostedService和BackgroundService的区别

前面的所有示例中,我们用的都是BackgroundService,而不是IHostedService。

这两者有什么区别呢?

可以这样简单的理解,IHostedService是原料,BackgroundService是一个用原料加工过一部分的半成品。

这两个都是不能直接当成成品来用的,都需要进行加工才能做成一个可用的成品。

同时也意味着,如果使用IHostedService可能会需要做比较多的控制。

基于前面的打印后台任务,在这里使用IHostedService来实现。

如果我们只是纯綷的把实现代码放到StartAsync方法中,那么可能就会有惊喜了。

public class PrinterHostedService : IHostedService, IDisposable

{

//other ....

public async Task StartAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
Console.WriteLine("Printer is working.");
await Task.Delay(TimeSpan.FromSeconds(_settings.PrinterDelaySecond), cancellationToken);
}
} public Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Printer is stopped");
return Task.CompletedTask;
}

}

运行之后,想用ctrl+c来停止,发现还是一直在跑。

ps一看,这个进程还在,kill掉之后才不会继续输出。。

问题出在那里呢?原因其实还是比较明显的,因为这个任务还没有启动成功,一直处于启动中的状态!

换句话说,StartAsync方法还没有执行完。这个问题一定要小心再小心。

要怎么处理这个问题呢?解决方法也比较简单,可以通过引用一个变量来记录要运行的任务,将其从StartAsync方法中解放出来。

public class PrinterHostedService3 : IHostedService, IDisposable

{

//others .....

private bool _stopping;

private Task _backgroundTask;

public Task StartAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Printer3 is starting.");
_backgroundTask = BackgroundTask(cancellationToken);
return Task.CompletedTask;
} private async Task BackgroundTask(CancellationToken cancellationToken)
{
while (!_stopping)
{
await Task.Delay(TimeSpan.FromSeconds(_settings.PrinterDelaySecond),cancellationToken);
Console.WriteLine("Printer3 is doing background work.");
}
} public Task StopAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Printer3 is stopping.");
_stopping = true;
return Task.CompletedTask;
} public void Dispose()
{
Console.WriteLine("Printer3 is disposing.");
}

}

这样就能让这个任务真正的启动成功了!效果就不放图了。

相对来说,BackgroundService用起来会比较简单,实现核心的ExecuteAsync这个抽象方法就差不多了,出错的概率也会比较低。

IHostBuilder的扩展写法

在注册服务的时候,我们还可以通过编写IHostBuilder的扩展方法来完成。

public static class Extensions

{

public static IHostBuilder UseHostedService(this IHostBuilder hostBuilder)

where T : class, IHostedService, IDisposable

{

return hostBuilder.ConfigureServices(services =>

services.AddHostedService());

}

public static IHostBuilder UseComsumeRabbitMQ(this IHostBuilder hostBuilder)
{
return hostBuilder.ConfigureServices(services =>
services.AddHostedService<ComsumeRabbitMQHostedService>());
}

}

使用的时候就可以像下面一样。

var builder = new HostBuilder()

//others ...

.ConfigureServices((hostContext, services) =>

{

services.AddOptions();

services.Configure(hostContext.Configuration.GetSection("AppSettings"));

        //basic usage
//services.AddHostedService<PrinterHostedService2>();
//services.AddHostedService<TimerHostedService>();
//services.AddHostedService<ComsumeRabbitMQHostedService>();
})
//extensions usage
.UseComsumeRabbitMQ()
.UseHostedService<TimerHostedService>()
.UseHostedService<PrinterHostedService2>()
//.UseHostedService<ComsumeRabbitMQHostedService>()
;

总结

Generic Host让我们可以用熟悉的方式来处理后台任务,不得不说这是一个很

NET Core中基于Generic Host来实现后台任务的更多相关文章

  1. 谈谈.NET Core中基于Generic Host来实现后台任务

    目录 前言 什么是Generic Host 后台任务示例 控制台形式 消费MQ消息的后台任务 Web形式 部署 IHostedService和BackgroundService的区别 IHostBui ...

  2. .NET Core 中基于 IHostedService 实现后台定时任务

    .NET Core 2.0 引入了 IHostedService ,基于它可以很方便地执行后台任务,.NET Core 2.1 则锦上添花地提供了 IHostedService 的默认实现基类 Bac ...

  3. ASP.NET Core 中基于 API Key 对私有 Web API 进行保护

    这两天遇到一个应用场景,需要对内网调用的部分 web api 进行安全保护,只允许请求头账户包含指定 key 的客户端进行调用.在网上找到一篇英文博文 ASP.NET Core - Protect y ...

  4. ASP.NET Core 中基于工厂的中间件激活

    IMiddlewareFactory/IMiddleware 是中间件激活的扩展点. UseMiddleware 扩展方法检查中间件的已注册类型是否实现 IMiddleware. 如果是,则使用在容器 ...

  5. .NET Core 中的通用主机和后台服务

    简介 我们在做项目的时候, 往往要处理一些后台的任务. 一般是两种, 一种是不停的运行,比如消息队列的消费者.另一种是定时任务. 在.NET Framework + Windows环境里, 我们一般会 ...

  6. .NET Core Generic Host Windows服务部署使用Topshelf

    此文源于前公司在迁移项目到.NET Core的过程中,希望使用Generic Host来管理定时任务程序时,没法部署到Windows服务的问题,而且官方也没给出解决方案,只能关注一下官方issue # ...

  7. 利用Topshelf把.NET Core Generic Host管理的应用程序部署为Windows服务

    背景 2019第一篇文章. 此文源于前公司在迁移项目到.NET Core的过程中,希望使用Generic Host来管理定时任务程序时,没法部署到Windows服务的问题,而且官方也没给出解决方案,只 ...

  8. 探索ASP.NET Core 3.0系列一:新的项目文件、Program.cs和generic host

    前言:在这篇文章中我们来看看ASP.Net Core 3.0应用程序中一些基本的部分—— .csproj项目文件和Program.cs文件.我将会介绍它们从 ASP.NET Core 2.x 中的默认 ...

  9. net core 的Generic Host 之Generic Host Builder

    前言 通用Host(Generic Host) 与 web Host 不同的地方就是通用Host解耦了Http请求管道,使得通用Host拥有更广的应用场景.比如:消息收发.后台任务以及其他非http的 ...

随机推荐

  1. Eclipse 使用中遇到的一些问题!

    解决办法~ 1.先检查本地svn 版本与Eclipse 中svn插件 的区别 2.发现版本一致,没解决,发现如图 发现   svn接口报错 javaHL(JNI) Not Available!@ 所以 ...

  2. Delphi编码转换

    1.Delphi 的 Utf-8 转换 - findumars - 博客园.html https://www.cnblogs.com/findumars/archive/2013/12/26/3492 ...

  3. php温习-变量,常量

    1.变量 内存中用于临时存储数据的一个空间,空间有一个名字子,变量都是以$开头 预定义变量:  $_GET  $_POST  $_REQUEST   $_SEVER  $_SEESION  $_COO ...

  4. 这真是奇葩的js题目

    url:http://javascript-puzzlers.herokuapp.com/ 有兴趣的可以一看,算是比较偏门自我感觉

  5. Android6.0------权限申请管理(单个权限和多个权限申请)

    Android开发时,到6.0系统上之后,有的权限就得申请才能用了. Android将权限分为正常权限 和 危险权限 Android系统权限分为几个保护级别.需要了解的两个最重要保护级别是 正常权限  ...

  6. List和数组的相互转化

    一.数组转化为list:Arrays.aslist(arr); public static void main(String[] args) { String[] arr={"apple&q ...

  7. HDU-4679-树的直径(树形dp)

    Terrorist’s destroy Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 65535/32768 K (Java/Othe ...

  8. poj3308 Paratroopers 最大流 最小点权覆盖

    题意:有一个n*m的矩阵,告诉了在每一行或者每一列安装大炮的代价,每一个大炮可以瞬间消灭这一行或者这一列的所有敌人,然后告诉了敌人可能出现的L个坐标位置,问如何安置大炮,使花费最小.如果一个敌人位于第 ...

  9. iOS-如何写好一个UITableView

    如何写好一个UITableView 字数5787 阅读3745 评论25 喜欢69 本文是直播分享的简单文字整理,直播共分为上.下两部分.第一部分:优酷 Or YouTube,第二部分:优酷 Demo ...

  10. 【Windows】netsh动态配置端口转发

    文章转载自傲风 使用多个虚拟机,将开发环境和工作沟通环境分开(即时通,办公系统都只能在windows下使用-),将开发环境的服务提供给外部访问时,需要在主机上通过代理配置数据转发. VirtualBo ...