使用 QNetworkAccessManager 进行同步下载






4.85/5 (7投票s)
如何使用 QNetworkAccessManager 同步从网络下载文件。
引言
有时,求助论坛坦率地说根本没有帮助。在网上搜索该主题,可以发现许多地方都有这个问题:“我如何使用 QNetworkManager 进行同步下载?”。好几次,第一个建议是“在异步模式下使用它。”这往往会让我生气,不仅仅是因为回复没有用。它也让我觉得是在说:“你不知道如何异步使用它,愚蠢吗?”
当然我知道怎么做,提问的人可能也知道。但问题仍然存在:我们有理由要同步下载。我们为什么还要问这个问题呢?
在我的情况下,原因是我想通过 HTTP 从 Web 服务器流式传输视频或音频文件,并立即开始播放,而无需先完成下载。此外,我想播放 Shoutcast 流,这些流根本不可能先下载,因为它们没有文件结束符。最后,我想直接通过 HTTP 播放 DVD,这涉及到将 1GB 的文件部分连接成一个虚拟的大镜像,并将其作为整体处理。就这么简单。
Qt 提供了 QNetworkManager
类,它支持代理服务器,如果需要可以进行代理身份验证(甚至支持 Microsoft NTLM),可以向远程服务器发送登录凭据,实现了 http put/get 方法,包括传递参数或数据,并且可以操作 http 标头以及评估服务器的回复。
换句话说:与 Web 服务器通信所需的一切。
那么:为什么要写一个新类?仅仅因为 Qt 的方法似乎无法处理同步下载,就要重新发明轮子吗?
答案是:不。它可以。而且很简单。
基础知识
首先,在发出请求后,代码必须等待回复。这一点很清楚,因为我们想知道发生了什么——我们是否连接成功,我们是否能够访问我们请求的文件?在此阶段可能会出现很多问题,所以我们需要添加一个检查。
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
connect(manager, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)),
SLOT(slotProxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)));
connect(manager, SIGNAL(authenticationRequired(QNetworkReply *, QAuthenticator *)),
SLOT(slotAuthenticationRequired(QNetworkReply *, QAuthenticator *)));
QNetworkRequest request;
request.setUrl(m_url);
request.setRawHeader("User-Agent", "Qt NetworkAccess 1.3");
m_pReply = manager->get(request);
使用 get()
发出请求后,我们进入一个事件循环,并等待网络访问管理器发出的 finished()
信号。当信号到达时,请求已完成——无论是成功还是失败。接下来,我们应该调用 reply->error()
来查看是否有问题并报告它,或者如果一切顺利,随后调用 reply->readAll()
或类似方法来获取我们的数据。
如果我们希望能够从任意偏移量开始读取,可以在调用 get(request) 之前插入这些行。
if (m_nPos)
{
QByteArray data;
QString strData("bytes=" + QString::number(m_nPos) + "-");
data = strData.toLatin1();
request.setRawHeader("Range", data);
}
到目前为止,人们可以在互联网上找到已经描述了上述步骤的资料。唉,存在一些缺点,这些缺点被证明是相当棘手的、致命的阻碍。
文件将在
finished()
触发之前完全下载到内存中。对于大文件,这可能需要很长时间,甚至会耗尽所有内存(导致您的应用程序崩溃)。-
使用一个更早触发的信号,例如
QNetworkRequest
回复对象的readyRead()
,您将获得大文件的第一部分,但无法获得其余部分。 -
无法在下载过程中向用户显示进度。
-
流无法以这种方式处理,因为
finish()
永远不会被调用——应用程序将冻结,最终耗尽内存并崩溃。 -
在非 GUI 线程中使用此代码时,
QEventLoop
可能(并且将会)导致死锁。
解决方案
要使此功能正常工作,需要结合几种小技巧。
-
使用
QThread
来处理传入的数据。 根据是在 GUI 模式还是非 GUI 模式下运行,使用不同的线程同步方法。
-
对于 GUI 线程,使用
QEventLoop
可以确保我们保持响应迅速的用户界面。
不幸的是,仍然有一些小问题存在。
如果数据处理线程不够快,内存仍然可能被填满。
应用程序会抱怨“
QTimer
必须在QThread
中创建”。这是一个 Qt 的缺陷:我们的线程是QThread
,而定时器实际上在工作。
第一个问题很可能在从网络播放压缩文件时出现,例如,128Kbit 的 MP4 从服务器拉取的速度几乎肯定会比互联网连接所能承受的慢。服务器可以比需要发送更多数据,从而在接收端逐渐填满内存。
代码
这里通过一些代码片段展示了如何解决这个问题。请下载完整的代码和示例以获得完整的解决方案。
首先,我们需要创建并启动一个 QThread
来处理传入的数据,同时保持我们的用户界面愉快和活跃。
webfile::webfile(QObject *parent /*= 0*/) :
QObject(parent),
m_pNetworkProxy(NULL)
{
// QThread is required, otherwise QEventLoop will block
m_pSocketThread = new QThread(this);
moveToThread(m_pSocketThread);
m_pSocketThread->start(QThread::HighestPriority);
}
在打开和读取操作中将使用该线程。请注意,我们必须检查我们是作为 GUI 线程运行还是不运行,并采取不同的行动。在非 GUI 线程中调用 QEventLoop
的 exec()
函数可能会导致死锁——来自 QNetworkRequest
等的信号将不会被接受,因此 quit()
插槽永远不会被激活。
bool webfile::open(qint64 offset /*= 0*/)
{
bool bSuccess = false;
if (isGuiThread())
{
// For GUI threads, we use the non-blocking call and use QEventLoop to wait and yet keep the GUI alive
QMetaObject::invokeMethod(this, "slotOpen", Qt::QueuedConnection,
Q_ARG(void *, &bSuccess),
Q_ARG(void *, &m_loop),
Q_ARG(qint64, offset));
m_loop.exec();
}
else
{
// For non-GUI threads, QEventLoop would cause a deadlock, so we simply use a blocking call.
// (Does not hurt as no messages need to be processed either during the open operation).
QMetaObject::invokeMethod(this, "slotOpen", Qt::BlockingQueuedConnection,
Q_ARG(void *, &bSuccess),
Q_ARG(void *, NULL),
Q_ARG(qint64, offset));
}
return bSuccess;
// Please note that it's perfectly safe to wait on the return Q_ARG,
// as we wait for the invokeMethod call to complete.
}
void webfile::slotOpen(void *pReturnSuccess, void *pLoop, qint64 offset /*= 0*/)
{
*(bool*)pReturnSuccess = workerOpen(offset);
if (pLoop != NULL)
{
QMetaObject::invokeMethod((QEventLoop*)pLoop, "quit", Qt::QueuedConnection);
}
}
参数通过 Q_ARG()
方便地作为指针传递。在这种情况下,这是完全合法的,因为我们等到插槽完成其任务,因此在线程仍然活动时它们永远不会被意外提取。这样我们也可以获得结果(在 read()
调用情况下是成功或数据)。
这是连接到服务器并等待结果的完整代码。
bool webfile::workerOpen(qint64 offset /*= 0*/)
{
qDebug() << "webfile::open(): start offset =" << offset;
clear();
resetReadFails();
close();
QNetworkAccessManager *manager = new QNetworkAccessManager(this);
if (m_pNetworkProxy != NULL)
manager->setProxy(*m_pNetworkProxy);
connect(manager, SIGNAL(proxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)),
SLOT(slotProxyAuthenticationRequired(const QNetworkProxy &, QAuthenticator *)));
connect(manager, SIGNAL(authenticationRequired(QNetworkReply *, QAuthenticator *)),
SLOT(slotAuthenticationRequired(QNetworkReply *, QAuthenticator *)));
QNetworkRequest request;
request.setUrl(m_url);
request.setRawHeader("User-Agent", "Qt NetworkAccess 1.3");
m_nPos = offset;
if (m_nPos)
{
QByteArray data;
QString strData("bytes=" + QString::number(m_nPos) + "-");
data = strData.toLatin1();
request.setRawHeader("Range", data);
}
m_pReply = manager->get(request);
if (m_pReply == NULL)
{
qDebug() << "webfile::open(): network error";
m_NetworkError = QNetworkReply::UnknownNetworkError;
return false;
}
// Set the read buffer size
m_pReply->setReadBufferSize(m_nBufferSize);
connect(m_pReply, SIGNAL(error(QNetworkReply::NetworkError)), SLOT(slotError(QNetworkReply::NetworkError)));
connect(m_pReply, SIGNAL(sslErrors(QList<QSslError>)), SLOT(slotSslErrors(QList<QSslError>)));
if (!waitForConnect(m_nOpenTimeOutms, manager))
{
qDebug() << "webfile::open(): timeout";
m_NetworkError = QNetworkReply::TimeoutError;
return false;
}
if (m_pReply == NULL)
{
qDebug() << "webfile::open(): cancelled";
m_NetworkError = QNetworkReply::OperationCanceledError;
return false;
}
if (m_pReply->error() != QNetworkReply::NoError)
{
qDebug() << "webfile::open(): error" << m_pReply->errorString();
m_NetworkError = m_pReply->error();
return false;
}
m_NetworkError = m_pReply->error();
m_strContentType = m_pReply->header(QNetworkRequest::ContentTypeHeader).toString();
m_LastModified = m_pReply->header(QNetworkRequest::LastModifiedHeader).toDateTime();
m_nSize = m_pReply->header(QNetworkRequest::ContentLengthHeader).toULongLong();
m_nResponse = m_pReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
m_strResponse = m_pReply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
m_bHaveSize = (m_nSize ? true : false);
m_nSize += m_nPos;
if (error() != QNetworkReply::NoError)
{
qDebug() << "webfile::open(): error" << error();
return false;
}
m_NetworkError = response2error(m_nResponse);
qDebug() << "webfile::open(): end response" << response()
<< "error" << error() << "size" << m_nSize;
return (response() == 200 || response() == 206);
}
别忘了等待函数。
bool webfile::waitForConnect(int nTimeOutms, QNetworkAccessManager *manager)
{
QTimer *timer = NULL;
QEventLoop eventLoop;
bool bReadTimeOut = false;
m_bReadTimeOut = false;
if (nTimeOutms > 0)
{
timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(slotWaitTimeout()));
timer->setSingleShot(true);
timer->start(nTimeOutms);
connect(this, SIGNAL(signalReadTimeout()), &eventLoop, SLOT(quit()));
}
// Wait on QNetworkManager reply here
connect(manager, SIGNAL(finished(QNetworkReply *)), &eventLoop, SLOT(quit()));
if (m_pReply != NULL)
{
// Preferrably we wait for the first reply which comes faster than the finished signal
connect(m_pReply, SIGNAL(readyRead()), &eventLoop, SLOT(quit()));
}
eventLoop.exec();
if (timer != NULL)
{
timer->stop();
delete timer;
timer = NULL;
}
bReadTimeOut = m_bReadTimeOut;
m_bReadTimeOut = false;
return !bReadTimeOut;
}
现在轮到你了!
这就是可以做到这一点的方式。其余的取决于您。如果您能发送补丁或错误报告,我将不胜感激,以便我改进我的代码。
除此之外,希望这些代码能为您省去一些我曾遇到的麻烦,所以:放松并享受吧。
历史
发布 v1.0 (2012.10.27)
发布 v1.1 (2013.03.25)
- 更新了 Qt5 的代码
- 添加了两个额外的错误消息(用于 HTTP 401 和 403)
发布 v1.2 (2013.03.27)
read()
返回的字节数可能少于请求的字节数(如果没有收到足够的数据,甚至为 0)。- Webfile monitor 在使用 seek 时计算出天文数字般的平均下载速率。
- v1.1 中我弄乱了下载文件,对此表示抱歉。
这让我们非常接近“工厂级”发布。
发布 v1.3 (2013.10.24)
- 终于添加了
readLine()
和readAll()
。 - 新的
httpfileinfo
和httpdir
类提供了一些基本功能,类似于 QFileInfo 和 QDir。 - 代码经过大量测试,现在可以被认为是稳定甚至“工厂级”的了。