功能目录:
- QNetworkAccessManager接口介绍
- 下载进度条展示,支持暂停,停止功能
- 显示下载/剩余大小,剩余时间,下载速度
- 多线程下载,不阻塞界面线程
- 文件断点续传下载
- 下载请求超时的处理
- 功能实现Demo地址
1.QNetworkAccessManager接口介绍
官方文档:http://doc.qt.io/archives/qt-5.8/qnetworkaccessmanager.html
可以一目了然的看到几个熟悉词汇的api:post、get、put、head,当然还有几个cookie相关的方法。
可以发现使用manager还需要几个类:QNetworkRequest 专门用于请求的,QNetworkReply 接收请求的响应.
比如1.只想从网页服务器上请求数据,使用get方法。
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
connect(manager, SIGNAL(finished(QNetworkReply*)),
this, SLOT(replyFinished(QNetworkReply*)));
QNetworkRequest request;
request.setUrl(QUrl("http://qt-project.org"));
reply = manager->get(request);
connect(reply, &QNetworkReply::finished, this, &MainWindow::finished);
//在finished函数中
QByteArray bytes = reply->readAll();//读取网页数据
2.附带数据向服务器请求返回数据,使用post,当post附带json数据时:
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
QNetworkRequest request;
//构造json数据
QJsonObject jsonObject;
jsonObject.insert("dcode", "code");
jsonObject.insert("ver", "version");
//json转QByteArray
QString strJson = QString(QJsonDocument(jsonObject).toJson(QJsonDocument::Compact));
QByteArray byte_json = strJson.toLocal8Bit();
request.setUrl(QUrl("http://qt-project.org"));//要请求的地址
//如果请求json数据,请求header要使用此设置
request.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/json"));
if (m_Reply != Q_NULLPTR) {
m_Reply->deleteLater();
}
m_Reply = manager->post(request, byte_json);
QEventLoop loop;//非阻塞方式,采用事件循环
QTimer timer;
connect(m_Reply, SIGNAL(finished()), &loop, SLOT(quit()));
QObject::connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
timer.start(5000);//设置超时5s
loop.exec(QEventLoop::ExcludeUserInputEvents);
if (m_Reply->error() == QNetworkReply::NoError){
QByteArray strJsonText = m_Reply->readAll();//请求返回数据
}
1.1. QNetworkRequest
同样看帮助文档:http://doc.qt.io/qt-5/qnetworkrequest.html
主要就是这几个写方法,分别对一个请求的不同类进行配置。
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请 求数据四个部分组成,下图给出了请求报文的一般格式。请求行组成:请求方法+空格+url+空格+协议版本+回车符+换行符。 详情见HTTP 消息结构
对于header,qt提供了一个枚举类型KnownHeaders分别表示不同项:
Constant | Value | Description |
QNetworkRequest::ContentDispositionHeader | 6 | Corresponds to the HTTP Content-Disposition header and contains a string containing the disposition type (for instance, attachment) and a parameter (for instance, filename). |
QNetworkRequest::ContentTypeHeader | 0 | Corresponds to the HTTP Content-Type header and contains a string containing the media (MIME) type and any auxiliary data (for instance, charset). |
QNetworkRequest::ContentLengthHeader | 1 | Corresponds to the HTTP Content-Length header and contains the length in bytes of the data transmitted. |
QNetworkRequest::LocationHeader | 2 | Corresponds to the HTTP Location header and contains a URL representing the actual location of the data, including the destination URL in case of redirections. |
QNetworkRequest::LastModifiedHeader | 3 | Corresponds to the HTTP Last-Modified header and contains a QDateTime representing the last modification date of the contents. |
QNetworkRequest::CookieHeader | 4 | Corresponds to the HTTP Cookie header and contains a QList<QNetworkCookie> representing the cookies to be sent back to the server. |
QNetworkRequest::SetCookieHeader | 5 | Corresponds to the HTTP Set-Cookie header and contains a QList<QNetworkCookie> representing the cookies sent by the server to be stored locally. |
QNetworkRequest::UserAgentHeader | 7 | The User-Agent header sent by HTTP clients. |
QNetworkRequest::ServerHeader | 8 | The Server header received by HTTP clients. |
1.2. QNetworkReply
帮助文档:http://doc.qt.io/qt-5/qnetworkreply.html
此类继承自QIODevice,可使用QIODevice的所有接口,包括readall读取接收的所有信息。
同时此类提供了finished信号,在响应完斥候发出此信号,可关联自定义槽函数函数,做响应处理。
提供了attribute属性函数,可以判断响应的类型,比如RedirectionTargetAttribute是目标url告知进行重定向
QNetworkReply不会自动释放空间,一定要主动处理内存释放,可以调用QObject::deleteLater令其自动释放空间
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)));
如果请求下载功能:
onDownloadProgress(qint64, qint64);显示的就是下载的进度,参数1和2分别代表本次下载字节数和文件总字节大小。
onReadyRead();下载过程中开始从服务器读取数据,用于读取数据写入本地文件中,采用数据追加的方式方便断点的续传:
void xx::onReadyRead()
{
if (m_reply == NULL) return;
QFile file(m_fileName);//本地文件名
if (file.open(QIODevice::WriteOnly | QIODevice::Append))
{
file.write(m_reply->readAll());
}
file.close();
}
onFinished():下载完成事件。
onError():下载错误事件
2.下载进度条展示,支持暂停,停止功能
QNetworkAccessManager使用get方法发送请求,使用QNetworkReply接受返回。QNetworkReply使用下载进程信号来反馈每次下载数据的大小和文件的总大小。
2.1 readyRead()
这是QNetworkReply的父类QIODevice提供的下载过程中的信号,可以在此信号中写入下载文件大小。
2.2 downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
bytesReceived代表每次下载文件字节大小,bytesTotal代表下载文件字节总大小。
//更新进度条;
QString sValue = QString("%1").arg(bytesReceived * 100 / (bytesTotal));//百分比,所以乘以100
ui.progressBar->setValue(sValue.toInt());
2.3 暂停和停止功能要点:需要解绑QNetworkReply的信号和槽,并调用QNetworkReply的abort方法和DeleteLater()方法。
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;
3.显示下载/剩余大小,剩余时间,下载速度
计算剩余大小,剩余时间和下载速度,需要根据下载过程中每次下载字节数和总下载字节数计算,所以还是在downloadProgress(qint64 bytesReceived, qint64 bytesTotal)
响应的槽函数中来计算。
// 下载大小MB/总大小MB
QString strDownCurrent = QStringLiteral("下载大小:%1/%2").arg(transformUnit(m_currentDownload)).arg(transformUnit(bytesTotal));
ui.lb_size->setText(strDownCurrent);
//剩余时间
qint64 ispeed = m_intervalDownload * 1000 / (timeNow - m_timeInterval);
qint64 timeRemain = (bytesTotal - bytesReceived) / ispeed;
QString strRemaind = QStringLiteral("剩余时间:%1").arg(transformTime(timeRemain));
ui.lb_remaind->setText(strRemaind);
//下载速度
QString strSpeed = QStringLiteral("下载速度:%1").arg(transformUnit(ispeed, true));
ui.lb_rate->setText(strSpeed);
4.多线程下载,不阻塞界面线程
此处有两种方法,一种是通过MoveToThread方法,通过一个继承与QObject的功能类实现。
二是继承QThread,通过QThread来实现多线程,我使用的是第二种,提供过一个继承与QThread的中间类。
当点击下载按钮时,启动下载线程:
m_strTargetAddress = "d:/Qt_Download.zip";//自定义目标下载地址
QString strUrl = ui.lineEdit->text();//服务器下载地址
if (m_pDownloadThread != NULL )
{
m_pDownloadThread->start();
}
5.文件断点续传下载
5.1关于断点续传,QNetworkRequest提供了方法SetRawHeader和"Range"字段来设置请求的字节大小和宽度。
在HTTP协议请求中,如果想从文件的某一位置接受数据,就要加上Range头部,Range头部的格式有如下几种情况:
表示头500个字节:bytes=0-499
表示第二个500字节:bytes=500-999
表示最后500个字节:bytes=-500
表示500字节以后的范围:bytes=500-
第一个和最后一个字节:bytes=0-0,-1
同时指定几个范围:bytes=500-600,601-999
在发出带Range的请求后,服务器会在Content-Range头部返回当前接受的范围和文件总大小,如:
Content-Range: 0-499/22400
这里0-499是指当前发送的数据的范围,而22400则是文件的总大小。
5.2要实现断点续传,只有客户端是不行的,服务器上的文件也必须支持断点续传,也就是说,在进行断点下载续传时,首先要判断要下载文件是否支持断点下载。
windows下如何判断支持要下载文件支持断点下载:
5.2.1 cmd下使用curl 命令加 -i 参数 和要请求的字节大小
eg: curl -i --range 0-9 http://sqdownb.onlinedown.net/down/55412_20161201034403.zip
如返回数据中包含Content-Range和 HTTP/1.1 206 Partial Content字节,例如执行上述命令时,返回结果如下:
HTTP/1.1 206 Partial Content
Date: Wed, 01 Aug 2018 03:12:14 GMT
Content-Type: application/zip
Content-Length: 10
Last-Modified: Wed, 07 Dec 2016 10:13:32 GMT
Connection: keep-alive
ETag: "5847e0cc-78ac98"
Content-Range: bytes 0-9/7908504
处Content-Range: bytes 0-9/7908504 返回的字节数7908504就是文件的总字节大小。
HTTP/1.1 206 Partial Content 代表状态值206 代表支持断点下载
所以此处返回带有Content-Range和HTTP/1.1 206 Partial Content字节,代表此链接是支持断点下载的。
5.2.2 Qt代码中通过QNetworkRequest发送请求"Range",通过返回值QNetworkReply的rawHeader("Content-Range")是否存在返回值来查看。
bool xx::bSupportBreak(const QUrl &url)
{
QNetworkAccessManager manager;
QEventLoop loop;
QTimer timer;
QNetworkRequest request;
request.setUrl(url);
request.setRawHeader("Range", "bytes=0-9");
//发出请求,获取文件地址的头部信息;
QNetworkReply *reply = manager.get(request);
if (!reply) return false;
connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
timer.start(2000);
loop.exec(QEventLoop::ExcludeUserInputEvents);
if (reply->error() != QNetworkReply::NoError)
{
// 请求发生错误;
qDebug() << reply->errorString();
return false;
}
else if (!timer.isActive())
{
// 请求超时超时,未获取到文件信息;
qDebug() << "Request Timeout";
return false;
}
timer.stop();
QByteArray range = reply->rawHeader("Content-Range");
if (!QString::fromLocal8Bit(range).isEmpty())
return true;
return false;
}
6.下载请求超时的处理
Qt5.8中不管是QNetworkAccessManager类还是QNetworkRequest类没有提供超时的机制,所以只能通过自己来计算。
在开始下载时,通过定时器,每5s检测下载字节发生的变化值。
_timeOut->start(5000);
如果5s内没有发生变化,则默认说明超时,超时的原因有很多,下载过程中网络断开,或者网络连接不稳定,网卡被禁用掉等。通过定时器超时事件一直循环检测是否有网络链接,如果有网络连接则继续下载,反之下载暂停。这样的好处是下载没完成之前,网络出现波动不会影响整体文件的下载,通过轮询下载,一直到文件下载完成。
不过此处我为了不让程序无节制的请求连接,如果断网超过30分钟就让程序断开请求,不再下载。
void xx::handleTimeOut()
{
if (m_bytesDown != m_bytesReceived) {
m_bytesDown = m_bytesReceived;
}
else if (!m_bClickPause) {
//网络断开时停止下载
if (m_reply != NULL)
{
stopDownload();
m_timeElapsed.start();
}
//30分钟循环超时(30分钟再次连接网络可以继续下载)
int nElapsed = m_timeElapsed.elapsed();
if (nElapsed >= 1 * 30 * 60 * 1000)
{
_timeOut->stop();
return;
}
//检测网络
bool bConnect = this->bSupportBreak(m_qStrUrl);
if (bConnect)
{
_timeOut->stop();
this->downloadFile(m_qStrUrl, m_qStrFilePath);
}
}
}
7.功能实现Demo地址
环境是VS2015 + win10 64位。
附带源码和可执行程序下载地址,同时还有编译成功的Openssl的32位和64位的文件。