写程序,难免会遇到需要做成系统服务的需求。Windows 下写系统服务需要实现一些特定的接口,做起来有一定难度,所以不少程序采用了 近似的备选方案 —— 做成带系统任务栏图标的桌面应用。但是,服务之所以是服务,就在于他有一个非常重要的特点:可以开机自启动,而且不需要用户登录。要不然每次重启还得人工去登录,是件多么辛苦的事情。Windows 当然是可以设置自动登录的,但如果是托管服务器,你真放心自动登录吗?
而 Linux 下面似乎就要方便得多,大概不需要 GUI 持续运行的程序都可以做成服务。
1. 在 Windows 中做服务
先说 Windows。如果你还在用 Windows XP,那我们就此别过 ……
1.1. Windows Service Wrapper
[Windows Service Wrapper] 是全称。其简称 WinSW 的知名度可能更高一些。
WinSW 基于 .NET Framework 4.6.1 和 .NET 5 实现,所以至少需要 Windows 7 SP1 / Windows Server 2008 R2 SP1 才可以使用。它可以把任意 Windows 程序封装成 Windows 服务,你所需要做的,只是写个配置文件,然后用 WinSW 注册一个 Windows 服务即可。WinSW 下载下来是个独立的可执行文件,使用前需要写一个与可执行文件名同名但扩展名是 .xml
的配置文件置于同一目录下。
举例来说,Nginx 本身并没有提供注册成 Windows 服务的能力,如果需要注册成 Windows 服务,就可以用 WinSW 来封装一下。把下载的 WinSW 可执行文件改名为 winsw.exe
(随便改成什么名字都行,配置文件名按相同的名称创建即可),放在 nginx 的主目录下面,创建配置文件之后的目录结构大概是这样:
[-] nginx
|-- conf
|-- ...(其他 nginx 的目录或文件)
|-- nginx.exe
|-- winsw.exe
`-- winsw.xml
winsw.xml
中的配置内容如下,看注释就能理解。
<service>
<!-- 配置服务名称 nginx-service,显示名称 Nginx Service,以及服务描述 -->
<id>nginx-service</id>
<name>Nginx Service</name>
<description>Nginx Service</description>
<!-- 服务运行的工作目录,给绝对路径 -->
<workingdirectory>C:\Local\Nginx</workingdirectory>
<!-- 服务可执行文件,给绝对路径 -->
<executable>C:\Local\Nginx\nginx.exe</executable>
<!-- 停止服务的可执行文件 -->
<stopexecutable>C:\Local\Nginx\nginx.exe</stopexecutable>
<!-- 停止服务的参数 -->
<stoparguments>-s stop</stoparguments>
<priority>Normal</priority>
<stoptimeout>15 sec</stoptimeout>
<stopparentprocessfirst>false</stopparentprocessfirst>
<!-- 配置服务类型是「自动」启动 -->
<startmode>Automatic</startmode>
<waithint>15 sec</waithint>
<sleeptime>1 sec</sleeptime>
<!-- 将服务的控制台输出(标准输出/错误输出)写入日志 -->
<!-- 其中 %BASE% 是指 winsw.exe 所在目录 -->
<!-- 参考:https://github.com/winsw/winsw/blob/master/doc/loggingAndErrorReporting.md -->
<logpath>%BASE%\logs</logpath>
<log mode="roll-by-time">
<pattern>yyyyMMdd</pattern>
</log>
</service>
这个配置创建了名为 nginx-service
的 Windows 服务,它在 Windows 的「服务 (services.msc
)」显示名称为 Nginx Service
。启动服务的时候直接运行 nginx.exe
来启动,这是一个会执行占用控制台的程序;而停止服务则是运行 nginx.exe -s stop
,可执行程序和参数分别配置在 <stopexecutable>
和 <stoparguments>
中 —— 由此不难推断,如果启动服务需要参数,是配置在 <arguments>
中的。
详细的配置可以在 github 库里的 XML configuratoin file 中查到,也可以查到一些示例。
配置完成之后运行 winsw.exe install
即可安装为 Windows 服务。安装完成之后可以使用 winsw.exe start
命令启动服务,也可以去 Windows 的服务管理器启动,或者使用 net start
命令来启动。github 库首页的 Usage 部分有完整的命令说明。
1.2. 用 .NET Framework/Core/5 自己写一个
用 .NET 写个服务还是比较容易的,因为有现成的包(组件)可以用:NuGet Gallery | Microsoft.Extensions.Hosting.WindowsServices,官方出品。它至少需要依赖两个包:
- NuGet Gallery | Microsoft.Extensions.Hosting
- NuGet Gallery | Microsoft.Extensions.Hosting.Abstractions
在引入组件之后,只需要少量代码就可以让当前 .NET 的 Console Application 成为一个支持 Windows 服务接口的服务程序。
// Program.cs
class Program {
static async Task<int> Main(string[] args) {
await CreateHostBuilder(args).Build().RunAsync();
}
public static IHostBuilder CreateHostBuilder(string[] args) {
return Host.CreateDefaultBuilder(args)
.ConfigureServices((hostContext, services) => {
services.AddHostedService<DaemonService>();
})
.UseWindowsService();
}
}
注意到 AddHostedService<DaemonServce>
,这里的 DaemonServce
是一个自己实现的服务业务类,命名自由,但需要从 Microsoft.Extensions.Hosting.BackgroundService
继承
class DaemonService : BackgroundService {
protected override async Task ExecuteAsync(CancellationToken stoppingToken) {
// TODO 提供服务内容的代码
}
}
服务的业务代码通常都是持续运行,或者监听的代码。如果是计划性/周期性的任务,可以考虑使用 Quartz 来实现。
程序完成后可以使用 Windows 提供的 sc
命令来注册/注销服务。假设生成的程序是 MyService.exe
,那么注册、配置和启动服务的命令如下:
sc create "my-service" binPath="C:\MyService\MyService.exe --service"
sc config "my-service" start= auto
sc start "my-service"
注意:binPath
中应该给绝对路径。
2. 在 Ubuntu 中做 Systemd 服务
Linux 下服务种类比较多,最近主要是用 Ubuntu,所以做 Ubuntu 下的 Systemd 服务。
假设我们写了一个 .NET 5 的 ASP.NET 应用,放在 /app/my-web/
,主文件是 MyWeb.dll
。如果用命令行启动这个 Web 应该应该是
cd /app/my-web
dotnet MyWeb.dll
接下来是写 Systemd 服务配置。配置文件名起为 my-web.service
,放在 /etc/systemd/system
目录下。内容(含注释)如下:
[Unit]
# 服务说明
Description=My Web Application
# 在启动网络服务之后启动
After=network.target
[Service]
# 总是重启(无论什么原因结束都会立即重启)
Restart=always
RestartSec=10
# 工作目录
WorkingDirectory=/app/my-web
# 启动服务的命令
ExecStart=/usr/bin/dotnet MyWeb.dll
# 通过杀主进程来结束服务
ExecStop=/bin/kill -HUP $MAINPID
TimeoutStopSec=5
KillMode=mixed
SyslogIdentifier=my-web
# 指定运行此服务的用户,涉及到目录访问权限等问题
User=james
[Install]
WantedBy=multi-user.target
配置完之后还不能马上启动服务,需要 systemd 重新加载配置,然后才启动服务:
sudo systemctl daemon-reload
sudo systemctl start my-web
顺便,再介绍一下,如果想在内容发布之后自动重启,需要加两个配置文件,一个 .path
监控变化,一个 .service
来重启 my-web
:
restart-my-web.path
[Path]
# 监控主文件 MyWeb.dll 的变动,如果有变动会触发 restart-my-web.service 启动
PathModified=/app/my-web/MyWeb.dll
[Install]
WantedBy=multi-user.target
restart-my-web.service
[Unit]
Description=My Web Restarter
After=network.target
[Service]
Type=oneshot
# 防抖,60 秒内只启动 1 次
ExecStartPre=/bin/sleep 60
# 重启 my-web.service
ExecStart=/bin/systemctl restart my-web.service
[Install]
WantedBy=multi-user.target
3. 小小的总结一下
做服务并不难,上面唯一的一个需要写代码的方式,还是开箱即用的组件实现的。但话说回来,做服务不难,做服务的设计还是有不少事情需要考虑。比如
- 如何监控服务的状态?—— 进程监控、心跳检查……
- 如何分析服务中出现的错误?—— 系统日志
- 如何提供 GUI 来对服务进行管理?—— Web 或其他 UI 跟服务进程进行交互(进程通信、管理 API 等)
- ……
既然做服务不难,那就不要太纠结如何“做”(提供)服务,还是多纠结纠结如何做好(设计)服务吧。