Qt完成Socket通讯

前言:

        整篇文章围绕“Soeket通讯”项目开展,作为学习笔记,不做具体技术的研究,涉及QtNetwork、QThread,建议参考目录跳读。后附源码。

目录

前言:

1. 项目内容

1.1 项目概述

1.2 项目分析

1.3 运行实例

1.4 代码结构

2. Qt的安装与配置

2.1 Qt在Mac的安装

2.2 Qt运行弹窗不显示

3. QtNetwork实现Udp收发数据

3.1 初试化

3.2 发送数据

3.3 接收数据

4. Qt信号与槽机制

4.1 定义

4.2 connect实现机制

4.3 发送信号

4.4 信号与槽按名称自动连接

5. QThread多线程

5.1 C++多线程知识

5.2 QThread

5.2.1 实现QThread

5.2.2 QThread操作

5.2.3 停止线程

5.3 发送线程sendMessageThread

5.3.1 sendMessageThread.h

5.3.2 sendMessageThread.cpp

5.4 数据处理线程

5.4.1 doDataThread.h

5.4.2 doDataThread.cpp

6. Qt的UI设计

7. 补充

8. 不足

9. 结语


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++多线程_非晚非晚的博客-CSDN博客

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使用详解_双子座断点的博客-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

文中多处,转载了其它技术详细的文章,若有侵权,联系删除。

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值