qt-QNetworkAccessManager网络管理类

13 篇文章 0 订阅 ¥99.90 ¥299.90
本文介绍了Qt中的QNetworkAccessManager类用于异步网络访问,避免界面阻塞。通过QNetworkAccessManager发送请求,响应以QNetworkReply对象形式返回。同时讨论了同步网络访问的需求,如登录场景,并给出了通过事件循环阻塞界面的简单解决方案,但这种方法在某些情况下可能导致问题,如控制台程序和特殊环境下的死锁,解决这些问题通常需要使用额外的线程。
摘要由CSDN通过智能技术生成

qt-网络管理类

学习于豆子,qt学习之路2
访问网络( 1)
在这里插入图片描述

Qt 进行网络访问的类是 QNetworkAccessManager

pro 文件中添加 QT +=network。

这个 API 本身就是异步设计,这意味着不需要为其开启线程,防止界面被锁死.
异步的设计避免了这一系列的问题,但是却要求我们使用更多的代码来监听返回。
这类似于我们前面提到的 QDialog::exec()和 QDialog::show()之间的区别。 QNetworkAccessManager 是使用信号槽来达到这一目的的.

一旦一个 QNetworkAccessManager 实例创建完毕,我们就可以使用它发送网络请求。这些请求都返回 QNetworkReply 对象作为响应。这个对象一般会包含有服务器响应的数据。

这里声明了一个 NetWorker 的内部类,然后声明了这个内部类的 d 指针。 d 指针是 C++
程序常用的一种设计模式。它的存在于 C++ 程序的编译有关。在 C++ 中,保持二进制兼
容性非常重要。如果你能够保持二进制兼容,则当以后升级库代码时,用户不需要重新编译
自己的程序即可直接运行(如果你使用 Qt5.0 编译了一个程序,这个程序不需要重新编译
就可以运行在 Qt5.1 下,这就是二进制兼容;如果不需要修改源代码,但是必须重新编译
才能运行,则是源代码兼容;如果必须修改源代码并且再经过编译,例如从 Qt4 升级到 Qt5,
则称二者是不兼容的)。保持二进制兼容的很重要的一个原则是不要随意增加、删除成员变
量。因为这会导致类成员的寻址偏移量错误,从而破坏二进制兼容。为了避免这个问题,我
们将一个类的所有私有变量全部放进一个单独的辅助类中,而在需要使用这些数据的类值提
供一个这个辅助类的指针。注意,由于我们的辅助类是私有的,用户不能使用它,所以针对
这个辅助类的修改不会影响到外部类,从而保证了二进制兼容。关于二进制兼容的问题,我
// !!! Qt5
#ifndef NETWORKER_H
#define NETWORKER_H
#include <QObject>
class QNetworkReply;
class NetWorker : public QObject
{
Q_OBJECT
public:
	static NetWorker * instance();
	~NetWorker();
	void get(const QString &url);
signals:
	void finished(QNetworkReply *reply);
private:
	class Private;
	friend class Private;
	Private *d;
	
	explicit NetWorker(QObject *parent = 0);
	NetWorker(const NetWorker &) Q_DECL_EQ_DELETE;
	NetWorker& operator=(NetWorker rhs) Q_DECL_EQ_DELETE;
};
#endif // NETWORKER_H

NetWorker是一个单例类,因此它有一个instance()函数用来获得这唯一的实例。作为单例模式,要求构造函数、拷贝构造函数和赋值运算符都是私有的,因此我们将这三个函数都放在 private 块中。注意我们增加了一个Q_DECL_EQ_DELETE宏。这个宏是 Qt5 新增加的,意思是将它所修饰的函数声明为 deleted(这是 C++11 的新特性)。如果编译器支持= delete语法,则这个宏将会展开为= delete,否则则展开为空。我们的NetWorker只有一个get函数,顾名思义,这个函数会执行 HTTP GET 操作;一个信号finished(),会在获取到服务器响应后发出。private 块中还有三行关于Private的代码:

class Private;
friend class Private;
Private *d;
下面来看NetWorker的实现。

class NetWorker::Private
{
public:
    Private(NetWorker *q) :
        manager(new QNetworkAccessManager(q))
    {}
 
    QNetworkAccessManager *manager;
};

Private是NetWorker的内部类,扮演者前面我们所说的那个辅助类的角色。NetWorker::Private类主要有一个成员变量QNetworkAccessManager *,把QNetworkAccessManager封装起来。NetWorker::Private需要其被辅助的类NetWorker的指针,目的是作为QNetworkAccessManager的 parent,以便NetWorker析构时能够自动将QNetworkAccessManager析构。当然,我们也可以通过将NetWorker::Private声明为QObject的子类来达到这一目的。

NetWorker *NetWorker::instance()
{
    static NetWorker netWorker;
    return &netWorker;
}

instance()函数很简单,我们声明了一个 static 变量,将其指针返回。这是 C++ 单例模式的最简单写法,由于 C++ 标准要求类的构造函数不能被打断,因此这样做也是线程安全的。

NetWorker::NetWorker(QObject *parent) :
    QObject(parent),
    d(new NetWorker::Private(this))
{
    connect(d->manager, &QNetworkAccessManager::finished,
            this, &NetWorker::finished);
}
 
NetWorker::~NetWorker()
{
    delete d;
    d = 0;
}

构造函数参数列表我们将 d 指针进行赋值。构造函数内容很简单,我们将QNetworkAccessManager的finished()信号进行转发。也就是说,当QNetworkAccessManager发出finished()信号时,NetWorker同样会发出自己的finished()信号。析构函数将 d 指针删除。由于NetWorker::Private是在堆上创建的,并且没有继承QObject,所以我们必须手动调用delete运算符。

void NetWorker::get(const QString &url)
{
    d->manager->get(QNetworkRequest(QUrl(url)));
}

get()函数也很简单,直接将用户提供的 URL 字符串提供给底层的QNetworkAccessManager,实际上是将操作委托给底层QNetworkAccessManager进行。
现在我们将 QNetworkAccessManager进行了简单的封装。下一章我们开始针对 OpenWeatherMap 的 API 进行编码。

或者

1】头文件
想要利用QNetworkAccessManager类,必须在pro文件中添加对应库network,如下:
QT += network
【2】示例文件
文件1:

 1 #ifndef MAINWINDOW_H
 2 #define MAINWINDOW_H
 3 
 4 #include <QMainWindow>
 5 #include <QtNetWork>
 6 
 7 namespace Ui
 8 {
 9 class MainWindow;
10 }
11 
12 class MainWindow : public QMainWindow
13 {
14     Q_OBJECT
15 
16 public:
17     explicit MainWindow(QWidget *parent = 0);
18     ~MainWindow();
19 
20 private:
21     void get(QUrl u);
22 
23 private slots:
24     void on_pushButton_clicked();
25     void finished();
26 
27 private:
28     Ui::MainWindow *ui;
29 
30     QUrl m_url;
31     QString m_htmlText;
32     QNetworkReply *m_pReply;
33     QNetworkAccessManager m_manager;
34 };
35 
36 #endif // MAINWINDOW_H

文件21 #include "mainwindow.h"
 2 #include "ui_mainwindow.h"
 3 
 4 MainWindow::MainWindow(QWidget *parent) :
 5     QMainWindow(parent),
 6     ui(new Ui::MainWindow),
 7     m_pReply(Q_NULLPTR)
 8 {
 9     ui->setupUi(this);
10 }
11 
12 MainWindow::~MainWindow()
13 {
14     delete ui;
15 }
16 
17 void MainWindow::get(QUrl u)
18 {
19     m_url = u;
20 
21     if (m_pReply != Q_NULLPTR)
22     { // 更改reply指向位置前一定要保证之前的定义了自动delete
23         m_pReply->deleteLater();
24     }
25 
26     QNetworkRequest request;
27     request.setUrl(m_url);
28     m_pReply = m_manager.get(request);
29     qDebug() << "start get";
30     connect(m_pReply, &QNetworkReply::finished, this, &MainWindow::finished);
31 }
32 
33 void MainWindow::finished()
34 {
35     QByteArray bytes = m_pReply->readAll();
36 
37     m_pReply->deleteLater();
38     m_pReply = Q_NULLPTR;
39 
40     const QVariant redirectionTarget = m_pReply->attribute(QNetworkRequest::RedirectionTargetAttribute);
41     if (!redirectionTarget.isNull())
42     { //如果网址跳转重新请求
43         const QUrl redirectedUrl = m_url.resolved(redirectionTarget.toUrl());
44         qDebug() << "redirectedUrl:" << redirectedUrl.url();
45         get(redirectedUrl);
46         return;
47     }
48 
49     qDebug() << "finished";
50     m_htmlText = bytes;
51     qDebug() << "get ready,read size:" << m_htmlText.size();
52 
53     // 写入文件
54     QFile f("result.html");
55     f.open(QFile::WriteOnly);
56     f.write(bytes);
57 }
58 
59 void MainWindow::on_pushButton_clicked()
60 {
61     m_htmlText = "";
62     get(QUrl("http://www.baidu.com/"));
63 }
返回的结果文件:
在运行目录中查找result.html文件,双击运行,然后与百度首页作对比,可以发现get获取的数据中不包含图片信息。

如果你仔细观察就会发现,即便我们没有添加任何相关代码,QNetworkAccessManager的网络访问并不会阻塞 GUI 界面。也就是说,即便是在进行网络访问的时候,我们的界面还是可以响应的。相比之下,如果你对 Java 熟悉,就会了解到,在 Java 中,进行 Socket 通讯时,界面默认是阻塞的,当程序进行网络访问操作时,界面不能对我们的操作做出任何响应。由此可以看出,QNetworkAccessManager的网络访问默认就是异步的、非阻塞的。这样的实现固然很好,也符合大多数程序的应用情形:我们当然希望程序界面能够始终对用户操作做出响应。不过,在某些情况下,我们还是希望会有一些同步的网络操作。典型的是登录操作。在登录时,我们必须要等待网络返回结果,才能让界面做出响应:是验证成功进入系统,还是验证失败做出提示?这就是本章的主要内容:如何使用QNetworkAccessManager进行同步网络访问。

当我们重新运行先前编译好的程序,可以看看这样一个操作:由于我们的界面是不阻塞的,那么当我们第一次点击了 Refresh 按钮之后,马上切换城市再点击一次 Refresh 按钮,就会看到第一次的返回结果一闪而过。这是因为第一次网络请求尚未完成时,用户又发送了一次请求,Qt 会将两次请求的返回结果顺序显示。这样处理结果可能会出现与预期不一致的情况(比如第一次请求响应由于某种原因异常缓慢,第二次却很快,此时第二次结果会比第一次先到,那么很明显,当第一次结果返回时,第二次的结果就会被覆盖掉。我们假设认为用户需要第二次的返回,那么就会出现异常)。

要解决这种情况,我们可以在有网络请求时将界面锁死,不允许用户进行更多的操作(更好的方法是仅仅锁住某些按钮,而不是整个界面。不过这里我们以锁住整个界面为例)。我们的解决方案很简单:当QNetworkAccessManager发出请求之后,我们进入一个新的事件循环,将操作进行阻塞。我们的代码示例如下:

void fetchWeather(const QString &cityName)
{
    QEventLoop eventLoop;
    connect(netWorker, &NetWorker::finished,
            &eventLoop, &QEventLoop::quit);
    QNetworkReply *reply = netWorker->get(QString("http://api.openweathermap.org/data/2.5/weather?q=%1&mode=json&units=metric&lang=zh_cn").arg(cityName));
    replyMap.insert(reply, FetchWeatherInfo);
    eventLoop.exec();
}

注意,我们在函数中创建了一个QEventLoop实例,将其quit()与NetWorker::finished()信号连接起来。当NetWorker::finished()信号发出时,QEventLoop::quit()就会被调用。在NetWorker::get()执行之后,调用QEventLoop::exec()函数开始事件循环。此时界面就是被阻塞。

现在我们只是提供了一种很简单的思路。当然这并不是最好的思路:程序界面直接被阻塞,用户获得不了任何提示,会误以为程序死掉。更好的做法是做一个恰当的提示,不过这已经超出我们本章的范畴。更重要的是,这种思路并不完美。如果你的程序是控制台程序(没有 GUI 界面),或者是某些特殊的情况下,会造出死锁!控制台程序中发送死锁的原因在于在非 GUI 程序中另外启动事件循环会将主线程阻塞,QNetworkAccessManager的所有信号都不会收到。“某些特殊的情况”,我们会在后面有关线程的章节详细解释。不过,要完美解决这个问题,我们必须使用另外的线程。这里有一个通用的解决方案,感兴趣的童鞋可以详细了解下。

  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值