前言
最近了解到qt-solutions这个开源项目,仔细研究一番,发现其中的QtServer项目能在Windows系统中创建系统服务,Linux/Unix系统中能作为守护进程使用,之前一直以为编写服务需要使用Windows api来实现,没想到这么简单。
本来之前就想把Aria2.exe封装成后台服务,然后内网穿透,在配和使用AriaNG Web界面访问,实现远程控制下载,迫于工作量直接放弃,
仔细研究发现QtServer库实现极其简单,于是就又尝试了一番,半天就搞定了实现Aria2.exe的系统服务,又仔细研究了下Aria2.exe的Json-Rpc访问接口。
正文导读
QtServer开源库
QtServer库:https://github.com/MliesMoT/qt-solutions/tree/master/qtservice
qt-solutions/qtservice 是 Qt 官方提供的一个跨平台服务(守护进程)开发库,用于将 Qt 应用程序转换为系统服务(Windows 服务或 Linux/Unix 守护进程)。该库属于 Qt Solutions 系列(现已归档,但仍可独立使用)。
下载源码发现,代码量也不高,其中examples包含了多个实例,
编译后使用windeployqt打包就能直接安装到控制面板->服务上,
examples 示例中的代码量也不高,甚至改改名字就能直接用的程度,
在Qt Creator中使用时直接拷贝src到项目目录下
再.pro文件中添加 include(src/qtservice.pri)
在引用 #include "qtservice.h"
就能直接使用了,简单好用。
在Visual Studio 2019 开发环境中使用就有点要老命了,可能纯属个例,当我直接使用 Qt->Improt .pri File To Project
引入qtservice.pri
的时候直接一堆异常,无法解析的外部命令
浪费一段时间无果,
于是我只能先用Qt Creater编译QtServer库,在Vs中引用头文件和lib,DLL。
首先加载buildlib项目:
修改buildlib.pro文件中的输出目录,同时添加Lib输出,如下示例:
TEMPLATE=lib
CONFIG += qt dll qtservice-buildlib
mac:CONFIG += absolute_library_soname
win32|mac:!wince*:!win32-msvc:!macx-xcode:CONFIG += debug_and_release build_all
include(../src/qtservice.pri)
TARGET = $$QTSERVICE_LIBNAME
DESTDIR = $$QTSERVICE_LIBDIR
win32 {
DLLDESTDIR = $$PWD
QMAKE_DISTCLEAN += $$PWD\\$${QTSERVICE_LIBNAME}.dll
}
target.path = $$DESTDIR
INSTALLS += target
# 在构建库时定义 QT_QTSERVICE_EXPORT
DEFINES += QT_QTSERVICE_EXPORT
重新编译得到lib和DLL文件
在Vs中附加包含目录 src中的头文件目录 ,DLL复制到生成目录下
添加引用,完成QtServer服务的调用
#include "qtservice.h"
#pragma comment(lib, "qtServer/lib/QtSolutions_Service-head.lib")
通过继承QtService<QApplication>
实现start()
,stop()
,pause()
,resume()
方法就实现了系统服务的启动
,停止
,暂停
,重启
功能。
通过 setServiceDescription()
方法实现服务具体描述
如: setServiceDescription("一个启动Aria2.exe的后台服务,可通过修改aria2.conf文件重启服务来修改默认配置,默认使用RPC令牌和6800端口");
会在服务的描述信息中展示上述文本内容。
这里只需要启动和停止两个功能,就只需要实现start()和stop()两个方法就可以了。
使用QProcess
通过命令行启动Aria2.exe程序,通过aria2.conf
设置默认参数,
如果需要更改端口或值RPC令牌,直接修改aria2.conf配置文件重启服务就行了。
QString dirs=QCoreApplication::applicationDirPath();
Aria2FilePath=dirs+"/aria2c.exe";
ConfFilePath=dirs+"/aria2.conf";
if(!QFileInfo::exists(Aria2FilePath) || !QFileInfo::exists(ConfFilePath))
return ConfLost;
process=new QProcess();
//重载信号,需要使用 QOverload 或 qOverload(Qt5.7+):
QObject::connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
[this](int exitCode, QProcess::ExitStatus exitStatus) {
if (exitStatus == QProcess::CrashExit) {
q_ptr->logMessage(QString("QProcess 进程意外结束,错误码:%1 ").arg(exitCode), QtServiceBase::Error);
ErrorOver();
}
});
QString CommentStr=QString("\"%1\" --conf-path=\"%2\" \r\n")
.arg(Aria2FilePath).arg(ConfFilePath);
process->start(CommentStr);
if(!process->waitForStarted(10000))
{
process->terminate();
if (!process->waitForFinished(1000)) {
process->kill();
}
delete process;
process=nullptr;
return NG;
}
//! 通过RPC服务判断是否启动Aria2,没有启动就停止服务
if(Lib_NetWork::NetWorkGlobal()->getVersion()=="")
return NG;
return OK;
使用QCoreApplication
中断服务的运行,
这在服务未能正确启动时
终止后续服务启动状态,非常适用。
void Aria2InteractiveServerPrivate::ErrorOver()
{
QCoreApplication *app = q_ptr->application();
app->quit();
}
关闭服务,先使用json-Rpc接口通知Aria2.exe程序强制退出,在通过QProcess进程的强制结束,关闭服务。如果只关闭QProcess,很容易随机 出现Aria2.exe程序挂起的后台程序
//! 强制结束
Lib_NetWork::NetWorkGlobal()->forceShutDown();
//! 关闭 QProcess
if(process!=nullptr)
{
process->terminate();
if (!process->waitForFinished(1000)) {
process->kill();
}
delete process;
process=nullptr;
return NG;
}
Http访问Json-RPC接口
通过 Aria2的手册 可以查询到Json-RPC接口的调用传参,
这其中需要注意的是编码格式和JSON字符串的拼接,特别是有RPC令牌和没有RPC令牌两种情况下的拼接字符串。
在QT中执行HTTP请求的时候,基本都会封装使用这个方法;
使用QEventLoop
堵塞事件等待HTTP请求完成,
使用QTimer
防止HTTP请求超时。
bool HttpGet(QString httpUrl,QString& Error,QByteArray& result)
{
//! 超时时间
QTimer * out_timer = new QTimer();
QNetworkRequest request;
//设置请求头---浏览器
request.setHeader(QNetworkRequest::UserAgentHeader,"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36 Edg/93.0.961.38");
request.setHeader(QNetworkRequest::ContentTypeHeader,"application/x-www-form-urlencoded");
request.setUrl(QUrl(httpUrl));
QNetworkAccessManager* manager = new QNetworkAccessManager();
QNetworkReply *reply = manager->get(request);
//! 堵塞线程
QEventLoop eventLoop;
//! 请求结束
QObject::connect(reply, SIGNAL(finished()),&eventLoop, SLOT(quit()));
QObject::connect(reply, SIGNAL(aboutToClose()),&eventLoop, SLOT(quit()));
//! 线程超时
QObject::connect(out_timer,&QTimer::timeout,reply,&QNetworkReply::abort);
out_timer->start(5000); //等待5秒 5秒内无响应自动中断
//! 堵塞线程等待响应
eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
//! 清除计时器
out_timer->stop();
delete out_timer;
out_timer=nullptr;
result=reply->readAll();
if(reply->error() != QNetworkReply::NoError)
{
Error="Http请求异常:"+QString(reply->error());
if(result!="")
{
//"{\"id\":\"qwer\",\"jsonrpc\":\"2.0\",\"error\":{\"code\":1,\"message\":\"Unauthorized\"}}"
QJsonParseError Jsonerror;
QJsonDocument resultDoc=QJsonDocument::fromJson(result,&Jsonerror);
if(Jsonerror.error==QJsonParseError::NoError)
{
Error+=",message:"+resultDoc["error"].toObject()["message"].toString();
}
}
return false;
}
return true;
}
需要参考手册上把各个参数按照指定结构拼接成JSON字符串,
在使用.toBase64().toPercentEncoding()
转码,需要注意要不然识别不了。
QString getVersion()
{
//! 如果服务启动,那么应该能获取到aria2的状态
QString Error;
QString Jsonrpc="http://localhost:"+rpc_listen_port+"/jsonrpc?params=";
QJsonArray params;
params.append("token:"+rpc_secret);
QJsonObject object;
object.insert("jsonrpc","2.0");
object.insert("id","qwer");
object.insert("method","aria2.getVersion");
object.insert("params",params);
QJsonDocument Document(object);
QByteArray JsonStr=Document.toJson(QJsonDocument::Compact);
QString Dataparams= JsonStr.toBase64().toPercentEncoding();
Jsonrpc.append(Dataparams);
//qDebug()<<"[Jsonrpc] "<<Jsonrpc;
QByteArray result;
if(!HttpGet(Jsonrpc,Error,result))
{
qDebug()<<Error;
return "";
}
QString Version="";
QJsonParseError Jsonerror;
QJsonDocument resultDoc=QJsonDocument::fromJson(result,&Jsonerror);
if(Jsonerror.error==QJsonParseError::NoError)
{
Version =resultDoc["result"]["version"].toString();
}
qDebug().noquote()<<Version;
return Version;
}
输出:
{"id":"qwer","jsonrpc":"2.0","result":{"enabledFeatures":["Async DNS","BitTorrent","Firefox3 Cookie","GZip","HTTPS","Message Digest","Metalink","XML-RPC","SFTP"],"version":"1.37.0"}}
拼接JSON字符串基本都差不多一个模版,改个方法名称就能直接用
QString Error;
QString Jsonrpc="http://localhost:"+rpc_listen_port+"/jsonrpc?params=";
QJsonArray params;
params.append("token:"+rpc_secret);
QJsonObject object;
object.insert("jsonrpc","2.0");
object.insert("id","qwer");
object.insert("method","aria2.forceShutdown");
object.insert("params",params);
QJsonDocument Document(object);
QByteArray JsonStr=Document.toJson(QJsonDocument::Compact);
qDebug().noquote()<<"JsonStr: \n"<<JsonStr;
QString Dataparams= JsonStr.toBase64().toPercentEncoding();
Jsonrpc.append(Dataparams);
QByteArray result;
if(!HttpGet(Jsonrpc,Error,result))
{
qDebug()<<Error;
}
// qDebug().noquote()<<"Jsonrpc: \n"<<Jsonrpc;
qDebug().noquote()<<"Result: \n" <<result;
return ;
安装服务
这样一来基本服务的所有功能都实现了只需要通过windeployqt 打包exe,
在管理员权限的CMD中输入命令行:
Aria2_QtServer.exe -i //! 安装服务
Aria2_QtServer.exe -u //! 卸载服务
Aria2_QtServer.exe -t //! 停止服务
Aria2_QtServer.exe -s //! 开始服务
就安装完成了:
使用AriaNGg访问,随便找个镜像下载:
搞定!
Aria2 支持HTTP/HTTPS 链接,FTP 链接,BitTorrent 种子文件或磁力链接,Metalink 文件, SFTP 协议,配合AriaNG的界面化使用,
一个简易版的迅雷下载就完成了!