1——Fortune Server/Client
最后更新于:2022-04-01 07:21:04
本系列所有文章可以在这里查看[http://blog.csdn.net/cloud_castle/article/category/2123873](http://blog.csdn.net/cloud_castle/article/category/2123873)
最近发觉学习Qt官方demo的好处越来越多,除了了解特定模块与类的用法,更能学习一些平时注意不到的小的编程技巧,以及优秀的编程习惯,总之,好处多多啦~
考虑到学习是个往复的过程,好记性不如烂笔头嘛,勤快点记下来免得日后忘记,也方便官方demo不在身边的话也能随时取得到,最后也为学习Qt的网友们做个浅显的参考。
所以打算争取将Qt5官方demo一一解析在此博客中。做此工作本意也是为了学习,能力所限,不妥之处还请各位多多指正~~以上,则是为Qt5官方Demo解析集创作的缘由。
/****************************************************************************************************************/
先从一个简单的network例子开始吧~
Fortune Server/Client 由两个程序构成 (Fortune Server Example)和(Fortune Client Example)。也就是我们所说的服务器和客户端
首先看下Qt对这个例子的介绍:Demonstrates how to create a server for a network service.
fortuneserver一共就3个文件,先来看Server的main.cpp:
~~~
#include <QApplication>
#include <QtCore>
#include <stdlib.h>
#include "server.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
Server server;
server.show();
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
return app.exec();
}
~~~
前面一大段是版权所有,开源许可之类的东西,我们就不管它了。这段代码唯一值得说的就是13行,可以理解为随机种子的初始化。记得使用它要#include
server.h:
~~~
#ifndef SERVER_H
#define SERVER_H
#include <QDialog>
QT_BEGIN_NAMESPACE
class QLabel;
class QPushButton;
class QTcpServer;
class QNetworkSession;
QT_END_NAMESPACE
//! [0]
class Server : public QDialog
{
Q_OBJECT
public:
Server(QWidget *parent = 0);
private slots:
void sessionOpened();
void sendFortune();
private:
QLabel *statusLabel;
QPushButton *quitButton;
QTcpServer *tcpServer;
QStringList fortunes;
QNetworkSession *networkSession;
};
//! [0]
#endif
~~~
QT_BEGIN_NAMESPACE和QT_END_NAMESPACE宏在源代码中是这样定义的:
~~~
# define QT_BEGIN_NAMESPACE namespace QT_NAMESPACE {
# define QT_END_NAMESPACE }
~~~
也就是说,如果你定义以下内容:
~~~
QT_BEGIN_NAMESPACE
class QListView;
QT_END_NAMESPACE
~~~
那么,在编译时就会变成这样:
~~~
namespace QT_NAMESPACE {
class QListView;
}
~~~
仅当在编译Qt时,加上-qtnamespace选项时,这两个宏才会有作用,这时,Qt作为第三方库,要使用用户自定义的命名空间来访问Qt中的类,如QListView *view = new QT_NAMESPACE::QListView
如果我们只需要声明类的指针或者引用,使用前向声明而不是#include可以减少头文件之间的依赖。这在大型项目中减少编译时间是很必要的。
QTcpServer *tcpServer;
QNetworkSession *networkSession;
这是最核心的两个类声明了,前者提供了一个基于TCP的服务器,后者则提供了系统网络接口控制。
好了,来看重头Server.cpp,为了简明部分注释就直接加注释在后面了
~~~
#include <QtWidgets>
#include <QtNetwork>
#include <stdlib.h>
#include "server.h"
Server::Server(QWidget *parent)
: QDialog(parent), tcpServer(0), networkSession(0) //初始化列表
{
statusLabel = new QLabel;
quitButton = new QPushButton(tr("Quit"));
quitButton->setAutoDefault(false);
QNetworkConfigurationManager manager;
if (manager.capabilities() & QNetworkConfigurationManager::NetworkSessionRequired) { // 检测平台是否具有网络通讯能力
QSettings settings(QSettings::UserScope, QLatin1String("QtProject")); // QSettings用来将用户配置保存到注册表、INI文件中。这里是提取配置
settings.beginGroup(QLatin1String("QtNetwork"));
const QString id = settings.value(QLatin1String("DefaultNetworkConfiguration")).toString();
settings.endGroup();
QNetworkConfiguration config = manager.configurationFromIdentifier(id);
if ((config.state() & QNetworkConfiguration::Discovered) !=
QNetworkConfiguration::Discovered) {
config = manager.defaultConfiguration(); // 没有取到用户保存信息则使用默认配置
}
networkSession = new QNetworkSession(config, this); // 使用该配置新建网络会话
connect(networkSession, SIGNAL(opened()), this, SLOT(sessionOpened()));
statusLabel->setText(tr("Opening network session."));
networkSession->open();
} else {
sessionOpened();
}
//! [2]
fortunes << tr("You've been leading a dog's life. Stay off the furniture.")
<< tr("You've got to think about tomorrow.")
<< tr("You will be surprised by a loud noise.")
<< tr("You will feel hungry again in another hour.")
<< tr("You might have mail.")
<< tr("You cannot kill time without injuring eternity.")
<< tr("Computers are not intelligent. They only think they are.");
//! [2]
connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));
//! [3]
connect(tcpServer, SIGNAL(newConnection()), this, SLOT(sendFortune()));
//! [3]
QHBoxLayout *buttonLayout = new QHBoxLayout;
buttonLayout->addStretch(1);
buttonLayout->addWidget(quitButton);
buttonLayout->addStretch(1);
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(statusLabel);
mainLayout->addLayout(buttonLayout);
setLayout(mainLayout);
setWindowTitle(tr("Fortune Server"));
}
void Server::sessionOpened()
{
// 类似构造函数中的读取配置,将网络会话配置保存在注册表中
if (networkSession) {
QNetworkConfiguration config = networkSession->configuration();
QString id;
if (config.type() == QNetworkConfiguration::UserChoice)
id = networkSession->sessionProperty(QLatin1String("UserChoiceConfiguration")).toString();
else
id = config.identifier();
QSettings settings(QSettings::UserScope, QLatin1String("QtProject"));
settings.beginGroup(QLatin1String("QtNetwork"));
settings.setValue(QLatin1String("DefaultNetworkConfiguration"), id);
settings.endGroup();
}
//! [0] //! [1]
tcpServer = new QTcpServer(this);
if (!tcpServer->listen()) { // 新建 Tcp 服务并开始监听,这个监听是基于所有地址,任意端口的<span style="font-family: Arial, Helvetica, sans-serif;"> </span>
QMessageBox::critical(this, tr("Fortune Server"),
tr("Unable to start the server: %1.")
.arg(tcpServer->errorString()));
close();
return;
}
//! [0]
QString ipAddress;
QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses(); // 这里返回的是所有主机能够监听到的IPv4的地址
for (int i = 0; i < ipAddressesList.size(); ++i) {
if (ipAddressesList.at(i) != QHostAddress::LocalHost &&
ipAddressesList.at(i).toIPv4Address()) { // 取出第一个非主机地址的IPv4地址,注意这里使用了toIPv4Address()进行转换,因为原类型是QHostAddress的
ipAddress = ipAddressesList.at(i).toString();
break;
}
}
// 如果没有发现则使用主机的IPv4地址
if (ipAddress.isEmpty())
ipAddress = QHostAddress(QHostAddress::LocalHost).toString();
statusLabel->setText(tr("The server is running on\n\nIP: %1\nport: %2\n\n"
"Run the Fortune Client example now.")
.arg(ipAddress).arg(tcpServer->serverPort())); // 端口号由serverPort()获得
//! [1]
}
//! [4]
void Server::sendFortune()
{
//! [5]
QByteArray block;
QDataStream out(&block, QIODevice::WriteOnly); // QDataSteam作为二进制输入输出流,使用QTcpSocket传输数据的常用方法
out.setVersion(QDataStream::Qt_4_0); // 第(1)点
//! [4] //! [6]
out << (quint16)0; // 第(2)点
out << fortunes.at(qrand() % fortunes.size()); // (qrand() % fortunes.size())根据上文理解为随机的0-6就好
out.device()->seek(0);
out << (quint16)(block.size() - sizeof(quint16)); // 第(3)点
//! [6] //! [7]
// 注意这个QTcpSocket对象由tcpSetver创建,这意味着clientConnection是作为tcpServer的子对象存在
QTcpSocket *clientConnection = tcpServer->nextPendingConnection();
connect(clientConnection, SIGNAL(disconnected()), // 确保失去连接后内存被释放
clientConnection, SLOT(deleteLater()));
//! [7] //! [8]
clientConnection->write(block); // 数据的写入是异步的
clientConnection->disconnectFromHost(); // 数据发送完毕后关闭Socket
//! [5]
}
//! [8]
~~~
第一点,很多地方都有讲这个out.setVersion(QDataStream::Qt_4_0),就说编码统一什么的,让人听的一头雾水。明确的说,为了适应新的功能,一些Qt类的数据类的序列化格式已经在一些新版本的Qt发生了改变。也就是说,如果基于Qt5.2的数据发送到Qt4.0的客户机上,很可能他取出的数据与你发送的数据不同。这样就需要规定一个统一的数据流格式,这里将版本设置为Qt_4_0,也就是以4.0版本的数据流序列化格式发送数据。客户机也只有设置了相同的版本才能保证数据的准确解析。
第二点,为了保证在客户端能接收到完整的文件,我们都在数据流的最开始写入完整文件的大小信息,这样客户端就可以根据大小信息来判断是否接受到了完整的文件。而在服务器端,我们在发送数据时就要首先发送实际文件的大小信息,但是,文件的大小一开始是无法预知的,所以我们先使用了out<<(quint16) 0;在block的开始添加了一个quint16大小的空间,也就是两字节的空间,它用于后面放置文件的大小信息。
第三点,当文件输入完成后我们在使用out.device()->seek(0);返回到block的开始,加入实际的文件大小信息,也就是后面的代码,它是实际文件的大小:out<<(quint16) (block.size() – sizeof(quint16));
好了,服务端大致这些,实际就我们一般的使用中,使用默认的网络配置就可以了,所以核心程序还是很简单的。
下面来看客户端代码吧~与fortuneServer一样,fortuneClient中也只有3个文件,main.cpp保持了Qt一贯的简约风格,我们就跳过它来看client.h:
~~~
#ifndef CLIENT_H
#define CLIENT_H
#include <QDialog>
#include <QTcpSocket>
QT_BEGIN_NAMESPACE
class QComboBox;
class QDialogButtonBox;
class QLabel;
class QLineEdit;
class QPushButton;
class QTcpSocket;
class QNetworkSession;
QT_END_NAMESPACE
//! [0]
class Client : public QDialog
{
Q_OBJECT
public:
Client(QWidget *parent = 0);
private slots:
void requestNewFortune();
void readFortune();
void displayError(QAbstractSocket::SocketError socketError); // 注意到这个参数是QString型的枚举量
void enableGetFortuneButton();
void sessionOpened();
private:
QLabel *hostLabel;
QLabel *portLabel;
QComboBox *hostCombo;
QLineEdit *portLineEdit;
QLabel *statusLabel;
QPushButton *getFortuneButton;
QPushButton *quitButton;
QDialogButtonBox *buttonBox;
QTcpSocket *tcpSocket;
QString currentFortune;
quint16 blockSize; // 这里的quint16为了封装平台间的差异,如果直接使用int在不同的平台下可能长度不同
QNetworkSession *networkSession;
};
//! [0]
#endif
~~~
一般来讲privat,private slots放在最下面,因为被查看的最少。
既然说的QTcpSocket,需要介绍一下它的两种网络编程的方式:
第一种是异步(non-blocking)方法。业务调度和执行时控制返回到Qt的事件循环。当操作完成时,QTcpSocket发出信号。例如,QTcpSocket::connectToHost()是立即返回的,而当连接已经建立后,QTcpSocket再发出connected()信号表明自己已成功连接。
第二种是同步(blocking)的方法。在非图形用户界面和多线程应用程序,可以调用WAITFOR...()函数(例如,QTcpSocket:: waitForConnected())暂停直到操作完成。
在这个Demo中,是以异步方式建立的网络。Blocking Fortune Example中使用了同步的方法,这个下次来看。
client.cpp:
~~~
#include <QtWidgets>
#include <QtNetwork>
#include "client.h"
//! [0]
Client::Client(QWidget *parent)
: QDialog(parent), networkSession(0)
{
//! [0]
hostLabel = new QLabel(tr("&Server name:"));
portLabel = new QLabel(tr("S&erver port:"));
hostCombo = new QComboBox;
hostCombo->setEditable(true);
// find out name of this machine
QString name = QHostInfo::localHostName(); // 获取主机名
if (!name.isEmpty()) {
hostCombo->addItem(name);
QString domain = QHostInfo::localDomainName(); // 获取DNS域名。当然,个人PC机是没有的
if (!domain.isEmpty())
hostCombo->addItem(name + QChar('.') + domain);
}
if (name != QString("localhost"))
hostCombo->addItem(QString("localhost"));
// find out IP addresses of this machine
QList<QHostAddress> ipAddressesList = QNetworkInterface::allAddresses();
// add non-localhost addresses
for (int i = 0; i < ipAddressesList.size(); ++i) { 第(1)点
if (!ipAddressesList.at(i).isLoopback())
hostCombo->addItem(ipAddressesList.at(i).toString());
}
// add localhost addresses
for (int i = 0; i < ipAddressesList.size(); ++i) { 第(2)点
if (ipAddressesList.at(i).isLoopback())
hostCombo->addItem(ipAddressesList.at(i).toString());
}
portLineEdit = new QLineEdit;
portLineEdit->setValidator(new QIntValidator(1, 65535, this)); // 输入限制为1-65535的整型,限制输入应该成为一个良好的习惯
hostLabel->setBuddy(hostCombo);
portLabel->setBuddy(portLineEdit);
statusLabel = new QLabel(tr("This examples requires that you run the "
"Fortune Server example as well."));
getFortuneButton = new QPushButton(tr("Get Fortune"));
getFortuneButton->setDefault(true); // 这个属性设置意味着该按键将响应回车键
getFortuneButton->setEnabled(false);
quitButton = new QPushButton(tr("Quit"));
buttonBox = new QDialogButtonBox; // QDialogButtonBox中第一设置为ActionRole的按钮将被设置为Default,它将响应回车按下事件
buttonBox->addButton(getFortuneButton, QDialogButtonBox::ActionRole);
buttonBox->addButton(quitButton, QDialogButtonBox::RejectRole);
//! [1]
tcpSocket = new QTcpSocket(this);
//! [1]
connect(hostCombo, SIGNAL(editTextChanged(QString)),
this, SLOT(enableGetFortuneButton()));
connect(portLineEdit, SIGNAL(textChanged(QString)),
this, SLOT(enableGetFortuneButton()));
connect(getFortuneButton, SIGNAL(clicked()),
this, SLOT(requestNewFortune()));
connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));
//! [2] //! [3]
connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(readFortune()));
//! [2] //! [4]
connect(tcpSocket, SIGNAL(error(QAbstractSocket::SocketError)),
//! [3]
this, SLOT(displayError(QAbstractSocket::SocketError)));
//! [4]
QGridLayout *mainLayout = new QGridLayout;
mainLayout->addWidget(hostLabel, 0, 0);
mainLayout->addWidget(hostCombo, 0, 1);
mainLayout->addWidget(portLabel, 1, 0);
mainLayout->addWidget(portLineEdit, 1, 1);
mainLayout->addWidget(statusLabel, 2, 0, 1, 2); // 参数分别表示,第2行,第0列,行跨度为1,列跨度为2,最后还有一个Qt::Alignment属性
mainLayout->addWidget(buttonBox, 3, 0, 1, 2);
setLayout(mainLayout);
setWindowTitle(tr("Fortune Client"));
portLineEdit->setFocus();
QNetworkConfigurationManager manager; // 这里主要用来保存网络设置,可以参见Server端代码
if (manager.capabilities() & QNetworkConfigurationManager::NetworkSessionRequired) {
// Get saved network configuration
QSettings settings(QSettings::UserScope, QLatin1String("QtProject"));
settings.beginGroup(QLatin1String("QtNetwork"));
const QString id = settings.value(QLatin1String("DefaultNetworkConfiguration")).toString();
settings.endGroup();
// If the saved network configuration is not currently discovered use the system default
QNetworkConfiguration config = manager.configurationFromIdentifier(id);
if ((config.state() & QNetworkConfiguration::Discovered) !=
QNetworkConfiguration::Discovered) {
config = manager.defaultConfiguration();
}
networkSession = new QNetworkSession(config, this);
connect(networkSession, SIGNAL(opened()), this, SLOT(sessionOpened()));
getFortuneButton->setEnabled(false);
statusLabel->setText(tr("Opening network session."));
networkSession->open();
}
//! [5]
} //构造函数完
//! [5]
//! [6]
void Client::requestNewFortune()
{
getFortuneButton->setEnabled(false);
blockSize = 0;
tcpSocket->abort(); // 终止当前连接并重置socket,任何等待发送的数据都将被丢弃
//! [7]
tcpSocket->connectToHost(hostCombo->currentText(),
portLineEdit->text().toInt()); //连接到主机,第一个参数是IP地址,第二个参数是端口号,第三个参数默认值为读写
//! [7]
}
//! [6]
//! [8]
void Client::readFortune()
{
//! [9]
QDataStream in(tcpSocket);
in.setVersion(QDataStream::Qt_4_0); // 版本号需要保持一致
if (blockSize == 0) {
if (tcpSocket->bytesAvailable() < (int)sizeof(quint16))
return;
//! [8]
//! [10]
in >> blockSize; // 因为blockSize是quint16,所以只接收到in数据流前16个字节,而该数据则是整个数据流的长度
}
if (tcpSocket->bytesAvailable() < blockSize)
return; // 第(3)点
//! [10] //! [11]
QString nextFortune; //注意这个局部QString变量的作用
in >> nextFortune;
if (nextFortune == currentFortune) {
QTimer::singleShot(0, this, SLOT(requestNewFortune())); // 单次的定时器,重新请求新数据
return;
}
//! [11]
//! [12]
currentFortune = nextFortune; // 否则将这个值赋给全局变量
//! [9]
statusLabel->setText(currentFortune);
getFortuneButton->setEnabled(true);
}
//! [12]
//! [13]
void Client::displayError(QAbstractSocket::SocketError socketError) // 这个SocketError是抽象基类的枚举对象,因此对所有套接字都是通用的
{
switch (socketError) {
case QAbstractSocket::RemoteHostClosedError:
break;
case QAbstractSocket::HostNotFoundError:
QMessageBox::information(this, tr("Fortune Client"),
tr("The host was not found. Please check the "
"host name and port settings."));
break;
case QAbstractSocket::ConnectionRefusedError:
QMessageBox::information(this, tr("Fortune Client"),
tr("The connection was refused by the peer. "
"Make sure the fortune server is running, "
"and check that the host name and port "
"settings are correct."));
break;
default:
QMessageBox::information(this, tr("Fortune Client"),
tr("The following error occurred: %1.")
.arg(tcpSocket->errorString()));
}
getFortuneButton->setEnabled(true);
}
//! [13]
void Client::enableGetFortuneButton() // 判断语句封装在函数体内,而不是每次调用时还要自己判断
{
getFortuneButton->setEnabled((!networkSession || networkSession->isOpen()) && // 省 if 的方法
!hostCombo->currentText().isEmpty() &&
!portLineEdit->text().isEmpty());
}
void Client::sessionOpened() // 保存网络配置,详见Server端类似代码
{
// Save the used configuration
QNetworkConfiguration config = networkSession->configuration();
QString id;
if (config.type() == QNetworkConfiguration::UserChoice)
id = networkSession->sessionProperty(QLatin1String("UserChoiceConfiguration")).toString();
else
id = config.identifier();
QSettings settings(QSettings::UserScope, QLatin1String("QtProject"));
settings.beginGroup(QLatin1String("QtNetwork"));
settings.setValue(QLatin1String("DefaultNetworkConfiguration"), id);
settings.endGroup();
statusLabel->setText(tr("This examples requires that you run the "
"Fortune Server example as well."));
enableGetFortuneButton();
}
~~~
第一点,注意下面两个if 只有一个 ! 的区别。首先添加了环回网路,在我的机子上测试为192.168.1.106、192.168.23.1、192.168.19.1(均属于本地局域网)
第二点,添加了环回网路,地址为127.0.0.1(Windows下的虚拟接口,永不会down~)
第三点,这点很关键,实际上,尤其在慢速网络中,TCP基于数据流的发送方式会使客户端在一次接收中无法收到一整包的数据。那么,程序在判断接收到的数据长度小于队首的数值时,说明这包数据并不完整,不过没有关系,程序直接返回,等待接下来的片段包,而QTcpSocket会将这些片段缓存,当收到数据长度等于队首长度数值时,再将这些缓存数据全部读出。这也是为什么我们在Server端加入数据长度帧的原因所在。
好了,Fortune Server/Client例程大概就这些~看起来代码很长,其实真正用在发送接收的也不过几句话而已。当然我们自己在做项目的时候也应该像demo一样把各种可能的异常尽量加在处理函数中,这对程序的健壮性是有好处的~