添加多线程下载示例

This commit is contained in:
metanoia1989 2020-10-25 20:03:47 +08:00
parent 3aed340f11
commit 7eb9ffa28c
9 changed files with 589 additions and 0 deletions

73
MultiplethreadDownload/.gitignore vendored Normal file
View File

@ -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

View File

@ -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

View File

@ -0,0 +1,13 @@
#include "widget.h"
#include <QApplication>
#include <QScreen>
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();
}

View File

@ -0,0 +1,21 @@
#include "utils.h"
#include <QStringList>
#include <QLocale>
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;
}

View File

@ -0,0 +1,12 @@
#ifndef UTILS_H
#define UTILS_H
#include <QtGlobal>
class Utils
{
public:
static QString sizeFormat(qint64 bytes);
};
#endif // UTILS_H

View File

@ -0,0 +1,240 @@
#include "widget.h"
#include "ui_widget.h"
#include "utils.h"
#include <QUrl>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QMessageBox>
#include <QRegExp>
#include <QRegExpValidator>
#include <QFileDialog>
#include <QDir>
#include <QThread>
#include <QEventLoop>
#include <QFuture>
#include <QtConcurrent/QtConcurrent>
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<QPair<qint64, qint64>> 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<qint64, qint64>& 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<void> future = QtConcurrent::map(vec, mapCaller);
QFutureWatcher<void> futureWatcher;
QEventLoop loop;
connect(&futureWatcher, &QFutureWatcher<void>::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);
}

View File

@ -0,0 +1,39 @@
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QNetworkAccessManager>
#include <QMutex>
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

View File

@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Widget</class>
<widget class="QWidget" name="Widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>635</width>
<height>209</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>209</height>
</size>
</property>
<property name="windowTitle">
<string>多线程下载文件Demo</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<item>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>URL:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="urlInput"/>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="downloadBtn">
<property name="text">
<string>下载</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>保存路径:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="savePathInput"/>
</item>
<item row="1" column="2">
<widget class="QPushButton" name="brwoserPathBtn">
<property name="text">
<string>浏览</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>原始线程数:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="threadCountSpinbox">
<property name="minimum">
<number>1</number>
</property>
<property name="value">
<number>1</number>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="pauseBtn">
<property name="text">
<string>暂停</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>下载进度:</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QProgressBar" name="downProgressBar">
<property name="value">
<number>0</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
<property name="textDirection">
<enum>QProgressBar::TopToBottom</enum>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QCheckBox" name="dwnFinishedOpenChbox">
<property name="text">
<string>下载完成是否打开</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>下载速度:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="downloadSpeedLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>文件大小:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="filesizeLabel">
<property name="text">
<string/>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -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