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
文件2:
1 #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的所有信号都不会收到。“某些特殊的情况”,我们会在后面有关线程的章节详细解释。不过,要完美解决这个问题,我们必须使用另外的线程。这里有一个通用的解决方案,感兴趣的童鞋可以详细了解下。