Qt-系统相关(2)多线程&网络

发布于:2024-11-28 ⋅ 阅读:(13) ⋅ 点赞:(0)

Qt多线程

在 Qt 中,多线程的处理⼀般是通过 QThread类 来实现。
QThread 代表⼀个在应⽤程序中可以独⽴控制的线程,也可以和进程中的其他线程共享数据。
QThread 对象管理程序中的⼀个控制线程。

QThread 常⽤ API:

 

使用线程 

关于创建线程的步骤:

1. ⾃定义⼀个类,继承于 QThread,并且只有⼀个线程处理函数(和主线程不是同⼀个线程),这个线程处理函数主要就是重写⽗类中的 run() 函数。
2. 线程处理函数⾥⾯写⼊需要执⾏的复杂数据处理;
3. 启动线程不能直接调⽤ run() 函数,需要使⽤对象来调⽤ start() 函数实现线程启动;
4. 线程处理函数执⾏结束后可以定义⼀个信号来告诉主线程;
5. 最后关闭线程。

用多线程实现定时器功能

 

创建一个新的类:

记得勾选下面的 add Q_OBJECT 

然后重写这个类的run函数

也定义了一个信号

然后在Widget构造函数中初始化,还有定义槽函数

 执行结果:

关于这个start函数,这个才是真正调用系统API创建线程,新的线程创建出来后,会自动的执行run函数。

可以使用一个wait,让一个线程等待另一个线程的结束。

另外之前也说过,只有主线程才能针对界面的控件进行状态的修改。

理解Qt的多线程 

我们之前学过的多线程,是站在服务器的角度上来看待的,对于服务器来说:多线程最主要的目的就是要充分利用多核CPU的计算资源。

而对于客户端来说,多线程仍然非常有意义,但是与服务器的侧重点不同。

对于普通用户来说,“使用体验”是非常重要的话题。

客户端上的程序很少会使用多线程把计算资源吃完。

相比之下,客户端的多线程主要是通过多线程的方式来执行一些耗时的IO操作,这样可以避免主线程被卡死,造成不好的用户体验。比如我们用迅雷下载文件,当文件很大,要20分钟才能下载完,那么在下载的过程中,用户的界面不可能得一直阻塞住吧?

因此,我们可以使用一个单独的新线程来处理这种IO密集的操作。

线程安全问题 

谈到线程,始终绕不开的就是线程安全问题。

在Qt中实现线程互斥和同步常⽤的类有: 

互斥锁:QMutex、QMutexLocker
条件变量:QWaitCondition
信号量:QSemaphore
读写锁:QReadLocker、QWriteLocker、QReadWriteLock

简单使用互斥锁 

场景:两个线程对一个变量进行++,每个线程执行5000次。

重写run函数

 初始化操作

结果:

 对于Qt的互斥锁

条件变量 

在 Qt 中,专⻔提供了 QWaitCondition类 来解决像上述这样的问题。
特点:QWaitCondition 是 Qt 框架提供的条件变量类,⽤于线程之间的消息通信和同步。

使用示例:

QMutex mutex;
QWaitCondition condition;
//在等待线程中
mutex.lock();
//检查条件是否满⾜,若不满⾜则等待
while (!conditionFullfilled()) 
{
 condition.wait(&mutex); //等待条件满⾜并释放锁
}
//条件满⾜后继续执⾏
//...
mutex.unlock();
//在改变条件的线程中
mutex.lock();
//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();

信号量 

特点:QSemaphore 是 Qt 框架提供的计数信号量类,⽤于控制同时访问共享资源的线程数量。
⽤途:限制并发线程数量,⽤于解决⼀些资源有限的问题。

示例:

QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作

Qt网络 

 和多线程类似, Qt 为了⽀持跨平台, 对⽹络编程的 API 也进⾏了重新封装

在进⾏⽹络编程之前, 需要在项⽬中的 .pro ⽂件中添加 network 模块.
添加之后要⼿动编译⼀下项⽬, 使 Qt Creator 能够加载对应模块的头⽂件.

UDP Socket 

主要的类有两个. QUdpSocket QNetworkDatagram 

QUdpSocket 表⽰⼀个 UDP 的 socket ⽂件。 

QNetworkDatagram 表⽰⼀个 UDP 数据报 

UDP回显服务器 

一般一个正经的服务器是很少会带有图形化界面的,一般都是命令行。

服务器部分 

 用一个listWidget来显示信息

 初始化部分:

主要执行逻辑:

客户端部分 

界面部分

初始化代码:

按钮对应的槽函数实现:

再次修改Widget构造函数,通过信号槽来处理服务器的响应

 

用lambda表达式的方式

客户端演示:

服务器演示:

我们也可以启动多个客户端向服务器发起请求。 

关于这里面的一些细节 

在传参的时候关于什么时候用 引用类型,什么时候用值类型:

大的原则上是尽量使用引用类型。

但是有些时候,比如不同类型相互转换的时候,大概率使用值类型

两个问题:

这个Udp服务器是否能放到Linux云服务上呢?

大概率不行。这取决于这个云服务器是否安装了图形化界面,Qt程序需要依赖图形化界面来运行。

能否用现在的Udp客户端连接云服务器上linux的Udp服务器?

是可以的。

一般的商业公司都不会用Qt编写服务器,但是会用Qt编写客户端。 

 

TCPSocket

 核⼼类是两个: QTcpServer QTcpSocket

QTcpServer ⽤于监听端⼝, 和获取客⼾端连接. 

 关于QTcpServer:

 

QTcpSocket ⽤⼾客⼾端和服务器之间的数据交互 

 关于QTcpSocket:

QByteArray ⽤于表⽰⼀个字节数组. 可以很⽅便的和 QString 进⾏相互转换.
例如:
使⽤ QString 的构造函数即可把 QByteArray 转成 QString.
使⽤ QString 的 toUtf8 函数即可把 QString 转成 QByteArray

 

 TCP回显服务器

服务器部分 

 初始化:

 处理新连接的槽函数:

这里跟UDP就有明显不同了,主要是对连接的处理

void Widget::processConnection()
{
    // 注意和UDP的区别
    // 1.先获取新连接对应的socket
    QTcpSocket* clientSocket = server->nextPendingConnection();
    // 构建日志
    QString log = "客户端:" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "上线了";
    ui->listWidget->addItem(log);

    // 2.设置连接可读就绪的槽函数
    connect(clientSocket,&QTcpSocket::readyRead,clientSocket,[=](){
        // 读取请求
        QString request = clientSocket->readAll();
        // 构建响应
        QString response = prase(request);
        // 返回响应
        clientSocket->write(response.toUtf8());

        // 打印日志
        QString log = "客户端:" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + " 说: " + response;
        ui->listWidget->addItem(log);
    });

    // 3.设置连接断开时的槽函数
    connect(clientSocket,&QTcpSocket::disconnected,clientSocket,[=](){
        // 打印日志
        QString log = "客户端:" + clientSocket->peerAddress().toString() + ":" + QString::number(clientSocket->peerPort()) + "下线了";
        ui->listWidget->addItem(log);
        clientSocket->deleteLater(); // 下一个事件循环再删除
    });
}

QString Widget::prase(QString &str)
{
    return str;
}

客户端部分 

客户端界面:

 

 初始化:

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    this->setWindowTitle("Tcp客户端");

    socket = new QTcpSocket(this);

    socket->connectToHost(ip,port); // 和服务器建立连接

    // 等待连接是否出错
    if(!socket->waitForConnected())
    {
        QMessageBox::critical(this,"连接出错",socket->errorString());
        exit(1);
    }

    // 处理连接可读就绪槽函数
    connect(socket,&QTcpSocket::readyRead,socket,[=](){
        QString response = socket->readAll();
        // 这里省略了解析过程
        QString log = "服务器说: " + response;
        ui->listWidget->addItem(log);
    });
}

 按钮槽函数的设计

void Widget::on_pushButton_clicked()
{
    QString text = ui->lineEdit->text();
    if(text.isEmpty()) return;
    ui->lineEdit->clear();
    socket->write(text.toUtf8());
    // 打印日志
    QString log = "客户端说: " + text;
    ui->listWidget->addItem(log);
}

演示效果:

服务器:

这里也是支持多个客户端同时连接的。

客户端:

 服务器能发送响应,客户端也能接收响应。

关于这里的一些细节

在TCP服务器处理请求这里,还是不够严谨的,因为TCP是字节流传输的,所以会存在数据“粘包”问题,这里我们并没有对其进行处理。这里严谨的做法应该是:将数据放到一个大的接收缓冲区中,然后约定好应用层协议的格式(报文定长,特殊字符等等)。

关于服务器那里,对于每一个客户端上线时,都会创建一个socket,随着客户端越来越多,如果这个socket不释放的话,就会造成内存泄漏,还有更严重的文件描述符泄漏。

我们可以通过delete手动释放,但是我们要考虑这个delete一定得被执行到,不会被return 异常这些情况而导致没有执行到。

Qt给了一种“半自动”的垃圾回收机制,

上述这个操作,不是立即销毁clientSocket,而是告诉Qt在下一轮事件循环中,再进行上述的销毁操作。

 

 HTTP Client

Qt提供了HTTP客户但,但是没有提供HTTP服务器的库。

原因也很简单,一个正经的服务器是不需要图形化界面的,也就是不会用Qt来开发。

HTTPClient关键类主要是三个. QNetworkAccessManager , QNetworkRequest , QNetworkReply 

QNetworkAccessManager 提供了 HTTP 的核⼼操作:

 

QNetworkRequest 表⽰⼀个 HTTP 请求(不含 body):

如果需要发送⼀个带有 body 的请求(⽐如 post), 会在 QNetworkAccessManager 的 post ⽅法
中通过单独的参数来传⼊ body

 

 

其中的 QNetworkRequest::KnownHeaders 是⼀个枚举类型, 常⽤取值: 

 

QNetworkReply 表⽰⼀个 HTTP 响应. 这个类同时也是 QIODevice 的⼦类:

 

此外, QNetworkReply 还有⼀个重要的信号 finished 会在客⼾端收到完整的响应数据之后触发

 

发送get请求 

客户端界面:

 这里我们以请求百度首页为例,得到的响应大概率是一个HTML格式的文件,这里我们的QplainTextEdit可以看到响应原始的模样。而QTextEdit天然会对HTML进行解析。

 初始化:

slot槽函数

void Widget::on_pushButton_clicked()
{
    QUrl url = ui->lineEdit->text();
    if(url.isEmpty()) return;
    ui->lineEdit->clear();
    // 构建http请求对象
    QNetworkRequest request(url);
    // 获取http请求响应
    QNetworkReply* response =  manager->get(request);
    // 构建槽函数来处理响应
    connect(response,&QNetworkReply::finished,response,[=](){
        if(response->error() == QNetworkReply::NoError)
        {
            // 响应正确
            QString html = response->readAll();
            ui->plainTextEdit->setPlainText(html);
        }
        else
        {
            // 响应出错
            ui->plainTextEdit->setPlainText(response->errorString());
        }
        response->deleteLater(); // 记得销毁
    });
}

 运行结果:

发送 POST 请求代码也是类似. 使⽤ manager->post() 即可 

 

Qt音频 

这里简单介绍一下Qt的音频

在 Qt 中,⾳频主要是通过 QSound 类来实现。但是需要注意的是 QSound 类只⽀持播放 wav 格式的⾳频⽂件。也就是说如果想要添加⾳频效果,那么⾸先需要将 ⾮wav格式 的⾳频⽂件转换为 wav 格式。

 

另外:使⽤ QSound 类时,需要添加模块:multimedia

它的核心API就一个:
 

 

先准备一个.wav格式的音频,然后可以用qrc管理起来。

示例:

#include <QSound> //添加⾳频头⽂件
Widget::Widget(QWidget *parent)
 : QWidget(parent)
 , ui(new Ui::Widget)
{
 ui->setupUi(this);
 QSound *sound = new QSound(":/1.wav",this); //实例化对象
 connect(ui->btn,&QPushButton::clicked,[=](){
 
 sound->play(); //播放
 
 });
}