diff --git a/MultiplethreadDownload/.gitignore b/MultiplethreadDownload/.gitignore new file mode 100644 index 0000000..fab7372 --- /dev/null +++ b/MultiplethreadDownload/.gitignore @@ -0,0 +1,73 @@ +# This file is used to ignore files which are generated +# ---------------------------------------------------------------------------- + +*~ +*.autosave +*.a +*.core +*.moc +*.o +*.obj +*.orig +*.rej +*.so +*.so.* +*_pch.h.cpp +*_resource.rc +*.qm +.#* +*.*# +core +!core/ +tags +.DS_Store +.directory +*.debug +Makefile* +*.prl +*.app +moc_*.cpp +ui_*.h +qrc_*.cpp +Thumbs.db +*.res +*.rc +/.qmake.cache +/.qmake.stash + +# qtcreator generated files +*.pro.user* + +# xemacs temporary files +*.flc + +# Vim temporary files +.*.swp + +# Visual Studio generated files +*.ib_pdb_index +*.idb +*.ilk +*.pdb +*.sln +*.suo +*.vcproj +*vcproj.*.*.user +*.ncb +*.sdf +*.opensdf +*.vcxproj +*vcxproj.* + +# MinGW generated files +*.Debug +*.Release + +# Python byte code +*.pyc + +# Binaries +# -------- +*.dll +*.exe + diff --git a/MultiplethreadDownload/MultiplethreadDownload.pro b/MultiplethreadDownload/MultiplethreadDownload.pro new file mode 100644 index 0000000..80aad04 --- /dev/null +++ b/MultiplethreadDownload/MultiplethreadDownload.pro @@ -0,0 +1,26 @@ +QT += core gui network concurrent + +greaterThan(QT_MAJOR_VERSION, 4): QT += widgets + +CONFIG += c++17 + +# You can make your code fail to compile if it uses deprecated APIs. +# In order to do so, uncomment the following line. +#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 + +SOURCES += \ + main.cpp \ + utils.cpp \ + widget.cpp + +HEADERS += \ + utils.h \ + widget.h + +FORMS += \ + widget.ui + +# Default rules for deployment. +qnx: target.path = /tmp/$${TARGET}/bin +else: unix:!android: target.path = /opt/$${TARGET}/bin +!isEmpty(target.path): INSTALLS += target diff --git a/MultiplethreadDownload/main.cpp b/MultiplethreadDownload/main.cpp new file mode 100644 index 0000000..5e1bf5b --- /dev/null +++ b/MultiplethreadDownload/main.cpp @@ -0,0 +1,13 @@ +#include "widget.h" + +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + Widget w; + w.show(); + w.move(QApplication::screens().at(0)->geometry().center() - w.frameGeometry().center()); + return a.exec(); +} diff --git a/MultiplethreadDownload/utils.cpp b/MultiplethreadDownload/utils.cpp new file mode 100644 index 0000000..4184e90 --- /dev/null +++ b/MultiplethreadDownload/utils.cpp @@ -0,0 +1,21 @@ +#include "utils.h" +#include +#include + + +QString Utils::sizeFormat(qint64 bytes) +{ + qreal size = bytes; + QStringList list; + list << "KB" << "MB" << "GB" << "TB"; + + QStringListIterator i(list); + QString unit("bytes"); + + while (size >= 1024.0 && i.hasNext()) { + unit = i.next(); + size /= 1024.0; + } + return QString().setNum(size, 'f', 2) + " " + unit; +} + diff --git a/MultiplethreadDownload/utils.h b/MultiplethreadDownload/utils.h new file mode 100644 index 0000000..19fb8c0 --- /dev/null +++ b/MultiplethreadDownload/utils.h @@ -0,0 +1,12 @@ +#ifndef UTILS_H +#define UTILS_H + +#include + +class Utils +{ +public: + static QString sizeFormat(qint64 bytes); +}; + +#endif // UTILS_H diff --git a/MultiplethreadDownload/widget.cpp b/MultiplethreadDownload/widget.cpp new file mode 100644 index 0000000..b824ae6 --- /dev/null +++ b/MultiplethreadDownload/widget.cpp @@ -0,0 +1,240 @@ +#include "widget.h" +#include "ui_widget.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Widget::Widget(QWidget *parent) + : QWidget(parent) + , ui(new Ui::Widget) +{ + ui->setupUi(this); + + // 设置URL链接验证器 + QRegExp regex(R"(http[s]://.+)"); + auto *validator = new QRegExpValidator(regex, nullptr); + ui->urlInput->setValidator(validator); + + // 设置最大的线程并发数 + int processer_num = QThread::idealThreadCount(); + processer_num = processer_num > maxThreadNum ? maxThreadNum : processer_num; + ui->threadCountSpinbox->setMaximum(processer_num); + ui->threadCountSpinbox->setValue(processer_num); + + // 设置保存路径为当前程序运行路径 + ui->savePathInput->setText(QDir::currentPath()); + + requestManager = new QNetworkAccessManager(this); +} + +Widget::~Widget() +{ + delete ui; +} + + +/** + * @brief 展示错误信息 + * @param msg + */ +void Widget::showError(const QString& msg) +{ + auto msgBox = new QMessageBox(this); + msgBox->setWindowTitle(tr("请求出错")); + msgBox->setIcon(QMessageBox::Critical); + msgBox->setText(msg); + msgBox->exec(); + ui->downloadBtn->setEnabled(true); +} + +/** + * @brief 获取要下载的文件大小 + * @param url + */ +qint64 Widget::getFileSize(const QString& url) +{ + QEventLoop event; + QNetworkRequest request; + request.setUrl(QUrl(url)); + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + QNetworkReply *reply = requestManager->head(request); + connect(reply, &QNetworkReply::errorOccurred, [this, reply](QNetworkReply::NetworkError error){ + if (error != QNetworkReply::NoError) { + return showError(reply->errorString()); + } + }); + connect(reply, &QNetworkReply::finished, &event, &QEventLoop::quit); + event.exec(); + qint64 fileSize = 0; + if (reply->rawHeader("Accept-Ranges") == QByteArrayLiteral("bytes") + && reply->hasRawHeader(QString("Content-Length").toLocal8Bit())) { + fileSize = reply->header(QNetworkRequest::ContentLengthHeader).toUInt(); + } + reply->deleteLater(); + return fileSize; +} + + +/** + * @brief 开始下载文件 + * @param url + * @param filename + */ +void Widget::singleDownload(const QString& url, const QString& filename) +{ + QFile file(filename); + if (file.exists()) + file.remove(); + if (!file.open(QIODevice::WriteOnly)) { + return showError(file.errorString()); + } + + QEventLoop event; + QNetworkAccessManager mgr; + QNetworkRequest request; + request.setUrl(QUrl(url)); + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + QNetworkReply *reply = mgr.get(request); + connect(reply, &QNetworkReply::readyRead, this, [&file, reply](){ + file.write(reply->readAll()); + }); + connect(reply, &QNetworkReply::finished, &event, &QEventLoop::quit); + connect(reply, &QNetworkReply::errorOccurred, [this, reply](QNetworkReply::NetworkError error){ + if (error != QNetworkReply::NoError) showError(reply->errorString()); + }); + event.exec(); + file.flush(); + file.close(); + ui->downloadBtn->setEnabled(true); +} + +/** + * @brief 多线程下载 + * @param url + * @param fileSize + * @param filename + * @param threadCount + */ +void Widget::multiDownload(const QString &url, qint64 fileSize, const QString &filename, int threadCount) +{ + QFile file(filename); + if (file.exists()) + file.remove(); + if (!file.open(QIODevice::WriteOnly)) { + return showError(file.errorString()); + } + file.resize(fileSize); + + // 任务等分 + qint64 segmentSize = fileSize / threadCount; + QVector> vec(threadCount); + for (int i = 0; i < threadCount; i++) { + vec[i].first = i * segmentSize; + vec[i].second = i * segmentSize + segmentSize - 1; + } + vec[threadCount-1].second = fileSize; // 余数部分加入最后一个 + + qint64 bytesReceived = 0; // 下载接收的总字节数 + // 任务队列 + auto mapCaller = [&, this](const QPair& pair) -> qint64 { + QEventLoop event; + QNetworkAccessManager mgr; + QNetworkRequest request; + request.setUrl(url); + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + request.setRawHeader("Range", QString("bytes=%1-%2").arg(pair.first).arg(pair.second).toLocal8Bit()); + QNetworkReply *reply = mgr.get(request); + qint64 writePos = pair.first; + connect(reply, &QNetworkReply::readyRead, [&, reply, this](){ + QByteArray data = reply->readAll(); + lock.lock(); + file.peek(writePos); + file.write(data); + bytesReceived += data.size(); + lock.unlock(); + writePos += data.size(); + }); + connect(reply, &QNetworkReply::finished, &event, &QEventLoop::quit); + event.exec(); + return writePos - pair.first; + }; + QFuture future = QtConcurrent::map(vec, mapCaller); + QFutureWatcher futureWatcher; + QEventLoop loop; + connect(&futureWatcher, &QFutureWatcher::finished, &loop, &QEventLoop::quit, Qt::QueuedConnection); + futureWatcher.setFuture(future); + if (!future.isFinished()) { + loop.exec(); + } + file.close(); + qDebug() << "下载完毕!!"; + ui->downloadBtn->setEnabled(true); +} + +/** + * @brief 点击开始下载 + */ +void Widget::on_downloadBtn_clicked() +{ + QString url = ui->urlInput->text().trimmed(); + if (url.isEmpty()) { + return showError(tr("请求链接不允许为空!")); + return ; + } + + // 选择保存路径 + QString savePath = ui->savePathInput->text().trimmed(); + if (savePath.isEmpty()) { + savePath = QFileDialog::getExistingDirectory(); + if (savePath.isEmpty()) return; + } else { + if(!QDir(savePath).exists()) { + return showError(tr("路径不存在!")); + } + } + + qint64 fileSize = getFileSize(url); + QString sizeText = fileSize == 0 ? "未知大小" : Utils::sizeFormat(fileSize); + ui->filesizeLabel->setText(sizeText); + ui->downloadBtn->setEnabled(false); + + int process_num = ui->threadCountSpinbox->text().toInt(); + + QDir::setCurrent(savePath); + QString filename = QFileInfo(url).fileName(); + if (fileSize == 0 || process_num == 1) { + singleDownload(url, filename); + } else { + multiDownload(url, fileSize, filename, process_num); + } +} + +/** + * @brief 设置保存下载文件的路径 + */ +void Widget::on_brwoserPathBtn_clicked() +{ + QString savePath = QFileDialog::getExistingDirectory(); + if (!savePath.isEmpty()) { + ui->savePathInput->setText(savePath); + } +} + +void Widget::download_progress_change(qint64 bytesReceived, qint64 bytesTotal) +{ + if (bytesTotal <= 0) + return; + ui->downProgressBar->setMaximum(10000); // 最大值 + ui->downProgressBar->setValue((bytesReceived * 10000) / bytesTotal); // 当前值 + ui->downProgressBar->setTextVisible(true); +} diff --git a/MultiplethreadDownload/widget.h b/MultiplethreadDownload/widget.h new file mode 100644 index 0000000..4ab03f6 --- /dev/null +++ b/MultiplethreadDownload/widget.h @@ -0,0 +1,39 @@ +#ifndef WIDGET_H +#define WIDGET_H + +#include +#include +#include + +QT_BEGIN_NAMESPACE +namespace Ui { class Widget; } +QT_END_NAMESPACE + +class Widget : public QWidget +{ + Q_OBJECT + +public: + Widget(QWidget *parent = nullptr); + ~Widget(); + +protected: + void showError(const QString& msg); + qint64 getFileSize(const QString& url); + void singleDownload(const QString& url, const QString& filename); + void multiDownload(const QString& url, qint64 fileSize, const QString& filename, int threadCount); + + +private slots: + void on_downloadBtn_clicked(); + void on_brwoserPathBtn_clicked(); + void download_progress_change(qint64 bytesReceived, qint64 bytesTotal); + +private: + QNetworkAccessManager *requestManager; + const int maxThreadNum = 8; + QMutex lock; + + Ui::Widget *ui; +}; +#endif // WIDGET_H diff --git a/MultiplethreadDownload/widget.ui b/MultiplethreadDownload/widget.ui new file mode 100644 index 0000000..612b305 --- /dev/null +++ b/MultiplethreadDownload/widget.ui @@ -0,0 +1,157 @@ + + + Widget + + + + 0 + 0 + 635 + 209 + + + + + 0 + 0 + + + + + 16777215 + 209 + + + + 多线程下载文件Demo + + + + QLayout::SetDefaultConstraint + + + + + + + URL: + + + + + + + + + + 下载 + + + + + + + 保存路径: + + + + + + + + + + 浏览 + + + + + + + 原始线程数: + + + + + + + 1 + + + 1 + + + + + + + 暂停 + + + + + + + 下载进度: + + + + + + + 0 + + + false + + + QProgressBar::TopToBottom + + + + + + + 下载完成是否打开 + + + + + + + + + + + 下载速度: + + + + + + + + + + + + + + 文件大小: + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 9a9b38c..c0fdb9e 100644 --- a/README.md +++ b/README.md @@ -9,3 +9,11 @@ ![](./QProxyStyleTest/screenshot.png) +## 多线程下载 +[MultiplethreadDownload](./MultiplethreadDownload/) + +测试视频下载地址 https://www.learningcontainer.com/wp-content/uploads/2020/05/sample-mp4-file.mp4 +下完的文件好像是有问题的。。。。请帮忙看一下 + +参考内容 +* CoverEars(迅雷不及掩耳,Qt版多线程下载器) https://github.com/xj361685640/CoverEars-Qt \ No newline at end of file