抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

misakivv的博客

霜蹄千里骏,风翮九霄鹏

QT多线程02

继承QObject的线程

image-20231108095856871.png

继承QObject类更加灵活。它通过 QObject::moveToThread()方法将它移到一个 QThread 线程里 执行。那么可以通过主线程发送信号去调用 QThread 线程的方法如上图的 fun4(),fun5()等等。这些方 法都是在 QThread 线程里执行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QThread>
#include <QDebug>
#include <QPushButton>
#include <QMutexLocker>
#include <QMutex>
/* 工人类 */
class Worker;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* 开始线程按钮 */
QPushButton *pushButton1;
QPushButton *pushButton2;
/* 全局线程 */
QThread workerThread;
/* 工人类 */
Worker *worker;
private slots:
/* 按钮 1 点击开启线程 */
void pushButton1Clicked();
/* 按钮 2 点击打断线程 */
void pushButton2Clicked();
/* 用于接收工人是否在工作的信号 */
void handleResults(const QString &);
signals:
/* 工人开始工作(做些耗时的操作 ) */
void startWork(const QString &);
};
/* Worker 类,这个类声明了 doWork1 函数,将整个 Worker 类移至线程 workerThread*/
class Worker : public QObject
{
Q_OBJECT
private:
/* 互斥锁 */
QMutex lock;
/* 标志位 */
bool isCanRun;
public slots:
/* 耗时的工作都放在槽函数下,工人可以有多份不同的工作,但是每次只能去做一份 */
void doWork1(const QString &parameter) {
/* 标志位为真 */
isCanRun = true;
/* 死循环 */
while (1) {
/* 此{}作用是 QMutexLocker 与 lock 的作用范围,获取锁后,
* 运行完成后即解锁 */
{
QMutexLocker locker(&lock);
/* 如果标志位不为真 */
if (!isCanRun) {
/* 跳出循环 */
break;
}
}
/* 使用 QThread 里的延时函数,当作一个普通延时 */
QThread::sleep(2);
emit resultReady(parameter + "doWork1 函数");
}
/* doWork1 运行完成,发送信号 */
emit resultReady("打断 doWork1 函数");
}
// void doWork2();...
public:
/* 打断线程(注意此方法不能放在槽函数下) */
void stopWork() {
qDebug()<<"打断线程"<<endl;
/* 获取锁后,运行完成后即解锁 */
QMutexLocker locker(&lock);
isCanRun = false;
}
signals:
/* 工人工作函数状态的信号 */
void resultReady(const QString &result);
};
#endif // MAINWINDOW_H

第 51105 行,声明一个 Worker 的类继承 QObject 类,这里是参考 Qt 的 QThread 类的帮助文档的 写法。 第 6288 行,我们把耗时的工作都放于槽函数下。工人可以有不同的工作,但是每次只能去做一份。 这里不同于继承 QThread 类的线程 run(),继承 QThread 的类只有 run()在新线程里。而继承 QObject 的类,使用 moveToThread()可以把整个继承的 QObject 类移至线程里执行,所以可以有 doWork1(),doWork2…等等耗时的操作,但是这些耗时的操作都应该作为槽函数,由主线程去调用。 第 67~80 行,进入循环后使用互拆锁判断 isCanRun 变量的状态,为假即跳出 while 循环,直到 doWork1 结束。注意,虽然 doWork1 结束了,但是线程并没有退出(结束)。因为我们把这个类移到 线程里了,直到这个类被销毁。或者使用 quit()和 exit()退出线程才真正的结束!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
mainwindow.cpp


#include "mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置显示位置与大小 */
this->setGeometry(0, 0, 800, 480);
pushButton1 = new QPushButton(this);
pushButton2 = new QPushButton(this);
/* 设置按钮的位置大小 */
pushButton1->setGeometry(300, 200, 80, 40);
pushButton2->setGeometry(400, 200, 80, 40);
/* 设置两个按钮的文本 */
pushButton1->setText("开启线程");
pushButton2->setText("打断线程");
worker = new Worker;
/* 将 worker 类移至线程 workerThread */
worker->moveToThread(&workerThread);
/* 信号槽连接 */
/* 线程完成销毁对象 */
connect(&workerThread, SIGNAL(finished()),
worker, SLOT(deleteLater()));
connect(&workerThread, SIGNAL(finished()),
&workerThread, SLOT(deleteLater()));
/* 发送开始工作的信号,开始工作 */
connect(this, SIGNAL(startWork(QString)),
worker, SLOT(doWork1(QString)));
/* 接收到 worker 发送过来的信号 */
connect(worker, SIGNAL(resultReady(QString)),
this, SLOT(handleResults(QString)));
/* 点击按钮开始线程 */
connect(pushButton1, SIGNAL(clicked()),
this, SLOT(pushButton1Clicked()));
/* 点击按钮打断线程 */
connect(pushButton2, SIGNAL(clicked()),
this, SLOT(pushButton2Clicked()));
}
MainWindow::~MainWindow()
{
/* 打断线程再退出 */
worker->stopWork();
workerThread.quit();
/* 阻塞线程 2000ms,判断线程是否结束 */
if (workerThread.wait(2000)) {
qDebug()<<"线程结束"<<endl;
}
}
void MainWindow::pushButton1Clicked()
{
/* 字符串常量 */
const QString str = "正在运行";
/* 判断线程是否在运行 */
if(!workerThread.isRunning()) {
/* 开启线程 */
workerThread.start();
}
/* 发送正在运行的信号,线程收到信号后执行后返回线程耗时函数 + 此字符串 */
emit this->startWork(str);
}
void MainWindow::pushButton2Clicked()
{
/* 如果线程在运行 */
if(workerThread.isRunning()) {
/* 停止耗时工作,跳出耗时工作的循环 */
worker->stopWork();
}
}
void MainWindow::handleResults(const QString & results)
{
/* 打印线程的状态 */
qDebug()<<"线程的状态:"<<results<<endl;
}

第 20 行,工人类实例化。继承 QObject 的多线程类不能指定父对象。 第 24 行,工人类实例化后,工人类将自己移至 workerThread 线程里执行。 第 29~32 行,线程结束后,我们需要使用 deleteLater 来销毁 worker 对象和 workerThread对象分配 的内存。deleteLater 会确认消息循环中没有这两个线程的对象后销毁。

网络编程

1
2
3
4
5
6
7
Qt网络模块为我们提供了编写 TCP / IP 客户端和服务器的类。它提供了较低级别的类,例如代表低级网络
概念的 QTcpSocket,QTcpServer 和 QUdpSocket,以及诸如 QNetworkRequest,QNetworkReply
和 QNetworkAccessManager 之类的高级类来执行使用通用协议的网络操作。 它还提供了诸如
QNetworkConfiguration,QNetworkConfigurationManager和QNetworkSession等类,实现承载
管理。
想要在程序中使用 Qt 网络模块,我们需要在 pro 项目配置文件里增加下面的一条语句。
QT += network

获取本机的网络信息

为什么先写获取本机网络信息的内容呢?在建立网络通信之前我们至少得获取对方的 IP地址。在网络应 用中,经常需要用到本机的主机名、IP 地址、MAC 地址等网络信息,通常通在 Windows 通过调出命 令行 cmd 窗口输入 ipconfig 或者在 Linux 系统中使用 ifconfig 命令就可以查看相关信息了,在这里我 们利用 Qt 做出一个可以查询的界面和功能出来。

Qt 提供了 QHostInfo 和 QNetworkInterface 类可以用于此类信息查询。更多关于 QHostInfo和 QNetworkInterface 的相关函数可以在 Qt 的帮助文档中找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//networkhostinfo.pro


QT += core gui network
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
CONFIG += c++11
# The following define makes your compiler emit warnings if you use
# any Qt feature that has been marked deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS
# You can also make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain
#version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the
#APIs deprecated before Qt 6.0.0
SOURCES += \
main.cpp \
mainwindow.cpp
HEADERS += \
mainwindow.h
# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QPushButton>
#include <QTextBrowser>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTimer>
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private:
/* 点击获取和清空文本按钮 */
QPushButton *pushButton[2];
/* 文本浏览框用于显示本机的信息 */
QTextBrowser *textBrowser;
/* 水平 Widget 容器和垂直 Widget 容器*/
QWidget *hWidget;
QWidget *vWidget;
/* 水平布局和垂直布局 */
QHBoxLayout *hBoxLayout;
QVBoxLayout *vBoxLayout;
/* 定时器 */
QTimer *timer;
/* 获取本机的网络的信息,返回类型是 QString */
QString getHostInfo();
private slots:
/* 定时器槽函数,点击按钮后定时触发 */
void timerTimeOut();
/* 显示本机信息 */
void showHostInfo();
/* 启动定时器 */
void timerStart();
/* 清空 textBrowser 的信息 */
void clearHostInfo();
};
#endif // MAINWINDOW_H

头文件里主要是声明两个按钮和一个文本浏览框。另外还有一个定时器,声明一些槽函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
//mainwindow.cpp

#include "mainwindow.h"
#include <QtNetwork/QNetworkInterface>
#include <QtNetwork/QHostInfo>
#include <QThread>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
/* 设置位置与大小 */
this->setGeometry(0, 0, 800, 480);
/* 点击获取本地信息按钮和清空文本按钮 */
pushButton[0] = new QPushButton();
pushButton[1] = new QPushButton();
pushButton[0]->setText("获取本机信息");
pushButton[1]->setText("清空文本信息");
/* 按钮的大小根据文本自适应,
* 注意 setSizePolicy 需要在布局中使用 */
pushButton[0]->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Fixed);
pushButton[1]->setSizePolicy(QSizePolicy::Fixed,
QSizePolicy::Fixed);
/* 水平 Widget 和垂直 Widget 用于添加布局 */
hWidget = new QWidget();
vWidget = new QWidget();
/* 水平布局和垂直布局 */
hBoxLayout = new QHBoxLayout();
vBoxLayout = new QVBoxLayout();
/* 文本浏览框 */
textBrowser = new QTextBrowser();
/* 添加到水平布局 */
hBoxLayout->addWidget(pushButton[0]);
hBoxLayout->addWidget(pushButton[1]);
/* 将水平布局设置为 hWidget 的布局 */
hWidget->setLayout(hBoxLayout);
/* 将文本浏览框和 hWidget 添加到垂直布局 */
vBoxLayout->addWidget(textBrowser);
vBoxLayout->addWidget(hWidget);
/* 将垂直布局设置为 vWidget 的布局 */
vWidget->setLayout(vBoxLayout);
/* 设置 vWidget 为中心部件 */
setCentralWidget(vWidget);
/* 定时器初始化 */
timer = new QTimer();
/* 信号槽连接 */
connect(pushButton[0], SIGNAL(clicked()),
this, SLOT(timerStart()));
connect(pushButton[1], SIGNAL(clicked()),
this, SLOT(clearHostInfo()));
connect(timer, SIGNAL(timeout()),
this, SLOT(timerTimeOut()));
}
MainWindow::~MainWindow()
{
}
void MainWindow::timerStart()
{
/* 清空文本 */
textBrowser->clear();
/* 定时 1s */
timer->start(1000);
}
void MainWindow::timerTimeOut()
{
/* 显示本机信息 */
showHostInfo();
/* 停止定时器 */
timer->stop();
}
QString MainWindow::getHostInfo()
{
/* 通过 QHostInfo 的 localHostName 函数获取主机名称 */
QString str = "主机名称:" + QHostInfo::localHostName() + "\n";
/* 获取所有的网络接口,
* QNetworkInterface 类提供主机的 IP 地址和网络接口的列表 */
QList<QNetworkInterface> list = QNetworkInterface::allInterfaces();
/* 遍历 list */
foreach (QNetworkInterface interface, list) {
str+= "网卡设备:" + interface.name() + "\n";
str+= "MAC 地址:" + interface.hardwareAddress() + "\n";
/* QNetworkAddressEntry 类存储 IP 地址子网掩码和广播地址 */
QList<QNetworkAddressEntry> entryList = interface.addressEntries();
/* 遍历 entryList */
foreach (QNetworkAddressEntry entry, entryList) {
/* 过滤 IPv6 地址,只留下 IPv4 */
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol) {
str+= "IP 地址:" + entry.ip().toString() + "\n";
str+= "子网掩码:" + entry.netmask().toString() + "\n";
str+= "广播地址:" + entry.broadcast().toString() + "\n\n";
}
}
}
/* 返回网络信息 */
return str;
}
void MainWindow::showHostInfo()
{
/* 获取本机信息后显示到 textBrowser */
textBrowser->insertPlainText(getHostInfo());
}
void MainWindow::clearHostInfo()
{
/* 判断 textBrowser 是否为空,如果不为空则清空文本 */
if (!textBrowser->toPlainText().isEmpty())
/* 清空文本 */
textBrowser->clear();
}
1
2
3
4
5
第 90~123 行,是本例最重要的代码。
第 93 行,通过 QHostInfo 的 localHostName 函数获取主机名称。
第 97~98 行,通过 QNetworkInterface::allInterfaces()获取网络接口列表 list 类存储 IP 地址子网掩码和广播地址。如果我们用 qDebug()函数打印出 list,可以发现获取了所有的网络信息。而我们要提取网络里面的网络信息使用 QNetworkAddressEntry。
第 106~107 行,使用 QNetworkAddressEntry 从 interface 接口里使用函数addressEntries(),获取所有的条目。就可以使用 QNetworkAddressEntry 的对象 entry 获取 IP地址子网掩码和广播地址。
第 110~118 行,因为获取的 entries 在一个 QNetworkInterface 下可能有两个 IP,分别是 ipv4和 ipv6。这里使用 ip().protocol()来判断协议的类型,只留下 ipv4 类型的信息。筛选信息在我们写程序常常需要的。

套接字

socket编程

socket概述

  • 接字,用于描述IP地址和端口号
  • socket时链接运行在网络上的两个程序间的双向通信的端点。
  • 通讯两端都有socket,数据在两个socket之间通过IO进行传输。

套接字是一种特殊的I/O接口,在代码内体现为特殊的文件描述符,socket是一种常见的进程间通信。

套接字主要有三种类型:

1
2
3
4
5
6
7
1、流式套接字(SOCK_STREAM)
流式套接字提供可靠的,面向连接的通信流,保证数据传输的可靠性和有序性。TCP通信使用该
套接字。
2、数据报套接字(SOCK_DGRAM)
提供不可靠、无连接的通信流,不保证可靠传输、无序。UDP通信使用该套接字。
3、原始套接字(SOCK_RAW)
允许对底层协议进行访问,功能强大但不方便。

TCP连接的建立:三次握手

TCP连接的释放:四次挥手

socket通信

  • 服务器端将一个套接字绑定到一个特定的端口,并通过这个套接字等待和监听客户的连接请求。
  • 客户端根据服务器所在的主机和端口号发送连接请求。
  • 如果一切正常,服务器接受连接请求,并且获得一个新的绑定到不同端口地址的套接字。
  • 服务器端和客户端通过读写套接字进行通讯。

流程图

image-20231108143151865.png

服务器端

如果需要使用TCP协议创建一个服务器,需要以下步骤:

  • 创建套接字
  • 绑定套接字
  • 设置监听模式
  • 接受客户端的连接请求
  • 接受/发送数据
  • 断开连接

创建套接字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
socket()函数的用法:
头文件
#include <sys/types.h>
#include <sys/socket.h>
函数原型
int socket(int domain, int type, int protocol);
函数参数
domain:选择通信协议,常见的协议:
AF_INET:IPv4通信协议
AF_INET6:IPv6通信协议
AF_UNIX:本地通信
type:套接字类型
protocol:协议值,通常情况为0
函数返回值
成功:非负套接字文件描述符
失败:-1

评论