把程序做成系统服务

写程序,难免会遇到需要做成系统服务的需求。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,官方出品。它至少需要依赖两个包:

在引入组件之后,只需要少量代码就可以让当前 .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
注意:需要提前准备好 .NET 5 的运行环境,可参考在 Ubuntu 上安装 .NET - .NET | Microsoft Docs

接下来是写 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 等)
  • ……

既然做服务不难,那就不要太纠结如何“做”(提供)服务,还是多纠结纠结如何做好(设计)服务吧。


边城客栈
全栈技术专栏,公众号「边城客栈」,[链接]

一路从后端走来,终于走在了前端!

56.2k 声望
26.5k 粉丝
0 条评论
推荐阅读
2022,二着二着又混过一年
收到思否小姐姐的活动提醒,才发觉又到了年底,该写“总结”了。说起总结,总有些倦——每天工作要写日报、项目上要写周报、月底要写月报、季度还有季总结,当然还有半年总结和年终总结……一年大约是 250 个工作日、50...

边城6阅读 793评论 2

封面图
Ubuntu20.04 从源代码编译安装 python3.10
Ubuntu 22.04 Release DateUbuntu 22.04 Jammy Jellyfish is scheduled for release on April 21, 2022If you’re ready to use Ubuntu 22.04 Jammy Jellyfish, you can either upgrade your current Ubuntu syste...

ponponon1阅读 4k评论 1

使用kubeasz部署高可用kubernetes集群
本实验采用kubeasz作为kubernetes环境部署工具,它是一个基于二进制方式部署和利用ansible-playbook实现自动化来快速部署高可用kubernetes集群的工具,详细介绍请查看kubeasz官方。本实验用到的所有虚拟机默认软...

李朝阳2阅读 482

windows 批处理bat,设置定时关机
文章来源:[链接] {代码...}

jigsaw2阅读 3.2k

解决Ubuntu宿主机下面安装Vmware时候vmmon和vmnet模块缺失导致的报错
场景在Ubuntu 20操作系统下面安装Vmware执行开始运行,启动虚拟机发现报错类似于下面这样的 {代码...} 手动启动Vmware模块,发现有两个模块启动失败 {代码...} 执行命令安装缺失的模块 {代码...} 解决方案查看VMw...

龚正阳阅读 1.9k

解决宏碁非凡S3 安装Win11时无法找到驱动器问题
1 问题描述机型:宏碁非凡S3 2022款CPU:i5 1240P安装系统:Win11 专业版问题描述:安装系统时,在选择驱动器界面无法找到驱动器,如下图所示2 解决流程查了一下网上的解决办法,进入BIOS把VMD Controller关闭掉...

xcghvgshjdfghsd阅读 977

Harbor私有仓库搭建并配置https对接docker与kubernetes
默认情况下,Harbor 不附带证书。可以在没有安全保护的情况下部署 Harbor,以便您可以通过 HTTP 连接到它。在生产环境中,推荐始终使用 HTTPS。要配置 HTTPS,必须创建 SSL 证书。可以使用由受信任的第三方 CA 签...

李朝阳2阅读 879

一路从后端走来,终于走在了前端!

56.2k 声望
26.5k 粉丝
宣传栏