4.1 Qt 中的网络文件下载

        本文是《用 Qt 实现电子白板》的其中一节,建议全章阅读。


        在电子白板的系统层,资源文件的下载是比较重要的功能。在 Qt 中,怎样实现一个稳定、容错性高,又好用的网络文件下载功能模块,是本文的重点。

        从网络下载文件会面临许多的问题:网络暂时不通、TCP连接卡顿、连接断开,用户会等待焦虑,被失败搞得焦头烂额。

        所以文件下载需要支持卡顿检测、进度汇报,断点续传,更进一步的需要有取消下载,优先下载(次要任务规避)等功能。

        在实现方式上,主要的方案有两部分:

  • 用 QNetworkManager 实现 HTTP会话与网络流
  • 实现一个特殊的 QIODevice 管理数据流,也管理下载任务

        为什么要管理数据流呢?QNetworkAccessManager 提供的 QNetworkReply 本身也是一个 QIODevice 数据流,但是一旦重试,新的请求会是一个新的 QNetworkReply 对象,你肯定不希望外部使用你这个下载功能的人,去管理这多个 QNetworkReply 对象吧,所以需要实现一个代理形式的 QIODevice 数据流,这是一个派生实现 QIODevice 的实用场景。

网络处理逻辑

        启动一个网络文件下载请求,在 Qt 用 QNetworkManager  实现,最简单的代码是这样的:

void HttpStream::open(QUrl url)
{
    static QNetworkAccessManager manager;
    QNetworkRequest request(url);
    QNetworkReply* reply(manager.get(request));
    reply_ = reply;
    reopen();
}

void HttpStream::reopen()
{
    QObject::connect(reply_, &QNetworkReply::finished, this, &HttpStream::onFinished);
    QObject::connect(reply_, &QNetworkReply::readyRead, this, &HttpStream::onReadyRead);
    void (QNetworkReply::*errorOccurred)(QNetworkReply::NetworkError) = 
    QObject::connect(reply_, errorOccurred, this, &HttpStream::onError);
}

         这里的 QNetworkAccessManager 肯定是不能销毁的,一旦销毁,请求就不会执行下去。所以需要使用 static 关键字定义对象,但是还是建议结合其他的考虑,用更好的方式来管理 manager 对象的生命期。QNetworkAccessManager 管理了 HTTP 连接池和内部线程池,所以一般情况下,为了复用这些资源,可以考虑采用全局单例的模式。

        这段代码还引出了 HttpStream 类,它就是派生 QIODevice 实现代理形式的数据流类。 

        QNetworkAccessManager 的 get 方法发出 HTTP GET 请求,显然 post 方法发出 POST 请求,HTTP 还有 PUT、DELETE、HEAD 等请求类型,如果不了解的话,可以搜索一下相关资料。

        QNetworkReply 对象上有三个常用的信号(finished,readyRead,error,readyRead 实际上是 QIODevice 的信号),我们用三个方法(onFinished,onReadyRead,onError)来分别处理。

        在三个回调方法中,并不需要线程同步,因为 Qt 的“信号-槽”技术,会保证在接收信号对象的关联线程中调用信号回调。其中 finished 信号肯定是最后触发的,也肯定会有,所以一般在 onFinished 中释放 QNetworkReply 对象。代码如下:

void HttpStream::onFinished()
{
    sender()->deleteLater();
}

        需要注意的是,你不能立即 delete 该对象,而是要通过 deleteLater 延迟释放。这是 QNetworkReply 的一个坑,一不小心就会导致莫名其妙的 Crash。因为 QNetworkReply 在 发出 finished 信号后,还会做一些自己的收尾动作,而在 C++ 中继续访问一个被 delete 了的对象,是很危险的行为。(更新:Qt 5.15 已经修复了这个问题)

        在对 error 信号的处理中,我们用了一个特殊的成员函数指针语法(如下),为什么要这样麻烦呢?因为 QNetworkReply 的 error 方法是一个重载方法,另外一个不带参数的方法返回保存的错误码,不是一个信号(signals)方法。但是建立信号连接的 connect 方法无法判断应该用哪个,通过这种方式可以帮助编译器做出选择。(更新:Qt 5.15 已经意识到这个问题,将 error 信号名称改成了 errorOccurred)

void (QNetworkReply::*errorOccurred)(QNetworkReply::NetworkError) = &QNetworkReply::error;

        对  error 信号的处理,主要工作是重试网络请求,实现代码大致如下:

void HttpStream::onError(QNetworkReply::NetworkError e)
{
    if (e <= QNetworkReply::UnknownNetworkError
            || e >= QNetworkReply::InternalServerError) {
        QNetworkRequest request = reply_->request();
        QNetworkReply * reply = reply_->manager()->get(request);
        std::swap(reply, reply_);
        reopen(); // 重新建立信号连接
        return;
    }
    setErrorString(reply_->errorString());
    emit error(e);
}

         首先,并不是所有的错误都需要重试,常见的错误一般是网络方面的错误(错误码 =< UnknownNetworkError)或者服务端异常错误(错误码 >= InternalServerError),其他的错误一般是内容不存在或者客户端的问题,重试也是没有效果的。

        然后,重试网络请求,就是把请求重新发送一遍,通过老的 QNetworkReply 对象可以取到之前的 QNetworkRequest 以及 manager 对象。

        在处理网络连接时,一般少不了定时器的使用。使用定时器,可以检测网络卡顿,卡死,帮助更快速的重试恢复网络传输。网络 Socket API 一般需要很长时间(最长可能有65分钟)才能通知 TCP 连接断开,用户显然是不能仍受这么长时间的。所以通过周期性的定时器,定期计算网络传输质量,可以及时放弃质量差的 TCP 连接,通过重新建立连接有可能会改善传输质量。

        在 Qt 中实现定时器一般有两种方法:一个是 QObject 的 timerEvent,通过 startTimer(int) 启动一个定期的 QTimeEvent 事件流;另一个是使用 QTimer 对象,但是本质上还是定期的 QTimeEvent。与信号回调一样,timerEvent 的处理线程必定是 QObject 对象的关联线程,所以这里仍然不需要处理线程同步。

        当检测到网络卡顿、卡死(比如 1 分钟内滑动平均速度小于 10K,或者 1 秒内没有数据),就需要主动重新发送请求,这通过取消(abort)上一个请求来实现。如果外部取消整个下载任务,也是通过 abort 取消请求,为了区分两种情况,需要引入一个变量记录一下。

void HttpStream::timerEvent(QTimerEvent *)
{
    if (...) {
        reply_->abort();
    }
}

        当 QNetworkReply 被 abort 之后,会发出 error,finished 信号,这就回到之前的信号处理方法中了。 

数据流处理逻辑

        以上我们实现了 HTTP 文件下载的网络连接管理,接下来看看数据传输的实现方法。

        我们通过类 HttpStream 继承 QIODevice,实现了一个自定义的数据流。数据流有读写两个管道,这里我们只需要实现读管道就行了,最关键的是实现 readData 方法。因为 QNetworkReply  管理的数据,所以只要转发调用就完成了。

qint64 HttpStream::readData(char *data, qint64 maxlen)
{
    return reply_->read(data, maxlen);
}

        对于写数据(writeData),我们安排一个空的实现:

qint64 HttpStream::writeData(const char *, qint64)
{
    assert(false);
    return 0;
}

        方法 readData 是 QIODevice 内部逻辑在调用,QIODevice 还会帮助我们记录读指针的位置,所以不需要操心 pos() 的更新。

         当外部没有及时将数据读走时,QNetworkReply 会保存数据。但是一旦重试请求,老的 reply 就丢弃了,这部分数据就丢失了,所以下一次请求需要重复下载这部分数据。然而,尝试在 HttpStream  中保存这部分数据,会导致实现 QIODevice 相关接口方法的方案变得比较复杂,并且网络卡顿时,一般不会有数据没有被读取,所以我们不做这个优化。大家有兴趣的话,作为练习,可以考虑一下怎么实现。

        我们实现的是随机访问(random-access)模式的数据流,所以最好让 size() 方法能够正常工作。HTTP 协议一般会在应答的 ContentLength 头域中提供文件大小,所以可以这么实现:

qint64 HttpStream::size() const
{
    return reply_->header(QNetworkRequest::ContentLengthHeader).toLongLong();
}

        怎么支持断点续传的功能呢?其实对于 QIODevice 来说,这就是一个随机读取的功能,先通过 seek() 定位读指针到某个位置,然后继续 read()。所以实现一下 seek() 方法:

bool HttpStream::seek(qint64 pos)
{
    if (!QIODevice::seek(pos))
        return false;
    reply_->abort();
    return true;
}

        因为读指针的位置是在 QIODevice 中维护的,所以调用一下基类的 seek 是有必要的。其他我们需要做的只是 abort 当前的 QNetworkReply 就可以了。回忆一下上面的逻辑, abort 是不是会触发 HTTP 请求重试。那么,在重试的时候,从新的位置下载就可以了。

void HttpStream::onError(QNetworkReply::NetworkError e)
{
    if (e <= QNetworkReply::UnknownNetworkError
            || e >= QNetworkReply::InternalServerError) {
        QNetworkRequest request = reply_->request();
        qint64 size = pos();
        if (size > 0)
            request.setRawHeader("Range", "bytes=" + QByteArray::number(size) + "-");
        QNetworkReply * reply = reply_->manager()->get(request);
        std::swap(reply, reply_);
        reopen(); // 重新建立信号连接
        return;
    }
    setErrorString(reply_->errorString());
    emit error(e);
}

        把之前重试请求的代码拿出来,增加数据位置相关的处理。如果读指针的位置不是 0,就添加 Range 头域(请参考 HTTP 协议),让服务器从指定位置给我们发送数据,从而实现了断点续传的功能。

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
QT3.3文白皮书,转自红联.作者说是翻词典给翻译出来的,对学QT编程的人应该有些帮助. 目录 1 介绍 …………………………………………………………………………………………4 1.1 执行摘要………………………………………………………………………………4 2 窗口部件 ……………………………………………………………………………………5 2.1 一个“Hello”的例子 ………………………………………………………………5 2.2 内建窗口部件…………………………………………………………………………6 2.3 自定义窗口部件………………………………………………………………………7 3 信号与槽 ……………………………………………………………………………………8 3.1 一个简单的信号与槽的例子…………………………………………………………9 3.2 元对象编译器…………………………………………………………………………10 4 图形界面程序 ………………………………………………………………………………11 4.1 主窗口类 ……………………………………………………………………………11 4.2 多文档接口……………………………………………………………………………13 4.3 对话框…………………………………………………………………………………13 4.4 锚接窗口………………………………………………………………………………14 4.5 设置……………………………………………………………………………………15 4.6 多线程…………………………………………………………………………………15 5 QT 设计器……………………………………………………………………………………15 5.1 Qt 助手 ………………………………………………………………………………16 5.2 图形界面程序实例 ……………………………………………………………………17 6 2D/3D 图形 …………………………………………………………………………………18 6.1 2D 图形 ………………………………………………………………………………18 6.2 3D 图形 ………………………………………………………………………………21 6.3 一个3D 实例 …………………………………………………………………………22 7 数据库…………………………………………………………………………………………24 7.1 执行SQ 命令…………………………………………………………………………24 7.2 数据相关部件 …………………………………………………………………………26 8 国际化…………………………………………………………………………………………26 8.1 Unicode ………………………………………………………………………………27 8.2 文本入口和渲染 ………………………………………………………………………27 8.3 翻译应用程序 …………………………………………………………………………27 8.4 Qt 语言学家……………………………………………………………………………28 9 风格与主题……………………………………………………………………………………29 9.1 内建风格 ………………………………………………………………………………29 9.2 风格相关部件 …………………………………………………………………………29 9.3 自定义风格 ……………………………………………………………………………29 10 布局…………………………………………………………………………………………30 10.1 内建布局管理器 ……………………………………………………………………30 10.2 嵌套的布局 …………………………………………………………………………31 10.3 自定义布局 …………………………………………………………………………32 11 事件…………………………………………………………………………………………32 11.1 事件的产生 …………………………………………………………………………32 11.2 事件的传递 …………………………………………………………………………32 12 输入/输出与网络 …………………………………………………………………………33 12.1 文件输入/输出………………………………………………………………………33 12.2 XM …………………………………………………………………………………34 12.3 进程间
Qt是一个跨平台的C++应用程序开发框架,MinGW是一个基于GNU工具集的Windows开发环境,OpenCV是一个计算机视觉库,版本4.1Qt和MinGW环境的使用方法如下: 首先,确保已经安装了Qt和MinGW,并配置好相关环境变量。 然后,下载OpenCV 4.1版本的源代码,并解压到指定文件夹。 接下来,使用CMake进行配置和编译OpenCV。打开CMake GUI,设置源代码路径和编译输出路径,点击Configure进行配置。 在配置过程,选择MinGW Makefiles作为生成器,并勾选"WITH_QT"选项,这样编译时将会生成Qt相关的代码。 配置完成后,点击Generate生成Makefile。然后打开命令行窗口,进入编译输出路径,执行"mingw32-make"命令进行编译。编译完成后,执行"mingw32-make install"命令进行安装。 编译和安装完成后,在Qt项目添加OpenCV的头文件路径和库文件路径。打开Qt项目的.pro文件,添加以下代码: ``` INCLUDEPATH += /path/to/opencv/include LIBS += -L/path/to/opencv/lib -lopencv_core410 -lopencv_highgui410 ``` 其,/path/to/opencv是OpenCV安装的路径,根据实际情况进行修改。 最后,可以在Qt代码引入OpenCV的头文件,使用OpenCV提供的函数和类进行图像处理和计算机视觉相关的操作。 总之,使用Qt、MinGW和OpenCV 4.1进行开发,需要先配置和编译OpenCV,并将生成的库文件配置到Qt项目,然后就可以在Qt使用OpenCV相关功能了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting Horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值