Qt多线程
QThread 常⽤ API:
使用线程
关于创建线程的步骤:
用多线程实现定时器功能
创建一个新的类:
记得勾选下面的 add Q_OBJECT
然后重写这个类的run函数
也定义了一个信号
然后在Widget构造函数中初始化,还有定义槽函数
执行结果:
关于这个start函数,这个才是真正调用系统API创建线程,新的线程创建出来后,会自动的执行run函数。
可以使用一个wait,让一个线程等待另一个线程的结束。
另外之前也说过,只有主线程才能针对界面的控件进行状态的修改。
理解Qt的多线程
我们之前学过的多线程,是站在服务器的角度上来看待的,对于服务器来说:多线程最主要的目的就是要充分利用多核CPU的计算资源。
而对于客户端来说,多线程仍然非常有意义,但是与服务器的侧重点不同。
对于普通用户来说,“使用体验”是非常重要的话题。
客户端上的程序很少会使用多线程把计算资源吃完。
相比之下,客户端的多线程主要是通过多线程的方式来执行一些耗时的IO操作,这样可以避免主线程被卡死,造成不好的用户体验。比如我们用迅雷下载文件,当文件很大,要20分钟才能下载完,那么在下载的过程中,用户的界面不可能得一直阻塞住吧?
因此,我们可以使用一个单独的新线程来处理这种IO密集的操作。
线程安全问题
谈到线程,始终绕不开的就是线程安全问题。
在Qt中实现线程互斥和同步常⽤的类有:
简单使用互斥锁
场景:两个线程对一个变量进行++,每个线程执行5000次。
重写run函数
初始化操作
结果:
对于Qt的互斥锁
条件变量
使用示例:
QMutex mutex;
QWaitCondition condition;
//在等待线程中
mutex.lock();
//检查条件是否满⾜,若不满⾜则等待
while (!conditionFullfilled())
{
condition.wait(&mutex); //等待条件满⾜并释放锁
}
//条件满⾜后继续执⾏
//...
mutex.unlock();
//在改变条件的线程中
mutex.lock();
//改变条件
changeCondition();
condition.wakeAll(); //唤醒等待的线程
mutex.unlock();
信号量
示例:
QSemaphore semaphore(2); //同时允许两个线程访问共享资源
//在需要访问共享资源的线程中
semaphore.acquire(); //尝试获取信号量,若已满则阻塞
//访问共享资源
//...
semaphore.release(); //释放信号量
//在另⼀个线程中进⾏类似操作
Qt网络
和多线程类似, Qt 为了⽀持跨平台, 对⽹络编程的 API 也进⾏了重新封装
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:
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):
其中的 QNetworkRequest::KnownHeaders 是⼀个枚举类型, 常⽤取值:
QNetworkReply 表⽰⼀个 HTTP 响应. 这个类同时也是 QIODevice 的⼦类:
发送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的音频
另外:使⽤ 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(); //播放
});
}