资讯详情

资讯详情

Qt5服务化实践:用QtService将程序封装为Windows后台服务

简介基于Qt5版本的QtService服务库是一份从Qt4的qt-solutions迁移而来的后台服务组件面向需要用Qt编写Windows或Linux系统服务的C开发者。它保留QtService原有接口同时加入64位编译配置解决了Qt5环境下常见的移植构建问题适合有Qt基础、想快速集成服务能力的开发者。资源共52个文件以C源码.cpp/.h、Qt工程文件.pro、Visual Studio项目配置.vcxproj为主体另含QDoc格式API文档、HTML帮助页及示例工程压缩包仅96KB体积轻量但结构完整。已有2457人学习下载作者在多个实际项目中验证过稳定性工程文件依照个人需求定制读者可灵活调整编译选项配合文档和示例能快速理解服务封装逻辑免去从零移植、编译踩坑的时间成本。1. 从把程序变成服务说起QtService解决的是什么问题有段时间我一直在处理一个挺磨人的需求手头有个基于Qt5写的后台数据采集程序原本一直由人工启动、最小化到托盘运行结果客户那边电脑重启后总是忘了开数据就断档。最初想了个土办法把快捷方式丢进启动文件夹结果客户机器上有时候会弹UAC有时候启动顺序不对反正就是不稳定。后来被逼得不得不去研究正经做法——把Qt程序做成Windows服务让系统自己拉起它登录不登录都照样跑。系统服务这件事本身并没什么新鲜的。Windows上有服务控制管理器SCMLinux上有systemd或者init.d都是为了管理后台进程的生命周期。问题在于一个跑QT事件循环的图形程序要变成无界面的服务进程中间有一大堆细节服务如何注册、如何接收启动/停止命令、如何与SCM交互、如何处理没有桌面会话时的文件访问和日志。全手写的话Windows下要处理SERVICE_TABLE_ENTRY、ServiceMain、HandlerEx、SetServiceStatus那一套Win32 APILinux下要处理daemonize、pid文件、信号处理。一套项目要跨平台支持等于要写两套底层而且这两套代码的坑密度都还不小。QtService正好把这一层封装掉了。它最早是Qt Solutions组件库的一员后来在Qt 4时代被很多企业项目使用到了Qt5虽然不再是官方发布包的一部分但源代码仍然可用而且因为接口稳定不少老项目和新项目都在继续用。它的核心思路是你用普通QApplication的方式写逻辑继承一个QtService类实现start和stop两个虚函数然后库自己负责和操作系统的服务机制对接。安装、卸载、启动、停止、查询状态这些零碎操作它通过命令行参数和QtServiceController全部封装好了开发者的主要精力可以放在业务逻辑上。说白了QtService就是一个“胶水库”把Qt的事件循环翻译成系统服务能听懂的语言。做完那个数据采集程序的服务化改造之后我后来又在其他几个项目里用过这个库逐渐攒了一些和Qt5配合使用的经验和坑。这篇文章就把这些内容梳理出来适合已经有一定Qt基础、但没碰过服务开发的同学做参考。2. 编译与集成Qt5环境下获取源码和CMake手工接入2.1 先搞清楚现在的源码状态不少人在网上搜QtService会找到一堆老链接大多是Qt 4时代的产物点进去下载回来的工程文件也是 .pro 格式用的是qmake。需要先澄清一点Qt官方很久不再把QtService作为正式发布组件了官方仓库里的代码也长期处于维护但低频的状态。不过这并不意味着它就不能用恰恰相反它的接口非常稳定从Qt4到Qt5再到兼容版本的Qt6过渡核心接口基本没怎么变过。想要拿到源码通常有两个途径一个是去GitHub上找Qt Solutions的历史镜像仓库搜索qt-solutions-qt-service这种名字另一个是用集成在Qt安装包里的旧版例子有时候Qt安装目录自带了一部分补充组件。拿到源码之后你会发现核心文件不算多qt-solutions-qt-service/src目录下面主要有qtServiceBase.cpp/h、qtServiceController.cpp/h、qt_unixsocketserver等文件Windows平台还依赖一个qtServiceUtils.pri或类似文件来定义平台相关的东西。整个库的体量不大完全可以以源码形式塞进自己的工程没必要非得编成独立动态库。2.2 CMake手动集成的完整步骤因为我这些年一直用CMake管理构建所以没有走qmake的路线。下面这套接入流程我在Qt 5.12到5.15的工程里都验证过可以直接抄作业。第一步把下载下来的QtService源码目录整个拷贝到项目third_party/qt_service/下面保留src子目录。第二步在CMakeLists.txt里新建一个静态库目标把src里的.cpp文件加进去。需要注意qt_unixsocketserver.cpp这个文件在Windows下不需要编译它是Linux下用来做服务间通信的条件编译的时候要排除掉。set(QT_SERVICE_SRC ${CMAKE_CURRENT_SOURCE_DIR}/third_party/qt_service/src/qtServiceBase.cpp ${CMAKE_CURRENT_SOURCE_DIR}/third_party/qt_service/src/qtServiceController.cpp ${CMAKE_CURRENT_SOURCE_DIR}/third_party/qt_service/src/qtservice_p.h ) add_library(QtService STATIC ${QT_SERVICE_SRC}) target_include_directories(QtService PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/third_party/qt_service/src) target_link_libraries(QtService PUBLIC Qt5::Core)第三步在自己的可执行程序目标上链接这个静态库并且需要在编译时定义一个宏。QtService的代码里有个关键开关默认情况下它把main的入口也封装了如果你的程序想自己控制main函数展开就需要加上-DQT_SERVICE_EXPORT或者根据源码里的预编译条件调整。我的经验是直接以源码方式编译时把QT_NO_QT_SERVICE_CONTROLLER的定义去掉、确认QT_SERVICE_EXPORT相关宏按源码注释设置避免重复定义main入口。add_executable(MyDaemon main.cpp MyService.cpp) target_link_libraries(MyDaemon PRIVATE QtService Qt5::Core Qt5::Network) target_compile_definitions(MyDaemon PRIVATE QTSERVICE_MAIN)这里补充一句为什么要用静态库而不是直接编译成DLL服务程序通常以标准用户身份安装到系统里动态库的路径解析、依赖顺序都容易出问题静态链接之后生成一个独立的exe部署省心太多了。另外QtService库本身只依赖QtCore不会把你的可执行文件体积撑大多少。2.3 编译时常见的几个报错接入过程中我踩过的三个问题基本可以覆盖大多数人会遇到的编译失败场景。第一个找不到Q_OBJECT或者元对象编译器报错。QtService的类里是有Q_OBJECT宏的如果你在CMake里忘了加AUTOMOC或者AUTOMOC没正确扫描到third_party目录moc就不生成链接的时候会报一堆undefined reference。解决办法是确保set(CMAKE_AUTOMOC ON)并且target_include_directories里包含src目录。第二个和Qt5自带的QLocalSocket/QLocalServer版本冲突。QtService库里为了做服务命令通信在Windows下会用到命名管道某些版本里它自己实现了一套socket封装如果同一工程里又引入了QtNetwork里的QLocalSocket偶尔会有符号重复。这个坑出现频率不算高但一旦出现可以先检查是不是同时引用了Qt5::Network和QtService里的socket源文件导致的把重复文件从编译列表里移除即可。第三个Qt 5.15之后用高版本编译器编译某些老代码没写nullptr转换。比如qtServiceBase.cpp里和字符串转换有关的地方用C17标准编译时可能报警告甚至报错。遇到这种问题我都是直接小范围改源码把reinterpret_cast补上或者把老式QString::fromAscii改为QString::fromUtf8。3. 理解服务生命周期start/stop回调和事件循环的关系3.1 QtService的类体系与职责分配源码看多了之后会发现QtService库的核心其实就三个角色。第一个是QtServiceBase这是服务类本身负责封装操作系统服务控制逻辑包括注册回调、处理SCM命令、维护状态。它的内部有一个ServicePrivate实现类在Windows平台会进入ServiceMain函数然后调用一个消息循环来等待控制命令。第二个是QtServiceController这是一个独立的、专门和正在运行的服务进行通信的工具类它不运行在服务进程内部而是运行在外部进程里例如控制台程序或安装程序。第三个角色是命令辅助入口也就是库提供的QtService::exec()或者说main替代机制它统一解析argc/argv里的服务控制参数。理解了这个角色分配再去看代码就很清晰了你的服务进程内部主要重写的是QtServiceBase那一侧install、uninstall、start、stop这些控制命令实际上是通过外部进程调用QtServiceController完成的。3.2 回调函数里的代码到底什么时候跑刚开始写服务程序的人最容易犯的错误是把初始化逻辑一股脑塞进构造函数然后把耗时任务扔给start()函数最后发现服务启动超时被SCM杀掉。QtService的流程是这样的服务被SCM启动后库内部先完成基础初始化然后调用你重写的start()函数start()返回之前库会把服务状态标记为正在启动如果start()执行时间过长SCM会认为服务启动失败。官方推荐的做法是start()里只做轻量的准备工作然后立刻返回真正耗时的工作放到一个新的线程或者用Qt的事件队列异步执行。class MyService : public QtService { public: MyService(int argc, char **argv) : QtService(argc, argv, MyDataCollector) { setServiceDescription(Data collection background service); setStartupType(QtServiceController::Automatic); } protected: void start() override { // 轻量启动立即返回 workerThread new QThread(); worker new CollectorWorker(); worker-moveToThread(workerThread); connect(workerThread, QThread::started, worker, CollectorWorker::run); workerThread-start(); } void stop() override { // 通知线程安全退出并等待 worker-requestInterruption(); workerThread-quit(); workerThread-wait(3000); } private: QThread *workerThread nullptr; CollectorWorker *worker nullptr; };千万要记住stop()里也不能做太长时间的清理逻辑SCM等待服务停止也是有超时时间的默认一般是30秒但生产环境里配较短超时很常见。如果stop()阻塞太久服务会被SCM标记为停止失败短时间内无法重启。3.3 服务进程里的QApplication事件循环再有就是我之前一直没想明白的一点服务程序到底要不要QApplication答案是有的而且必须有。QtService底层会创建一个QCoreApplication的实例在Windows服务环境里它不是QApplication因为没有图形界面也不需要QApplication。这意味着你在服务里不能直接用QWidget相关的东西凡是依赖QGuiApplication的模块也都要避开。初始化的时候库内部创建的是QCoreApplication并保留对它的引用所以你在start()函数里可以通过QCoreApplication::instance()拿到应用实例然后正常连接信号槽、使用QTimer、使用QNetworkAccessManager这些都依赖Qt的事件循环。事件循环在服务里是通过exec()进人主循环的。也就是说QCoreApplication::exec()或者说service.exec()一旦进入服务进程就持续运行直到收到停止命令。这一点和普通Qt GUI程序非常相似区别只是没人会看到任何界面。4. 安装、卸载与调试命令行参数的隐藏玩法4.1 服务控制命令的完整清单QtService的代码里内置了一套命令行约定你不需要自己写参数解析器。以编译出的MyDaemon.exe为例常见的调用方式如下# 安装服务注册到SCM并设置为自动启动 MyDaemon.exe -install # 卸载服务 MyDaemon.exe -uninstall # 启动服务要求服务已安装 MyDaemon.exe -start # 停止服务 MyDaemon.exe -stop # 暂停服务 MyDaemon.exe -pause # 继续运行暂停的服务 MyDaemon.exe -resume # 查询服务状态 MyDaemon.exe -status这些命令本质上是通过QtServiceController去和SCM通信的和你的服务进程是不是在运行没有关系。比如你执行-install的时候服务程序会被当作控制程序来运行它不会进入服务主逻辑只是调用SCM的API把服务注册信息写入注册表。开发的时候还有一个特别有用的参数-debug。带上这个参数启动服务不会真正注册到SCM而是以前台进程的模式运行控制台窗口会保留直接输出日志。这个模式对排查初始化崩溃、路径问题非常有用。4.2 服务参数的存储位置小细节QtService在安装服务的时候会把命令行参数里的可执行文件路径记录下来。Windows服务注册表里ImagePath这一项的值是一个带引号的完整路径例如C:\Program Files\MyApp\MyDaemon.exe。如果程序目录里还有需要访问的配置文件服务默认的工作目录很可能不是这个exe所在目录而是系统目录所以程序内部不能依赖相对路径。关于服务启动类型、服务描述、显示名称可以在服务类的构造函数里配置MyService::MyService(int argc, char **argv) : QtService(argc, argv, MyDataCollector) { setServiceDescription(Data collection service); // Automatic开机自动启动Manual手动 setStartupType(QtServiceController::Automatic); }Windows下可以通过sc qc MyDataCollector命令查看配置是否生效也可以直接用sc start MyDataCollector、sc stop MyDataCollector来快速测试不一定非要每次都用QtServiceController。4.3 调试模式下要注意的差异我说句实在话服务程序最坑的不是写代码而是老大难问题本地调试好好的一进服务就崩。因为服务进程没有交互式桌面权限上下文、环境变量、工作目录全部和普通双击运行不一样。用-debug模式能解决一部分问题但它模拟不了SCM传入的身份和Session隔离。比较务实的调试策略是先用-debug模式把业务逻辑跑通再安装成服务配合Windows事件查看器和自定义日志文件做现场排查。不要试图在Visual Studio里F5直接调试已安装的服务那个流程很痛苦我试过几次还是日志法最有效。5. 服务场景里的三座大山路径、身份与日志5.1 工作目录不是你想的那样普通双击程序工作目录默认是启动位置的目录。但在Windows Service里SCM启动服务进程时工作目录几乎总是C:\Windows\System32这是无数服务化程序的第一个坑。你在代码里用什么QFile(config.ini)它会去System32目录下找。解决办法只有一招绝对路径。在main函数很早的时候根据可执行文件路径拼出程序目录然后统一封装一个ConfigManagerQString appDir QCoreApplication::applicationDirPath(); QString configPath appDir /config.ini;注意QCoreApplication::applicationDirPath()返回的是exe所在的路径这个路径在服务进程和普通进程下拿到的是同一个值所以用它作为基准是最稳的。5.2 Session 0隔离带来的权限变化从Vista开始Windows引入了Session 0隔离服务进程运行在Session 0普通用户登录桌面的进程运行在Session 1及以后。这带来的直接后果是服务进程无法直接显示UI、无法访问用户桌面上的文件除非路径写死且权限放开。QtService本身不管这些它只是确保服务能运行但业务代码里一旦涉及特定用户的目录例如C:\Users用户名\AppData就得评估权限。有个常见的做法是让服务以LocalSystem身份运行这样权限很大基本上什么目录都能读但随之而来的是安全隐患如果业务逻辑里有网络监听被攻破就等于整个机器沦陷。另一个做法是用专门的域用户账号或服务账号运行服务然后在代码里通过QFileInfo::setPermissions或显式ACL设置目录访问权限。我的建议是在Windows服务里尽量把数据读写集中在ProgramData或者自己创建的数据目录不要和服务进程账号的HKCU较劲。补充一句Linux平台的情况QtService在Unix-like系统上通过写pid文件、捕获SIGTERM信号来模拟服务控制行为上和Windows差异不小但好在权限模型相对清晰大多是文件权限问题不涉及Session隔离。5.3 日志问题的务实解法服务程序不能像普通程序那样把日志输出到stdout骗自己除非你用-debug模式。生产运行下SCM不会为你保留任何标准输出。这时候有几种选择第一种是QtService本身提供日志重定向能力在构造函数里setLogFile(C:/ProgramData/MyApp/service.log)它会定期把qDebug、qWarning写到文件里。第二种是自己封装一个文件日志Logger每次写日志前按日期滚动。第三种是接第三方日志库比如spdlog或QsLog。三种我都用过我的偏好是业务日志用spdlog写滚动文件设置最大大小和备份数QtService自己的日志层留空只在非常早期初始化崩溃时用。原因很简单QtService的日志重定向实现比较基础不支持格式化、不支持分级、没有滚动长期跑会产生一个巨大的文件。如果服务崩溃在启动阶段日志文件又没来得及被spdlog创建那QtService的setLogFile就是你最后的救命稻草它能记录到库内部的初始化事件。所以哪怕最终不用它记录业务日志也建议设置一个基础日志路径。6. 那些我至今还在用的排错流程和检查清单服务开发做得多了我形成了自己的一套固定排查流程遇到问题先按顺序过一遍省了不少诊断时间。第一确认服务有没有真的安装。执行sc query MyDataCollector如果返回的服务状态是1060错误或者其他提示说明SERVICE_NAME写错了。检查注册表HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MyDataCollector看看ImagePath路径是否完整。第二确认程序是不是真的转起来了。Windows事件查看器里Windows日志-系统来源为Service Control Manager里面会有服务的启动/停止记录包括错误码。如果启动失败错误码通常能直接定位问题比如操作系统找不到指定路径十有八九是ImagePath错了服务在启动期间崩溃错误码后面经常跟着异常模块。第三打开配置文件审计。在-debug模式下加上--log-verbose之类的参数让程序在启动早期就输出当前工作目录、配置文件路径、日志文件路径、当前用户名。这几条信息至少能排除掉一半的环境问题。第四验证服务账号有没有文件访问权限。这一步最容易被忽略。在服务配置里临时切到LocalSystem跑一次如果程序就能正常运行那基本是文件权限问题而不是代码逻辑问题。做完这些还解决不了那就只能加日志重编译了。这很痛苦但服务开发和普通程序开发不同远程调试的成本极高所以日志从一开始就要当作第一优先级来做。我现在的习惯是服务程序的启动日志必须含时间戳和功能模块名第一时间能看出走到了哪一行。如果让我给一个基于Qt5版本的QtService项目做个总结性建议那就是不要试图让它做超越服务范畴的事情不要依赖桌面GUI不要对工作目录做任何假设把所有配置文件的基准目录都用applicationDirPath拼出来日志按天滚动启动和停止回调保持轻量。做到这几条这个库用起来会非常顺手。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →