首页 Qt 学习之路 2 Qt 学习之路 2(68):访问网络(4)

Qt 学习之路 2(68):访问网络(4)

19 7

前面几章我们了解了如何使用QNetworkAccessManager 访问网络。在此基础上,我们已经实现了一个简单的查看天气的程序。在这个程序中,我们使用QNetworkAccessManager进行网络的访问,从一个网络 API 获取某个城市的当前天气状况。

如果你仔细观察就会发现,即便我们没有添加任何相关代码,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的所有信号都不会收到。“某些特殊的情况”,我们会在后面有关线程的章节详细解释。不过,要完美解决这个问题,我们必须使用另外的线程。这里有一个通用的解决方案,感兴趣的童鞋可以详细了解下。

19 评论

恒古炎 2013年11月8日 - 17:32

请问如何禁用窗口的拉伸光标?

回复
豆子 2013年11月8日 - 21:44

你可以试试 QWidgetsetFixedSize() 函数

回复
恒古炎 2013年11月8日 - 21:51

窗口大小不变了,但是在窗体边缘的拉伸光标依旧在,这个我找文档的事件也没看到 lol 求指导, 我是用layout()->setSizeConstraint(QLayout::SetFixedSize);来禁止大小的,请问两者有什么差异吗?

回复
豆子 2013年11月8日 - 23:24

如果你需要让窗口大小根据其内容固定,可以使用 setSizeConstraint() 函数;setFixedSize() 函数则是显式给定一个固定值。至于光标的问题,Qt4 是没有这个问题的;Qt5 的确存在这个问题,并且现在确定是一个 bug,在 Qt5.1.2 中将修复这个问题。

回复
Hacky 2013年12月11日 - 15:30

豆子 你好 我想在Qt下实现TCP网络通信,建立一个可以接受大量客户端的服务器程序,这个如何实现呢?

回复
豆子 2013年12月11日 - 20:05

可以参考下QTcpServer和QTcpSocket这两个类

回复
Hacky 2013年12月11日 - 20:15

那Qt有没有那种像windows下完成端口的高性能处理多客户的线程池之类的模型呢?

回复
豆子 2013年12月13日 - 14:54

这个不大清楚,不过你可以找个第三方 C++ 库都是可以的

回复
Hacky 2013年12月13日 - 17:15

谢谢你。博文不错,持续关注,继续加油

datde 2014年6月3日 - 23:46

请问如何实现网络UDP(包括广播/组播和单播)和TCP通信,在工程中这2类方式用的很多,谢谢豆子

回复
豆子 2014年6月5日 - 09:35

Qt 提供了 QUdpSocket 和 QTcpSocket 两个类用于实现 UDP 和 TCP。你可以看看这两个类的文档。之前因为感觉 UDP 和 TCP 相对大部分应用比较底层,所以没有详细说明

回复
Jesse 2016年4月30日 - 21:00

您说的控制台程序使用eventLoop.exec();会导致死锁,是因为在控制台程序里QNetworkAccessManager的操作或finish信号不是在eventLoop.exec();处理的?

而GUI程序不会死锁是因为QNetworkAccessManager的操作在eventLoop.exec();里完成?

回复
Jesse 2016年5月1日 - 15:15

恩,您链接的文章里确实说了
“Calling the exec() function of QEventLoop can cause a deadlock in non GUI threads – signals sent by QNetworkRequest and such would not be accepted and so the quit() slot would never be activated.”
但是他的代码里其实也在non GUI线程用了eventLoop.exec();:
webfile::open->webfile::slotOpen->webfile::workerOpen->webfile::waitForConnect->eventLoop.exec();,
不过除了QNetworkAccessManager::finished外,他还连接了Timer超时和QNetworkReply::readyRead到QEventLoop::quit。

回复
豆子 2016年5月7日 - 21:19

控制台程序开启另外的事件循环会导致主线程阻塞,主线程阻塞就不能接受别的信号,因此会发生死锁

回复
ccppaa 2016年5月24日 - 09:53

你好,如果想写类似连锁超市收银系统,应该往哪块学习,是看QTcpserver和QTcpsocket这两个吗?还是看别的什么

回复
豆子 2016年5月24日 - 21:23

这个具体要看你的需要了,QTcpServerQTcpSocket是 TCP 协议的封装,QNetworkAccessManager是 HTTP 协议的封装,这要看你的系统使用的是哪个协议,还有很多其他的问题需要考虑

回复
ccppaa 2016年5月25日 - 09:22

也就是说,qt是能写出这样的系统,现在有准备要学习写,但是完全没有方向,之前有单独的超市收银,是用的http,如果是分布式的,会不会TCP更方便一些呢

回复
LinWM 2017年7月24日 - 18:04

“调用QEventLoop::exec()函数开始事件循环。此时界面就是被阻塞。”:这句话阻塞我觉得有待斟酌,阻塞会让人以为UI线程卡死、无响应,我认为QEventLoop::exec();调用是用自己的事件循环取代了UI线程的事件循环,让事件响应不再在UI线程的事件循环中而是在自己事件循环中,类似于QMessageBox::exec();

回复
豆子 2017年7月25日 - 09:15

是的,就是你说的意思。个人理解这就是 UI 循环被阻塞

回复

发表评论

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2