前言:
整篇文章围绕“Soeket通讯”项目开展,作为学习笔记,不做具体技术的研究,涉及QtNetwork、QThread,建议参考目录跳读。后附源码。
目录
1. 项目内容
1.1 项目概述
a.在qt上开发一个简单的“Socket通讯”(本程序运行两个进程,一个做客户端,一个做服务端,和自己进行一个网络通讯)
b.建立通讯后,使用多线程来收发数据。程序A开始发送数字1给程序B,程序A为了确保B收到,会持续的发送1(间隔10ms,间隔时间可设置),程序B收到后,进行处理把收到的数据加1后(延迟100ms),发回给A,A收到后循环和B进行加1互发,直到谁先到达100后,停止,并通知对方停止发送数据,连续不断,任何一方都可以手动按下停止发送来终止两边的数据发送。 注:这里的程序应该有如下几个线程:1.主线程、2.发送线程、3.接收线程、4.数据处理线程。
c.自动停止发送后,程序A给串口发送字符“GameOver!”。
1.2 项目分析
qt同时运行A、B两个程序,程序A作为客户端,程序B作服务端,AB通过电脑的不同端口进行网络通讯(类似简单的“聊天室”),且一个程序需要同时进行数据处理和数据的持续发送,涉及到QtNetwork、QThread的使用。程序A给串口发送字符,则通过发送到窗口作为模拟。
发送线程为主程序,而接收线程直接通过监听端口实现,只创建接收线程与数据处理线程两个子线程。
1.3 运行实例
Qt完成Socket通讯
1.4 代码结构
2. Qt的安装与配置
2.1 Qt在Mac的安装
可直接进入qt官网下载:http://download.qt.io/archive/qt/
qt提供了源码下载,本博主是按照b站的一位up来的,其内容包含qt安装与基本的使用,链接:Qt 5.14.2 下载、安装、使用教程,Qt+vs2019开发环境搭建_哔哩哔哩_bilibili
由于本博主使用的是Mac,需安装的组件与Windows不同,故又查看了另一位博主的,链接:Mac上Qt安装和配置教程_mac qt_小熊coder的博客-CSDN博客
2.2 Qt运行弹窗不显示
初次运行一个空窗口,电脑下行程序坞有图标但不显示弹窗。
原因是SDK版本问题,修改项目.pro文件来忽略,需要右键项目文件清除再重新构建。插入下段代码:
# 忽略版本警告,使窗口正常弹出
CONFIG += sdk_no_version_check
QMAKE_MACOSX_DEPLOYMENT_TARGET = 10.15
内容转载自:QT-MAC 运行后程序坞出现图标,但是不显示界面_mac qt 不显示界面_璎珞qc的博客-CSDN博客
3. QtNetwork实现Udp收发数据
项目以程序A的视角进行讲解。
引入qt的网络模块,在.pro文件插入下段代码:
QT += network
数据要通过tcp/udp协议传输,简单区别一句话,“tcp更长,udp更快,tcp连接需要三次握手四次分手,tcp更安全”,详细区别建议回归《计算机网络》课程。本项目使用udp。
3.1 初试化
头文件需要引用QtNetwork、QString、QDebug。
先忽略,connect槽函数,与sendThread发送线程;创建ip,数据a、b,udp发送对象sender、接收对象receiver(两者均是在头文件定义的指针)。
receiver绑定端口号,实现监听,当此端口接受到数据,会发送readyRead()信号。
//widget.cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
//当前网络ip,指定要发送的地址
ip="192.168.0.101";
setWindowTitle("客户端");
a=b=0;//a为程序A的数(要发送的数),b则为从程序B接受到的数
sender=new QUdpSocket(this);
receiver=new QUdpSocket(this);
//创建发送线程
sendThread=new sendMessageThread(this);
//开启监听绑定端口号,获取6666端口的活动
receiver->bind(QHostAddress::Any, 6666, QUdpSocket::ShareAddress);
//槽函数
connect(receiver, SIGNAL(readyRead()), this, SLOT(ReceiveMessage()));
//停止发送线程
connect(this, SIGNAL(stopSendMessageThread()), sendThread, SLOT(stopSend()));
//connect
connect(this, SIGNAL(startSendMessageThread()), sendThread, SLOT(startSend()));
connect(sendThread, SIGNAL(sendMessage()), this, SLOT(SendMessage()));
connect(this, SIGNAL(delAllThread()), sendThread, SLOT(recReturn()));
connect(sendThread, SIGNAL(finished()), sendThread, SLOT(deleteLater()));
//开启发送线程
sendThread->start();
}
3.2 发送数据
通过TCP,UDP传输数据需要先将数据转化为QByteArray类型。
//widget.cpp
//获取数据
void Widget::SendMessage()
{
//将要发送的整数转化为QString,其存储字符
//再转化为QByteArray,其存储字符的原始字节数据
QString text1=QString::number(a);
//QString.toLatin1()也可,QString转化为char也是这个方法;而toUtf8()涵盖中文
QByteArray datagram=text1.toUtf8();
//将数据发送到指定ip地址上,占用55555端口。
sender->writeDatagram(datagram.data(), datagram.size(), QHostAddress(ip), 55555);
//在发送窗口换行显示
ui->sendEdit->appendPlainText(text1);
//在qt终端上输出调试信息,会自动换行
qDebug() << "Send Over!" << endl;
}
int类型转QString类型为 QString str = QString::number(a);
QString类型转int类型为 int a = str.toInt();
详细了解QByteArray:QByteArray_友善啊,朋友的博客-CSDN博客
3.3 接收数据
同样忽略,connect槽函数,与dataThread处理数据线程。
//widget.cpp
void Widget::ReceiveMessage()
{
emit stopSendMessageThread();//停止正在持续的发送线程
while(receiver->hasPendingDatagrams()){//等待数据,完整传输
QByteArray datagram;
datagram.resize(receiver->pendingDatagramSize());
receiver->readDatagram(datagram.data(), datagram.size());
text2.clear();
text2.prepend(datagram);//转化为QString类型
ui->receEdit->appendPlainText(text2);
b=text2.toInt();
qDebug() << "Receive Over!" << endl;
//
//创建处理数据的线程,开始处理数据
dataThread=new doDataThread(this);
connect(this, SIGNAL(sendMesToDoDataThread(int, int)), dataThread, SLOT(recMesFromMain(int, int)));
connect(dataThread, SIGNAL(sendMesToMain(int, int)), this, SLOT(recMesFromDoDataThread(int, int)));
connect(dataThread, SIGNAL(sendOneSendToMain()), this, SLOT(recOneSendFromDataThread()));
connect(dataThread, SIGNAL(sendOverGameToMain()), this, SLOT(recOverGameFromDataThread()));
//可以在子线程运行时绑定信号????
connect(dataThread, SIGNAL(startSendMessageThread()), sendThread, SLOT(startSend()));
connect(dataThread, SIGNAL(stopSendMessageThread()), sendThread, SLOT(stopSend()));
//结束当下数据处理进程
connect(dataThread, SIGNAL(finished()), dataThread, SLOT(deleteLater()));
emit sendMesToDoDataThread(a, b);
dataThread->start();//由dataThread判断是否开启发送线程
//由于会持续的接受到对方的发送,对数据判断是否满足更新条件
if(a>b&&a!=-1&&b!=-1)
emit startSendMessageThread();
}
}
QString类型转QByteArray类型 QByteArray datagram = str.toUtf();
QByteArray数据添加到QString类型 str.prepend(datagram);
4. Qt信号与槽机制
4.1 定义
信号与槽机制是Qt的一大特点。代码发送信号“emit signal;” 通过connect连接的槽函数将会响应并执行。实现代码跳转,类似回调函数,功能覆盖所有引入头文件的内容。两者是“多对多”关系,即一个信号可以触发多个槽函数,触发顺序按建立连接时顺序进行;一个槽函数也可以连接多个信号;信号也可以连接信号。且两者可以重载。
信号(signals):只有定义,类似函数定义,不具体实现。
槽函数(slots):可以将槽函数当做普通函数,有private、protected、public修饰。
通过connet连接的信号与槽函数必须保证两者参数类型一致,信号的参数数量可以大于槽函数。信号与信号的连接也一致。
示例:
//widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QtNetwork>
#include <QString>
#include "doDataThread.h"
#include "sendMessageThread.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
signals:
void stopSendMessageThread();//暂时停止
void startSendMessageThread();
void sendMesToDoDataThread(int, int);
void delAllThread();//结束所有子线程
private slots:
void on_sendClient_clicked();//发送按钮
void on_overPushButton_clicked();//断开按钮
//使用槽机制,要放到这
void ReceiveMessage();
void SendMessage();
void recMesFromDoDataThread(int, int);
void recOneSendFromDataThread();//recDoOneSendFromDoDataThread
void recOverGameFromDataThread();
private:
Ui::Widget *ui;
int a;
int b;
QString ip;
QString text1;
QString text2;
QUdpSocket *sender;
QUdpSocket *receiver;
doDataThread *dataThread;
sendMessageThread *sendThread;
};
#endif // WIDGET_H
4.2 connect实现机制
常用的connect两种形式:
宏定义:SIGNAL、SLOT为宏
connect(this, SIGNAL(sendMesToDoDataThread(int, int)), dataThread, SLOT(recMesFromMain(int, int)));
// 信号与信号的连接格式为
// connect( , SIGNAL(), , SIGNAL());
函数指针:
connect(ui->but_send,&QPushButton::clicked,this,&MainWindow::btn_send_data);
//QSerialPort global_port;//端口函数
connect(&global_port,&QSerialPort::readyRead,this,&MainWindow::receive_data);
1. 第一种方式为qt4版本以前的,这有一个缺陷,编译器不做参数类型检测,下列不合格代码也能运行:
//底层实现 通过SIGNAL("sendMesToDoDataThread") SLOT("recMesFromMain") 去查找
connect(this, SIGNAL(sendMesToDoDataThread(int)), dataThread, SLOT(recMesFromMain(int, int)));
2. 而对于信号与槽的重载,第二种连接会产生冲突,我们需要给定一个准确的函数指针,如下:
void (Widget:: *sendMessage)(int, int) = &Widget::sendMesToDoDataThread;
void (doDataThread:: *recMes)(int, int) = &doDataThread::recMesFromMain;
connect(this, sendMessage, dataThread, recMes);
注意:在多线程中,QThread创建的线程均为子线程,只有主程序作为主线程。
1. connect连接信号与槽函数只能在主线程中进行,在子线程会出现不可预错误。
2. connect可以在需要时建立,也可以disconnect断开连接,但尽量放到开头初试化。
3. 主线程与子线程之间可以通过connect信号与槽机制实现传值。
4. 可以在子线程运行时connect绑定,但注意线程运行内容避免错误调用,例如“3.3 接受数据”。
4.3 发送信号
emit sendMesToDoDataThread(a, b);
更多信号与槽机制详情:
Qt信号与槽机制_qt信号与槽机制原理_luckyone906的博客-CSDN博客
QT中connect函数的几种用法详解总结_qt connect_疯狂的挖掘机的博客-CSDN博客
QT 主线程子线程互相传值_qt主线程给子线程传数据_双子座断点的博客-CSDN博客
4.4 信号与槽按名称自动连接
假设当前项目的ui文件为widget.ui,则在项目的根目录下有一个文件ui_widget.h,这是ui文件经编译后产生的代码文件,打开可在末尾见到
QMetaObject::connectSlotsByName(Widget);
这条语句实现了信号与槽按名称自动连接的机制,在ui文件设计界面,右键控件,点击“转到槽..”自动生成的槽函数就是通过这个实现与信号的连接,槽函数的命名规则如下:
// 这里链接的是按钮pushButton的点击clicked()信号
on_pushButton_clicked()
// on_<控件的对象名称>_<信号名>()
// 其参数要求与信号一致
格式:on_<控件的对象名称>_<信号名>()
这里便不需要手动connect了。
5. QThread多线程
5.1 C++多线程知识
thread:创建线程,包含子线程的信息,对线程结束方式的处理,等待操作。
mutex:锁,锁定变量,代码块不能被中断。
atomic:原子,原子操作是最小的单位,不可中断,可替代mutex使用,减少上锁开锁的消耗。
condition_variable:阻塞当前线程,等待信号,再继续运行。
future:async同步执行或创建子线程异步执行,返回future类型值;通过promise获取thread的返回值。
转载于:
C++11 多线程(std::thread)详解_jcShan709的博客-CSDN博客
更多详情:
C++ 多线程编程(一):std::thread的使用_std::thread用法_N阶魔方的博客-CSDN博客
C++项目实战-多进程(一篇文章)_c++ 多进程_陈达书的博客-CSDN博客
5.2 QThread
5.2.1 实现QThread
Qt通过继承QThread来实现线程,必须重写run()运行函数。
//sendMessageThread.h
#ifndef SENDMESSAGE_H
#define SENDMESSAGE_H
#include <QThread>
#include <QMutex>
#include <QDebug>
class sendMessageThread : public QThread
{
Q_OBJECT
public:
explicit sendMessageThread(QObject *parent = 0);
protected:
virtual void run() override;
//以上部分实现QThread必须包含
signals:
//void resultDo(int);
void sendMessage();
private:
bool m_Stop;//true不发送
bool m_Return;//true退出
QMutex *m_mutex;
private slots:
void stopSend();
void startSend();
void recReturn();
};
#endif // SENDMESSAGE_H
5.2.2 QThread操作
以“3.1 初始化”代码sendThread为例
//创建发送线程
sendThread=new sendMessageThread(this);
//当线程结束会发送finished()信号,连接槽函数使线程结束自动释放。
connect(sendThread, SIGNAL(finished()), sendThread, SLOT(deleteLater()));
//开启发送线程,运行run()
sendThread->start();
5.2.3 停止线程
如何有效的停止线程?
1. 可以设置bool stop;
通过信号槽函数来控制其值,以判断其值来停止,继续,结束线程。这是笨拙而有效的方法。这并非停止线程,只是否定了线程的操作,线程依然在运行。例如下面的,sendMessageThread。
2. condition_variable阻塞线程
3. quick()、terminate()
注:如何强制线程退出,例如线程运行休眠sleep()后操作数据,如何退出或避免数据被修改。
详见:
Qt之QThread(深入理解)_Mr.codeee的博客-CSDN博客
qt 线程同步-互斥量(Qmutex)_alex1801的博客-CSDN博客
Qt线程QThread开启和安全退出,QMutex线程加锁_qthread加锁_卧_听风雨的博客-CSDN博客
QT中QThread的各个方法,UI线程关系,事件关系详解(1)_linux qt qthread_luckyone906的博客-CSDN博客
5.3 发送线程sendMessageThread
5.3.1 sendMessageThread.h
//sendMessageThread.h
#ifndef SENDMESSAGE_H
#define SENDMESSAGE_H
#include <QThread>
#include <QMutex>
#include <QDebug>
class sendMessageThread : public QThread
{
Q_OBJECT
public:
explicit sendMessageThread(QObject *parent = 0);
protected:
virtual void run() override;
//以上部分实现QThread必须包含
signals:
void sendMessage();//执行一次发送操作
private:
bool m_Stop;//true不发送
bool m_Return;//true退出
QMutex *m_mutex;
private slots:
void stopSend();
void startSend();
void recReturn();
};
#endif // SENDMESSAGE_H
5.3.2 sendMessageThread.cpp
正如“5.2.3 停止线程”所言,通过布尔值m_Stop来控制sendMessageThread是否执行发送操作,可停止发送操作,但发送线程一直在循环运行,使线程对CPU产生了无用的消耗。
//sendMessageThread.cpp
#include "sendMessageThread.h"
sendMessageThread::sendMessageThread(QObject *parent)
:QThread(parent), m_Stop(true), m_Return(false)
{
m_mutex=new QMutex;
qDebug() << "Send Thread : " << QThread::currentThreadId();
}
void sendMessageThread::run()
{
//通过m_Return来控制发送线程最终退出
while (!m_Return) {
m_mutex->lock();
//通过m_Stop来控制是否执行发送操作,当发送线程一直在循环运行
if(!m_Stop){
//发送信息
emit sendMessage();
qDebug() << "Send Message..." <<endl;
//msleep(10);
msleep(10);
}
m_mutex->unlock();
}
qDebug() << "SendMessageThread Over!" << endl;
}
void sendMessageThread::stopSend()
{
//接收到绑定的信号即停止发送操作
m_mutex->lock();
m_Stop=true;
m_mutex->unlock();
qDebug() << "SendMessageThread STOP!" << endl;
}
void sendMessageThread::startSend()
{
//接收到绑定的信号即继续发送操作
m_mutex->lock();
m_Stop=false;
m_mutex->unlock();
qDebug() << "SendMessageThread START!" << endl;
}
void sendMessageThread::recReturn()
{
//接收到绑定的信号即退出循环,结束线程
m_mutex->lock();
m_Return=true;
m_mutex->unlock();
}
5.4 数据处理线程
5.4.1 doDataThread.h
//doDataThread.h
#ifndef DODATATHREAD_H
#define DODATATHREAD_H
#include <QThread>
#include <QMutex>
#include <QDebug>
class doDataThread : public QThread
{
Q_OBJECT
public:
explicit doDataThread(QObject *parent = 0);
protected:
virtual void run() override;
signals:
void sendMesToMain(int, int);//将处理完的数据发送给主线程
void stopSendMessageThread();//暂时停止
void startSendMessageThread();//继续执行发送操作
void sendOneSendToMain();//由于结束时需要,向对方执行一次数据发送告知对方结束
void sendOverGameToMain();//结束,发送“OverGame”
private:
int a;
int b;
QMutex *m_mutex;
private slots:
void recMesFromMain(int, int);//从主线程获取数据
};
#endif // DODATATHREAD_H
5.4.2 doDataThread.cpp
//doDataThread.cpp
#include "doDataThread.h"
doDataThread::doDataThread(QObject *parent)
:QThread(parent)
//, m_Stop(false)
{
m_mutex=new QMutex;
a=b=0;
qDebug() << "DoData Thread : " << QThread::currentThreadId();
}
void doDataThread::run()
{
m_mutex->lock();
//由于多次发送,要根据接收到的数据判断是否需要更新
if(b==-1){
//停止发送
emit stopSendMessageThread();
}
else if(b==100&&a<b){
emit sendOverGameToMain();
//停止发送
emit stopSendMessageThread();
}
else if(b==99&&a<b){
a=b+1;
emit sendOverGameToMain();
//停止发送
emit stopSendMessageThread();
emit sendMesToMain(a, b);
emit sendOneSendToMain();
}
else if(b>a){
a=b+1;
emit sendMesToMain(a, b);
//emit startSendMessageThread();
}
m_mutex->unlock();
qDebug() << "doDataThread Over! a: " << a << " b:" << b << endl;
}
void doDataThread::recMesFromMain(int a, int b)
{
//从主线程获得需要处理的数据
m_mutex->lock();
this->a=a;
this->b=b;
m_mutex->unlock();
}
6. Qt的UI设计
qt的ui设计可以通QtDesigner去拖拽基础组件去设计,ui文件为xml。而不同于QtWidget的版本,QtQuick采用QML,新的ui界面描述语言。
ui常用控件详见:Qt常用控件介绍(一)_qt控件介绍_六竹书生__wa的博客-CSDN博客
导入控件详见:Qt UI界面美化教程1:“飞扬青云” Qt精美控件使用教程1 | 码农家园
7. 补充
1. 使用到c11新特性需要在工程下.pro文件添加下行代码。
CONFIG += c++11
2. 在上传代码时,发现自己访问不了github,查了一下是域名解析的问题,要通过更新hosts文件解决了。不过博主找到一个FastGithub应用可以解决,通过其开源的README文件提供的地址直接下载应用。
应用链接:Gitee 极速下载/fastgithub - 码云 - 开源中国
git使用详细见:还不会使用 GitHub ? GitHub 教程来了!万字图文详解 - 知乎 (zhihu.com)
8. 不足
1. 在发送线程sendMessageThread中,通过布尔值去停止发送操作,发送线程依然在运行,这对CPU是一种多余的消耗。可以采用condition_variable去真实的阻塞发送线程。
2. 处理数据线程doDataThread,重复的创建释放线程也是一种消耗。
3. 示例开始,有时会出现数据为“1, 3”时A,B循环发送3,是UDP的安全性不足还是QMutex的使用导致?
4. 如何优雅的退出子线程。
9. 结语
源码地址:qt建立socket通讯.github
文中多处,转载了其它技术详细的文章,若有侵权,联系删除。