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. js 冒泡型事件

  2. Memcached stats items 命令

    Memcached stats items 命令用于显示各个 slab 中 item 的数目和存储时长(最后一次访问距离现在的秒数). 语法: stats items 命令的基本语法格式如下: sta ...

  3. CTR的贝叶斯平滑

    参考论文: Click-Through Rate Estimation for Rare Events in Online Advertising 参考的博客: 1.https://jiayi797. ...

  4. 关于Gulp

    Gulp & webpack 配置详解http://www.jianshu.com/p/2d9ed1fe3e8c 使用 Gulphttp://hwaphon.site/?p=439 前端构建工 ...

  5. Python 基础教程

    Python 基础教程 Python是一种解释型.面向对象.动态数据类型的高级程序设计语言. Python由Guido van Rossum于1989年底发明,第一个公开发行版发行于1991年. 像P ...

  6. ItemsControl的应用

    ItemsControl是集合类控件的基类,如:ListBox.ComboBox.TreeView 所以,我们可以直接应用“ItemsControl”自定义我们“需要的”集合类型控件

  7. vim 正则 捕获

    在正规表达式中使用 \( 和 \) 符号括起正规表达式,即可在后面使用\1 \2等变量来访问捕获的内容. 将捕获内容前后交换,如下: :s/\(\haha\)\(hehe\)/\\/

  8. Python实现CSV数据的读取--两种方法实现

    方法一: 方法二:

  9. VS2013 VC++的.cpp文件调用CUDA的.cu文件中的函数

    CUDA 8.0在函数的调用中方便的让人感动.以下是从网上学到的VC++的.cpp文件调用CUDA的.cu文件中的函数方法,和一般的VC++函数调用的方法基本没差别. 使用的CUDA版本为CUDA 8 ...

  10. Java导出Excel(有数据库导出代码)

    /** * 导出 * @param request * @param response * @throws Exception */ @SuppressWarnings("unchecked ...