Qt中解决Tcp粘包问题
Qt中解决Tcp粘包问题
- Qt中解决Tcp粘包问题——以文件发送为例
- 服务器端
- 客户端
- 效果演示
- 注意点
Qt中解决Tcp粘包问题——以文件发送为例
创建的工程如下图所示:
服务器端
界面的布局以及名称如下图所示:
并且在Qt中增加网络模块
QT += core gui network
-
主函数中的代码不动,和创建时一样
#include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[]) {QApplication a(argc, argv);MainWindow w;w.show();return a.exec(); }
-
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
-
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()); }
-
重写
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); }
-
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
-
主函数依旧不变动,与创建时保持一致
#include "mainwindow.h"#include <QApplication>int main(int argc, char *argv[]) {QApplication a(argc, argv);MainWindow w;w.show();return a.exec(); }
-
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
-
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); }
-
子线程中用于接受数据的类
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();} }
效果演示
注意点
-
这里用到的子线程,因为主线程时ui线程,主要用于ui界面窗口的操作,子线程进行数据操作,这里子线程时负责Tcp套接字通信的
-
由于套接字对象是不可以在主线程和子线程中进行传递的,所以只好传递文件描述符,重写了
QTcpServer
类,自定义了一个MyTcpServer
类,继承自QTcpServer
-
其中为了避免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都是小端存储,但是作者的电脑这里确实大端存储,需要注意