简介:基于Qt5版本的QtService服务库,是一套从qt-solutions旧版平滑迁移而来的后台服务框架,核心解决在Windows/Linux环境下将Qt程序封装为系统服务的问题,适合面向服务端后台程序开发、希望摆脱Qt4老旧依赖的Qt5开发者。作者在多个实际工程项目中反复使用并修正,稳定性得到验证;工程文件按自身场景定制,内置64位编译配置,也保留了修改余地,可适配不同项目需要。资源包共52个文件,体积仅96KB,代码保留include/src/examples模块划分,其中cpp/h源文件与pro/vcxproj工程文件对应清晰,另有html和qdoc说明文档、VS筛选/用户配置文件及Makefile,层次完整,便于定位。目前已有2459人次在CSDN学习下载。通过阅读源码,可重点掌握QtService命名空间的核心类组织、跨平台服务控制逻辑、本地Socket通信与多线程处理方式,以及服务程序的安装、启停和卸载流程;示例工程中展示了server、controller、interactive三种调用模式,方便对照实际需求改造。附属的release/debug目录还可快速验证编译链接结果。对于需要在Qt5中重构老服务端程序或新写守护进程的开发者,这套精简实例具有直接借鉴价值。
1. 服务不是黑匣子:把 Qt 程序变成 Windows 服务的桥
如果你写过需要“开机就运行、用户退出也运行”的 Qt 程序,大概率遇到过这种尴尬:程序写好双击能跑,一放到“任务计划程序”里就失效,锁屏后直接断掉,要么被系统杀掉,要么弹窗等人点确定。真正的 Windows 服务是独立于用户会话存在的,由系统服务控制管理器(SCM)拉起,不依赖桌面登录状态。QtService 正是解决这个问题的现成组件:它把 Qt 程序包装成 Windows 服务或 Unix 守护进程,一套业务代码,既能前台调试,又能后台常驻。适合谁用?被 Windows 服务 API 劝退的客户端开发者,以及需要把采集、上报、中转类程序做成无人值守工具的从业者。这就是本文要拆的东西:基于 Qt5 版 QtService 的编译、集成与排障全流程。
2. 直接用 QtService 之前:SCM、服务入口与 Qt 事件循环
2.1 为什么普通 Qt 程序当不了 Windows 服务
Windows 服务的运行机制和普通 exe 完全不同。普通程序由用户双击启动,进程活在某个用户会话里,用户注销进程就结束;服务则由 SCM 负责启动、停止、重启,进程活在 Session 0,与用户桌面隔离。更关键的是入口点差异:服务程序导出的不是 main 函数,而是 ServiceMain 回调,进程启动后必须在规定时间内向 SCM 报告“已进入运行状态”,否则 SCM 会判定启动超时并直接终止进程。
这意味着,直接把一个 QApplication 的 main 函数编译成 exe,再手动注册成服务,基本跑不起来——你没有实现 ServiceMain,没有处理 SERVICE_STATUS 状态上报,SCM 根本不知道这个进程在想什么。常见的蹩脚做法是:把 Qt 程序丢进任务计划,配置“用户登录时运行”,再用脚本保持会话不退出。这套方案有两个硬伤:一是用户必须登录且不能锁屏;二是安全策略一变,计划任务触发失败没有任何提示,你只能对着黑匣子猜。
QtService 把这一整层平台差异封装进了库内部。它替你实现了 ServiceMain、状态上报、安装/卸载所需的 SCM 调用,同时保留一个标准的 Qt 事件循环入口。你的业务类只需要继承 QtService 这个模板类,实现 start() 和 stop() 两个虚函数,剩下的服务化脏活由库完成。
2.2 拆开看:QtService 其实是三层结构
从使用角度看,这个库包含三个角色:QtServiceController、QtServiceBase、以及你的业务类(通常写成 QtService<QCoreApplication> 的派生类)。
- QtServiceController 运行在外部控制端,不是服务进程本身。它负责向 SCM 注册服务、启动服务、停止服务、查询服务状态,以及卸载服务。你可以把它理解成一个“遥控器”,它通过 SCM 的接口和真正的服务进程通信。
- QtServiceBase 是库内部的核心实现,负责解析命令行参数、判断当前进程是被 SCM 拉起还是被手动调试,然后走不同的初始化分支。它还持有服务名、服务描述、启动类型等元数据。
- 业务派生类是你唯一要写的部分。模板参数可以是 QCoreApplication 或 QApplication,这决定服务进程里有没有 GUI 对象。一般后台服务选 QCoreApplication 就够。
Windows 平台上,QtServiceBase 内部会调用 OpenSCManager、CreateService、StartService 这一串 API;Unix 平台上则换成守护进程和 Unix socket 机制。因此移植业务代码时,你接触的接口不变,变的是库内部实现。
2.3 事件循环不能死在半路
我最初拆这个库时犯过一个典型的错:在服务的 start() 里写了一个 while(1) 循环做轮询,结果服务装好后一启动就报 1053 错误。原因不是权限,而是 start() 必须在短时间内返回,让 Qt 事件循环跑起来,才能由库向 SCM 汇报“服务已运行”。如果 start() 被死循环卡住,事件循环永远没有机会启动,状态上报自然也发不出去。
正确姿势是把重活交给事件循环。比如在 start() 中启动一个 QTimer,或者把耗时的轮询任务丢进工作线程,然后立即让出控制权。QtService 的价值恰恰在于:它允许你用 Qt 的信号槽、定时器、网络模块组织服务逻辑,而不是去写繁琐的 Windows API 服务回调。
3. 编译与集成:把 QtService 源码拆进自己的工程
3.1 源码拿到手,先看目录结构再动手
QtService 是典型的 Qt Solutions 组件,源码包不大,核心文件集中在顶层目录。一般会包含 qtservice.h、qtservice.cpp、qtservice_p.h,以及平台相关的私有实现。Windows 下主要依赖 qtservice_p.h 里的 SCM 封装,Unix 下则多出 Unix socket 相关源文件。
我的建议是:不要预编译成单独的库文件,直接把必要源码加入工程一起编译。原因是这个库更新节奏慢,不同 Qt 版本对旧代码的兼容性差异明显,源码编译时发现问题能当场改,预编译库出了问题反而难排查。把 qtservice.cpp 和平台相关文件放入 SOURCES 后,整体编译成本几乎可以忽略。
3.2 用 .pro 文件把源码编进工程
假设你的工程叫 MyGuardService,源码目录结构如下:
QT += core QT -= gui CONFIG += console CONFIG -= app_bundle TEMPLATE = app TARGET = MyGuardService DEFINES += QT_DEPRECATED_WARNINGS # QtService 库源码,直接编译进工程 SOURCES += \ qtservice/qtservice.cpp \ src/main.cpp \ src/myservice.cpp HEADERS += \ qtservice/qtservice.h \ src/myservice.h这里有几个容易忽略的参数。
QT -= gui决定了服务进程不带 GUI 模块,如果你确实需要 QApplication 并且服务里要创建窗口对象,这一行就要删掉,改为QT += gui。但对于纯后台服务,保留QT -= gui能减少依赖,也避免在 Session 0 里创建不可见窗口带来的混乱。
CONFIG += console是一个双刃剑:在调试阶段加上它,你可以在前台运行时看到 qDebug 输出;服务化部署时这个窗口不会显示,但对排障有帮助。我一般建议调试期保留,发布前去掉。
TARGET就是服务注册后对应的可执行文件名称,注册服务和查日志都要用这个名字,别随意改。
3.3 写一个最小可复现的服务类
新建 myservice.h 和 myservice.cpp,声明一个 QtService 派生类:
#include <QTimer> #include <QCoreApplication> #include "qtservice.h" class MyGuardService : public QtService<QCoreApplication> { public: MyGuardService(int argc, char **argv); void start() override; void stop() override; private: QTimer m_timer; };实现文件里把业务起点放在定时器回调中,保证 start() 不阻塞:
#include "myservice.h" #include <QDebug> MyGuardService::MyGuardService(int argc, char **argv) : QtService<QCoreApplication>(argc, argv, "MyGuardService") { setServiceDescription(QStringLiteral("某数据上报服务:定时采集并转发数据")); setServiceFlags(QtServiceBase::CanBeSuspended); } void MyGuardService::start() { qDebug() << "service start"; connect(&m_timer, &QTimer::timeout, this, [this]() { // 这里写真正的业务逻辑:读配置、采集数据、上报结果 qDebug() << "tick"; }); m_timer.start(5000); } void MyGuardService::stop() { qDebug() << "service stop"; m_timer.stop(); }构造函数里传给 QtService 的第三个参数是“服务内部名”,必须唯一,不能和系统已有服务重名。setServiceDescription 填的是服务在管理器里显示的中文描述,方便别人一眼看出这个服务是干什么的。start() 里只做连接和启动定时器,立刻返回,事件循环由 service.exec() 驱动,这是和 Windows 服务状态机配合的关键。
main 函数只做一件事:
#include <QCoreApplication> #include "myservice.h" int main(int argc, char *argv[]) { MyGuardService service(argc, argv); return service.exec(); }exec() 内部会根据命令行参数自动分流:带调试参数则按普通进程启动,否则按服务方式启动。这样同一份 exe 既能前台调试,又能后台部署。
3.4 用 QtServiceController 完成安装、启动、停止
服务程序本身不具备自安装能力,需要单独写一个控制端程序来调用 SCM。这里我一般复用同一个工程,用一个命令参数分支来执行控制操作,也可以单独建一个控制台应用:
#include <QCoreApplication> #include <QString> #include "qtservice.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QtServiceController controller(QStringLiteral("MyGuardService")); const QString binPath = QStringLiteral("C:/deploy/MyGuardService.exe"); if (controller.installService(binPath, QtServiceController::AutoStart)) { qDebug() << "install ok"; } if (controller.isInstalled()) { qDebug() << "installed already"; } if (controller.startService()) { qDebug() << "start ok"; } if (controller.isRunning()) { qDebug() << "service running"; } if (controller.stopService()) { qDebug() << "stop ok"; } if (controller.uninstallService()) { qDebug() << "uninstall ok"; } return 0; }installService 的第一个参数必须是 exe 的绝对路径,第二个参数选 AutoStart 表示开机自启,选 Manual 则表示手动启动。这里有一个容易混淆的点:installService 之后服务并没有立即启动,需要再调 startService 才行。很多人以为“装好了就等于跑起来了”,结果服务列表里能看到但状态是“已停止”,就以为自己写错了。
4. 实战避坑:服务起不来、写不进文件、调试像黑匣子的五个经典坑
4.1 Error 1053:服务启动后又自动回滚
现象是服务管理器里点“启动”,转几圈后弹窗报“服务没有及时响应启动或控制请求”,服务状态回到已停止。
原因几乎都出在 start() 里:要么业务初始化耗时太长,超过 SCM 的 30 秒等待窗口;要么 start() 里跑死循环,事件循环没机会执行,状态上报永远停在“启动中”。SCM 一旦等不到 SERVICE_RUNNING 状态,就判定启动失败并终止进程。
解决分两步。第一步,把初始化里耗时超过 100ms 的操作全部挪到工作线程,start() 只负责把线程拉起来并立刻返回。第二步,确认你的业务循环不是裸 while,而是基于 QTimer、Qt 网络事件或工作线程信号槽,让事件循环保持活着。
4.2 日志文件神秘消失,全跑进 System32
现象是程序里用相对路径写日志,服务跑起来后日志文件找不到;到 C:\Windows\System32 目录里能翻到同名文件。
原因是服务进程由 SCM 启动,工作目录默认不是 exe 所在目录,而是系统目录。你代码里写的相对路径全被解析到 System32 下了。更隐蔽的是,across 不同系统版本,这个行为还会因为权限变化出现差异,表现为“在我机器上能写,到服务器上写不了”。
解决是代码里禁止使用相对路径,统一通过 QStandardPaths::writableLocation 或配置文件中读取的绝对路径来定位文件。比如日志目录写成 C:/ProgramData/MyGuardService/logs,并在服务启动时先 QDir().mkpath 创建目录。从那以后我只要在服务代码里看到相对路径,一律当 bug 处理。
4.3 Qt Creator 里一跑就退出,调试窗口像黑匣子
现象是用 Qt Creator 直接按 F5 调试服务程序,进程“正常退出”,没有任何报错,也没有进入你的断点。
原因是服务程序在非服务环境下启动时,QtService 会尝试向 SCM 注册服务并启动,但当前根本没有安装过该服务,调用链最终失败退出;更麻烦的是调试器无法 attach 到由 SCM 拉起的进程,你按 F5 等于在调试一个注定要自杀的进程。
解决是用服务程序自带的前台模式。QtService 生态里最常见的方式是给 exe 传-debug参数,它会跳过 SCM 交互,直接作为普通进程运行,此时所有 qDebug 输出都进控制台,断点也能正常命中。调试完再换成无参数方式部署。
4.4 服务里创建的窗口没人看得到
现象是服务代码里 new 了一个 QWidget,桌面端完全看不到窗口,程序也没报错。
原因是 Windows Vista 之后的 Session 0 隔离机制:服务进程运行在独立的 Session 0,用户桌面在 Session 1 或更高编号会话,两个会话的窗口互不可见。这不是 Qt 的问题,是系统安全设计。
解决是明确服务职责:服务里不要做任何需要用户可见界面的操作。真要提供配置界面,应该写成独立配置程序,用配置文件或命名管道与服务进程通信;或者让服务把数据写入共享目录,由桌面程序读取展示。
4.5 开机自启不生效,但手动启动正常
现象是服务安装成功,手动启动一切正常,重启电脑后服务处于“已停止”状态。
原因通常是安装服务时 StartMode 用了 Manual,或者服务安装后依赖的目录/文件路径在开机阶段还不可用(比如网络驱动器尚未挂载)。
解决是把安装代码里的第二个参数明确设为 AutoStart,同时检查服务依赖项。如果服务依赖数据库或网络,在 QtServiceController 里没有直接暴露依赖配置入口,需要在服务安装后用sc config MyGuardService start=auto和sc qfailure MyGuardService检查依赖。另外,绝大多数“开机不自启”案例最后都发现是安装时根本没传 AutoStart,捡了芝麻丢了西瓜。
5. 验收巡检:从安装顺序到状态检查,再到故障恢复配置
5.1 一套完整的安装与验证步骤
服务类程序写好后,建议按固定顺序走一遍验收流程,少走弯路。安装前先确认 exe 路径不含空格和中文,尽量放 C:/deploy 这种纯英文目录;然后以管理员身份打开命令行,运行控制端程序的安装指令。
接着打开系统服务管理器,确认服务存在且启动类型符合预期,手动启动一次,确认状态为“正在运行”。再打开任务管理器,找到对应进程,确认进程没有被反复拉起又退出。最后重启一遍机器,观察服务是否自动进入运行态。
这整套流程跑完,才算是完成了基础验收。如果你是熟手,可以跳过服务管理器,直接敲命令:
sc query MyGuardService sc qc MyGuardServicesc query输出的 STATE 列能直观看到服务是否在运行,sc qc能查看启动类型和服务二进制路径。我每次部署完都会强制跑一遍这两条命令,结果对了再干别的。
5.2 配置故障恢复:服务崩溃不能裸奔
Windows 服务默认崩溃后不会自动重启,这一点很多人不知道。如果服务是守门程序,业务进程崩了,整个链路就断了。配置故障恢复可以不改代码,直接通过 sc 命令完成:
sc failure MyGuardService reset= 86400 actions= restart/5000/restart/10000/restart/30000这条命令的意思是:服务崩溃后 5 秒自动重启,再失败等 10 秒重启,第三次失败等 30 秒重启,一天内重置失败计数。这个配置建议在安装脚本里一并执行,而不是事后再手动补。它的价值在于:大多数临时性故障(端口占用、依赖服务未就绪)都会在自动重启后自愈,你不需要半夜爬起来手动点启动。
5.3 从退出码和日志反推问题
服务进程异常退出时,Windows 事件查看器里会留下来源为 Service Control Manager 的日志,里面带有退出码。常见退出码 1053 表示启动超时,1067 表示进程意外终止,1079 表示服务指定账号与已运行服务冲突。
代码层面,我习惯在一开始就把日志落盘做扎实:启动时记录 pid、路径、参数,停止时记录退出原因。这样服务部署到现场后,即使界面完全不可见,也能靠日志还原崩溃时的上下文。不要依赖 qDebug 输出,服务部署后没人盯着控制台。
6. 一个实用技巧:让崩溃后自动重启真正生效的细节
上面的sc failure命令能解决进程级崩溃,但有三种情况它救不回来:一是服务启动时直接报“参数错误”这类配置问题,永远不会进入运行态;二是服务因系统关机而被结束,那是正常流程;三是服务进程还在但内部业务线程死锁,进程活着,业务死了。
最后一种最隐蔽。我遇到过服务进程状态“正在运行”,但内部定时器已停摆、网络连接全部断开的情况。这时候靠 SCM 的 failure 配置没用,因为进程没退出。我的习惯是在业务里加一个心跳文件:工作线程每 30 秒更新一次文件时间戳,外部用任务计划每 5 分钟检查一次时间戳,超过 2 分钟未更新就重启服务:
for /f "tokens=*" %i in ('powershell -Command "(Get-Item C:/data/heartbeat.txt).LastWriteTime"') do set t=%i脚本判断当前时间和文件时间的差值,超阈值就调用sc stop MyGuardService加sc start MyGuardService。这种做法治不了代码级死锁的根,但能让服务在工作线程假死后自动恢复,算是投入产出比很高的兜底方案。
从那以后我每次写完服务都会强制走一遍三连检查:安装前看路径和启动类型、启动后看事件循环有没有活着、部署后看故障恢复有没有配置。这三步能挡住九成以上的现场事故,希望帮到你。
本文还有配套的精品资源,点击获取