当前位置: 首页 > backend >正文

Qt中解决Tcp粘包问题

Qt中解决Tcp粘包问题

  • Qt中解决Tcp粘包问题——以文件发送为例
    • 服务器端
    • 客户端
    • 效果演示
    • 注意点

Qt中解决Tcp粘包问题——以文件发送为例

创建的工程如下图所示:
在这里插入图片描述

服务器端

界面的布局以及名称如下图所示:
在这里插入图片描述
并且在Qt中增加网络模块

QT       += core gui network
  1. 主函数中的代码不动,和创建时一样

    #include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[])
    {QApplication a(argc, argv);MainWindow w;w.show();return a.exec();
    }
    
  2. mainwindow.h

    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H#include <QMainWindow>
    #include "mytcpserver.h"QT_BEGIN_NAMESPACE
    namespace Ui {
    class MainWindow;
    }
    QT_END_NAMESPACEclass MainWindow : public QMainWindow
    {Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();signals:void start(QString name); //信号的作用就是通知子线程可以开始工作了,发送文件private slots:void on_start_clicked();void on_selectFile_clicked();void on_send_clicked();private:Ui::MainWindow *ui;MyTcpServer* m_server;
    };
    #endif // MAINWINDOW_H
  3. mainwindow.cpp

    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    #include <QThread>
    #include "sendfile.h"
    #include <QMessageBox>
    #include <QRandomGenerator>
    #include <QFileDialog>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
    {ui->setupUi(this);//设置默认值ui->port->setText("8989");m_server = new MyTcpServer(this);connect(m_server,&MyTcpServer::newClient,this,[=](qintptr socket){//这时候需要在子线程中进行通信,主线程是ui线程,主要负责一些窗口、ui的处理动作QThread *subThread = new QThread;//注意这里千万不能给其指定父对象,如:this,否则移动到子线程对象中会报错SendFile* worker = new SendFile(socket);worker->moveToThread(subThread); //将其移动到子线程中//接受主线程的信号,让对象worker开始执行working函数connect(this,&MainWindow::start,worker,&SendFile::working);//提示,不要试图通过子线程去调用ui控件中的一些数据//当worker对象中的函数执行完毕后,就发出一个信号,我们根据这个信号把资源进行释放connect(worker,&SendFile::done,this,[=](){qDebug()<<"销毁子线程和人物对象资源....";//这里建议先调用 quit再调用wait,因为调用quit的时候,还有一些人物没有做完subThread->quit();subThread->wait();//deleteLater方法是用来释放资源的,这个方法是QObject中的subThread->deleteLater();worker->deleteLater();});connect(worker,&SendFile::text,this,[=](QByteArray msg){//为了能够清楚看出粘包问题,这里先定义一些颜色QVector<QColor> colors = {Qt::red,Qt::green,Qt::black,Qt::blue,Qt::darkRed,Qt::cyan,Qt::magenta};int index = QRandomGenerator::global()->bounded(colors.size());ui->msg->setTextColor(colors.at(index));ui->msg->append(msg);});//启动子线程,但是此时移动到子线程中的对象还是不能进行工作的,需要主线程给worker对象发信号//此时worker对象中的函数执行的时候,才算是在子线程中进行工作subThread->start();});
    }MainWindow::~MainWindow()
    {delete ui;
    }void MainWindow::on_start_clicked()
    {unsigned short port = ui->port->text().toShort();//开始监听m_server->listen(QHostAddress::Any,port);
    }void MainWindow::on_selectFile_clicked()
    {QString path = QFileDialog::getOpenFileName(this);if(!path.isEmpty()){ui->path->setText(path);}
    }void MainWindow::on_send_clicked()
    {if(ui->path->text().isEmpty()){QMessageBox::information(this,"提示","要发送的文件不能为空");return;}//信号的作用就是通知子线程可以开始工作了,sendFile的对象working就可以发送文件执行working函数了emit start(ui->path->text());
    }
    
  4. 重写QTcpServer

    // 自定义一个MyTcpServer类,并且继承自QTcpServer
    #ifndef MYTCPSERVER_H
    #define MYTCPSERVER_H#include <QTcpServer>class MyTcpServer : public QTcpServer
    {Q_OBJECT
    public:explicit MyTcpServer(QObject *parent = nullptr);protected://重写函数,主要是为了跨线程传递通信套接字文件描述符void incomingConnection(qintptr socketDescriptor);signals:void newClient(qintptr socket);
    };#endif // MYTCPSERVER_H//源文件
    #include "mytcpserver.h"MyTcpServer::MyTcpServer(QObject *parent): QTcpServer{parent}
    {}void MyTcpServer::incomingConnection(qintptr socketDescriptor)
    {emit newClient(socketDescriptor);
    }
    
  5. SendFile 子线程中进行文件发送的类

    #ifndef SENDFILE_H
    #define SENDFILE_H#include <QObject>
    #include <QTcpSocket>class SendFile : public QObject
    {Q_OBJECT
    public:explicit SendFile(qintptr socket ,QObject *parent = nullptr);void working(QString path);signals:void done();void text(QByteArray msg);private:qintptr m_socket;QTcpSocket *m_tcp;
    };#endif // SENDFILE_H//源文件
    #include "sendfile.h"
    #include <QThread>
    #include <QDebug>
    #include <QFile>
    #include<QtEndian>SendFile::SendFile(qintptr socket ,QObject *parent) : QObject{parent}
    {//用于通信的套接字保存下来m_socket = socket;//这里不可实例化一个QTcpSocket对象,因为这个构造函数是主线程中调用的,实在主线程中创建的//m_tcp = new QTcpSocket;}void SendFile::working(QString path)
    {qDebug()<<"当前的线程ID:"<<QThread::currentThread();m_tcp = new QTcpSocket;m_tcp->setSocketDescriptor(m_socket);   //设置好之后,这个对象就可以进行通信了//若是客户端断开连接,那么服务器端的套接字对象就会发送信号disconnectedconnect(m_tcp,&QTcpSocket::disconnected,this,[=](){m_tcp->close();m_tcp->deleteLater();emit done();qDebug()<<"客户端的数据已经接受完毕,并且断开连接,开始销毁套接字对象,拜拜六。。。。。。。。。";});qDebug()<<"要发送文件的名字:"<<path;QFile file(path);bool bl = file.open(QFile::ReadOnly);if(bl){//实际上是一行行的读取文件然后发送出去while(!file.atEnd()){QByteArray line = file.readLine();qDebug()<<"line.size():"<<line.size();//添加包头int len = qToLittleEndian(line.size());//int len = qToBigEndian(line.size());qDebug()<<"len.size():"<<len;//重新创建一个数据,并且将这一行的数据的数据长度放入数据头部QByteArray data((char*)(&len),4);//追加内容data.append(line);//发送数据给客户端m_tcp->write(data);emit text(data);QThread::msleep(50);  //msleep 表示要休眠的时间,单位是毫秒}}file.close();
    }
    

客户端

界面布局如下:
在这里插入图片描述
并且在Qt中增加网络模块

QT       += core gui network
  1. 主函数依旧不变动,与创建时保持一致

    #include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[])
    {QApplication a(argc, argv);MainWindow w;w.show();return a.exec();
    }
  2. mainwindow.h 代码如下

    #ifndef MAINWINDOW_H
    #define MAINWINDOW_H#include <QMainWindow>QT_BEGIN_NAMESPACE
    namespace Ui {
    class MainWindow;
    }
    QT_END_NAMESPACEclass MainWindow : public QMainWindow
    {Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();private slots:void on_connect_clicked();signals:void startConnect(QString ip,unsigned short port);private:Ui::MainWindow *ui;
    };
    #endif // MAINWINDOW_H
    
  3. mainwindow.cpp 代码如下

    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    #include "recvfile.h"
    #include <QThread>
    #include <QMessageBox>
    #include <QRandomGenerator>
    #include<QDebug>MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
    {ui->setupUi(this);qDebug()<<"主线程id:"<<QThread::currentThread();ui->ip->setText("127.0.0.1");ui->port->setText("8989");//创建一个子线程对象QThread * subThread = new QThread;//注意这里千万不能给其指定父对象,如:this,否则移动到子线程对象中会报错RecvFile * worker = new RecvFile;worker->moveToThread(subThread);connect(this,&MainWindow::startConnect,worker,&RecvFile::connectServer);connect(worker,&RecvFile::connectOK,this,[=](){QMessageBox::information(this,"提示","已经成功连接到了服务器......");});connect(worker,&RecvFile::message,this,[=](QByteArray msg){//为了能够清楚看出粘包问题,这里先定义一些颜色QVector<QColor> colors = {Qt::red,Qt::green,Qt::black,Qt::blue,Qt::darkRed,Qt::cyan,Qt::magenta};int index = QRandomGenerator::global()->bounded(colors.size());ui->msg->setTextColor(colors[index]);ui->msg->append(msg);});connect(worker,&RecvFile::gameOver,this,[=](){qDebug()<<"销毁子线程已经工作的任务对象";//这里建议先调用 quit再调用wait,因为调用quit的时候,还有一些人物没有做完subThread->quit();subThread->wait();//deleteLater方法是用来释放资源的,这个方法是QObject中的subThread->deleteLater();worker->deleteLater();});//启动子线程,但是此时移动到子线程中的对象还是不能进行工作的,需要主线程给worker对象发信号//此时worker对象中的函数执行的时候,才算是在子线程中进行工作subThread->start();
    }MainWindow::~MainWindow()
    {delete ui;
    }void MainWindow::on_connect_clicked()
    {QString ip = ui->ip->text();unsigned short port = ui->port->text().toUShort();emit startConnect(ip,port);
    }
    
  4. 子线程中用于接受数据的类RecvFile

    //头文件
    #ifndef RECVFILE_H
    #define RECVFILE_H#include <QObject>
    #include<QTcpSocket>class RecvFile : public QObject
    {Q_OBJECT
    public:explicit RecvFile(QObject *parent = nullptr);//连接服务器void connectServer(QString ip,unsigned short port);//把数据从读缓冲区一点点拆分开来void dealData();signals:void connectOK();void message(QByteArray msg);void gameOver();private:QTcpSocket* m_tcp;
    };#endif // RECVFILE_H//源文件
    #include "recvfile.h"
    #include <QHostAddress>
    #include <QDebug>
    #include <QThread>
    #include <QtEndian>
    RecvFile::RecvFile(QObject *parent): QObject{parent}
    {}void RecvFile::connectServer(QString ip, unsigned short port)
    {qDebug()<<"子线程线程id:"<<QThread::currentThread();//连接服务器m_tcp = new QTcpSocket;//非阻塞函数,这里并不代表客户端和服务器已经建立起了连接m_tcp->connectToHost(QHostAddress(ip),port);//若是成功建立连接,会发出connected信号,调用connectOK函数connect(m_tcp,&QTcpSocket::connected,this,&RecvFile::connectOK);connect(m_tcp,&QTcpSocket::readyRead,this,[=](){// QByteArray all = m_tcp->readAll();// emit message(all);dealData();m_tcp->close();m_tcp->deleteLater();emit gameOver();});
    }void RecvFile::dealData()
    {//注意,数据包是一行行的送过来的unsigned int totalBytes = 0;  //当前一行的数据包的数据大小unsigned int recvBytes = 0;   //接受数据包的大小QByteArray block;//判断通信的套接字通信的读缓冲区中还有多少数据可以读//这里也是递归函数的终止条件if(m_tcp->bytesAvailable() == 0){qDebug()<<"没有数据拜拜了。。。。。。。。。。。。。。。。";return ;}//有数据,首先先读包头if(m_tcp->bytesAvailable() >= sizeof(int)){//先读取四个字节,得到包头,知道这一行的数据包长度QByteArray head = m_tcp->read(sizeof(int));//大端数据转换成小端数据//totalBytes =qFromBigEndian(*(int*)head.data());totalBytes = qFromLittleEndian(*(int*)head.data());qDebug()<<"包头的长度12:"<<totalBytes;}else{return;}//读取数据块while(totalBytes - recvBytes > 0 && m_tcp->bytesAvailable()){//没有读完就接着读,把一行的数据都读完,所以这个block采用内容追加的方法读取block.append(m_tcp->read(totalBytes - recvBytes));recvBytes = block.size();}//qDebug()<<block;//当前数据包的数据读完了if(totalBytes == recvBytes){emit message(block);}//但是这个读缓冲区中可能有其他时刻发送过来的数据包//如果还有数据,那么继续读下一个数据包if(m_tcp->bytesAvailable() > 0 ){//递归函数开始qDebug()<<"开始递归调用。。。。。。";dealData();}
    }

效果演示

在这里插入图片描述
在这里插入图片描述

注意点

  1. 这里用到的子线程,因为主线程时ui线程,主要用于ui界面窗口的操作,子线程进行数据操作,这里子线程时负责Tcp套接字通信的

  2. 由于套接字对象是不可以在主线程和子线程中进行传递的,所以只好传递文件描述符,重写了QTcpServer类,自定义了一个MyTcpServer类,继承自QTcpServer

  3. 其中为了避免Tcp通信中的粘包问题

    • 在发送端(服务器端)进行封包,在数据包前加上4字节的数据,表明一次传递的数据包的长度。核心代码如下:
       QFile file(path);bool bl = file.open(QFile::ReadOnly);if(bl){//实际上是一行行的读取文件然后发送出去while(!file.atEnd()){QByteArray line = file.readLine();qDebug()<<"line.size():"<<line.size();//添加包头int len = qToLittleEndian(line.size());//int len = qToBigEndian(line.size());qDebug()<<"len.size():"<<len;//重新创建一个数据,并且将这一行的数据的数据长度放入数据头部QByteArray data((char*)(&len),4);//追加内容data.append(line);//发送数据给客户端m_tcp->write(data);emit text(data);QThread::msleep(50);  //msleep 表示要休眠的时间,单位是毫秒}}//file.close();
      
    • 在客户端,进行拆包,把发送端,发送来的每一个数据包都先读取四字节,知道什么一次数据的长度。核心代码如下:
      
      void RecvFile::connectServer(QString ip, unsigned short port)
      {qDebug()<<"子线程线程id:"<<QThread::currentThread();//连接服务器m_tcp = new QTcpSocket;//非阻塞函数,这里并不代表客户端和服务器已经建立起了连接m_tcp->connectToHost(QHostAddress(ip),port);//若是成功建立连接,会发出connected信号,调用connectOK函数connect(m_tcp,&QTcpSocket::connected,this,&RecvFile::connectOK);connect(m_tcp,&QTcpSocket::readyRead,this,[=](){// QByteArray all = m_tcp->readAll();// emit message(all);dealData();m_tcp->close();m_tcp->deleteLater();emit gameOver();});
      }void RecvFile::dealData()
      {//注意,数据包是一行行的送过来的unsigned int totalBytes = 0;  //当前一行的数据包的数据大小unsigned int recvBytes = 0;   //接受数据包的大小QByteArray block;//判断通信的套接字通信的读缓冲区中还有多少数据可以读//这里也是递归函数的终止条件if(m_tcp->bytesAvailable() == 0){qDebug()<<"没有数据拜拜了。。。。。。。。。。。。。。。。";return ;}//有数据,首先先读包头if(m_tcp->bytesAvailable() >= sizeof(int)){//先读取四个字节,得到包头,知道这一行的数据包长度QByteArray head = m_tcp->read(sizeof(int));//大端数据转换成小端数据//totalBytes =qFromBigEndian(*(int*)head.data());totalBytes = qFromLittleEndian(*(int*)head.data());qDebug()<<"包头的长度12:"<<totalBytes;}else{return;}//读取数据块while(totalBytes - recvBytes > 0 && m_tcp->bytesAvailable()){//没有读完就接着读,把一行的数据都读完,所以这个block采用内容追加的方法读取block.append(m_tcp->read(totalBytes - recvBytes));recvBytes = block.size();}//qDebug()<<block;//当前数据包的数据读完了if(totalBytes == recvBytes){emit message(block);}//但是这个读缓冲区中可能有其他时刻发送过来的数据包//如果还有数据,那么继续读下一个数据包if(m_tcp->bytesAvailable() > 0 ){//递归函数开始qDebug()<<"开始递归调用。。。。。。";dealData();}
      }
      
  4. 另外需要注意,在进行数据封包的时候,在加上数据长度之前,要进行大小端的转换,一般来说个人PC都是小端存储,但是作者的电脑这里确实大端存储,需要注意

http://www.xdnf.cn/news/7660.html

相关文章:

  • Runtipi - 开源个人家庭服务器管理工具
  • C#调用GTS控制板
  • DeepSeek+PiscTrace+YOLO:迅速实现Mask掩码抠图
  • IEEE 802.1Q协议下封装的VLAN数据帧格式
  • 【ISP算法精粹】什么是global tone mapping和local tone mapping?
  • 异步复位,同步释放
  • FineBI 和 Axure工具比较——数据分析VS原型设计
  • 常见回归损失函数详解:L1 Loss, L2 Loss, Huber Loss
  • 能碳一体化的核心功能模块
  • 【图像大模型】Kolors:基于自监督学习的通用视觉色彩增强系统深度解析
  • 抓包分析工具与流量监控软件
  • C语言入门
  • SQLite基础及优化
  • 从0到1搭建shopee测评自养号系统:独立IP+硬件伪装+养号周期管理​
  • [概率论基本概念1]什么是经验分布
  • 【NLP 76、Faiss 向量数据库】
  • Easylogging使用笔记
  • 【开源】一个基于 Vue3 和 Electron 开发的第三方网易云音乐客户端,具有与官方客户端相似的界面布局
  • pom.xml中的runtime
  • 关于汇编语言与接口技术——单片机串行口的学习心得
  • thread 的mutex优化
  • 基于FFT变换的雷达信号检测和参数估计matlab性能仿真
  • 每日两道leetcode(今天开始刷基础题模块——这次是之前的修改版)
  • ES 调优帖:关于索引合并参数 index.merge.policy.deletePctAllowed 的取值优化
  • 数字展厅是什么?怎样实现数字展厅的落地?
  • matlab编写的BM3D图像去噪方法
  • SpringBoot-4-Spring Boot项目配置文件和日志配置
  • 电子制造企业智能制造升级:MES系统应用深度解析
  • centos7安装mysql8.0
  • Android trace presentFence屏幕显示的帧