65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 QNetworkAccessManager 进行同步下载

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (7投票s)

2012 年 10 月 27 日

CPOL

6分钟阅读

viewsIcon

76232

downloadIcon

3029

如何使用 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);
}

到目前为止,人们可以在互联网上找到已经描述了上述步骤的资料。唉,存在一些缺点,这些缺点被证明是相当棘手的、致命的阻碍。

  1. 文件将在 finished() 触发之前完全下载到内存中。对于大文件,这可能需要很长时间,甚至会耗尽所有内存(导致您的应用程序崩溃)。

  2. 使用一个更早触发的信号,例如 QNetworkRequest 回复对象的 readyRead(),您将获得大文件的第一部分,但无法获得其余部分。

  3. 无法在下载过程中向用户显示进度。

  4. 流无法以这种方式处理,因为 finish() 永远不会被调用——应用程序将冻结,最终耗尽内存并崩溃。

  5. 在非 GUI 线程中使用此代码时,QEventLoop 可能(并且将会)导致死锁。

解决方案

要使此功能正常工作,需要结合几种小技巧。

  1. 使用 QThread 来处理传入的数据。

  2. 根据是在 GUI 模式还是非 GUI 模式下运行,使用不同的线程同步方法。

  3. 对于 GUI 线程,使用 QEventLoop 可以确保我们保持响应迅速的用户界面。

不幸的是,仍然有一些小问题存在。

  1. 如果数据处理线程不够快,内存仍然可能被填满。

  2. 应用程序会抱怨“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 线程中调用 QEventLoopexec() 函数可能会导致死锁——来自 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()
  • 新的 httpfileinfohttpdir 类提供了一些基本功能,类似于 QFileInfo 和 QDir。
  • 代码经过大量测试,现在可以被认为是稳定甚至“工厂级”的了。
© . All rights reserved.