Qt 之 HTTP 请求下载(支持断点续传)

2023-05-16

简述

最近在研究了一下用Qt 的方法来实现http下载,Qt 中的Http请求主要用到了QNetworkAccessManagerQNetworkReplyQNetworkRequest 这三块。本篇文章主要叙述如何用Qt 的方法进行HTTP 请求下载文件,能够支持断点续传(断点续传即能够手动停止下载,下次可以从已经下载的部分开始继续下载未完成的部分,而没有必要从头开始上传下载),并且实时更新下载信息。整体代码考虑十分周到,对各种情况也做了相应的处理,并且有通俗易懂的注释。好了,代码走起!

代码之路

在讲解代码之前先看一下效果图:

效果:

这里写图片描述

从图中可以看出点击start按钮,进行下载,stop按钮暂停当前下载,close按钮停止当前下载,并删除已经下载的临时文件,并将所有参数重置, 这里界面中下载链接输入框为空是因为我在代码中默认了url,也可以在输入框中输入url进行下载。


代码主要包含两个部分:

1、DownLoadManager : 用来请求下载,向界面传递下载信息,并将下载的内容保存到文件中

2、MyHttpDownload : 用来接收下载链接,利用DownLoadManager进行下载,更新界面,并对当前下载进行操作(包括:开始、暂停、停止下载)。

1、DownLoadManager

#include "downloadmanager.h"
#include <QFile>
#include <QDebug>
#include <QFileInfo>
#include <QDir>

#define DOWNLOAD_FILE_SUFFIX    "_tmp"

DownLoadManager::DownLoadManager(QObject *parent)
    : QObject(parent)
    , m_networkManager(NULL)
    , m_url(QUrl(""))
    , m_fileName("")
    , m_isSupportBreakPoint(false)
    , m_bytesReceived(0)
    , m_bytesTotal(0)
    , m_bytesCurrentReceived(0)
    , m_isStop(true)
{
    m_networkManager = new QNetworkAccessManager(this);
}

DownLoadManager::~DownLoadManager()
{}

// 设置是否支持断点续传;
void DownLoadManager::setDownInto(bool isSupportBreakPoint)
{
    m_isSupportBreakPoint = isSupportBreakPoint;
}

// 获取当前下载链接;
QString DownLoadManager::getDownloadUrl()
{
    return m_url.toString();
}

// 开始下载文件,传入下载链接和文件的路径;
void DownLoadManager::downloadFile(QString url , QString fileName)
{
    // 防止多次点击开始下载按钮,进行多次下载,只有在停止标志变量为true时才进行下载;
    if (m_isStop)
    {
        m_isStop = false;
        m_url = QUrl(url);

        // 这里可用从url中获取文件名,但不是对所有的url都有效;
//      QString fileName = m_url.fileName();

        // 将当前文件名设置为临时文件名,下载完成时修改回来;
        m_fileName = fileName + DOWNLOAD_FILE_SUFFIX;

        // 如果当前下载的字节数为0那么说明未下载过或者重新下载
        // 则需要检测本地是否存在之前下载的临时文件,如果有则删除
        if (m_bytesCurrentReceived <= 0)
        {
            removeFile(m_fileName);
        }

        QNetworkRequest request;
        request.setUrl(m_url);

        // 如果支持断点续传,则设置请求头信息
        if (m_isSupportBreakPoint)
        {
            QString strRange = QString("bytes=%1-").arg(m_bytesCurrentReceived);
            request.setRawHeader("Range", strRange.toLatin1());
        }

        // 请求下载;
        m_reply = m_networkManager->get(request);

        connect(m_reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(onDownloadProgress(qint64, qint64)));
        connect(m_reply, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
        connect(m_reply, SIGNAL(finished()), this, SLOT(onFinished()));
        connect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
    }   
}

// 下载进度信息;
void DownLoadManager::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    if (!m_isStop)
    {
        m_bytesReceived = bytesReceived;
        m_bytesTotal = bytesTotal;
        // 更新下载进度;(加上 m_bytesCurrentReceived 是为了断点续传时之前下载的字节)
        emit signalDownloadProcess(m_bytesReceived + m_bytesCurrentReceived, m_bytesTotal + m_bytesCurrentReceived);
    }   
}

// 获取下载内容,保存到文件中;
void DownLoadManager::onReadyRead()
{
    if (!m_isStop)
    {
        QFile file(m_fileName);
        if (file.open(QIODevice::WriteOnly | QIODevice::Append))
        {
            file.write(m_reply->readAll());
        }
        file.close();
    }   
}

// 下载完成;
void DownLoadManager::onFinished()
{
    m_isStop = true;
    // http请求状态码;
    QVariant statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);

    if (m_reply->error() == QNetworkReply::NoError)
    {
        // 重命名临时文件;
        QFileInfo fileInfo(m_fileName);
        if (fileInfo.exists())
        {
            int index = m_fileName.lastIndexOf(DOWNLOAD_FILE_SUFFIX);
            QString realName = m_fileName.left(index);
            QFile::rename(m_fileName, realName);
        }
    }
    else
    {
        // 有错误输出错误;
        QString strError = m_reply->errorString();
        qDebug() << "__________" + strError;
    }

    emit signalReplyFinished(statusCode.toInt());
}

// 下载过程中出现错误,关闭下载,并上报错误,这里未上报错误类型,可自己定义进行上报;
void DownLoadManager::onError(QNetworkReply::NetworkError code)
{
    QString strError = m_reply->errorString();
    qDebug() << "__________" + strError;

    closeDownload();
    emit signalDownloadError();
}

// 停止下载工作;
void DownLoadManager::stopWork()
{
    m_isStop = true;
    if (m_reply != NULL)
    {
        disconnect(m_reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(onDownloadProgress(qint64, qint64)));
        disconnect(m_reply, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
        disconnect(m_reply, SIGNAL(finished()), this, SLOT(onFinished()));
        disconnect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
        m_reply->abort();
        m_reply->deleteLater();
        m_reply = NULL;
    }
}

// 暂停下载按钮被按下,暂停当前下载;
void DownLoadManager::stopDownload()
{
    // 这里m_isStop变量为了保护多次点击暂停下载按钮,导致m_bytesCurrentReceived 被不停累加;
    if (!m_isStop)
    {
        //记录当前已经下载字节数
        m_bytesCurrentReceived += m_bytesReceived;
        stopWork();
    }   
}

// 重置参数;
void DownLoadManager::reset()
{
    m_bytesCurrentReceived = 0;
    m_bytesReceived = 0;
    m_bytesTotal = 0;
}

// 删除文件;
void DownLoadManager::removeFile(QString fileName)
{
    // 删除已下载的临时文件;
    QFileInfo fileInfo(fileName);
    if (fileInfo.exists())
    {
        QFile::remove(fileName);
    }
}

// 停止下载按钮被按下,关闭下载,重置参数,并删除下载的临时文件;
void DownLoadManager::closeDownload()
{
    stopWork();
    reset();
    removeFile(m_fileName);
}

2、MyHttpDownload

#include "myhttpdownload.h"
#include "downloadmanager.h"
#include <QDebug>

#define UNIT_KB 1024            //KB
#define UNIT_MB 1024*1024       //MB
#define UNIT_GB 1024*1024*1024  //GB

#define TIME_INTERVAL 300       //0.3s

MyHttpDownload::MyHttpDownload(QWidget *parent)
    : QWidget(parent)
    , m_downloadManager(NULL)
    , m_url("")
    , m_timeInterval(0)
    , m_currentDownload(0)
    , m_intervalDownload(0)
{
    ui.setupUi(this);
    initWindow();
}

MyHttpDownload::~MyHttpDownload()
{

}

void MyHttpDownload::initWindow()
{
    ui.progressBar->setValue(0);
    connect(ui.pButtonStart, SIGNAL(clicked()), this, SLOT(onStartDownload()));
    connect(ui.pButtonStop, SIGNAL(clicked()), this, SLOT(onStopDownload()));
    connect(ui.pButtonClose, SIGNAL(clicked()), this, SLOT(onCloseDownload()));
    // 进度条设置样式;
    ui.progressBar->setStyleSheet("\
                QProgressBar\
                {\
                    border-width: 0 10 0 10;\
                    border-left: 1px, gray;\
                    border-right: 1px, gray;\
                    border-image:url(:/Resources/progressbar_back.png);\
                }\
                QProgressBar::chunk\
                {\
                    border-width: 0 10 0 10;\
                    border-image:url(:/Resources/progressbar.png);\
                }");
}

// 开始下载;
void MyHttpDownload::onStartDownload()
{
    // 从界面获取下载链接;
    m_url = ui.downloadUrl->text();
    if (m_downloadManager == NULL)
    {
        m_downloadManager = new DownLoadManager(this);
        connect(m_downloadManager , SIGNAL(signalDownloadProcess(qint64, qint64)), this, SLOT(onDownloadProcess(qint64, qint64)));
        connect(m_downloadManager, SIGNAL(signalReplyFinished(int)), this, SLOT(onReplyFinished(int)));
    }

    // 这里先获取到m_downloadManager中的url与当前的m_url 对比,如果url变了需要重置参数,防止文件下载不全;
    QString url = m_downloadManager->getDownloadUrl();
    if (url != m_url)
    {
        m_downloadManager->reset();
    }
    m_downloadManager->setDownInto(true);
    m_downloadManager->downloadFile(m_url, "F:/MyHttpDownload/MyDownloadFile.zip");
    m_timeRecord.start();
    m_timeInterval = 0;
    ui.labelStatus->setText(QStringLiteral("正在下载"));
}

// 暂停下载;
void MyHttpDownload::onStopDownload()
{
    ui.labelStatus->setText(QStringLiteral("停止下载"));
    if (m_downloadManager != NULL)
    {
        m_downloadManager->stopDownload();
    }
    ui.labelSpeed->setText("0 KB/S");
    ui.labelRemainTime->setText("0s");
}

// 关闭下载;
void MyHttpDownload::onCloseDownload()
{
    m_downloadManager->closeDownload();
    ui.progressBar->setValue(0);
    ui.labelSpeed->setText("0 KB/S");
    ui.labelRemainTime->setText("0s");
    ui.labelStatus->setText(QStringLiteral("关闭下载"));
    ui.labelCurrentDownload->setText("0 B");
    ui.labelFileSize->setText("0 B");
}

// 更新下载进度;
void MyHttpDownload::onDownloadProcess(qint64 bytesReceived, qint64 bytesTotal)
{
    // 输出当前下载进度;
    // 用到除法需要注意除0错误;
    qDebug() << QString("%1").arg(bytesReceived * 100 / bytesTotal + 1);
    // 更新进度条;
    ui.progressBar->setMaximum(bytesTotal);
    ui.progressBar->setValue(bytesReceived);

    // m_intervalDownload 为下次计算速度之前的下载字节数;
    m_intervalDownload += bytesReceived - m_currentDownload;
    m_currentDownload = bytesReceived;

    uint timeNow = m_timeRecord.elapsed();

    // 超过0.3s更新计算一次速度;
    if (timeNow - m_timeInterval > TIME_INTERVAL)
    {
        qint64 ispeed = m_intervalDownload * 1000 / (timeNow - m_timeInterval);
        QString strSpeed = transformUnit(ispeed, true);
        ui.labelSpeed->setText(strSpeed);
        // 剩余时间;
        qint64 timeRemain = (bytesTotal - bytesReceived) / ispeed;
        ui.labelRemainTime->setText(transformTime(timeRemain));

        ui.labelCurrentDownload->setText(transformUnit(m_currentDownload));
        ui.labelFileSize->setText(transformUnit(bytesTotal));

        m_intervalDownload = 0;
        m_timeInterval = timeNow;
    }
}

// 下载完成;
void MyHttpDownload::onReplyFinished(int statusCode)
{
    // 根据状态码判断当前下载是否出错;
    if (statusCode >= 200 && statusCode < 400)
    {
        qDebug() << "Download Failed";
    }
    else
    {
        qDebug() << "Download Success";
    }
}

// 转换单位;
QString MyHttpDownload::transformUnit(qint64 bytes , bool isSpeed)
{
    QString strUnit = " B";
    if (bytes <= 0)
    {
        bytes = 0;
    }
    else if (bytes < UNIT_KB)
    {
    }
    else if (bytes < UNIT_MB)
    {
        bytes /= UNIT_KB;
        strUnit = " KB";
    }
    else if (bytes < UNIT_GB)
    {
        bytes /= UNIT_MB;
        strUnit = " MB";
    }
    else if (bytes > UNIT_GB)
    {
        bytes /= UNIT_GB;
        strUnit = " GB";
    }

    if (isSpeed)
    {
        strUnit += "/S";
    }
    return QString("%1%2").arg(bytes).arg(strUnit);
}

// 转换时间;
QString MyHttpDownload::transformTime(qint64 seconds)
{
    QString strValue;
    QString strSpacing(" ");
    if (seconds <= 0)
    {
        strValue = QString("%1s").arg(0);
    }
    else if (seconds < 60)
    {
        strValue = QString("%1s").arg(seconds);
    }
    else if (seconds < 60 * 60)
    {
        int nMinute = seconds / 60;
        int nSecond = seconds - nMinute * 60;

        strValue = QString("%1m").arg(nMinute);

        if (nSecond > 0)
            strValue += strSpacing + QString("%1s").arg(nSecond);
    }
    else if (seconds < 60 * 60 * 24)
    {
        int nHour = seconds / (60 * 60);
        int nMinute = (seconds - nHour * 60 * 60) / 60;
        int nSecond = seconds - nHour * 60 * 60 - nMinute * 60;

        strValue = QString("%1h").arg(nHour);

        if (nMinute > 0)
            strValue += strSpacing + QString("%1m").arg(nMinute);

        if (nSecond > 0)
            strValue += strSpacing + QString("%1s").arg(nSecond);
    }
    else
    {
        int nDay = seconds / (60 * 60 * 24);
        int nHour = (seconds - nDay * 60 * 60 * 24) / (60 * 60);
        int nMinute = (seconds - nDay * 60 * 60 * 24 - nHour * 60 * 60) / 60;
        int nSecond = seconds - nDay * 60 * 60 * 24 - nHour * 60 * 60 - nMinute * 60;

        strValue = QString("%1d").arg(nDay);

        if (nHour > 0)
            strValue += strSpacing + QString("%1h").arg(nHour);

        if (nMinute > 0)
            strValue += strSpacing + QString("%1m").arg(nMinute);

        if (nSecond > 0)
            strValue += strSpacing + QString("%1s").arg(nSecond);
    }

    return strValue;
}

标注: 代码注释中提到可以根据url来获取文件名,下方给予解释说明。

QString QUrl::fileName(ComponentFormattingOptions options = FullyDecoded) const
Returns the name of the file, excluding the directory path.
Note that, if this QUrl object is given a path ending in a slash, the name of the file is considered empty.
If the path doesn’t contain any slash, it is fully returned as the fileName.

在Qt助手中我们找到此方法,根据加粗的字段可以看出fileName()方法也可能返回为空,所以不是都有效。

好了,代码到此也就结束了,整体代码不算很难理解,也算是比较简单的一个http请求下载的小实例,后续我将会继续对http下载进行分析,具体包括将当前下载信息保存到本地、对http请求的文件进行分块下载等,欢迎大家一起交流。

代码下载


看代码其实实现起来不是那么难,但是一步想把功能做全做完善不是那么容易的事,我希望的是能够尽善尽美。在编码过程中也遇到了很多的问题包括下载速度的计算,下载信息的更新,与界面的交互这些都需要处理好。涉及到计算就要把握计算的值是否正确(或者说准确)以及是否计算结果越界导致程序崩溃。就如何控制下载速度的计算,我想不同的人有不同的思路,算出来的结果也不一定完全一致,但是大致时间段的速度应该是相近的,也可以用360等工具进行大致的测试。整篇文章的代码是经过不断地测试,改进之后的,代码中可能存在问题或者有一些不适当,如有还请高人指出。

这两个月来写博客的经历,让我觉得认认真真写一篇博客就像写一篇精美的文章一样,需要经过深思熟虑,前后推敲,反复修改。在写此篇博客之前,我对代码进行了整理,同时也加上了一些注释。其实有时候过多的注释也显得多余,好的代码需要足够精简但在一定程度上又需要一定的可阅读性。而可阅读性的代码胜过大量的注释,所以在编码实现功能的同时需要对自己的代码进行自我审视。子曰: 吾日三省吾身,不亦说乎。因此,在我们飞快地敲击键盘来码代码的同时要放慢速度,留下足够的时间来检测自己的代码。希望在以后的学习过程中,能够养成良好的编程习惯,不快不慢,不急不躁,Keep Moving

不知不觉,夜已深,前行中的小猪仍在前行中——窗外弥留着虫儿的鸣叫声,在蒙蒙的秋雨中显得更加清脆,悦耳。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Qt 之 HTTP 请求下载(支持断点续传) 的相关文章

随机推荐

  • 尝试使用绕线法制作数字电路

    最近在学习电路制作的过程中 xff0c 发现使用洞洞板 xff0c 很难处理好具有很多复杂引脚的集成电路 用 锡接走线法 的话 xff0c 集成电路不能太多太复杂 xff0c 否则板子上很难排开 也可以用比较细的导线 xff0c 飞线焊接
  • 用C语言实现一个简单的HTTP客户端(HTTP Client)

    用C语言实现一个简单的HTTP Client xff08 HTTP客户端 xff09 作者 xff1a gobitan xff08 雨水 xff09 日期 xff1a 2007 04 03 转载请注明出处 http blog csdn ne
  • C语言常见的自定义数据类型(1)—— 结构体

    目录 1 结构体 1 1 结构体的定义 1 2 结构体的自引用 1 3 结构体类型的重命名 1 4 结构体的嵌套 2 结构体大小的计算 2 1 结构体内存对齐 2 2 嵌套结构体大小的计算 2 3 offsetof函数 2 4 修改默认对齐
  • 一篇解决!小白迷惑:Go mod本地包导入

    最近在学习go xff0c 在导入本都包遇到一个问题 xff0c 根据网上许多教程来都走不通 xff0c 最后在官网得到了最正确的答案 官网教程 xff1a Call your code from another module The Go
  • Linux nf_conntrack连接跟踪的实现

    连接跟踪 xff0c 顾名思义 xff0c 就是识别一个连接上双方向的数据包 xff0c 同时记录状态 下面看一下它的数据结构 xff1a struct nf conn Usage count in here is 1 for hash t
  • 组播MAC地址和各类IP地址

    MAC地址是以太网二层使用的一个48bit xff08 6字节十六进制数 xff09 的地址 xff0c 用来标识设备位置 MAC地址分成两部分 xff0c 前24位是组织唯一标识符 xff08 OUI Organizationally u
  • Linux内核中的软中断、tasklet和工作队列详解

    TOC 本文基于Linux2 6 32内核版本 引言 软中断 tasklet和工作队列并不是Linux内核中一直存在的机制 xff0c 而是由更早版本的内核中的 下半部 xff08 bottom half xff09 演变而来 下半部的机制
  • Linux内核中的各种锁

    Linux内核中的各种锁 在现代操作系统里 xff0c 同一时间可能有多个内核执行流在执行 xff0c 因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问 尤其是在多处理器系统上 xff0c 更需要一些同步
  • Linux进程间通信方式

    进程与进程通信的概念进程通信的应用场景进程通信的几种方式 管道 管道简介管道原理 管道如何通信管道如何创建管道读写实现 管道api与用法 普通管道流管道命名管道 实现原理api与应用 匿名管道和有名管道总结 信号 信号来源信号生命周期和处理
  • C语言字节对齐问题详解

    引言 考虑下面的结构体定义 xff1a span class hljs keyword typedef span span class hljs keyword struct span span class hljs built in ch
  • git使用

    git使用 git clone时报如下错误 原因解决方法 TortoiseGit clone时报错 问题原因解决方法 git log使用git回归代码 git使用 本文记录的是工作中git是使用问题 xff0c 无脑模式 xff0c 遇到什
  • 内存对齐算法

    字节对齐是在分配内存时需要考虑的问题 xff0c 两个小算法 xff1a 1 最容易想到的算法 unsigned int calc align unsigned int n unsigned align if n align align 6
  • vscode快捷键整理

    1 注释 xff08 1 xff09 方式 注释 取消注释 xff1a Ctrl 43 xff08 2 xff09 方式 注释 xff1a Ctrl 43 Shift 43 取消注释 xff1a Ctrl 43 Shift 43 2 代码排
  • Qt之实现移动的方块(蚂蚁线)

    一 简介 移动的小方块或者说是类似移动的蚂蚁线 xff0c 从一篇文章看到的 xff0c 挺有趣的就自己做了一个 xff0c 可以自由添加方块的个数 xff0c 起始位置 xff0c 方块的宽度 xff0c 方块移动速度等待参数 xff0c
  • Docker 突然挂掉 failed to create shim task: OCI runtime create failed: container_linux.go:345: ...

    目录 问题描述 xff1a 参考 解决方案 最佳方案 xff1a 问题描述 xff1a docker Error response from daemon failed to create shim task OCI runtime cre
  • Qt之事件过滤器(eventFilter)详解

    1 2 1 Qt中事件是如何进行传递 1 2 2 Qt中的事件过滤器 xff08 eventFilter xff09 1 2 3 如何自己模拟发送事件消息 一 Qt中事件过滤器详解 我们先看下另外两个相关的方法 xff0c 一个是给对象安装
  • Qt实现微信截图功能(一)

    简述 Qt 之 简单截图功能 xff08 一 xff09 实现鼠标选中区域截图Qt 之 简单截图功能 xff08 二 xff09 实现可移动选中区域Qt 之 简单截图功能 xff08 三 xff09 实现可拖拽选中区域 在之前的文章中有带大
  • Qt之QMenu菜单去除投影效果(阴影)

    一 简述 我们使用Qt中的菜单 xff0c 正常情况下样式是跟随当前系统菜单的样式 xff0c 我们可以使用样式表进行修饰 xff0c 改变原有风格 xff0c 但是window系统上菜单边框四周会附带阴影的效果 xff0c 样式是无法取消
  • Qt 之 设置窗口边框的圆角

    Qt技术学习班开始了 xff0c 更多精彩 好玩的内容等着你 xff0c 赶紧报名吧 群号 xff1a 655815739 Qt在设置窗口边框圆角时有两种方式 xff0c 一种是设置样式 xff0c 另一种是在paintEvent事件中绘制
  • Qt 之 HTTP 请求下载(支持断点续传)

    简述 最近在研究了一下用Qt 的方法来实现http下载 xff0c Qt 中的Http请求主要用到了QNetworkAccessManager QNetworkReply QNetworkRequest 这三块 本篇文章主要叙述如何用Qt