QT学习总结--多线程断点下载

zhy

发布于 2020.04.03 17:56 阅读 2928 评论 0

   实现多线程下载文件,先考虑如何实现动态的添加组件和线程,在这里采用了QListWidget来做为下载项目的容器,比较的简单,将下载组件组合在一个自定义容器类DownLoadItem里,这样可以很方便的创建下载项目。

 

  最终效果:

 

 

   先说一下组件创建遇到的问题(下载项目重叠):

   

这个问题可以通过设置样式表来解决:

QListView::item {height:80px;}

 

然后考虑如何关联线程和组件,可以在DownLoadItem类里创建一个下载进程。

void DownloadItem::initControl()
{
    //设置打开文件和打开文件夹不可见
	ui.openBtn->setVisible(false);
	ui.openFolder->setVisible(false);
    m_httpDownload = new HttpDownloadManager();
    //连接按钮的信号与槽
    //播放暂停按钮
	connect(ui.startBtn, &QPushButton::clicked, this, &DownloadItem::onStartBtnClicked);
    //删除按钮
	connect(ui.deleteBtn, &QPushButton::clicked, this, &DownloadItem::onDeleteBtnCliked);
    //打开文件按钮
	connect(ui.openBtn, &QPushButton::clicked, this, &DownloadItem::onOpenBtnClicked);
    //打开文件夹按钮
	connect(ui.openFolder, &QPushButton::clicked, this, &DownloadItem::onOpenFolderBtnClicked);

    //连接线程的信号
    //更新下载信息
	connect(m_httpDownload, &HttpDownloadManager::signalUpdatFileInfo, this, &DownloadItem::onUpdateFileInfo);
    //进度
	connect(m_httpDownload, &HttpDownloadManager::signalProgressChanged, this, &DownloadItem::onProgressChanged);
    //状态改变,完成状态隐藏开始暂停按钮,显示文件,文件夹按钮
	connect(m_httpDownload, &HttpDownloadManager::signalStateChanged, this, &DownloadItem::onHttpStateChanged);
    //下载速度
	connect(m_httpDownload, &HttpDownloadManager::signalSpeedValue, this, &DownloadItem::onSpeedValue);
    //设置下载链接,开始下载
	m_httpDownload->setUrl(m_url);
    m_httpDownload->start();
}
//读取数据
void HttpDownloader::onDatareadReady()
{
	const QByteArray&& buffer = m_reply->readAll();
        //已经写入的字节
	m_file->seek(m_startPoint + m_readySize);
	m_file->write(buffer);
        //读取大小和速度大小
	m_readySize += buffer.size();
	m_speedValue += buffer.size();
        //向上反馈
	emit downloadDatareadReady();
}

 

    然后再考虑下载线程类HttpDownloadManager,该类在在下载时会产生多个子线程HttpDownloader用于文件分块下载,同时该类起到了一个中间者的作用,对HttpDownloader线程的信号进行数据处理,然后再通过信号反馈给DownloadItem类。

 

 

HttpDownloadManager::HttpDownloadManager()
    : QObject()
{
    m_speedTimer = new QTimer(ptr);
    //一秒钟一次,方便计算速度
    m_speedTimer->setInterval(1000);
    m_speedTimer->start();
    //将对象移入内置线程
    thread = new QThread;
    this->moveToThread(thread);
    //定时器信号计算下载速度
    connect(m_speedTimer, &QTimer::timeout, this, &HttpDownloadManager::onSpeedTimeout);
}

//开始下载
bool HttpDownloadManager::startDownloadUrl(const QString& url)
{
        //主线程开启
        thread->start();        
        
        m_state = DownloadState_e::DWaiting;
	HttpDownloadStateChanged(d->m_state);
	//获取文件大小
	if ((m_totalSize = getFileSize(url)) == -1){
		qDebug() << "Get File Size Error";
		return false;
	}
	QString fileName = replaceNoExistFileName(QUrl(url).fileName());
	//通知获取到了文件名和文件大小
	emit signalUpdatFileInfo(fileName, HttpUtils::bytetoSize(m_totalSize));
        //放在临时文件夹
	m_filePath = QDir::tempPath() + "/" + fileName;
	m_file = new QFile(m_filePath);
	if (!d->m_file->open(QFile::WriteOnly))
	{
		qDebug() << "Can not open file : " + m_file->errorString();
		m_file->close();
		delete m_file;
		m_file = nullptr;
		return false;
	}
	//设置文件大小
	m_httpdownloaderlst.clear();
        //分块下载,SHARDCOUNT为3
	for (int index = 0; index < SHARDCOUNT; index++)
	{
		qint64 startPoint = d->m_totalSize * index / SHARDCOUNT;
		qint64 endPoint = d->m_totalSize * (index + 1) / SHARDCOUNT;
		if (index > 0){
			startPoint += 1;
		}
                //开启子线程分块下载
		HttpDownloader* httpdownloader = new HttpDownloader(nullptr);
		httpdownloader->resetSpeedValue();
                //添加到线程列表
		m_httpdownloaderlst << httpdownloader;
                //子线程下载完成
		connect(httpdownloader, SIGNAL(downloadFinished()), this, SLOT(onDownLoadFinished()));
                //子线程读取数据
		connect(httpdownloader, SIGNAL(downloadDatareadReady()), this, SLOT(onDownloadDatareadReady()), Qt::QueuedConnection);
                //下载失败
		connect(httpdownloader, SIGNAL(downloadError(int, const QString&)), this, SLOT(onDownloadError(int, const QString&)));
		//通知子线程停止下载
		connect(this, &HttpDownloadManager::signalpause, httpdownloader, &HttpDownloader::pause);
                //通知子线程开始下载
		connect(this, &HttpDownloadManager::signalrestart, httpdownloader, &HttpDownloader::restart);
                
		httpdownloader->start(index, url, m_file, startPoint, endPoint, 0);
		m_state = DownloadState_e::DDownload;
    }
    return true;
}

//下载速度
void HttpDownloadManager::onSpeedTimeout()
{
	if (m_state == DownloadState_e::DDownload){
		qint64 speedsize = 0;
		for (const auto iter : m_httpdownloaderlst){
			speedsize += iter->getSpeedValue();
			iter->resetSpeedValue();
		}
                //发送信号,界面修改下载速度
		emit signalSpeedValue(speedsize);
	}
}

 

    最后再来看一下HttpDownloader子线程类

void HttpDownloader::start(int index, const QString &url, QFile *file, qint64 startPoint, qint64 endPoint, qint64 readySize)
{
	if (m_state == DownloadState_e::DDownload){
		//正在下载中
		return;
	}
        //线程编号
	m_index = index;
        //下载地址
	m_url = url;
        //下载的文件
	m_file = file;
         //开始字节和结束字节
	m_startPoint = startPoint;
	m_endPoint = endPoint;
        //已读入字节
	m_readySize = readySize;
        //设置下载地址
	QNetworkRequest request;
	request.setUrl(m_url);

	if (endPoint > 0){
		QString range = QString("bytes=%1-%2").arg(m_startPoint + m_readySize).arg(endPoint);
		request.setRawHeader("Range", range.toUtf8());
	}
	
	m_reply = m_manager.get(request);
	connect(m_reply, SIGNAL(finished()), this, SLOT(onHttpDownloadFinished()));
	connect(m_reply, SIGNAL(readyRead()), this, SLOT(onDatareadReady()));
	connect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));

	m_state = DownloadState_e::DDownload;
}

 

三个类的关系:DownloadItem->HttpDownloadManager->HttpDownloader
                                        (包含一个)                     (包含多个) 

 

用户的操作通过DownloadItem向下传递,进程的变化通过HttpDownloader向上反馈。

 

在做这个练习的时候,经常漏写一些代码:

   1. 在删除项目的时候,没有先写file.close()函数,导致文件一直无法删除

   2. 不用的指针记得删除,否则程序很容易出现异常结束,这个问题遇到了好几次

   3. 下载结束后线程记得关闭,调用quit()结束线程