您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關怎么在.NET Core中利用基于Generic Host實現后臺任務,文章內容質量較高,因此小編分享給大家做個參考,希望大家閱讀完這篇文章后對相關知識有一定的了解。
Generic Host是ASP.NET Core 2.1中的新增功能,它的目的是將HTTP管道從Web Host的API中分離出來,從而啟用更多的Host方案。
現在2.1版本的Asp.Net Core中,有了兩種可用的Host。
Web Host –適用于托管Web程序的Host,就是我們所熟悉的在Asp.Net Core應用程序的Mai函數中用CreateWebHostBuilder創建出來的常用的WebHost。
Generic Host (ASP.NET Core 2.1版本才有) – 適用于托管非 Web 應用(例如,運行后臺任務的應用)。 在未來的版本中,通用主機將適用于托管任何類型的應用,包括 Web 應用。 通用主機最終將取代 Web 主機,這大概也是這種類型的主機叫做通用主機的原因。
這樣可以讓基于Generic Host的一些特性延用一些基礎的功能。如:如配置、依賴關系注入和日志等。
Generic Host更傾向于通用性,換句話就是說,我們即可以在Web項目中使用,也可以在非Web項目中使用!
雖然有時候后臺任務混雜在Web項目中并不是一個太好的選擇,但也并不失是一個解決方案。尤其是在資源并不充足的時候。
比較好的做法還是讓其獨立出來,讓它的職責更加單一。
下面就先來看看如何創建后臺任務吧。
我們先來寫兩個后臺任務(一個一直跑,一個定時跑),體驗一下這些后臺任務要怎么上手,同樣也是我們后面要使用到的。
這兩個任務統一繼承BackgroundService這個抽象類,而不是IHostedService這個接口。后面會說到兩者的區別。
1、一直跑的后臺任務
先上代碼
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的奧秘,我覺得還是直接看下面的代碼比較容易懂。
/// <summary> /// Listens for Ctrl+C or SIGTERM and calls <see cref="IApplicationLifetime.StopApplication"/> to start the shutdown process. /// This will unblock extensions like RunAsync and WaitForShutdownAsync. /// </summary> /// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param> /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns> public static IHostBuilder UseConsoleLifetime(this IHostBuilder hostBuilder) { return hostBuilder.ConfigureServices((context, collection) => collection.AddSingleton<IHostLifetime, ConsoleLifetime>()); } /// <summary> /// Enables console support, builds and starts the host, and waits for Ctrl+C or SIGTERM to shut down. /// </summary> /// <param name="hostBuilder">The <see cref="IHostBuilder" /> to configure.</param> /// <param name="cancellationToken"></param> /// <returns></returns> public static Task RunConsoleAsync(this IHostBuilder hostBuilder, CancellationToken cancellationToken = default) { return hostBuilder.UseConsoleLifetime().Build().RunAsync(cancellationToken); }
這里涉及到了一個比較重要的IHostLifetime,Host的生命周期,ConsoleLifeTime是默認的一個,可以理解成當接收到ctrl+c這樣的指令時,它就會觸發停止。
接下來,寫一下nlog的配置文件
<?xml version="1.0" encoding="utf-8" ?> <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xsi:schemaLocation="NLog NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" autoReload="true" internalLogLevel="Info" > <targets> <target xsi:type="File" name="ghost" fileName="logs/ghost.log" layout="${date}|${level:uppercase=true}|${message}" /> </targets> <rules> <logger name="GHost.*" minlevel="Info" writeTo="ghost" /> <logger name="Microsoft.*" minlevel="Info" writeTo="ghost" /> </rules> </nlog>
這個時候已經可以通過命令啟動我們的應用了。
dotnet run -- --environment Staging
這里指定了運行環境為Staging,而不是默認的Production。
在構造HostBuilder的時候,可以通過UseEnvironment或ConfigureHostConfiguration直接指定運行環境,但是個人更加傾向于在啟動命令中去指定,避免一些不可控因素。
這個時候大致效果如下:
雖然效果已經出來了,不過大家可能會覺得這個有點小打小鬧,下面來個略微復雜一點的后臺任務,用來監聽并消費RabbitMQ的消息。
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形式的后臺任務是怎么處理的。
這種模式下的后臺任務,其實就是十分簡單的了。
我們只要在Startup的ConfigureServices方法里面注冊我們的幾個后臺任務就可以了。
public void ConfigureServices(IServiceCollection services) { services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); services.AddHostedService<PrinterHostedService2>(); services.AddHostedService<TimerHostedService>(); services.AddHostedService<ComsumeRabbitMQHostedService>(); }
啟動Web站點后,我們發了20條MQ消息,再訪問了一下Web站點的首頁,最后是停止站點。
下面是日志結果,都是符合我們的預期。
可能大家會比較好奇,這三個后臺任務是怎么混合在Web項目里面啟動的。
答案就在下面的兩個鏈接里。
https://github.com/aspnet/Hosting/blob/2.1.1/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs
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的擴展方法來完成。
public static class Extensions { public static IHostBuilder UseHostedService<T>(this IHostBuilder hostBuilder) where T : class, IHostedService, IDisposable { return hostBuilder.ConfigureServices(services => services.AddHostedService<T>()); } 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<AppSettings>(hostContext.Configuration.GetSection("AppSettings")); //basic usage //services.AddHostedService<PrinterHostedService2>(); //services.AddHostedService<TimerHostedService>(); //services.AddHostedService<ComsumeRabbitMQHostedService>(); }) //extensions usage .UseComsumeRabbitMQ() .UseHostedService<TimerHostedService>() .UseHostedService<PrinterHostedService2>() //.UseHostedService<ComsumeRabbitMQHostedService>() ;
關于怎么在.NET Core中利用基于Generic Host實現后臺任務就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。