文章目录
概要
根据上篇文章中的Aria2.exe下载程序,简单创建一个使用QNetworkReply,QNetworkRequest下载http/https资源的包含输出进度的控制台程序。
Aria2.exe 相关可以查看
整体架构流程
1. 设计控制台传递参数的字符串格式
如:v 输出所有参数说明
void OutVersionINFO()
{
std::cout<<"QT 下载工具:用来下载http/https资源,命令行空格隔开"<<std::endl;
std::cout<<"================================"<<std::endl;
std::cout<<" -V 显示命令行参数"<<std::endl;
std::cout<<" -url:? http/https资源链接(必须) 如(-url:https://xxx//x/x.txt)"<<std::endl;
std::cout<<" -filename: 完整文件名(可选) 如(-filename:x.txt)"<<std::endl;
std::cout<<" -outdir: 输出到指定目录(必须) 如(-outdir:c:/Temp)"<<std::endl;
std::cout<<" -md5: 文件完整性md5验证(可选) 如(-md5:78abd5ed0d2faa66749ad59a4c869def)"<<std::endl;
std::cout<<" -recount: 下载失败后重连次数(可选,默认10次) 如(-recount:5)"<<std::endl;
std::cout<<" 完整示例:"<<std::endl;
std::cout<<" \"C:\\Users\\admin\\Desktop\\text1\\QtDownEngine.exe\" -url:\"http://xxx/xxxpe.xxx\" -outdir:\"C:\\Users\\admin\\Desktop\\MSCV2017 RELEASE DLL\\text\" -md5:\"a78406f15a3c4da746c0f335c109519f\" -recount:20"<<std::endl;
std::cout<<"================================"<<std::endl;
}
2. 定义下载数据结构并且能通过main获取数据
定义个下载的数据结构DownParameter,包括http/https资源链接,输出到指定目录,文件完整性md5字符串等信息
//! 下载文件参数结构
struct DownParameter
{
QString httpurl; //! -url:
QString filename; //! -filename:
QString outdir; //! -outdir:
QString Md5; //! -md5:
//! 重连次数
int Reconnection=10; //! -recount:10
bool isvalid()
{
return (httpurl.trimmed()!="" && outdir.trimmed()!="");
}
//! 打印/调试
void Print()
{
std::cout<<"Down Parameter -->"<<std::endl;
std::cout<<"httpurl: "<<httpurl.toStdString().c_str()<<std::endl;
std::cout<<"filename: "<<filename.toStdString().c_str()<<std::endl;
std::cout<<"outdir: "<<outdir.toStdString().c_str()<<std::endl;
std::cout<<"Md5: "<<Md5.toStdString().c_str()<<std::endl;
std::cout<<"Reconnection: "<<Reconnection<<std::endl;
}
};
将main传入的参数绑定到结构体上:
注意使用setlocale(LC_ALL, "zh_CN.utf8") 设置控制台输出为utf-8格式,否则乱码!
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
setlocale(LC_ALL, "zh_CN.utf8"); // 设置本地化为简体中文 UTF-8
DownParameter Parameter;
//! 解析命令行
for(int i=0;i<argc;i++)
{
QString val=QString::fromStdString(argv[i]).trimmed();
std::cout <<"[i] "<<i<<" [val] "<<val.toStdString().c_str() << std::endl;
if(val.toUpper()=="-V")
{
goto Print_Format;
}
if(val.mid(0,5).toLower()=="-url:")
{
Parameter.httpurl=val.mid(5);
continue;
}
if(val.mid(0,10).toLower()=="-filename:")
{
Parameter.filename=val.mid(10);
continue;
}
if(val.mid(0,8).toLower()=="-outdir:")
{
Parameter.outdir=val.mid(8);
continue;
}
if(val.mid(0,5).toLower()=="-md5:")
{
Parameter.Md5=val.mid(5);
continue;
}
if(val.mid(0,9).toLower()=="-recount:")
{
Parameter.Reconnection=fmax(val.mid(9).toInt(),1);
continue;
}
}
if(!Parameter.isvalid())
goto Print_Format;
//! ----
//! ----
//! 其他操作
}
3. 创建下载类Dal_QtDownEngine实现使用QNetworkReply下载数据相关功能
Dal_QtDownEngine.h: 简单实现写入文件流
class Dal_QtDownEngine:public QObject
{
Q_OBJECT
public:
Dal_QtDownEngine(QObject* parent=nullptr);
~Dal_QtDownEngine();
//! 开始下载
bool StartDown(DownParameter _parameter);
//! 返回文件的完整路径
QString Filefullpath(){return Filepath;}
//! 返回异常信息;
QString Error(){return ErrorStr;}
//! 获取md5内容
QString GetMd5(QString filepath);
public slots:
///已下载
void HaveFulfilled(qint64 bytesRead,qint64 totalBytes);
///写入文件流
void WriteFileData();
//! 文件效验 有则继续没有则重头下载
void file_Validation();
//! 一秒钟执行一次
void TimeOutByONE();
protected:
//! 实现退出下载
//! 退出下载必须关闭文件流
void QuitDown();
private:
//! 参数结构
DownParameter Parameter;
//! 文件另存路径
QString Filepath;
//! 文件另存路径 TEMP缓存路径
QString FilepathTEMP;
///下载
QNetworkReply *reply =nullptr;
///文件大小
qint64 filesize=0;
///文件已写入字节 历史写入
qint64 FileDownSize=0;
///写入文件指针
QFile* fileOpera=nullptr;
//! 计时器
//! 每一秒计算下载速度等数据
QTimer* LTimer=nullptr;
//! 累计下载 用于计算下载速度
qint64 bytesRead_old_Value;
qint64 bytesRead_Value;
qint64 totalBytes_Value;
//! 异常信息
QString ErrorStr;
//! 标识 线程id
QString Identification;
};
4. 使用QTimer 定时计算下载速度,下载大小,倒计时等
在 Dal_QtDownEngine下载类中声明一个定时器,每秒计算下载速度
LTimer= new QTimer();
connect(LTimer,&QTimer::timeout,this,&Dal_QtDownEngine::TimeOutByONE);
技术细节
- 临时下载文件效验
检查本地路径下是否有已经下载过的临时文件,
有则追加下载,没有则创建一个临时文件
void Dal_QtDownEngine::file_Validation()
{
if(IsNotNULL(fileOpera))
{
fileOpera->close();
delete fileOpera;
fileOpera=nullptr;
}
///文件已下载/已写入效验
fileOpera = new QFile(FilepathTEMP);
if(QFileInfo::exists(FilepathTEMP))
{
if(!fileOpera->open(QIODevice::Append))
{
delete fileOpera;
fileOpera = nullptr;
throw QString("打开临时写入文件失败!");
}
}
else
{
if(!fileOpera->open(QIODevice::WriteOnly))
{
delete fileOpera;
fileOpera = nullptr;
throw QString("创建临时写入文件失败!");
}
}
//! 获取文件已写入大小作为下一次下载的开始
FileDownSize=fileOpera->size();
}
- 绑定QNetworkReply::readyRead信号将数据写入文件
读取QNetworkReply数据直接写入文件中
#define IsNotNULL(_Parament_) (_Parament_!=NULL && _Parament_!=nullptr)
void Dal_QtDownEngine::WriteFileData()
{
if(IsNotNULL(reply) && IsNotNULL(fileOpera))
{
QByteArray temparray= reply->readAll();
if(IsNotNULL(fileOpera) && temparray.length()>0)
fileOpera->write(temparray);
temparray.clear();
}
}
- 定时器计算下载速度,下载倒计时,下载进度等
计算下载速度,下载倒计时,下载进度并参考Aria2.exe程序以
[#18312 11.96MiB/1.06GiB(1%) CN:1 DL:5.48MiB ETA:03m17s]
的格式输出相关信息
18312 下载控制台程序在任务管理器中的PID,可通过PID结束进程
而字符串的解析也可以参考上篇文章,都是一样的格式!
void Dal_QtDownEngine::TimeOutByONE()
{
//! 每秒统计 "[#2d9da7 3.0MiB/1.0GiB(22%) CN:1 DL:7.4MiB ETA:21h23m2s]";
if(bytesRead_old_Value==0)
{
//! 第一次不统计
bytesRead_old_Value=bytesRead_Value;
return;
}
//std::cout<<"[bytesRead_old_Value] "<<bytesRead_old_Value<<" [bytesRead_Value] "<<bytesRead_Value<<" [totalBytes_Value] "<<totalBytes_Value;
qint64 bytesRead_old=bytesRead_old_Value;
qint64 bytesRead=bytesRead_Value;
qint64 totalBytes=totalBytes_Value;
bytesRead_old_Value=bytesRead_Value;
qint64 downSpeed= ceil((double)(bytesRead-bytesRead_old));
size_t mtimes= ceil((double)(totalBytes-bytesRead)/downSpeed);
double v=roundf(((double)(bytesRead+FileDownSize)/(double)(totalBytes+FileDownSize))*100);
QString Schedule=QString("[#%1 %2/%3(%4%) CN:1 DL:%5 ETA:%6]")
.arg(Identification)
.arg(GetsizeD(bytesRead+FileDownSize))
.arg(GetsizeD(totalBytes+FileDownSize))
.arg(fmin(v,100))
.arg(GetsizeD(downSpeed))
.arg(GetCountZero(mtimes));
//! 打印下载进度
std::cout<<Schedule.toStdString().c_str()<<std::endl;
}
- 初始化QNetworkReply下载
整个下载功能的相关实现
包括下载失败重新下载,md5验证等。
bool Dal_QtDownEngine::StartDown(DownParameter _parameter)
{
//! 进程id
DWORD pid = GetCurrentProcessId();
Parameter=_parameter;
Identification=QString::number(pid);
//! 是否下载成功
bool ISsucceed=true;
filesize=0;
FileDownSize=0;
bytesRead_old_Value=0;
bytesRead_Value=0;
totalBytes_Value=0;
QString filename="";
LTimer= new QTimer();
connect(LTimer,&QTimer::timeout,this,&Dal_QtDownEngine::TimeOutByONE);
QEventLoop eventLoop;
QNetworkRequest request;
//! 失败重连 重连次数
int Reconnection=0;
try {
//! 验证远程链接
if(!urlFormat_Validation(Parameter.httpurl,filesize,filename))
{
goto out;
}
if(Parameter.filename.trimmed()=="")
Parameter.filename=filename;
Filepath=Parameter.outdir+"/"+Parameter.filename;
FilepathTEMP=Filepath+".TEMP";
do{
Reconnection++;
if(Reconnection>1)
std::cout<<QString::number(Reconnection).toStdString().c_str()<<".连接异常断开!正在重连...("<<ErrorStr.toStdString().c_str()<<");"<<std::endl;
//! 简单的大小判断
QFileInfo info(Filepath);
if(info.exists())
{
//! 文件如果已存在 不下载直接进行md5验证
if(info.size()==filesize && filesize>0)
goto MD5_Verify;
else
QFile::remove(Filepath); //大小不一致 删除旧版本
}
//! 打开 TEMP 临时文件流
file_Validation();
//! 开始统计
LTimer->start(1000);
//! 开始下载
request=QNetworkRequest();
request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); //启动重定向
request.setRawHeader("Range",tr("bytes=%1-").arg(FileDownSize).toUtf8());
request.setUrl(QUrl(Parameter.httpurl));
QNetworkAccessManager* manager= new QNetworkAccessManager();
reply = manager->get(request);
QObject::connect(reply, SIGNAL(finished()),&eventLoop, SLOT(quit()));
QObject::connect(reply, SIGNAL(aboutToClose()),&eventLoop, SLOT(quit())); //! 异常也会退出
connect(reply,&QNetworkReply::readyRead,this,&Dal_QtDownEngine::WriteFileData);
connect(reply,&QNetworkReply::downloadProgress,this,&Dal_QtDownEngine::HaveFulfilled);
//! 堵塞进程 直到下载完成!
eventLoop.exec(QEventLoop::ExcludeUserInputEvents);
///关闭文件
if( IsNotNULL(fileOpera))
{
fileOpera->close();
}
if( IsNotNULL(reply) && (reply->error() != QNetworkReply::NoError))
{
ISsucceed=false;
ErrorStr="下载失败,原因未知!";
}
if( IsNotNULL(fileOpera)
&& fileOpera->size()==filesize )
{
ISsucceed=fileOpera->rename(Filepath);
if(!ISsucceed)
{
ErrorStr="文件重命名失败!";
}
}
//! md5 文件格式校验
MD5_Verify:
if(Parameter.Md5.trimmed()!="")
{
if(Parameter.Md5.trimmed().toUpper()!=GetMd5(Filepath).toUpper())
{
ISsucceed=false;
QFile::remove(Filepath); //md5校验失败删除文件
ErrorStr="文件["+Parameter.filename+"]Md5校验失败!";
}
}
if(ISsucceed) //成功下载,也跳出循环
break;
}
while(Reconnection<=Parameter.Reconnection);
} catch (QString error) {
ISsucceed=false;
ErrorStr=error;
}
out:
return ISsucceed;
}
小结
之所以要单独测试写一个使用QNetworkReply,QNetworkRequest下载http/https资源的输出进度的控制台程序,就是为了预防一种情况:
那就是手动强行结束下载或暂停下载功能导致软件闪崩。
比如这种异常:
QTdebug提示ASSERT: "bytes <= bufferSize" in file tools\qringbuffer.cpp, line 113
而通过QProcess调用cmd启动控制台下载,完美解决了这个问题。
不需要考虑下载过程中在什么地方插入暂停或结束判断,不用担心线程锁的问题,直接结束整个下载进程!
值得注意两点的是:
1. Qt的控制台程序,使用qDebug,qInfo输出信息在QProcess 的readAllStandardOutput() 是获取不到的,
只有使用std::cout才能在QProcess中获取到控制台输出;
#include <iostream>
std::cout<<"Out Text code -->"<<std::endl;
2. QProcess的kill()和close()可能无法关闭 [通过cmd调用的另外程序] ,至少测试的时候是没有退出的,就比如写的这个控制台程序。
后面可以通过任务管理器中PID结束进程:
Identification变量在程序运行的时候就获取了PID,并输出到了控制台,通过获取的PID强制结束下载进程。
//! 用来根据Identification 关闭进程
void KillProvess(){
HANDLE hProcess;
DWORD exitCode = 0;
DWORD th32ProcessID=Identification.toInt();
hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, th32ProcessID); // 假设你已经获取了进程ID
if (hProcess == NULL) {
// 处理错误
std::cout<<"OpenProcess is error :"<<Identification.toStdString().c_str()<<" losed !"<<std::endl;
}
if (!TerminateProcess(hProcess, exitCode)) {
// 处理错误
std::cout<<"关闭线程:"<<Identification.toStdString().c_str()<<"失败!"<<std::endl;
}
CloseHandle(hProcess);
}