概要

根据上篇文章中的Aria2.exe下载程序,简单创建一个使用QNetworkReplyQNetworkRequest下载http/https资源的包含输出进度的控制台程序。
Qt案例 创建使用QNetworkReply,QNetworkRequest下载http/https资源的输出进度的控制台程序-LMLPHP
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;
}

小结

之所以要单独测试写一个使用QNetworkReplyQNetworkRequest下载http/https资源的输出进度的控制台程序,就是为了预防一种情况:
那就是手动强行结束下载或暂停下载功能导致软件闪崩。
比如这种异常:
QTdebug提示ASSERT: "bytes <= bufferSize" in file tools\qringbuffer.cpp, line 113
而通过QProcess调用cmd启动控制台下载,完美解决了这个问题。
不需要考虑下载过程中在什么地方插入暂停或结束判断,不用担心线程锁的问题,直接结束整个下载进程!

值得注意两点的是:
1. Qt的控制台程序,使用qDebug,qInfo输出信息在QProcessreadAllStandardOutput() 是获取不到的,
只有使用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);
}
05-02 10:13