Worker Service是微软提供的一个项目模板,它继承于BackgroundService。在使用.NET Framework时,我们可以使用Windows Service来创建Windows服务,而在使用.NET Core及更高的.NET版本时,就可以使用Worker Service来取代Windows Service,以便创建跨平台服务。
在微软的.NET文档的运行时库(Runtime libraries)分支中,辅助角色服务(Worker Services)详细介绍了Worker Service的使用。
以下为微软文档的链接:
.NET 中的辅助角色服务 | Microsoft Docs
本文代码已上传至GitHub,项目链接如下:
https://github.com/XMNHCAS/WorkerServiceDemo
打开Visual Studio,创建一个新的Worker Service项目,如下图所示:
创建完成后,直接运行,会出现一个控制台窗口,并打印出运行信息:
项目创建成功后,Windows会预设一个Worker Service的模板,它主要文件为Program.cs和Worker.cs,其中Program.cs提供了服务的启动入口,而Worker则是服务的具体功能实现。
打开Program.cs,可以看到,它使用了CreateHostBuilder方法返回了一个添加了Worker类作为托管服务的host,然后调用它的Build方法生成实例并调用Run方法来运行该实例。整个Program.cs的结构与普通的控制台应用程序基本相同。
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace WorkerServiceDemo
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService();
});
}
}
实际上,Worker Service在部署完成后,就是一个没有窗口的控制台应用程序。在开发过程中,Worker Service的输出控制台界面使得它较Windows Service更容易调试。
接下来我们来使用Worker Service创建一个简单Demo服务,具体实现功能就是在一个日志文件中定时记录一些消息。
首先我们创建一个InfoLogWorker类,并使其继承于BackgroundService类,并将Program配置的服务改成InfoLogWorker。
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) =>
{
//services.AddHostedService();
services.AddHostedService();
});
}
可以看到,BackgroundService提供了三个虚方法和一个抽象方法,我们需要对StartAsync、StopAsync和ExecuteAsync三个方法进行重写。
其中StartAsync会在服务开启时被调用,而StopAsync则是在服务停止时被调用,ExecuteAsync是必须要被实现的方法,它定义了服务运行时需要执行的操作。
在重写完成这个三个方法之后,我们再创建一个ILogger的只读字段,并在构造函数中获取它的实例,之后将会使用它在调试时打印出我们需要的信息。然后我们再定义一个ServiceRunningMethod方法,用以定义服务运行时需要实现的具体功能。
具体定义如下:
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace WorkerServiceDemo
{
class LogInfoWorker : BackgroundService
{
///
/// 日志字段
///
private readonly ILogger logger;
///
/// 构造函数
///
///
///
public LogInfoWorker(ILogger logger) => this.logger = logger;
///
/// 服务启动时执行的操作
///
///
///
public override async Task StartAsync(CancellationToken cancellationToken)
{
await base.StartAsync(cancellationToken);
}
///
/// 服务运行时执行的操作
///
///
///
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Task task = ServiceRunningMethod(stoppingToken);
await Task.WhenAll(task);
}
///
/// 服务停止时执行的操作
///
///
///
public override async Task StopAsync(CancellationToken cancellationToken)
{
await base.StopAsync(cancellationToken);
}
///
/// 运行时执行的方法
///
///
///
private Task ServiceRunningMethod(CancellationToken stoppingToken)
{
return Task.Run(() => { }, stoppingToken);
}
}
}
接下来我们需要在定义好的方法中,实现运行日志的记录。首先定义日志文件输出目录的只读字段,并创建FileOperation方法,用以实现日志操作。
///
/// 日志输出目录
///
private readonly string LogPath = $"{AppDomain.CurrentDomain.BaseDirectory}LogInfo.log";
///
/// 文件操作
///
/// 操作类型
/// 调用此方法的方法
/// 写入的消息
private void FileOperation(FileMode fileMode, string methodName, string message)
{
FileStream fs = new FileStream(LogPath, fileMode, FileAccess.Write);
StreamWriter sw = new StreamWriter(fs);
sw.WriteLine($"{DateTime.Now} : [{methodName}] {message}");
sw.Close();
fs.Close();
}
然后在StartAsync和StopAsync中分别添加记录操作。
///
/// 服务启动时执行的操作
///
///
///
public override async Task StartAsync(CancellationToken cancellationToken)
{
FileOperation(FileMode.OpenOrCreate, "StartAsync", "Service started.");
logger.LogInformation($"{DateTime.Now} : Service has been requested to start.");
await base.StartAsync(cancellationToken);
}
///
/// 服务停止时执行的操作
///
///
///
public override async Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation($"{DateTime.Now} : Service has been requested to stop.");
await base.StopAsync(cancellationToken);
}
最后定义运行时的操作,在服务被停止的时候,退出循环,并记录服务已停止。我们在ExecuteAsync方法中创建一个新的Task实例来调用ServiceRunningMethod,并通过Task.WhenAll(),使得ServiceRunningMethod在服务被停止之前被一直执行。
///
/// 运行时执行的方法
///
///
///
private Task ServiceRunningMethod(CancellationToken stoppingToken)
{
return Task.Run(() =>
{
while (!stoppingToken.IsCancellationRequested)
{
//记录日志
logger.LogInformation($"{DateTime.Now} : Service is running.");
FileOperation(FileMode.Append, "ServiceRunningMethod", "Service is running.");
Thread.Sleep(2000);
}
logger.LogInformation($"{DateTime.Now} : Service stopped.");
//记录服务已停止
FileOperation(FileMode.Append, "ServiceRunningMethod", "Service stopped.");
}, stoppingToken);
}
虽然我们可以通过在ServiceRunningMethod退出循环后加入结束操作,但是假如我们需要在服务运行时使用多个子线程来执行不同的操作,如果在其中任一子线程中来加入这样的结束操作,就有可能会出现错误。此时我们可以通过调用IHostApplicationLifetime接口,来阻止服务停止,并执行我们需要的结束操作,然后在等待结束操作完成后手动停止服务。
首先先创建IHostApplicationLifetime的只读字段,并改写构造函数,添加IHostApplicationLifetime的参数,注入创建IHostApplicationLifetime实例。
///
/// 应用程序生命周期字段
///
private readonly IHostApplicationLifetime hostApplicationLifetime;
///
/// 构造函数
///
///
///
public LogInfoWorker(ILogger logger, IHostApplicationLifetime hostApplicationLifetime) =>
(this.logger, this.hostApplicationLifetime) = (logger, hostApplicationLifetime);
接着将ServiceRunningMethod中的停止操作分离为一个单独的方法。
///
/// 运行时执行的方法
///
///
///
private Task ServiceRunningMethod(CancellationToken stoppingToken)
{
return Task.Run(() =>
{
while (!stoppingToken.IsCancellationRequested)
{
//记录日志
logger.LogInformation($"{DateTime.Now} : Service is running.");
FileOperation(FileMode.Append, "ServiceRunningMethod", "Service is running.");
Thread.Sleep(2000);
}
}, stoppingToken);
}
///
/// 服务结束前的操作
///
private void BeforeStopMethod()
{
logger.LogInformation($"{DateTime.Now} : Service is stopping.");
Thread.Sleep(3000);
logger.LogInformation($"{DateTime.Now} : Service stopped.");
//记录服务已停止
FileOperation(FileMode.Append, "ServiceRunningMethod", "Service stopped.");
}
然后改写ExecuteAsync,使得服务在被停止前,先执行BeforeStopMethod,再手动将服务停止。
///
/// 服务运行时执行的操作
///
///
///
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
Task task = ServiceRunningMethod(stoppingToken);
await Task.WhenAll(task);
}
catch (Exception ex)
{
logger.LogError(ex.ToString());
FileOperation(FileMode.Append, "ExecuteAsync", ex.ToString());
}
finally
{
BeforeStopMethod();
//手动停止服务
hostApplicationLifetime.StopApplication();
}
}
最后运行结果如下:
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace WorkerServiceDemo
{
class LogInfoWorker : BackgroundService
{
///
/// 日志字段
///
private readonly ILogger logger;
///
/// 应用程序生命周期字段
///
private readonly IHostApplicationLifetime hostApplicationLifetime;
///
/// 日志输出目录
///
private readonly string LogPath = $"{AppDomain.CurrentDomain.BaseDirectory}LogInfo.log";
///
/// 构造函数
///
///
///
public LogInfoWorker(ILogger logger, IHostApplicationLifetime hostApplicationLifetime) =>
(this.logger, this.hostApplicationLifetime) = (logger,hostApplicationLifetime);
///
/// 服务启动时执行的操作
///
///
///
public override async Task StartAsync(CancellationToken cancellationToken)
{
FileOperation(FileMode.OpenOrCreate, "StartAsync", "Service started.");
logger.LogInformation($"{DateTime.Now} : Service has been requested to start.");
await base.StartAsync(cancellationToken);
}
///
/// 服务运行时执行的操作
///
///
///
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
try
{
Task task = ServiceRunningMethod(stoppingToken);
await Task.WhenAll(task);
}
catch (Exception ex)
{
logger.LogError(ex.ToString());
FileOperation(FileMode.Append, "ExecuteAsync", ex.ToString());
}
finally
{
BeforeStopMethod();
//手动停止服务
hostApplicationLifetime.StopApplication();
}
}
///
/// 服务停止时执行的操作
///
///
///
public override async Task StopAsync(CancellationToken cancellationToken)
{
logger.LogInformation($"{DateTime.Now} : Service has been requested to stop.");
await base.StopAsync(cancellationToken);
}
///
/// 运行时执行的方法
///
///
///
private Task ServiceRunningMethod(CancellationToken stoppingToken)
{
return Task.Run(() =>
{
while (!stoppingToken.IsCancellationRequested)
{
//记录日志
logger.LogInformation($"{DateTime.Now} : Service is running.");
FileOperation(FileMode.Append, "ServiceRunningMethod", "Service is running.");
Thread.Sleep(2000);
}
}, stoppingToken);
}
///
/// 服务结束前的操作
///
private void BeforeStopMethod()
{
logger.LogInformation($"{DateTime.Now} : Service is stopping.");
Thread.Sleep(3000);
logger.LogInformation($"{DateTime.Now} : Service stopped.");
//记录服务已停止
FileOperation(FileMode.Append, "ServiceRunningMethod", "Service stopped.");
}
///
/// 文件操作
///
/// 操作类型
/// 调用此方法的方法
/// 写入的消息
private void FileOperation(FileMode fileMode, string methodName, string message)
{
FileStream fs = new FileStream(LogPath, fileMode, FileAccess.Write);
StreamWriter sw = new StreamWriter(fs);
sw.WriteLine($"{DateTime.Now} : [{methodName}] {message}");
sw.Close();
fs.Close();
}
}
}
以下为部署需要用到的命令:
说明 | 命令 |
安装服务 | sc.exe create 服务名 binPath=路径 |
卸载服务 | sc.exe delete 服务名 |
启动服务 | sc.exe start 服务名 |
停止服务 | sc.exe stop 服务名 |
设置服务自动启动 | sc.exe config 服务名 start= auto |
设置服务手动启动 | sc.exe config 服务名 start= demand |
查看服务状态 | sc.exe query 服务名 |
需要注意,上面所有的命令都需要具有管理员权限的CMD或者PowerShell来进行执行。
Worker Service依赖的.NET框架为.NET Core3.1及以上,所以Worker Service项目是可以跨平台部署的,不过需要针对不同平台下载不同的包。针对Windows平台,我们需要使用NuGet添加Microsoft.Extensions.Hosting.WindowsServic程序包。在添加时,需要注意安装的版本,目前这个程序包的最新版本为6.0.0,适配与.NET6的项目,而本文实例使用的是.NET5,所以安装5.0.1版本的程序包。
在添加完成后,我们在Program.cs的CreateHostBuilder方法,添加UseWindowsService(),这样生成的可执行文件就可以作为服务被部署到Windows系统里了。
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseWindowsService() //配置为Windows服务
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService();
});
如果是在Linux上部署,就是引用Microsoft.Extensions.Hosting.Systemd程序包,然后在在Program.cs的CreateHostBuilder方法,添加.UseSystemd(),最后发布为Linux版本的可执行文件即可,然后用将生成出来的文件放到Linux系统中,按照Linux的普通服务部署即可。
安装服务首先需要生成可执行文件。而生成可部署的可执行文件有两种方法,一种是发布,一种是Release模式生成。微软官方文档演示的是发布,故这里不赘述。此处使用Release生成的文件。
将生成模式调成Release,然后右击项目选择生成,在项目的文件所在位置的bin文件夹内会生成一个Release文件夹,里面就是生成好的可执行文件。将这个文件夹拷贝到D盘。
打开Windows PowerShell(管理员),执行安装命令:
sc.exe create wsdemo binPath=D:\Release\net5.0\WorkerServiceDemo.exe
打开运行,输入services.msc,打开服务管理器,可以找到我们刚刚安装成功的wsdemo服务。
再回到PowerShell,执行卸载命令:
sc.exe delete wsdemo
切回到服务管理窗口,刷新一下,我们刚刚安装了的wsdemo就已经消失了。
在服务安装完成后,我们可以用以下命令来实现服务的启动与停止。如果在上面卸载了服务,那么再安装一次即可。
启动服务:sc.exe start wsdemo
停止服务:sc.exe stop wsdemo
执行启动服务命令后,打开服务管理窗口,可以看到服务已经成功运行。
执行停止命令后的,打开服务管理窗口,并打开wsdemo服务的属性,可以看到该服务已经停止。
回到程序的目录,可以看到生成了一个LogInfo.log文件,也成功记录的服务运行的信息,说明服务启停及运行正常。
在Windows下,启动服务和停止服务也可以直接使用服务管理器来手动启停。
将服务设置为自动启动和手动启动的命令如下:
自动启动:sc.exe config wsdemo start= auto
手动启动:sc.exe config wsdemo start= demand
需要注意,“=” 后面最好加上空格,否则在win10以下版本的系统可能会报错。
执行设置自动启动命令,打开服务管理窗口,可以看到服务被切换成自动启动。
执行设置手动启动命令,打开服务管理窗口,可以看到服务被切换成手动启动。
当然这个自动和手动也是可以在服务管理窗口进行手动设置的,使用命令的方式更适合批处理部署。
本文介绍了Worker Service的基础应用,实际使用时可根据这个思路进行功能扩展。微软官方文档有更加详细的说明及示例,如果需要再深入了解,可以多参考微软官方文档。