最近工作中有个通过Http下载文件的需求,于是参考Qt的官方例程,做了个工具类。可灵活应用于各种Qt项目。文章末尾贴出代码和注释。
1,基础知识
1.1 QNetworkAccessManager
网络访问 API 是围绕一个 QNetworkAccessManager 对象构建的,该对象包含它发送的请求的通用配置和设置。 它包含代理和缓存配置,以及与此类问题相关的信号,以及可用于监控网络操作进度的回复信号。 一个 QNetworkAccessManager 实例对于整个 Qt 应用程序应该足够了。 由于QNetworkAccessManager是基于QObject的,所以只能从它所属的线程中使用。 一旦创建了 QNetworkAccessManager 对象,应用程序就可以使用它通过网络发送请求。 提供了一组标准函数,它们接受请求和可选数据,每个函数都返回一个 QNetworkReply 对象。 返回的对象用于获取响应相应请求而返回的任何数据。
1.2 QNetworkReply
QNetworkReply 类包含与使用 QNetworkAccessManager 发布的请求相关的数据和元数据。 与 QNetworkRequest 一样,它包含一个 URL 和标头(解析和原始形式)、有关回复状态的一些信息以及回复本身的内容。 QNetworkReply 是一个顺序访问的 QIODevice,这意味着一旦从对象中读取数据,它就不再由设备保存。 因此,如果需要,应用程序有责任保留这些数据。 每当从网络接收到更多数据并进行处理时,就会发出 readyRead() 信号。 接收到数据时也会发出 downloadProgress() 信号,但如果对内容进行任何转换(例如,解压缩和删除协议开销),则其中包含的字节数可能不代表实际接收到的字节数。
1.3 std::unique_ptr
std::unique_ptr 是一个智能指针,它通过指针拥有和管理另一个对象,并在 unique_ptr 超出范围时处置该对象。 当发生以下任一情况时,使用关联的删除器处理该对象: 管理 unique_ptr 对象被破坏 管理 unique_ptr 对象通过 operator= 或 reset() 分配另一个指针。 通过调用 get_deleter()(ptr) 使用可能由用户提供的删除器处理该对象。 默认删除器使用删除操作符,它会销毁对象并释放内存。 unique_ptr 也可以不拥有任何对象,在这种情况下,值为空。
std::unique_ptr 有两个版本: 管理单个对象(例如用新对象分配) 管理动态分配的对象数组(例如,使用 new[] 分配) 该类满足 MoveConstructible 和 MoveAssignable 的要求,但既不满足 CopyConstructible 也不满足 CopyAssignable 的要求。
2,代码
DownloadTool.h
#pragma once
#include <QObject> // QObject类是Qt对象模型的核心
#include <QUrl> // QUrl类提供了使用URL的便捷接口
#include <QFile> // QFile类用于对文件进行读写操作
#include <QDir> // QDir类用于操作路径名及底层文件系统
#include <QPointer> // QPointer指针引用的对象被销毁时候,会自动指向NULL,解决指针悬挂问题
#include <QApplication> // 此处用于获取当前程序绝对路径
#include <QNetworkReply> // QNetworkReply类封装了使用QNetworkAccessManager发布的请求相关的回复信息。
#include <QNetworkAccessManager> // QNetworkAccessManager类为应用提供发送网络请求和接收答复的API接口
#include <memory> // 使用std::unique_ptr需要包含该头文件
#define DOWNLOAD_DEBUG // 是否打印输出
class DownloadTool : public QObject // 继承QObject
{
Q_OBJECT // 加入此宏,才能使用QT中的signal和slot机制
public:
// 构造函数参数: 1)http文件完整的url 2)保存的路径
explicit DownloadTool(const QString& downloadUrl, const QString& savePath, QObject* parent = nullptr);
~DownloadTool();
void startDownload(); // 开始下载文件
void cancelDownload(); // 取消下载文件
Q_SIGNALS:
void sigProgress(qint64 bytesRead, qint64 totalBytes, qreal progress); // 下载进度信号
void sigDownloadFinished(); // 下载完成信号
private Q_SLOTS:
void httpFinished(); // QNetworkReply::finished对应的槽函数
void httpReadyRead(); // QIODevice::readyRead对应的槽函数
void networkReplyProgress(qint64 bytesRead, qint64 totalBytes); // QNetworkReply::downloadProgress对应的槽函数
private:
void startRequest(const QUrl& requestedUrl);
std::unique_ptr<QFile> openFileForWrite(const QString& fileName);
private:
QString m_downloadUrl; // 保存构造时传入的下载url
QString m_savePath; // 保存构造时传入的保存路径
const QString defaultFileName = "tmp"; // 默认下载到tmp文件夹
QUrl url;
QNetworkAccessManager qnam;
QPointer<QNetworkReply> reply;
std::unique_ptr<QFile> file;
bool httpRequestAborted;
};
DownloadTool.cpp
#include "DownloadTool.h"
DownloadTool::DownloadTool(const QString& downloadUrl, const QString& savePath, QObject* parent)
: QObject(parent)
{
m_downloadUrl = downloadUrl;
m_savePath = savePath;
}
DownloadTool::~DownloadTool() {}
void DownloadTool::startDownload()
{
const QUrl newUrl = QUrl::fromUserInput(m_downloadUrl);
if (!newUrl.isValid()) {
#ifdef DOWNLOAD_DEBUG
qDebug() << QString("Invalid URL: %1: %2").arg(m_downloadUrl, newUrl.errorString());
#endif // DOWNLOAD_DEBUG
return;
}
QString fileName = newUrl.fileName();
if (fileName.isEmpty()) fileName = defaultFileName;
if (m_savePath.isEmpty()) { m_savePath = QApplication::applicationDirPath() + "/tmp"; }
if (!QFileInfo(m_savePath).isDir()) {
QDir dir;
dir.mkpath(m_savePath);
}
fileName.prepend(m_savePath + '/');
if (QFile::exists(fileName)) { QFile::remove(fileName); }
file = openFileForWrite(fileName);
if (!file) return;
startRequest(newUrl);
}
void DownloadTool::cancelDownload()
{
httpRequestAborted = true;
reply->abort();
}
void DownloadTool::httpFinished()
{
QFileInfo fi;
if (file) {
fi.setFile(file->fileName());
file->close();
file.reset();
}
if (httpRequestAborted) {
return;
}
if (reply->error()) {
QFile::remove(fi.absoluteFilePath());
#ifdef DOWNLOAD_DEBUG
qDebug() << QString("Download failed: %1.").arg(reply->errorString());
#endif // DOWNLOAD_DEBUG
return;
}
const QVariant redirectionTarget = reply->attribute(QNetworkRequest::RedirectionTargetAttribute);
if (!redirectionTarget.isNull()) {
const QUrl redirectedUrl = url.resolved(redirectionTarget.toUrl());
file = openFileForWrite(fi.absoluteFilePath());
if (!file) { return; }
startRequest(redirectedUrl);
return;
}
Q_EMIT sigDownloadFinished();
#ifdef DOWNLOAD_DEBUG
qDebug() << QString(tr("Downloaded %1 bytes to %2 in %3")
.arg(fi.size()).arg(fi.fileName(), QDir::toNativeSeparators(fi.absolutePath())));
qDebug() << "Finished";
#endif // DOWNLOAD_DEBUG
}
void DownloadTool::httpReadyRead()
{
if (file) file->write(reply->readAll());
}
void DownloadTool::networkReplyProgress(qint64 bytesRead, qint64 totalBytes)
{
qreal progress = qreal(bytesRead) / qreal(totalBytes);
Q_EMIT sigProgress(bytesRead, totalBytes, progress);
#ifdef DOWNLOAD_DEBUG
qDebug() << QString::number(progress * 100, 'f', 2) << "% "
<< bytesRead / (1024 * 1024) << "MB" << "/" << totalBytes / (1024 * 1024) << "MB";
#endif // DOWNLOAD_DEBUG
}
void DownloadTool::startRequest(const QUrl& requestedUrl)
{
url = requestedUrl;
httpRequestAborted = false;
reply = qnam.get(QNetworkRequest(url));
connect(reply, &QNetworkReply::finished, this, &DownloadTool::httpFinished);
connect(reply, &QIODevice::readyRead, this, &DownloadTool::httpReadyRead);
connect(reply, &QNetworkReply::downloadProgress, this, &DownloadTool::networkReplyProgress);
#ifdef DOWNLOAD_DEBUG
qDebug() << QString(tr("Downloading %1...").arg(url.toString()));
#endif // DOWNLOAD_DEBUG
}
std::unique_ptr<QFile> DownloadTool::openFileForWrite(const QString& fileName)
{
std::unique_ptr<QFile> file(new QFile(fileName));
if (!file->open(QIODevice::WriteOnly)) {
#ifdef DOWNLOAD_DEBUG
qDebug() << QString("Unable to save the file %1: %2.")
.arg(QDir::toNativeSeparators(fileName), file->errorString());
#endif // DOWNLOAD_DEBUG
return nullptr;
}
return file;
}
在代码中调用,这里我们以下载Docker安装包为例:
EMDownloadTool* dT;
dT = new EMDownloadTool("https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe", QApplication::applicationDirPath() + "/download");
dT->startDownload();
下载效果:
打印输出
下载后的文件