三十三、网络及多线程
以下界面设计基本全用ui文件设计
33.1 Qt网络编程
Qt 直接提供网络编程模块,基于TCP/IP 客户端和服务器相关各 种类。TCP 通信(QTcpSocket/QTcpServer )。UDP 通信 (QUdpSocket)。还有部分实现HTTP、FTP 等网络协议的高级类。 如QNetworkRequest/QNetworkAccessManager 等。
Qt 网络编程模块提供网络承载管理类,提供基于安全套接字层协 议(SSL)的安全网络通信类。
我们开发过程中,UDP、TCP、HTTP 通信类等,必须在程序中 引用Qt 网络模块,项目配置文件的一条配置语句:QT +=network。
这里介绍两种类:
QHostInfo 类:QHostInfo 类为主机名查找提供静态函数,主要 用来查询主机信息、包括主机名、ip 地址、DNS 域名等信息。
QNetworkInterface 类,主要用于获取主机所有IP 地址和网络 接口列表信息。
实现第一个例子:获取本地主机名称和IP地址与本地机器详细信息
个人主机信息已打上马赛克。
头文件
#ifndef GETHOSTNAMEIPINFO_H
#define GETHOSTNAMEIPINFO_H#include <QDialog>#include<QHostInfo>
#include<QNetworkInterface>
#include<QMessageBox>QT_BEGIN_NAMESPACE
namespace Ui { class gethostnameipinfo; }
QT_END_NAMESPACEclass gethostnameipinfo : public QDialog
{Q_OBJECTpublic:gethostnameipinfo(QWidget *parent = nullptr);~gethostnameipinfo();void GetHostNameAndIpAddress();private slots:void on_pushButton_getHostNameAndIP_clicked();void on_pushButton_getLocalInfo_clicked();private:Ui::gethostnameipinfo *ui;
};
#endif // GETHOSTNAMEIPINFO_H
源文件
#include "gethostnameipinfo.h"
#include "ui_gethostnameipinfo.h"// gethostnameipinfo 类的构造函数
gethostnameipinfo::gethostnameipinfo(QWidget *parent): QDialog(parent), ui(new Ui::gethostnameipinfo)
{// 调用 UI 生成的 setupUi 函数,初始化 UI 界面ui->setupUi(this);
}// gethostnameipinfo 类的析构函数
gethostnameipinfo::~gethostnameipinfo()
{// 释放 UI 指针所指向的资源delete ui;
}// 获取主机名和 IP 地址的函数
void gethostnameipinfo::GetHostNameAndIpAddress()
{// 获取本地主机名QString StrLocalHostName = QHostInfo::localHostName();// 将主机名显示在 lineEdit_hostName 控件中ui->lineEdit_hostName->setText(StrLocalHostName);// 存储本地 IP 地址的字符串,初始化为空QString StrLocalIpAddress = "";// 根据主机名获取主机信息QHostInfo hostinfo = QHostInfo::fromName(StrLocalHostName);// 获取主机的地址列表QList<QHostAddress> ipaddresslist = hostinfo.addresses();// 如果地址列表不为空if (!ipaddresslist.isEmpty()){// 遍历地址列表for (int i = 0; i < ipaddresslist.size(); i++){// 获取列表中的地址元素QHostAddress addresshost = ipaddresslist.at(i);// 如果地址是 IPv4 协议if (QAbstractSocket::IPv4Protocol == addresshost.protocol()){// 将该地址存储为本地 IP 地址StrLocalIpAddress = addresshost.toString();// 找到一个 IPv4 地址后,退出循环break;}}}// 将 IP 地址显示在 lineEdit_hostIp 控件中ui->lineEdit_hostIp->setText(StrLocalIpAddress);
}// 当点击 pushButton_getHostNameAndIP 按钮时调用的槽函数
void gethostnameipinfo::on_pushButton_getHostNameAndIP_clicked()
{// 调用 GetHostNameAndIpAddress 函数获取主机名和 IP 地址GetHostNameAndIpAddress();
}// 当点击 pushButton_getLocalInfo 按钮时调用的槽函数
void gethostnameipinfo::on_pushButton_getLocalInfo_clicked()
{// 存储信息的临时字符串QString strTemp = "";// 获取所有网络接口的列表QList<QNetworkInterface> netlist = QNetworkInterface::allInterfaces();// 遍历网络接口列表for (int i = 0; i < netlist.size(); i++){// 获取当前网络接口QNetworkInterface interfaces = netlist.at(i);// 将设备名称添加到 strTemp 字符串strTemp = strTemp + "设备名称:" + interfaces.name() + "\n";// 将硬件地址添加到 strTemp 字符串strTemp = strTemp + "硬件地址:" + interfaces.hardwareAddress() + "\n";// 获取当前网络接口的地址条目列表QList<QNetworkAddressEntry> entrylist = interfaces.addressEntries();// 遍历地址条目列表for (int k = 0; k < entrylist.count(); k++){// 获取当前地址条目QNetworkAddressEntry etry = entrylist.at(k);// 将 IP 地址添加到 strTemp 字符串strTemp = strTemp + "IP 地址:" + etry.ip().toString() + "\n";// 将子网掩码添加到 strTemp 字符串strTemp = strTemp + "子网地址:" + etry.netmask().toString() + "\n";}}// 显示包含所有信息的消息框QMessageBox::information(this, "主机所有信息", strTemp, QMessageBox::Yes);
}
-
GetHostNameAndIpAddress()
函数:QString StrLocalHostName = QHostInfo::localHostName();
:使用QHostInfo
类的静态函数localHostName()
获取本地主机的名称。ui->lineEdit_hostName->setText(StrLocalHostName);
:将获取的主机名设置到lineEdit_hostName
文本框中。QHostInfo hostinfo = QHostInfo::fromName(StrLocalHostName);
:根据主机名获取更详细的主机信息。QList<QHostAddress> ipaddresslist = hostinfo.addresses();
:获取主机的地址列表,包含多个可能的地址。if (!ipaddresslist.isEmpty()) {...}
:如果地址列表不为空,遍历列表。QHostAddress addresshost = ipaddresslist.at(i);
:获取列表中的每个地址。if (QAbstractSocket::IPv4Protocol == addresshost.protocol()) {...}
:检查地址是否为 IPv4 协议,若是,则存储为StrLocalIpAddress
并退出遍历,因为通常更关心 IPv4 地址。ui->lineEdit_hostIp->setText(StrLocalIpAddress);
:将找到的 IPv4 地址设置到lineEdit_hostIp
文本框中。
on_pushButton_getHostNameAndIP_clicked()
槽函数:GetHostNameAndIpAddress();
:当按钮被点击时,调用GetHostNameAndIpAddress()
函数获取并显示主机名和 IP 地址。
on_pushButton_getLocalInfo_clicked()
槽函数:QString strTemp = "";
:创建一个临时字符串用于存储信息。QList<QNetworkInterface> netlist = QNetworkInterface::allInterfaces();
:使用QNetworkInterface
类的静态函数allInterfaces()
获取所有网络接口的列表。for (int i = 0; i < netlist.size(); i++) {...}
:遍历网络接口列表。QNetworkInterface interfaces = netlist.at(i);
:获取每个网络接口。strTemp = strTemp + "设备名称:" + interfaces.name() + "\n";
和strTemp = strTemp + "硬件地址:" + interfaces.hardwareAddress() + "\n";
:将设备名称和硬件地址添加到strTemp
中。QList<QNetworkAddressEntry> entrylist = interfaces.addressEntries();
:获取每个网络接口的地址条目列表。for (int k = 0; k < entrylist.count(); k++) {...}
:遍历地址条目列表。QNetworkAddressEntry etry = entrylist.at(k);
:获取每个地址条目。strTemp = strTemp + "IP 地址:" + etry.ip().toString() + "\n";
和strTemp = strTemp + "子网地址:" + etry.netmask().toString() + "\n";
:将 IP 地址和子网掩码添加到strTemp
中。QMessageBox::information(this, "主机所有信息", strTemp, QMessageBox::Yes);
:将存储所有信息的strTemp
显示在消息框中。
QString QHostInfo::localHostName():
返回此计算机的主机名(如果已配置)。请注意,不能保证主机名是全局唯一的,尤其是在自动配置时。此函数不保证返回的主机名是完全限定域名 (FQDN)。为此,请使用 fromName() 将返回的名称解析为 FQDN。
QHostInfo QHostInfo::fromName(const QString &name):
查找给定主机名的IP地址。该函数在查找期间阻塞,这意味着程序的执行被暂停,直到查找结果准备好。在QHostInfo对象中返回查找结果。
如果您将文字IP地址传递给name而不是主机名,QHostInfo将搜索IP的域名(即QHostInfo将执行反向查找)。成功后,返回的QHostInfo将包含解析的域名和主机名的IP地址。
qtcore/qlist.html" rel="nofollow">QList<QHostAddress> QHostInfo::addresses() const:
返回与hostName()关联的IP地址列表。此列表可能为空。
QAbstractSocket::NetworkLayerProtocol QHostAddress::protocol() const:
返回主机地址的网络层协议
QList<QNetworkInterface> QNetworkInterface::allInterfaces():
返回在主机上找到的所有网络接口的列表。如果失败,它会返回一个零元素的列表。
QList<QNetworkAddressEntry> QNetworkInterface::addressEntries() const:
返回此接口拥有的IP地址列表及其关联的网络掩码和广播地址。
如果不需要网络掩码或播放地址或其他信息,您可以调用allAddress()函数来仅获取活动接口的IP地址。
实现第二个例子:查询域名或IP地址
界面由ui设计
头文件
#ifndef GETDOMAINIP_H
#define GETDOMAINIP_H#include <QDialog>#include<QHostInfo>QT_BEGIN_NAMESPACE
namespace Ui { class getdomainip; }
QT_END_NAMESPACEclass getdomainip : public QDialog
{Q_OBJECTpublic:getdomainip(QWidget *parent = nullptr);~getdomainip();QString ProtocolTypeName(QAbstractSocket::NetworkLayerProtocol pro);private slots:void LookupHostInfoFunc(const QHostInfo &host);void on_pushButton_GetDomainIP_clicked();void on_pushButton_ClearData_clicked();private:Ui::getdomainip *ui;
};
#endif // GETDOMAINIP_H
源文件
#include "getdomainip.h"
#include "ui_getdomainip.h"// getdomainip 类的构造函数,接收一个父窗口指针 parent
getdomainip::getdomainip(QWidget *parent): QDialog(parent), ui(new Ui::getdomainip)
{// 调用 UI 生成的 setupUi 函数,初始化 UI 界面ui->setupUi(this);// 在输入框 lineEdit_InputUrl 中设置默认的域名ui->lineEdit_InputUrl->setText("www.126.com");
}// getdomainip 类的析构函数,用于释放资源
getdomainip::~getdomainip()
{// 释放 UI 指针所指向的资源delete ui;
}// 根据网络层协议类型返回相应的协议名称
QString getdomainip::ProtocolTypeName(QAbstractSocket::NetworkLayerProtocol pro)
{// 使用 switch 语句根据不同的协议类型返回不同的协议名称switch (pro){case QAbstractSocket::IPv4Protocol:// IPv4 协议return "IPv4";case QAbstractSocket::IPv6Protocol:// IPv6 协议return "IPv6";case QAbstractSocket::AnyIPProtocol:// 任意 IP 协议return "Any";default:// 未知网络协议return "Unkown Network";}
}// 处理主机信息查找结果的函数
void getdomainip::LookupHostInfoFunc(const QHostInfo &host)
{// 获取主机的地址列表QList<QHostAddress> addresslist = host.addresses();// 遍历地址列表for (int i = 0; i < addresslist.count(); i++){// 获取列表中的当前地址元素QHostAddress host = addresslist.at(i);// 将协议类型添加到 plainTextEdit_DomainIP 文本编辑框中ui->plainTextEdit_DomainIP->appendPlainText("协议类型:" + ProtocolTypeName(host.protocol()));// 将本地 IP 地址添加到 plainTextEdit_DomainIP 文本编辑框中ui->plainTextEdit_DomainIP->appendPlainText("本地 IP 地址:" + host.toString());// 添加一个空行,用于分隔不同地址的信息ui->plainTextEdit_DomainIP->appendPlainText("");}
}// 当点击 pushButton_GetDomainIP 按钮时调用的槽函数
void getdomainip::on_pushButton_GetDomainIP_clicked()
{// 获取用户在输入框 lineEdit_InputUrl 中输入的域名QString strhostname = ui->lineEdit_InputUrl->text();// 将用户输入的域名添加到 plainTextEdit_DomainIP 文本编辑框中ui->plainTextEdit_DomainIP->appendPlainText("你所查询主机信息:" + strhostname);// 调用 QHostInfo 的 lookupHost 函数,根据输入的域名查找主机信息// 并将结果传递给 LookupHostInfoFunc 函数进行处理QHostInfo::lookupHost(strhostname, this, SLOT(LookupHostInfoFunc(QHostInfo)));
}// 当点击 pushButton_ClearData 按钮时调用的槽函数
void getdomainip::on_pushButton_ClearData_clicked()
{// 清空 plainTextEdit_DomainIP 文本编辑框中的内容ui->plainTextEdit_DomainIP->clear();
}
ProtocolTypeName(QAbstractSocket::NetworkLayerProtocol pro)
函数:- 接收一个
QAbstractSocket::NetworkLayerProtocol
类型的参数pro
,根据该参数使用switch
语句来确定协议的类型,并将其转换为字符串名称返回。这有助于在用户界面上显示协议类型的友好名称。
- 接收一个
LookupHostInfoFunc(const QHostInfo &host)
函数:QList<QHostAddress> addresslist = host.addresses();
:通过host.addresses()
获取host
的地址列表。for (int i = 0; i < addresslist.count(); i++) {...}
:遍历地址列表,对每个地址进行操作。QHostAddress host = addresslist.at(i);
:获取当前地址元素。ui->plainTextEdit_DomainIP->appendPlainText("协议类型:" + ProtocolTypeName(host.protocol()));
:使用ProtocolTypeName
函数将地址的协议类型转换为名称并添加到plainTextEdit_DomainIP
文本编辑框。ui->plainTextEdit_DomainIP->appendPlainText("本地 IP 地址:" + host.toString());
:将地址的字符串表示添加到plainTextEdit_DomainIP
文本编辑框。ui->plainTextEdit_DomainIP->appendPlainText("");
:添加一个空行,用于分隔不同地址的信息。
on_pushButton_GetDomainIP_clicked()
槽函数:QString strhostname = ui->lineEdit_InputUrl->text();
:从lineEdit_InputUrl
文本框中获取用户输入的域名。ui->plainTextEdit_DomainIP->appendPlainText("你所查询主机信息:" + strhostname);
:将用户输入的域名添加到plainTextEdit_DomainIP
文本编辑框中。QHostInfo::lookupHost(strhostname, this, SLOT(LookupHostInfoFunc(QHostInfo)));
:调用QHostInfo::lookupHost
函数,根据输入的域名查找主机信息,将结果通过信号槽机制传递给LookupHostInfoFunc
函数进行处理。
on_pushButton_ClearData_clicked()
槽函数:ui->plainTextEdit_DomainIP->clear();
:当点击pushButton_ClearData
按钮时,清空plainTextEdit_DomainIP
文本编辑框中的内容。
int QHostInfo::lookupHost(const QString &name, QObject *receiver, const char *member):
查找与主机名名称关联的IP地址,并返回查找的ID。当查找结果准备就绪时,将使用QHostInfo参数调用接收器中的插槽或信号成员。然后可以检查QHostInfo对象以获取查找结果。
33.2 UDP协议实战
1、UDP(用户数据报协议)是轻量的、不可靠的、面向数据报、无 连接的协议,用于可靠性要求不高的场合。两个应用程序之间进行 UDP 通信不需先建立持久的socket 连接,UDP 每次发送数据报都 需要指定目标地址和端口。
2、UDP 报文没有可靠性保证、顺序保证和流量控制字段等,可靠性 较差。但是正因为UDP 协议的控制选项较少,在数据传输过程中延 迟小、数据传输效率高,适合对可靠性要求不高的应用程序,或者可 以保障可靠性的应用程序,如DNS、TFTP、SNMP 等。
3、UDP 报头由4 个域组成,其中每个域各占用2 个字节,具体包括 源端口号、目标端口号、数据包长度、校验值。端口号有效范围 0--65535,假设端口号大于49151 的端口都代表动态端口。
4、QUdpSocket类从QAbstractSocket类继承,基本跟QTcpSocket 共用大部分的接口函数,主要区别在于QUdpSocket 以数据报传输 数据,不是以连续的数据流,发送方发送数据报使用函数 QUdpSocket::writeDataGram(),数据报长度一般不超过512 个字 节,每个数据报包含发送方和接收方的IP 地址和端口等数据信息。
5、UDP 数据接收使用QUdpSocket::bind()函数绑定端口,用于接 收传入的数据报,当有数据报传入发射readyRead()信号,使用 ReadDatagram()函数来读取接收数据报。UDP 消息传送有单播、 广播和组播三种模式。单播:一个UDP 客户端发出数据报只发送到另一个指定地址和端口 的UDP 客户端(一对一的数据传输)。 广播:一个UDP 客户端发出的数据,在同一个网络范围内其它所有 UDP 客户端都可以收到。组播(多播):UDP 客户端加入到另一个组播IP 地址指定的多播组, 成员向组播地址发送的数据报组内成员都可以接收到。
实现以下实例
界面由ui设计
头文件
#ifndef UDPCOMM_H
#define UDPCOMM_H#include <QMainWindow>#include<QUdpSocket>
#include<QtNetwork>QT_BEGIN_NAMESPACE
namespace Ui { class udpcomm; }
QT_END_NAMESPACEclass udpcomm : public QMainWindow
{Q_OBJECTpublic:udpcomm(QWidget *parent = nullptr);~udpcomm();QUdpSocket *udpsocket;QString GetLocalIpAddress();private slots:void on_pushButton_start_clicked();void on_pushButton_stop_clicked();void on_pushButton_sendmsg_clicked();void on_pushButton_broadcastmsg_clicked();void SocketReadyReadData();private:Ui::udpcomm *ui;
};
#endif // UDPCOMM_H
源文件
#include "udpcomm.h"
#include "ui_udpcomm.h"// udpcomm 类的构造函数,继承自 QMainWindow
udpcomm::udpcomm(QWidget *parent): QMainWindow(parent), ui(new Ui::udpcomm)
{// 调用 UI 生成的 setupUi 函数,初始化 UI 界面ui->setupUi(this);// 获取本地 IP 地址并存储在 strip 中QString strip = GetLocalIpAddress();// 将本地 IP 地址添加到 comboBoxtargetip 下拉框中ui->comboBoxtargetip->addItem(strip);// 创建一个 QUdpSocket 对象,作为 UDP 通信的套接字,并将其作为当前对象的子对象udpsocket = new QUdpSocket(this);// 将 udpsocket 的 readyRead 信号与 SocketReadyReadData 槽函数连接,用于处理接收到的数据connect(udpsocket, SIGNAL(readyRead()), this, SLOT(SocketReadyReadData()));
}// udpcomm 类的析构函数,用于释放资源
udpcomm::~udpcomm()
{// 释放 UI 指针所指向的资源delete ui;
}// 获取本地 IP 地址的函数
QString udpcomm::GetLocalIpAddress()
{// 获取本地主机名QString strHostName = QHostInfo::localHostName();// 根据主机名获取主机信息QHostInfo hostinfo = QHostInfo::fromName(strHostName);// 存储本地 IP 地址的字符串,初始化为空QString strLocalIp = "";// 获取主机的地址列表QList<QHostAddress> addresslist = hostinfo.addresses();// 如果地址列表不为空,则遍历地址列表if (!addresslist.isEmpty()){// 遍历地址列表for (int i = 0; i < addresslist.count(); i++){// 获取列表中的地址元素QHostAddress hostaddr = addresslist.at(i);// 如果地址是 IPv4 协议if (QAbstractSocket::IPv4Protocol == hostaddr.protocol()){// 将该地址存储为本地 IP 地址strLocalIp = hostaddr.toString();// 找到一个 IPv4 地址后,退出循环break;}}}// 返回本地 IP 地址return strLocalIp;
}// 处理接收到的数据的槽函数
void udpcomm::SocketReadyReadData()
{// 当有数据可读时,循环读取所有等待读取的数据报while (udpsocket->hasPendingDatagrams()){// 存储接收到的数据报的字节数组QByteArray datagrams;// 调整字节数组的大小以存储待读取的数据报大小datagrams.resize(udpsocket->pendingDatagramSize());// 存储发送方的地址和端口QHostAddress paddress;quint16 pport;// 读取数据报,并将发送方的地址和端口存储在 paddress 和 pport 中udpsocket->readDatagram(datagrams.data(), datagrams.size(), &paddress, &pport);// 将字节数组转换为 QString 类型QString strs = datagrams.data();// 构建发送方信息的字符串QString peer = "[From:" + paddress.toString() + ":" + QString::number(pport) + "]:";// 将发送方信息和数据报内容添加到 plainTextEditdispmsg 文本编辑框中ui->plainTextEditdispmsg->appendPlainText(peer + strs);}
}// 当点击 pushButton_start 按钮时调用的槽函数
void udpcomm::on_pushButton_start_clicked()
{// 获取 spinBoxbindport 中设置的端口号quint16 port = ui->spinBoxbindport->value();// 尝试将 udpsocket 绑定到该端口if (udpsocket->bind(port)){// 如果绑定成功,将信息添加到 plainTextEditdispmsg 文本编辑框中ui->plainTextEditdispmsg->appendPlainText("**************绑定成功**************");ui->plainTextEditdispmsg->appendPlainText("$$$$$$$$$$$$$$绑定端口$$$$$$$$$$$$$:" + QString::number(udpsocket->localPort()));// 禁用 start 按钮,启用 stop 按钮ui->pushButton_start->setEnabled(false);ui->pushButton_stop->setEnabled(true);}else{// 如果绑定失败,将信息添加到 plainTextEditdispmsg 文本编辑框中ui->plainTextEditdispmsg->appendPlainText("**************绑定失败**************");}
}// 当点击 pushButton_stop 按钮时调用的槽函数
void udpcomm::on_pushButton_stop_clicked()
{// 中止 UDP 套接字的操作udpsocket->abort();// 启用 start 按钮,禁用 stop 按钮ui->pushButton_start->setEnabled(true);ui->pushButton_stop->setEnabled(false);// 将信息添加到 plainTextEditdispmsg 文本编辑框中ui->plainTextEditdispmsg->appendPlainText("**************已停止服务**************");
}// 当点击 pushButton_sendmsg 按钮时调用的槽函数
void udpcomm::on_pushButton_sendmsg_clicked()
{// 获取 comboBoxtargetip 中选择的目标 IP 地址QString targetIpAddress = ui->comboBoxtargetip->currentText();// 将目标 IP 地址转换为 QHostAddress 类型QHostAddress targetaddress(targetIpAddress);// 获取 spinBoxbindport 中设置的端口号quint16 targetport = ui->spinBoxbindport->value();// 获取 lineEditmsg 中输入的消息QString strmsg = ui->lineEditmsg->text();// 将消息转换为 UTF-8 编码的字节数组QByteArray str = strmsg.toUtf8();// 发送数据报给指定的目标 IP 地址和端口udpsocket->writeDatagram(str, targetaddress, targetport);// 将发送的消息添加到 plainTextEditdispmsg 文本编辑框中ui->plainTextEditdispmsg->appendPlainText("[out]:" + str);// 清空 lineEditmsg 输入框ui->lineEditmsg->clear();// 将焦点设置到 lineEditmsg 输入框ui->lineEditmsg->setFocus();
}// 当点击 pushButton_broadcastmsg 按钮时调用的槽函数
void udpcomm::on_pushButton_broadcastmsg_clicked()
{// 获取 spinBoxbindport 中设置的端口号quint16 targetport = ui->spinBoxbindport->value();// 获取 lineEditmsg 中输入的消息QString strmsg = ui->lineEditmsg->text();// 将消息转换为 UTF-8 编码的字节数组QByteArray str = strmsg.toUtf8();// 发送广播数据报udpsocket->writeDatagram(str, QHostAddress::Broadcast, targetport);// 将广播的消息添加到 plainTextEditdispmsg 文本编辑框中ui->plainTextEditdispmsg->appendPlainText("[broadcast]:" + str);// 清空 lineEditmsg 输入框ui->lineEditmsg->clear();// 将焦点设置到 lineEditmsg 输入框ui->lineEditmsg->setFocus();
}
udpcomm
类:- 构造函数:
ui->setupUi(this);
:使用setupUi
函数初始化界面元素。QString strip = GetLocalIpAddress();
和ui->comboBoxtargetip->addItem(strip);
:调用GetLocalIpAddress
函数获取本地 IP 地址并添加到comboBoxtargetip
下拉框中,方便用户选择发送消息的目标地址。udpsocket = new QUdpSocket(this);
:创建QUdpSocket
对象,用于 UDP 通信,将其作为当前对象的子对象,以便自动管理其生命周期。connect(udpsocket, SIGNAL(readyRead()), this, SLOT(SocketReadyReadData()));
:将udpsocket
的readyRead
信号与SocketReadyReadData
槽函数连接,当有数据到达时会触发该槽函数。
- 析构函数:
delete ui;
:释放ui
指针所占用的内存。
GetLocalIpAddress
函数:QString strHostName = QHostInfo::localHostName();
和QHostInfo hostinfo = QHostInfo::fromName(strHostName);
:获取本地主机名并根据主机名获取主机信息。QList<QHostAddress> addresslist = hostinfo.addresses();
:获取主机的地址列表。- 循环和条件判断:遍历地址列表,找到第一个 IPv4 地址并存储在
strLocalIp
中。
SocketReadyReadData
槽函数:while (udpsocket->hasPendingDatagrams()) {...}
:当有数据等待读取时,循环读取。datagrams.resize(udpsocket->pendingDatagramSize());
:调整字节数组大小以存储待读取的数据报。udpsocket->readDatagram(datagrams.data(), datagrams.size(), &paddress, &pport);
:读取数据报并获取发送方的地址和端口。ui->plainTextEditdispmsg->appendPlainText(peer + strs);
:将接收到的数据添加到plainTextEditdispmsg
文本编辑框中。
on_pushButton_start_clicked
槽函数:quint16 port = ui->spinBoxbindport->value();
:获取用户在spinBoxbindport
中设置的端口号。if (udpsocket->bind(port)) {...}
:尝试将udpsocket
绑定到该端口,根据绑定结果更新界面状态和显示信息。
on_pushButton_stop_clicked
槽函数:udpsocket->abort();
:中止 UDP 套接字的操作。- 调整界面上
start
和stop
按钮的状态并显示服务停止信息。
on_pushButton_sendmsg_clicked
槽函数:- 获取目标 IP 地址、端口和消息,将消息转换为字节数组,使用
writeDatagram
发送数据报。 - 显示发送的消息并清空输入框,将焦点设置到输入框。
- 获取目标 IP 地址、端口和消息,将消息转换为字节数组,使用
on_pushButton_broadcastmsg_clicked
槽函数:- 类似
on_pushButton_sendmsg_clicked
,但使用QHostAddress::Broadcast
发送广播消息。
- 类似
- 构造函数:
void QIODevice::readyRead():
每次从设备当前读取通道读取新数据时,都会发出一次此信号。只有在新数据可用时,例如当网络数据的新有效负载到达您的网络套接字时,或者当新的数据块附加到您的设备时,才会再次发出此信号。
readyRead()不是递归发出的;如果您重新进入事件循环或在连接到readyRead()信号的插槽内调用waitForReadyRead(),信号将不会被重新发出(尽管waitForReadyRead()可能仍然返回true)。
实现从QIODevice派生的类的开发人员注意:当新数据到达时,您应该始终发出readyRead()(不要仅仅因为缓冲区中还有数据要读取而发出它)。在其他情况下不要发出readyRead()。
bool QUdpSocket::hasPendingDatagrams() const:
如果至少有一个数据报等待读取,则返回true;否则返回false。
qtcore/qtglobal.html#qint64-typedef" rel="nofollow">qint64 QUdpSocket::pendingDatagramSize() const:
返回第一个挂起的UDP数据报的大小。如果没有可用的数据报,此函数返回-1。
qtcore/qtglobal.html#qint64-typedef" rel="nofollow">qint64 QUdpSocket::readDatagram(char *data, qint64 maxSize, QHostAddress *address = nullptr, quint16 *port = nullptr):
接收不大于maxSize字节的数据报并将其存储在数据中。发送方的主机地址和端口存储在*address和*port中(除非指针为0)。
返回成功时数据报的大小;否则返回-1。
如果maxSize太小,则数据报的其余部分将丢失。为避免数据丢失,请在尝试读取之前,调用peningDatagramSize()来确定待处理数据报的大小。如果maxSize为0,则数据报将被丢弃。
bool QAbstractSocket::bind(const QHostAddress &address, quint16 port = 0, QAbstractSocket::BindMode mode = DefaultForPlatform):
使用BindMode模式绑定到端口端口上的地址。
对于UDP套接字,绑定后,只要UDP数据报到达指定的地址和端口,就会发出信号QUdpSocket::readyRead()。因此,此函数对于编写UDP服务器很有用。
对于TCP套接字,此函数可用于指定将哪个接口用于传出连接,这在多个网络接口的情况下很有用。
默认情况下,套接字使用DefaultForPlatform BindMode绑定。如果未指定端口,则选择随机端口。
成功后,函数返回true,套接字进入BoundState;否则返回false。
void QAbstractSocket::abort():
中止当前连接并重置套接字。与disconnect tFromHost()不同,此函数会立即关闭套接字,丢弃写入缓冲区中的任何待处理数据。
qtcore/qtglobal.html#qint64-typedef" rel="nofollow">qint64 QUdpSocket::writeDatagram(const char *data, qint64 size, const QHostAddress &address, quint16 port):
将数据报大小的数据发送到端口端口的主机地址地址。返回成功时发送的字节数;否则返回-1。
数据报总是写成一个块,数据报的最大大小高度依赖于平台,但可以低至8192字节,如果数据报太大,此函数将返回-1,error()将返回DatagramTooLargeError。
通常不建议发送大于512字节的数据报,因为即使它们发送成功,它们也可能在到达最终目的地之前被IP层分片。
警告:在连接的UDP套接字上调用此函数可能会导致错误并且没有发送数据包。如果您使用连接的套接字,请使用write()发送数据报。
33.3 TCP协议实战
1、传输控制协议(TCP,Transmission Control Protocol)是一种 面向连接的、可靠的、基于字节流的传输层通信协议。
2、TCP 拥塞控制算法(也称AIMD 算法)。该算法主要包括四个主 要部分:慢启动、拥塞避免、快速重传和快速恢复。
3、TCP 通信必须建立TCP 连接(客户端和服务器端),Qt 提供 QTcpSocket 类和QTcpServer 类专门用于建立TCP 通信程序。服务 器端用QTcpServer 监听端口及建立服务器;QTcpSocket 用于建立 连接后使用套接字(socket)进行通信。
4、QTcpServer 是从QOjbect 继承的类用于服务器建立网络监听, 创建网络socket 连接。
接下来是实战,这次实例和其他实例有所不同,这次实例要实现两个,一个服务端和一个客户端
首先是客户端
头文件
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>#include<QTcpSocket>
#include<QHostAddress>
#include<QHostInfo>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private:Ui::MainWindow *ui;QTcpSocket *tcpclient;QString getlocalip();protected:void closeEvent(QCloseEvent *event);private slots:void connectfunc();void disconnectfunc();void socketreaddata();void on_pushButton_Connect_clicked();void on_pushButton_Disconnect_clicked();void on_pushButton_Send_clicked();
};
#endif // MAINWINDOW_H
源文件
#include "mainwindow.h"
#include "ui_mainwindow.h"// 构造函数
MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{// 初始化界面ui->setupUi(this);// 创建一个 TCP 客户端对象tcpclient = new QTcpSocket(this);// 获取本地 IP 地址QString strip = getlocalip();// 将获取到的本地 IP 地址添加到组合框中ui->comboBoxIp->addItem(strip);// 当客户端连接成功时,调用 connectfunc 槽函数connect(tcpclient, SIGNAL(connected()), this, SLOT(connectfunc()));// 当客户端断开连接时,调用 disconnectfunc 槽函数connect(tcpclient, SIGNAL(disconnected()), this, SLOT(disconnectfunc()));// 当有数据可读时,调用 socketreaddata 槽函数connect(tcpclient, SIGNAL(readyRead()), this, SLOT(socketreaddata()));}// 析构函数
MainWindow::~MainWindow()
{delete ui;
}// 获取本地 IP 地址的函数
QString MainWindow::getlocalip()
{// 获取本地主机名QString hostname = QHostInfo::localHostName();// 根据主机名获取主机信息QHostInfo hostinfo = QHostInfo::fromName(hostname);// 存储本地 IP 地址的变量,初始化为空QString localip = "";// 获取主机的地址列表QList<QHostAddress> addlist = hostinfo.addresses();if (!addlist.isEmpty()){for (int i = 0; i < addlist.count(); i++){// 获取列表中的一个地址QHostAddress ahost = addlist.at(i);// 判断是否为 IPv4 地址if (QAbstractSocket::IPv4Protocol == ahost.protocol()){// 将 IPv4 地址转换为字符串存储localip = ahost.toString();break;}}}return localip;}// 窗口关闭事件处理函数
void MainWindow::closeEvent(QCloseEvent *event)
{// 如果客户端处于连接状态if (tcpclient->state() == QAbstractSocket::ConnectedState){// 断开与服务器的连接tcpclient->disconnectFromHost();}// 接受关闭事件event->accept();
}// 当连接成功时调用的槽函数
void MainWindow::connectfunc()
{// 在文本编辑框中显示连接成功的信息ui->plainTextEdit_DispMsg->appendPlainText("**********已经连接到服务器端**********");ui->plainTextEdit_DispMsg->appendPlainText("**********peer address:" + tcpclient->peerAddress().toString());ui->plainTextEdit_DispMsg->appendPlainText("**********peer port:" + QString::number(tcpclient->peerPort()));// 禁用连接按钮,启用断开连接按钮ui->pushButton_Connect->setEnabled(false);ui->pushButton_Disconnect->setEnabled(true);
}// 当断开连接时调用的槽函数
void MainWindow::disconnectfunc()
{// 在文本编辑框中显示断开连接的信息ui->plainTextEdit_DispMsg->appendPlainText("**********已断开与服务器端的连接**********");// 启用连接按钮,禁用断开连接按钮ui->pushButton_Connect->setEnabled(true);ui->pushButton_Disconnect->setEnabled(false);
}// 当有数据可读时调用的槽函数
void MainWindow::socketreaddata()
{// 循环读取数据,直到没有可读行while (tcpclient->canReadLine())// 读取一行数据并添加到文本编辑框中ui->plainTextEdit_DispMsg->appendPlainText("[in]:" + tcpclient->readLine());
}// 点击连接按钮时调用的槽函数
void MainWindow::on_pushButton_Connect_clicked()
{// 获取组合框中的 IP 地址QString addr = ui->comboBoxIp->currentText();// 获取端口号quint16 port = ui->spinBoxPort->value();// 连接到服务器tcpclient->connectToHost(addr, port);
}// 点击断开连接按钮时调用的槽函数
void MainWindow::on_pushButton_Disconnect_clicked()
{// 如果客户端处于连接状态if (tcpclient->state() == QAbstractSocket::ConnectedState)// 断开与服务器的连接tcpclient->disconnectFromHost();
}// 点击发送按钮时调用的槽函数
void MainWindow::on_pushButton_Send_clicked()
{// 获取输入框中的消息QString strmsg = ui->lineEdit_InputMsg->text();// 在文本编辑框中显示发送的消息ui->plainTextEdit_DispMsg->appendPlainText("[out]:" + strmsg);// 清空输入框ui->lineEdit_InputMsg->clear();// 将消息转换为 UTF-8 编码的字节数组QByteArray str = strmsg.toUtf8();// 追加一个换行符str.append('\n');// 发送消息tcpclient->write(str);
}
-
- 构造函数中,初始化了用户界面,创建了
QTcpSocket
对象tcpclient
,调用getlocalip
函数获取本地 IP 地址并添加到comboBoxIp
组合框中,同时将tcpclient
的信号与相应的槽函数进行连接。 getlocalip
函数用于获取本地主机的 IPv4 地址。首先获取主机名,然后根据主机名获取主机信息,从主机信息的地址列表中筛选出 IPv4 协议的地址并转换为字符串。closeEvent
函数处理窗口关闭事件,在关闭窗口时如果客户端处于连接状态则先断开连接。connectfunc
函数在客户端成功连接服务器时被调用,在文本编辑框中显示连接信息,并修改连接和断开连接按钮的状态。disconnectfunc
函数在客户端断开连接时被调用,在文本编辑框中显示断开连接信息,并修改连接和断开连接按钮的状态。socketreaddata
函数在有数据可读时被调用,会不断读取一行行的数据并添加到文本编辑框中。on_pushButton_Connect_clicked
函数在点击连接按钮时被调用,从组合框和端口号输入框获取信息,使用tcpclient
连接到相应的服务器。on_pushButton_Disconnect_clicked
函数在点击断开连接按钮时被调用,如果客户端处于连接状态则断开连接。on_pushButton_Send_clicked
函数在点击发送按钮时被调用,将输入框中的消息转换为 UTF-8 编码的字节数组并添加换行符,然后发送出去,同时在文本编辑框中显示发送的消息并清空输入框。
- 构造函数中,初始化了用户界面,创建了
QAbstractSocket::SocketState QAbstractSocket::state() const:
返回套接字的状态。
void QAbstractSocket::disconnectFromHost():
尝试关闭套接字。如果有待写入的待处理数据,QAbstractSocket将进入ClosingState并等待所有数据写入。最终,它将进入Unconnect tedState并发出disconnect()信号。
void QEvent::accept():
设置事件对象的接受标志,相当于调用setAccted(true)。
设置接受参数表示事件接收者想要该事件。不需要的事件可能会传播到父小部件。
bool QAbstractSocket::canReadLine() const:
从QIODevice::canReadLine()重新实现。
如果可以从套接字读取一行数据,则返回true;否则返回false。
void QAbstractSocket::connectToHost(const QString &hostName, quint16 port, QIODevice::OpenMode openMode = ReadWrite, QAbstractSocket::NetworkLayerProtocol protocol = AnyIPProtocol):
尝试在给定端口上连接到hostName。协议参数可用于指定要使用的网络协议(例如IPv4或IPv6)。
套接字在给定的openMode中打开,首先进入HostLookupState,然后执行hostName的主机名查找。如果查找成功,则发出hostFind(),QAbstractSocket进入Connecting State。然后它尝试连接到查找返回的一个或多个地址。最后,如果建立了连接,QAbstractSocket进入ConnectedState并发出connect()。
在任何时候,套接字都可以发出error()来表示发生了错误。
主机名可以是字符串形式的IP地址(例如,43.195.83.32),也可以是主机名(例如,example.com)。QAbstractSocket仅在需要时才会进行查找。端口按本机字节顺序排列。
实现服务端
头文件
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include <QMainWindow>
#include<QTcpServer>
#include<QtNetwork>QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACEclass MainWindow : public QMainWindow
{Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private:Ui::MainWindow *ui;QTcpServer *tcpserver;QTcpSocket *tcpsocket;QString GetLocalIpAddress();private slots:void clientconnect();void clientdisconnect();void socketreaddata();void newconnection();void on_pushButton_start_clicked();void on_pushButton_stop_clicked();void on_pushButton_send_clicked();protected:void closeEvent(QCloseEvent *event);
};
#endif // MAINWINDOW_H
源文件
#include "mainwindow.h"
#include "ui_mainwindow.h"// 构造函数,用于初始化主窗口
MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{// 调用 setupUi 函数,设置用户界面ui->setupUi(this);// 获取本地 IP 地址QString strip = GetLocalIpAddress();// 将获取到的本地 IP 地址添加到组合框中ui->comboBox_ip->addItem(strip);// 创建一个 TCP 服务器对象,并将其作为 MainWindow 的子对象tcpserver = new QTcpServer(this);// 当有新的连接请求时,调用 newconnection 槽函数connect(tcpserver, SIGNAL(newConnection()), this, SLOT(newconnection()));
}// 析构函数,用于清理资源
MainWindow::~MainWindow()
{delete ui;
}// 获取本地 IP 地址的函数
QString MainWindow::GetLocalIpAddress()
{// 获取本地主机的名称QString hostname = QHostInfo::localHostName();// 根据主机名称获取主机信息QHostInfo hostinfo = QHostInfo::fromName(hostname);// 存储本地 IP 地址的变量,初始化为空QString localip = "";// 获取主机的地址列表QList<QHostAddress> addresslist = hostinfo.addresses();// 遍历地址列表if (!addresslist.isEmpty()){for (int i = 0; i < addresslist.count(); i++){// 获取地址列表中的一个地址QHostAddress addrhost = addresslist.at(i);// 判断该地址是否为 IPv4 协议if (QAbstractSocket::IPv4Protocol == addrhost.protocol()){// 将 IPv4 地址转换为字符串存储localip = addrhost.toString();break;}}}return localip;
}// 当客户端连接成功时调用的槽函数
void MainWindow::clientconnect()
{// 在文本编辑框中添加客户端连接的信息ui->plainTextEdit_dispmsg->appendPlainText("*************客户端 socket 连接*************");ui->plainTextEdit_dispmsg->appendPlainText("*************peer address:" + tcpsocket->peerAddress().toString());ui->plainTextEdit_dispmsg->appendPlainText("*************peer port:" + QString::number(tcpsocket->peerPort()));
}// 当客户端断开连接时调用的槽函数
void MainWindow::clientdisconnect()
{// 在文本编辑框中添加客户端断开连接的信息ui->plainTextEdit_dispmsg->appendPlainText("*************客户端 socket 断开连接*************");// 延迟删除 tcpsocket 对象,以确保安全删除tcpsocket->deleteLater();
}// 当有数据可读时调用的槽函数
void MainWindow::socketreaddata()
{// 循环读取数据,只要有可读的行while (tcpsocket->canReadLine())// 将读取的数据添加到文本编辑框中ui->plainTextEdit_dispmsg->appendPlainText("[in]" + tcpsocket->readLine());
}// 当有新的连接请求时调用的槽函数
void MainWindow::newconnection()
{// 获取新的连接套接字tcpsocket = tcpserver->nextPendingConnection();// 当客户端连接成功时,调用 clientconnect 槽函数connect(tcpsocket, SIGNAL(connected()), this, SLOT(clientconnect()));clientconnect();// 当客户端断开连接时,调用 clientdisconnect 槽函数connect(tcpsocket, SIGNAL(disconnected()), this, SLOT(clientdisconnect()));// 当有数据可读时,调用 socketreaddata 槽函数connect(tcpsocket, SIGNAL(readyRead()), this, SLOT(socketreaddata()));// 当套接字状态发生改变时,调用 OnSocketStateChanged 槽函数connect(tcpsocket, SIGNAL(stateChanged(QAbstractSocket::SocketState)), this, SLOT(OnSocketStateChanged(QAbstractSocket::SocketState)));
}// 处理窗口关闭事件
void MainWindow::closeEvent(QCloseEvent *event)
{// 如果服务器正在监听,则关闭服务器if (tcpserver->isListening())tcpserver->close();// 接受关闭事件event->accept();
}// 当点击开始按钮时调用的槽函数
void MainWindow::on_pushButton_start_clicked()
{// 获取组合框中的 IP 地址QString ip = ui->comboBox_ip->currentText();// 获取端口号quint16 port = ui->spinBoxPort->value();// 将 IP 地址转换为 QHostAddress 类型QHostAddress address(ip);// 开始监听指定的 IP 地址和端口tcpserver->listen(address, port);// 在文本编辑框中添加开始监听的信息ui->plainTextEdit_dispmsg->appendPlainText("$$$$$$$$$$$$$$$$$$$开始监听$$$$$$$$$$$$$$$$$$$");ui->plainTextEdit_dispmsg->appendPlainText("$$$$$$$$$$$$$$$$$$$服务器地址$$$$$$$$$$$$$$$$$$$:" + tcpserver->serverAddress().toString());ui->plainTextEdit_dispmsg->appendPlainText("$$$$$$$$$$$$$$$$$$$服务器端口$$$$$$$$$$$$$$$$$$$:" + QString::number(tcpserver->serverPort()));// 禁用开始按钮,启用停止按钮ui->pushButton_start->setEnabled(false);ui->pushButton_stop->setEnabled(true);
}// 当点击停止按钮时调用的槽函数
void MainWindow::on_pushButton_stop_clicked()
{// 如果服务器正在监听if (tcpserver->isListening()){// 关闭服务器tcpserver->close();// 启用开始按钮,禁用停止按钮ui->pushButton_start->setEnabled(true);ui->pushButton_stop->setEnabled(false);}
}// 当点击发送按钮时调用的槽函数
void MainWindow::on_pushButton_send_clicked()
{// 获取输入框中的消息QString strmsg = ui->lineEdit_inputmsg->text();// 在文本编辑框中添加发送的消息ui->plainTextEdit_dispmsg->appendPlainText("[out]:" + strmsg);// 清空输入框ui->lineEdit_inputmsg->clear();// 将消息转换为 UTF-8 编码的字节数组QByteArray str = strmsg.toUtf8();// 在消息末尾添加换行符str.append("\n");// 发送消息tcpsocket->write(str);
}
-
- 构造函数中:
- 调用
ui->setupUi(this)
初始化用户界面。 - 调用
GetLocalIpAddress()
函数获取本地 IP 地址并添加到comboBox_ip
中。 - 创建
QTcpServer
对象tcpserver
,并将其信号newConnection
与newconnection
槽函数连接,以便处理新的连接请求。
- 调用
GetLocalIpAddress()
函数:- 获取本地主机名。
- 根据主机名获取主机信息。
- 遍历地址列表,找到 IPv4 地址并存储在
localip
中。
clientconnect()
槽函数:- 当客户端连接成功时,在
plainTextEdit_dispmsg
中显示连接信息。
- 当客户端连接成功时,在
clientdisconnect()
槽函数:- 当客户端断开连接时,在
plainTextEdit_dispmsg
中显示断开信息,并使用deleteLater()
安全删除tcpsocket
对象。
- 当客户端断开连接时,在
socketreaddata()
槽函数:- 当有数据可读时,循环读取数据并添加到
plainTextEdit_dispmsg
中。
- 当有数据可读时,循环读取数据并添加到
newconnection()
槽函数:- 当有新连接请求时,通过
tcpserver->nextPendingConnection()
获取新的连接套接字tcpsocket
。 - 将
tcpsocket
的多个信号与相应的槽函数连接,包括connected
、disconnected
、readyRead
和stateChanged
。
- 当有新连接请求时,通过
closeEvent()
函数:- 处理窗口关闭事件,若服务器正在监听则关闭服务器。
on_pushButton_start_clicked()
槽函数:- 从
comboBox_ip
和spinBoxPort
获取 IP 地址和端口号。 - 调用
tcpserver->listen()
开始监听,显示监听信息并更新按钮状态。
- 从
on_pushButton_stop_clicked()
槽函数:- 若服务器正在监听则关闭服务器并更新按钮状态。
on_pushButton_send_clicked()
槽函数:- 获取输入框中的消息,显示发送消息,清空输入框,将消息转换为 UTF-8 编码并添加换行符后发送。
- 构造函数中:
void QTcpServer::newConnection():
每次有新连接可用时都会发出此信号。
void QObject::deleteLater():
安排删除此对象。
当控制返回到事件循环时,对象将被删除。如果调用此函数时事件循环没有运行(例如,在QCoreApplication::exec()之前的对象上调用deleteAfter()),则一旦事件循环启动,该对象将被删除。如果在主事件循环停止后调用deleteAfter(),则该对象将不会被删除。从Qt 4.8开始,如果在驻留在没有运行事件循环的线程中的对象上调用deleteAfter(),则该对象将在线程完成时被销毁。
请注意,进入和离开新的事件循环(例如,通过打开模态对话框)不会执行延迟删除;对于要删除的对象,控件必须返回到调用deleteAfter()的事件循环。这不适用于在前一个嵌套事件循环仍在运行时删除的对象:Qt事件循环将在新的嵌套事件循环开始时立即删除这些对象。
注意:多次调用此函数是安全的;当传递第一个延迟删除事件时,对象的任何挂起事件都将从事件队列中删除。
QTcpSocket *QTcpServer::nextPendingConnection():
将下一个挂起的连接作为连接的QTcpSocket对象返回。
套接字是作为服务器的子服务器创建的,这意味着当QTcpServer对象被销毁时,它会自动删除。完成后显式删除对象仍然是一个好主意,以避免浪费内存。
如果在没有挂起连接时调用此函数,则返回0。
注意:返回的QTcpSocket对象不能从另一个线程使用。如果要使用来自另一个线程的传入连接,则需要覆盖incomingConnection()。
bool QTcpServer::isListening() const:
如果服务器当前正在侦听传入连接,则返回true;否则返回false。
两者进行通信
这两个看似很复杂,实际逻辑很简单,建议自己敲一遍,不懂的,看注释,捋一捋。
33.4 线程编程(互斥量 信号量)
一、线程
Qt 提供QThread 类进行多任务处理,QtThread 继承自 QOjbect 类,并且提供QMutex 类来实现同步。 互斥量(又称互斥锁),是一个可以处于两态之一的变量:解锁和加锁。
测试程序如下:
创建一个Class继承于QThread,文件名为workerthread
该文件的头文件为
#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H#include<QThread>
#include<QtDebug>class WorkerThread : public QThread
{
public:WorkerThread();protected:void run();
};#endif // WORKERTHREAD_H
该文件的源文件为
#include "workerthread.h"WorkerThread::WorkerThread()
{}void WorkerThread::run()
{while(true){for(int i=1;i<=5;i++){qDebug()<<i<<i<<i<<i<<i;}}}
工程头文件为
#ifndef DIALOG_H
#define DIALOG_H#include <QDialog>
#include"workerthread.h"
#include<QPushButton>
#include<QHBoxLayout>QT_BEGIN_NAMESPACE
namespace Ui { class Dialog; }
QT_END_NAMESPACEclass Dialog : public QDialog
{Q_OBJECTpublic:Dialog(QWidget *parent = nullptr);~Dialog();private:Ui::Dialog *ui;QPushButton *startbutton;QPushButton *stopbutton;QPushButton *exitbutton;WorkerThread *workerthread[5];public slots:void onSlotStart();void onSlotStop();
};
#endif // DIALOG_H
工程源文件为
// 引入自定义对话框类的头文件
#include "dialog.h"
// 引入由Qt Designer生成的用户界面类的头文件
#include "ui_dialog.h"// 定义Dialog类的构造函数,参数parent是指向父窗口部件的指针,默认值为nullptr
Dialog::Dialog(QWidget *parent): QDialog(parent) // 调用基类QDialog的构造函数,传递父窗口部件指针, ui(new Ui::Dialog) // 创建一个Ui::Dialog类的对象,并将其指针赋值给成员变量ui
{ui->setupUi(this); // 调用Ui::Dialog类的setupUi方法,将用户界面设置到当前对话框上// 创建一个“开始”按钮对象,并将其指针赋值给成员变量startbuttonstartbutton = new QPushButton("开始");// 创建一个“停止”按钮对象,并将其指针赋值给成员变量stopbuttonstopbutton = new QPushButton("停止");// 创建一个“退出”按钮对象,并将其指针赋值给成员变量exitbuttonexitbutton = new QPushButton("退出");// 创建一个水平布局对象hlayout,并将当前对话框作为其父窗口部件QHBoxLayout *hlayout = new QHBoxLayout(this);// 将“开始”按钮添加到水平布局中hlayout->addWidget(startbutton);// 将“停止”按钮添加到水平布局中hlayout->addWidget(stopbutton);// 将“退出”按钮添加到水平布局中hlayout->addWidget(exitbutton);// 连接“开始”按钮的clicked信号到当前对话框的onSlotStart槽函数// 当“开始”按钮被点击时,会触发onSlotStart函数connect(startbutton, SIGNAL(clicked()), this, SLOT(onSlotStart()));// 连接“停止”按钮的clicked信号到当前对话框的onSlotStop槽函数// 当“停止”按钮被点击时,会触发onSlotStop函数connect(stopbutton, SIGNAL(clicked()), this, SLOT(onSlotStop()));// 连接“退出”按钮的clicked信号到当前对话框的close槽函数// 当“退出”按钮被点击时,会关闭当前对话框connect(exitbutton, SIGNAL(clicked()), this, SLOT(close()));
}// 定义Dialog类的析构函数
Dialog::~Dialog()
{// 释放ui指针所指向的对象的内存delete ui;
}// 定义onSlotStart槽函数,当“开始”按钮被点击时会调用此函数
void Dialog::onSlotStart()
{// 循环5次,创建5个WorkerThread对象for (int i = 0; i < 5; i++){// 创建一个WorkerThread对象,并将其指针赋值给workerthread数组的第i个元素workerthread[i] = new WorkerThread();}// 循环5次,启动之前创建的5个WorkerThread对象for (int i = 0; i < 5; i++){// 调用WorkerThread对象的start方法,启动线程,当调用 start() 方法时,它会在一个新的线程中调用 run() 方法。workerthread[i]->start();}// 禁用“开始”按钮,防止重复点击startbutton->setEnabled(false);// 启用“停止”按钮,允许用户停止线程stopbutton->setEnabled(true);
}// 定义onSlotStop槽函数,当“停止”按钮被点击时会调用此函数
void Dialog::onSlotStop()
{// 循环5次,停止之前创建的5个WorkerThread对象for (int i = 0; i < 5; i++){// 调用WorkerThread对象的terminate方法,终止线程,会请求操作系统立即终止 workerthread[i] 对应的线程。workerthread[i]->terminate();// 调用WorkerThread对象的wait方法,等待线程终止workerthread[i]->wait();//在调用 terminate() 方法终止线程后,调用 wait() 方法可以确保在继续执行后续代码之前,被终止的线程已经完全停止。}// 启用“开始”按钮,允许用户再次启动线程startbutton->setEnabled(true);// 禁用“停止”按钮,防止重复点击stopbutton->setEnabled(false);
}
void QThread::start(QThread::Priority priority = InheritPriority)
通过调用run()开始执行线程。操作系统将根据优先级参数调度线程。如果线程已经在运行,则此函数不执行任何操作。
优先级参数的效果取决于操作系统的调度策略,特别是在不支持线程优先级的系统上(如Linux,详见sched_setscheduler留档),优先级将被忽略。
void QThread::terminate()
终止线程的执行。根据操作系统的调度策略,线程可能会立即终止,也可能不会立即终止。可以肯定的是,在终止()之后使用QThread::wait()。
当线程终止时,所有等待线程完成的线程都将被唤醒。
警告:此函数很危险,不鼓励使用。线程可以在其代码路径中的任何点终止。线程可以在修改数据时终止。线程没有机会在自己之后清理,解锁任何持有的互斥锁等。简而言之,仅在绝对必要时使用此函数。
可以通过调用QThread::setTerminationEnabled()显式启用或禁用终止。禁用终止时调用此函数会导致终止延迟,直到重新启用终止。有关详细信息,请参阅QThread::setTerminationEnabled()的留档。
bool QThread::wait(unsigned long time = ULONG_MAX)
阻塞线程,直到满足以下任一条件:
与此QThread对象关联的线程已完成执行(即当它从run()返回时)。如果线程已完成,此函数将返回true。如果线程尚未启动,它也会返回true。
时间毫秒已经过去。如果时间ULONG_MAX(默认值),那么等待永远不会超时(线程必须从run()返回)。如果等待超时,此函数将返回false。
这提供了与POSIXpthread_join()函数类似的功能。
二、信号量
生产者/消费者实例中对同步的需求有两处:
如果生产者过快地生产数据,将会覆盖消费者还没有读取的数据。
如果消费者过快地读取数据,将越过生产者并且读取到一些过期 数据。
测试程序:
main.cpp
#include <QCoreApplication> // 引入Qt核心应用程序类的头文件,用于创建Qt控制台应用程序
#include <QThread> // 引入Qt线程类的头文件,用于实现多线程编程
#include <QSemaphore> // 引入Qt信号量类的头文件,信号量用于线程间的同步
#include <QTime> // 引入Qt时间类的头文件,用于获取当前时间
#include <iostream> // 引入标准输入输出流库,用于输出信息到控制台// 定义数据的大小,即生产者需要生产的数据数量
const int datasize = 100;
// 定义缓冲区的大小,即缓冲区最多能容纳的数据数量
const int buffersize = 1;// 创建一个信号量freespace,初始值为buffersize,表示缓冲区初始有buffersize个空闲空间
QSemaphore freespace(buffersize);
// 创建一个信号量usedspace,初始值为0,表示缓冲区初始没有已使用的空间
QSemaphore usedspace(0);// 定义生产者线程类,继承自QThread
class producer : public QThread
{
protected:// 重写QThread的run()方法,线程启动后会自动执行该方法void run(){// 以当前时间为种子初始化随机数生成器,不过这里qsrand(NULL)会覆盖前面的设置,建议去掉qsrand(NULL)qsrand(QTime(0, 0, 0).secsTo(QTime::currentTime()));qsrand(NULL);// 生产者循环生产datasize个数据for (int i = 0; i < datasize; i++){// 生产者尝试获取一个空闲空间,如果没有空闲空间,线程会阻塞等待// 这一步确保了生产者不会在缓冲区满时继续生产数据,实现了线程同步freespace.acquire();// 输出当前生产的数据编号,并提示是生产者操作std::cerr << i << ":producer-->";// 生产者生产完数据后,释放一个已使用空间的信号量// 表示缓冲区中有一个新的数据可以被消费者消费usedspace.release();}}
};// 定义消费者线程类,继承自QThread
class consumers : public QThread
{
protected:// 重写QThread的run()方法,线程启动后会自动执行该方法void run(){// 消费者循环消费datasize个数据for (int i = 0; i < datasize; i++){// 消费者尝试获取一个已使用空间,如果没有已使用空间,线程会阻塞等待// 这一步确保了消费者不会在缓冲区为空时继续消费数据,实现了线程同步usedspace.acquire();// 输出当前消费的数据编号,并提示是消费者操作std::cerr << i << ":consumer\n";// 消费者消费完数据后,释放一个空闲空间的信号量// 表示缓冲区中有一个空间可以被生产者再次使用freespace.release();}}
};int main(int argc, char *argv[])
{// 创建一个Qt核心应用程序对象QCoreApplication a(argc, argv);// 创建生产者线程对象producer p;// 创建消费者线程对象consumers c;// 启动生产者线程,开始执行生产者的run()方法p.start();// 启动消费者线程,开始执行消费者的run()方法c.start();// 主线程等待生产者线程执行完毕p.wait();// 主线程等待消费者线程执行完毕c.wait();// 进入Qt应用程序的事件循环,等待事件发生return a.exec();
}//代码整体解释
//这段代码实现了一个经典的生产者 - 消费者模型,使用了 QSemaphore 来实现线程间的同步。
//生产者线程负责生产数据,消费者线程负责消费数据,
//通过信号量 freespace 和 usedspace 来控制缓冲区的使用,
//确保生产者不会在缓冲区满时继续生产,
//消费者不会在缓冲区空时继续消费,从而避免了数据竞争和不一致的问题。
//信号量的作用
//freespace 信号量:表示缓冲区中的空闲空间数量。
//生产者在生产数据前需要先获取一个空闲空间,生产完成后释放一个已使用空间。
//usedspace 信号量:表示缓冲区中的已使用空间数量。
//消费者在消费数据前需要先获取一个已使用空间,消费完成后释放一个空闲空间。
//线程同步的原理
//通过 acquire() 和 release() 方法,信号量可以控制线程的执行顺序。
//当一个线程调用 acquire() 方法时,如果信号量的值大于 0,则信号量的值减 1,线程继续执行;
//如果信号量的值为 0,则线程会阻塞等待,直到有其他线程调用 release() 方法增加信号量的值。
//这样就实现了线程间的同步。
void qsrand(qtglobal.html#uint-typedef" rel="nofollow">uint seed)
标准C++srand()函数的线程安全版本。
设置用于生成由qrand()返回的伪随机整数的新随机数序列的参数种子。
每个线程生成的随机数序列是确定性的。例如,如果两个线程调用qsrand(1)并随后调用qrand(),则线程将获得相同的随机数序列。
注意:此功能已弃用。在新应用程序中,请改用QR随机生成器。
int QTime::secsTo(const qtime.html#QTime" rel="nofollow">QTime &t) const
返回从此时间到t的秒数。如果t早于此时间,则返回的秒数为负数。
因为QTime测量一天内的时间,一天有86400秒,所以结果总是在-86400和86400之间。
secsTo()不考虑任何毫秒。
如果任一时间无效,则返回0。
void QSemaphore::acquire(int n = 1)
尝试获取由信号量保护的n个资源。如果n>可用(),则此调用将阻塞,直到有足够的资源可用。
void QSemaphore::release(int n = 1)
释放由信号量保护的n个资源。
三、互斥量
使用目的:保证共享数据操作的完整性,每个对象对应一个互斥锁标 记,此标记用来保证任一时刻只能有一个线程访问对象。
互斥量两态:加锁和解锁。
测试程序:售票窗口模拟程序运行结果
main.cpp
#include <QCoreApplication>
// 引入标准输入输出流库,用于在控制台输出信息
#include <iostream>
// 引入QMutex类,用于线程同步,防止多个线程同时访问共享资源
#include <QMutex>
// 引入QThread类,用于创建和管理线程
#include <QThread>
// 引入QObject类,是Qt中所有对象的基类,支持信号和槽机制
#include <QObject>// 定义售票员类,继承自QObject,以便使用信号和槽机制
class ticketsllers : public QObject
{public:// 构造函数声明ticketsllers();// 析构函数声明~ticketsllers();public slots:// 定义一个槽函数,当接收到特定信号时会被调用,用于执行售票操作void salefunc();public:// 指向票数量的指针,用于访问和修改剩余票数int *tickets;// 指向互斥锁的指针,用于线程同步,确保同一时间只有一个线程可以修改票数QMutex *metx;// 售票员的名称,用于在输出信息中区分不同的售票员std::string sellersname;
};// 售票员类的构造函数实现
ticketsllers::ticketsllers()
{// 初始化互斥锁指针为NULLmetx = NULL;// 初始化票数量指针为0tickets = 0;
}// 售票员类的析构函数实现,这里暂时为空,因为没有需要手动释放的资源
ticketsllers::~ticketsllers()
{}// 售票员类的售票槽函数实现
void ticketsllers::salefunc()
{// 当剩余票数大于0时,继续售票while ((*tickets) > 0){// 加锁操作,确保同一时间只有一个线程可以进入临界区(修改票数的代码段)metx->lock();// 输出售票员名称和当前售出的票数,并将剩余票数减1std::cout << sellersname << " : " << (*tickets)-- << std::endl;// 解锁操作,允许其他线程进入临界区metx->unlock();}
}int main(int argc, char *argv[])
{// 创建一个Qt核心应用程序对象,用于管理应用程序的生命周期和事件循环QCoreApplication a(argc, argv);// 初始化总票数为20int ticket = 20;// 创建一个互斥锁对象,用于线程同步QMutex mtx;// 创建一个线程对象,用于执行售票操作QThread th1;// 创建一个售票员对象ticketsllers seller1;// 将售票员对象的票数量指针指向总票数的地址,以便可以修改总票数seller1.tickets = &ticket;// 将售票员对象的互斥锁指针指向创建的互斥锁对象,用于线程同步seller1.metx = &mtx;// 设置售票员的名称seller1.sellersname = "seller kitty";// 将售票员对象移动到指定的线程中执行,使售票操作在该线程中进行seller1.moveToThread(&th1);// 连接线程的started信号到售票员对象的salefunc槽函数// 当线程启动时,会自动触发售票员的售票操作QObject::connect(&th1, &QThread::started, &seller1, &ticketsllers::salefunc);// 启动线程,开始执行售票操作th1.start();// 进入Qt应用程序的事件循环,等待事件发生,直到应用程序退出return a.exec();
}//线程同步:
//使用 QMutex 是为了避免多个线程同时修改共享资源(票的数量)而导致的数据不一致问题。
//通过 lock() 和 unlock() 方法确保同一时间只有一个线程可以修改票数。
//信号和槽机制:
//使用 QObject::connect() 函数将线程的 started 信号连接到售票员对象的 salefunc 槽函数,
//这样当线程启动时,会自动触发售票操作,实现了事件驱动的编程方式。
//多线程编程:
//使用 QThread 类创建线程,将售票操作放在独立的线程中执行,提高了程序的并发性能。
//继承 QObject:
//售票员类继承自 QObject ,是为了支持信号和槽机制,方便线程间的通信和事件处理。
//需要注意的是,当前代码只创建了一个线程和一个售票员,
//如果需要多个售票员同时售票,可以创建多个 ticketsllers 对象并分别启动对应的线程。
void QMutex::lock()
锁定互斥锁。如果另一个线程锁定了互斥锁,则此调用将阻塞,直到该线程解锁它。
如果此互斥锁为递归互斥锁,则允许在同一线程的同一互斥锁上多次调用此函数。如果此互斥锁为非递归互斥锁,则此函数将在递归锁定互斥锁时死锁。
void QMutex::unlock()
解锁互斥锁。尝试在与锁定它的线程不同的线程中解锁互斥锁会导致错误。解锁未锁定的互斥锁会导致未定义的行为。
void QObject::moveToThread(qthread.html" rel="nofollow">QThread *targetThread)
更改此对象及其子对象的线程亲和性。如果对象有父对象,则无法移动该对象。如果 target etThread 为 nullptr,则此对象及其子对象的所有事件处理都将停止,因为它们不再与任何线程关联。请注意,对象的所有活动计时器都将被重置。计时器首先在当前线程中停止,然后在目标线程中重新启动(间隔相同)。因此,在线程之间不断移动对象会无限期地推迟计时器事件。
QEvent::ThreadChange 事件将在线程关联性更改之前发送到此对象。您可以处理此事件以执行任何特殊处理。请注意,发布到此对象的任何新事件都将在 target etThread 中处理,前提是它是非 null:当它为 nullptr 时,不会发生此对象或其子对象的事件处理,因为它们不再与任何线程关联。
警告:此函数不是线程安全的;当前线程必须与当前线程亲和性相同。换句话说,此函数只能将对象从当前线程 “推送” 到另一个线程,它不能将对象从任何任意线程 “拉” 到当前线程。然而,此规则有一个例外:没有线程亲和性的对象可以 “拉” 到当前线程。