Bootstrap

QT项目二(支持大文件传输的文件传输程序)

目录

一、关键技术点

1、服务器多线程

2、收发数据

具体代码

1、服务器

main.cpp

myservices.cpp

myservices.h

thread.cpp

thread.h

客户端

main.cpp

widget.cpp

widget.h

mythread.cpp

mythread.h


一、关键技术点

1、服务器多线程

要实现服务器一对多需要自定义一个继承于qtcpserver的类,并重写该类中的inconingconnection函数。
在该函数中再new一个新线程,在线程中接收数据。当有客户端连接时,就会调用inconingconnection函数,实例化一个线程。

 

2、收发数据

1、connect(newsocet,SIGNAL(readyRead()),this,SLOT(parsing_data()));//有数据来就读取
服务器中readyRead(),一有数据就读取,但是经过多次实验发现:发送端发送数据与接收端接收端接收数据并不是一一对应的,这应该是qt做过某种优化。有可能,发送了几次数据,而接收端只触发一次信号。再有,如果发送端速率远远大于接收的速率,程序直接就崩溃了。如果是只是发一些小的文件,这些完全不用考虑,收发各一次就可以了。
    本项目想收发个超级大的文件,就必须解决该问题。归根到底就是,tcp发送与接收不一致导致的粘包问题。
    
    思路:
        1、自定义一个结构体,对收发数据进行分类。先保证双方具有收发条件(发送方能打开文件并读取文件,接收方能创建文件并写入文件),再进行发送数据。
        2、由于qt中用readyRead(),本来就会导致收发速率不一致,那就只有牺牲效率保证效果(我自己的想法)。进行约定,发送方发一个包,等到接收放接收后发一个反馈包后再发下一个(我这里默认接收方发的反馈包发送方一定能接收到,其实这里应该弄一个定时器,到了一定时间还没有反馈包就应该退出程序了,否则一直阻塞在等待反馈)。
        3、收发双方,每次收到的数据都会根据结构体不同的标志位,拥有不同的处理方式。
        4、客户端发数据线程与主界面用信号与槽交流打印进度条。
       
​




​

 

效果演示:

 

具体代码

1、服务器

main.cpp

#include <QCoreApplication>
#include "myservices.h"
​
int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    myservices server;
​
    server.listen(QHostAddress::AnyIPv4,6150);
​
    return a.exec();
}

myservices.cpp

#include "myservices.h"
​
myservices::myservices(QObject * parent )
    : QTcpServer(parent)
{
​
}

myservices.h

#ifndef MYSERVICES_H
#define MYSERVICES_H
#include<QTcpServer>
#include<QThread>
#include<QTcpSocket>
#include "thread.h"
#include<QVector>
#include<QAbstractSocket>
#include<QDebug>
#include<QHostAddress>
class myservices : public QTcpServer
{
    Q_OBJECT
public:
    myservices(QObject * parent =nullptr );
​
 private:
    void incomingConnection(qintptr s)
    {
        th = new qthread(s);
        th->start();
    }
public slots:
    //void stat_run();
​
private:
​
    qthread * th;
​
};
​
#endif // MYSERVICES_H

thread.cpp

 

#include "thread.h"
​
qthread::qthread(qintptr newsocket_qintptr)
    :newsocket_qintptr(newsocket_qintptr)
{
    newsocet =new QTcpSocket;
    newsocet->setSocketDescriptor(newsocket_qintptr);//由描述符获得套接字
​
    connect(newsocet,SIGNAL(readyRead()),this,SLOT(parsing_data()));//有数据来就读取
​
    port=  newsocet->peerPort();
​
}
void qthread::run()
{
​
}
​
void qthread::parsing_data()//每次收到的信息需要分类处理
{
​
    struct stu buf;//接收信息的结构体
    array1 = new QByteArray(newsocet->readAll());//接收信息的数组
    //qDebug()<<"mm"<<++mm;
    memcpy(&buf,array1->data(),sizeof(struct stu));//转化到结构体
    if(0 == buf.type)//本次数据不是文件
    {
        get_data(array1);
    }else if(1 == buf.type)//本次数据为文件相关
    {
        if(1 == buf.a )
            get_filename(array1);//获取文件名
        if(1 == buf.b)//客户端文件打开成功
        {
            type2 =1;
            read_num = buf.read_num;
            tail_num = buf.tail_num;
            // qDebug()<<"readnum:"<<read_num<<"tailnum:"<<tail_num;
            struct stu buf;
            buf.c =0;
            buf.d = 2;
            send_buf_to_client(buf);//服务器最后一次确认,客户端收到后直接发数据
            return ;
        }
        else if(-1 == buf.b )
        {//不能进行文件传输,两个都置零,意味整个过程需要重新来.并且删除已经创建的文件
            type2 =0;
            type1 =0;
            file->remove();
        }
        if((1==type1)&& (1==type2))//准备工作已经做好,可以进入接收数据函数
​
        {
            get_file();//开始接收数据
        }
    }
}
​
void qthread::get_data(QByteArray *array)//获取其它信息
{
    QByteArray  array2;
​
    int i =0;
    struct stu buf;//接收信息的结构体
    int read_num =0;//数据包个数
    int tail_num =0 ;//最后一次数句字数
    memcpy(&buf,array->data(),sizeof(struct stu));//转化到结构体
    read_num = buf.read_num ;//将数据包个数读出来
    tail_num = buf.tail_num;//将最后一次字数读出来
    while(read_num--)
    {
        memcpy(&buf, array->data()+i*sizeof(struct stu),sizeof(struct stu));//将每个数据包从array中拿出来,发的结构体,拿的时候也拿结构体大小
        array2.append(buf.ar);
        i++;
    }
    qDebug()<<array2.toStdString().c_str();//打印到屏幕
    array->resize(0);
}
​
​
void qthread::get_filename(QByteArray *array)//获取文件名
{
    qDebug()<<"获取文件名";
    struct stu buf;//接收信息的结构体
    struct stu buf2;//回信息的结构体
    memcpy(&buf,array->data(),sizeof(struct stu));//转化到结构体
    qDebug()<< buf.filename;
    //根据文件名检查文件名是否重名
    QString str( buf.filename);
    filename = str.section('/',-1);//去除路径,获得文件名,
    QFileInfo fi(filename);
    if( fi.isFile())//文件已经存在。
    {
        buf2.c = -1;
        qDebug()<<"文件已经存在";
    }else{//文件不存在,可以创建文件。
        file =  new QFile(filename);
        if( file->open(QIODevice::WriteOnly))
        {
            buf2.c = 2;
            type1=1;
            qDebug()<<"创建文件成功";
            qDebug()<<"type1"<<type1;
        }//文件创建成功标志置位
        else {buf2.c =-2;}//创建失败
    }
    buf2.d=0;
    send_buf_to_client(buf2);//反馈创建文件情况
    array->resize(0);
}
​
void qthread::get_file()
{
    struct stu buf;//接收数据结构体
    memcpy(&buf, (array1->data()),sizeof(buf));//将每个数据包从array中拿出来,发的结构体,拿的时候也拿结构体大小
    ii++;//收取数据包计数
    struct stu buff;//反馈数据结构体
    buff.d =1;//反馈数据标志位
    buff.c = 0;//反馈数据标志位
​
    if(ii == (read_num))//最后一个包
    {
        file->write(buf.ar,tail_num);
        type1 =0;
        type2 = 0;
        ii=0;
        file->close();//传输完毕,所有标志位复位。等待下一次传输
        qDebug()<< filename<<"传输完毕";
        return;
    }else{
        qDebug()<< buf.ar;
        file->write(buf.ar,sizeof(buf.ar));
    }
    if(read_num !=1)//文件太小只需要传输一次,不需要再反馈
    {
        usleep(1000);
        send_buf_to_client(buff);//反馈创建文件情况
    }
}
void qthread::send_buf_to_client(struct stu buf)//给客户端回消息
{
    qDebug()<<"port: " <<port<<"反馈信息";
    QByteArray array1;
    array1.resize(sizeof(struct stu));
    memcpy(array1.data(),&buf,sizeof(struct stu));
    qDebug()<<"fanhui: "<< newsocet->write(array1) << "d" << buf.d;
    newsocet->waitForBytesWritten();
}
​

thread.h

#ifndef THREAD_H
#define THREAD_H
#define MAX (1024*13)
#include<QThread>
#include<QTcpSocket>
#include<iostream>
#include<QAbstractSocket>
#include<QDebug>
#include<QHostAddress>
#include<QInternal>
#include<QFile>
#include<QByteArray>
#include<QFile>
#include<QFileInfo>
struct stu{
    char filename[128];
    char ar[MAX];
    int type;
    int read_num;
    int a;//告诉服务器创建文件
    int b;//告诉服务器文件打开情况
    int c;//服务器反馈信息
    int d;
    int tail_num;
};
​
​
class qthread : public QThread
{
    Q_OBJECT
public:
    qthread(qintptr newsocket_qintptr);
    ~qthread()
    {
        newsocet->close();
        delete newsocet;
    }
    void  run();
    void send_buf_to_client(struct stu);
public slots:
    void get_file();//收文件
    void get_data(QByteArray*);//收数据
    void parsing_data();//解析数据,判断
    void get_filename(QByteArray*);//收取文件名
​
signals:
​
private:
    qintptr newsocket_qintptr;//套接字描述符
    QTcpSocket *newsocet;//连接套接字
    struct stu stu1;
    int num =0;
    QHostAddress ip;
    QFile *file;
    int type1 =0;//文件创建成功标志(0失败)
    int type2 = 0;//客户端文件打开成功标志(0失败,)
    int read_num;
    int tail_num;
    int ii=0;
    QByteArray *array1;
    int mm=0;
    int d=0;
    QString  filename;
​
    int port;
};
​
#endif // THREAD_H
​

客户端

main.cpp

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

widget.cpp

#include "widget.h"
Widget::Widget(QWidget *parent)
    : QWidget(parent)
{
    // pgd = new QProgressDialog;
    box = new QMessageBox;
    tcp = new QTcpSocket;
    te1 = new QTextEdit;//提示框
    te1->setMinimumSize(200,200);
    le = new QLineEdit("192.168.174.1");
    bt1 = new QPushButton("发送信息");
    bt3 = new QPushButton("取消连接");
    bt2 =new QPushButton("连接");
    bt4 = new QPushButton("选择文件");
    bt5= new QPushButton("发送文件");
    te2 = new QTextEdit;//ip输入
    //    te2->setMaximumSize(200,70);
    QGridLayout *gri = new QGridLayout;
    gri->addWidget(te1,0,0,1,3);//提示框
    gri->addWidget(le,1,0,1,2);//行编辑框
    gri->addWidget(bt2,1,2,1,1);//连接按钮
    gri->addWidget(te2,2,0,1,2);//输入框
    gri->addWidget(bt4,2,2,1,1);//选择文件按钮
    gri->addWidget(bt1,3,0,1,1);//发送按钮
    gri->addWidget(bt5,3,1,1,1);//发送文件按钮
    gri->addWidget(bt3,3,2,1,1);//取消连接按钮
    setLayout(gri);
    bt1->setDisabled(true);
    bt5->setDisabled(true);
​
    th = new mythread(te1,le,te2);
    connect(bt2,SIGNAL(clicked(bool)),th,SLOT(connect_server()));//连接服务器
    connect(th,&mythread::connect_sucess,[&](){//线程返回连接成功,使能按钮
        bt5->setDisabled(false);
        bt1->setDisabled(false);
    });
    connect(bt3,SIGNAL(clicked(bool)),th,SLOT(discinnect()));//断开连接
    connect(th,&mythread::disconnect_success,[&](){//线程返回断开连接成功,使能按钮
        bt5->setDisabled(true);//失能按钮
        bt1->setDisabled(true);//失能按钮
    });
    connect(bt4,SIGNAL(clicked(bool)),th,SLOT(choose_filename()));//选择文件
    connect(bt1,SIGNAL(clicked(bool)),th,SLOT(send_data()));//发送信息
    connect(bt5,SIGNAL(clicked(bool)),th,SLOT(send_filename()));//发送文件
    connect(th,SIGNAL(remind_send_file()),this,SLOT(start_send_file()));//收到线程信号,可以让线程工作了
    pgd = new QProgressBar;
    pgd->setRange(0,100);
    pgd->setMinimumSize(500,50);
    pgd->setMaximumSize(500,50);
    //pgd->setParent(this);
    pgd->hide();
    connect(th,SIGNAL(send_progress(int)),pgd,SLOT(setValue(int)));//进度条实时赋值
​
}
​
Widget::~Widget()
{
​
}
void Widget::start_send_file()
{
    th->start();
    pgd->show();
}

widget.h

#ifndef WIDGET_H
#define WIDGET_H
​
#include <QWidget>
#include<QDebug>
#include<QLineEdit>
#include<QTextEdit>
#include<QPushButton>
#include<QVBoxLayout>
#include<QHBoxLayout>
#include<QTcpSocket>
#include<QHostAddress>
#include<QGridLayout>
#include<QByteArray>
#include<QFileDialog>
#include<QByteArray>
#include<QMessageBox>
#include<QProgressDialog>
#include<QThread>
#include<QProgressBar>
#include "mythread.h"
​
​
class Widget : public QWidget
{
    Q_OBJECT
​
public:
    Widget(QWidget *parent = 0);
    ~Widget();
public slots:
        void start_send_file();
​
//        void stop_send_file();
​
signals:
​
private:
    QTextEdit *te1;
    QTextEdit *te2;
    QLineEdit *le;
    QPushButton *bt1;
    QPushButton *bt2;
    QPushButton *bt3;
    QPushButton *bt4;
    QPushButton *bt5;
    QTcpSocket *tcp;
    QMessageBox *box;
    QStringList *filepaths;
    QFile *file;
    // int b=0;//服务器确认项,创建新文件成功(0失败)
     QProgressBar *pgd;
​
    int read_num;//传输次数
    int tail_num;//最后一次字节数
    int d=0;
    mythread *th;
};
#endif // WIDGET_H
​


mythread.cpp

 

#include "mythread.h"
​
mythread::mythread(QTextEdit *te1, QLineEdit *le,QTextEdit *te2)
    :te1(te1),le(le),te2(te2)
{
    tcp = new QTcpSocket;
    connect(tcp,SIGNAL(readyRead()),this,SLOT(recv_data()));//接收信息
    box = new QMessageBox;
}
​
void mythread::run()
{
    int i = 0;
    struct stu buf;
    buf.type =1;
    buf.a=0;
    buf.b = 0;//三个标志置位,代表本次数据为文件内容,服务器无需判断直接存储。
    QByteArray array;
    array.resize(sizeof(struct stu));
    int read_num1  = read_num;//传输次数
    while(1)//文件内容打包好发出去
    {
        if(d!=0)
        {
            i++;
            qDebug()<<"read"<<read_num<<"tail_num"<<tail_num;
            file->read(buf.ar,sizeof(buf.ar));
            qDebug()<<buf.ar;
            memcpy(array.data(),&buf,sizeof(struct stu));
            read_num--;
            tcp->write(array);
            usleep(3000);
            tcp->waitForBytesWritten(30000);
            d =0;
            emit send_progress((i%read_num1)>0?(i*100/read_num1 +1):(i*100/read_num1));//发送进度
        }
        if(!read_num)//传输完毕,退出循环
        {
            file->close();
            break;
        }
    }
    qDebug()<<"文件传输完毕";
}
​
void mythread::connect_server()//连接服务器
{
    QString str = le->text();
    tcp->connectToHost(str,6150);
    if(tcp->waitForConnected(1000)){
        te1->append("连接成功");
        emit connect_sucess();//告诉界面连接成功
    }else{
        te1->append("连接失败");
        emit connect_failed();
    }
}
void mythread::discinnect()//断开连接
{
    tcp->close();
    te1->append("连接断开");
    emit disconnect_success();
}
​
void mythread::send_data()//发送较长消息
{
    int i =0;
    int num =0;
    struct stu buf;
    buf.type =0;
    QByteArray array;//发送数据载体
    QByteArray array1 =te2->toPlainText().toStdString().c_str() ;//把数据从文本框取出来
    QByteArray array2 ;//中转载体
​
    array.resize(sizeof(struct stu));//重新设置数组大小
    num = (array1.length())/(sizeof(buf.ar ));//本次发送整次数
    buf.tail_num = (array1.length())%(sizeof( buf.ar)) ;//最后一次发送的字节数
    if(buf.tail_num != 0){  num += 1;}//本次发送最终次数
    buf.read_num = num;
    qDebug()<< "num"<<buf.read_num;
    while(num--)
    {
        if(num ==0 && buf.tail_num!= 0)//最后一次数据包字数
        {
            array2 = array1.mid(array1.length()-buf.tail_num,buf.tail_num);
        }else{
            array2 = array1.mid(i*MAX,MAX);
        }
        strncpy( buf.ar,array2.data(),sizeof(buf.ar));//数据存入结构体
        memcpy(array.data(),&buf,sizeof(struct stu));//把结构体存入数组
        qDebug()<<  tcp->write(array);//把数组发出去
        qDebug()<<buf.ar;
        i++;
    }
    te1->append("my: " +te2->toPlainText());
    te2->clear();
    qDebug()<<"sucess";
    return;
}
​
void mythread::recv_data()//接收信息
{
    QByteArray array;
    struct stu buf;
    array.resize(sizeof(struct stu));
    array =  tcp->readAll();
    memcpy(&buf,array.data(),sizeof(struct stu));
    if(-1 == buf.c)//文件已经存在
    {
        qDebug()<<"文件已经存在";
        box->setText("文件名重名,重新改个名字试试吧。");
        box->setInformativeText("注意:需要更改源文件名字,而不是直接在本软件中更改。");
        box->setStandardButtons( QMessageBox::Discard | QMessageBox::Cancel);
        box->setDefaultButton(QMessageBox::Save);
        box->exec();
        return;
    }
    if(-2 == buf.c)//文件打开失败
    {
        return;
    }
    if(buf.d ==1)//服务器每次接收确认
    {
        d =buf.d;
    }else if(buf.d  ==2)//服务器最后一次确认。
    {
        d =buf.d;
        emit remind_send_file();//提醒界面可以启动线程run函数了
    }
    if(2 == buf.c)//服务器创建并文件打开成功
        open_file();//打开本地文件
}
​
​
​
void mythread::choose_filename()//选择文件
{
    QStringList list = QFileDialog::getOpenFileNames();
​
    filepaths = new QStringList(list);
    te2->clear();
    for(int i=0; i<filepaths->length(); i++)
    {
        te2->append((*filepaths)[i]);
    }
}
​
void mythread::send_filename()  //打包文件名,并发送
{
    struct stu buf_filename;
    QByteArray array;
    array.resize(sizeof(struct stu));//给array分配空间
    QByteArray array1= QString((*filepaths)[0]).toStdString().c_str();//将文件名转存在数组中
    strncpy(buf_filename.filename,array1.data(),sizeof(buf_filename.filename));//数组内容打包到结构体中
    buf_filename.type =1;//提示服务器,本次数据属于文件子路
    buf_filename.a =1;//高速服务器本条消息为文件名
    buf_filename.read_num =1;//本次仅传输一次
    memcpy(array.data(),&buf_filename,sizeof(buf_filename));//把结构体存入数组
    qDebug()<<  tcp->write(array);//把数组发出去
    //    QString str( buf_filename.filename);
    //    QString  filename = str.section('/',-1);//去除路径,获得文件名,
    file = new QFile(buf_filename.filename);
    qDebug()<<buf_filename.filename <<"文件名";
}
​
​
​
void mythread::open_file()//打开本地文件
{
    struct stu buf;
    QByteArray array;
    buf.type=1;
    buf.a=0;
    array.resize(sizeof(struct stu));
    if(file->open(QIODevice::ReadOnly))
    {
        buf.b=1;
        if(!file->size())
        {
            buf.b=-1;
            box->setText("源文件不能为空。");
            box->setInformativeText("请查看源文件是否为空。");
            box->setStandardButtons( QMessageBox::Discard | QMessageBox::Cancel);
            box->setDefaultButton(QMessageBox::Save);
            box->exec();
            file->close();
            memcpy(array.data(),&buf,sizeof(struct stu));
            tcp->write(array);//告诉服务器,文件为空。
            return ;
        }
​
        buf.tail_num = (file->size()%sizeof(buf.ar));
        buf.read_num = file->size()/sizeof(buf.ar) + ((buf.tail_num)>0?1:0);
        read_num = buf.read_num;//计算出传输次数
        tail_num =  buf.tail_num;//最后一次字节数
        qDebug()<<"readnum:"<<read_num<<"tailnum:"<<tail_num;
        qDebug()<<"filenum"<<file->size();
​
        memcpy(array.data(),&buf,sizeof(struct stu));
        tcp->write(array);//告诉服务器,这边已经准备好了
        tcp->waitForBytesWritten(3000);
    }else{
        qDebug()<<"sdsdesdwe";
        buf.b=-1;
        box->setText("文件打开失败");
        box->setInformativeText("文件打开失败,请检查下文件。");
        box->setStandardButtons( QMessageBox::Discard | QMessageBox::Cancel);
        box->setDefaultButton(QMessageBox::Save);
        box->exec();
        memcpy(array.data(),&buf,sizeof(struct stu));
        tcp->write(array);
        return ;
    }
    return ;
}
​


mythread.h

#ifndef MYTHREAD_H
#define MYTHREAD_H
#define MAX (1024*13)
​
#include<QHostAddress>
#include<QByteArray>
#include<QFileDialog>
#include<QMessageBox>
#include<QProgressDialog>
#include<QThread>
#include<QTcpSocket>
#include<QDebug>
#include<QTextEdit>
#include<QLineEdit>
#include<QObject>
#include<QFile>
#include<QFileDialog>
#include<QMessageBox>
#include<QProgressBar>
struct stu{
    char filename[128];//文件名
    char ar[MAX];//数据体
    int type;//传输的是否是文件
    int read_num;//传输次数
    int a;//告诉服务器创建文件
    int b;//告诉服务器文件打开情况
    int c;//服务器反馈信息
    int d=1;
    int tail_num;//最后一次字节数
};
​
class mythread : public QThread
{
    Q_OBJECT
public:
    mythread(QTextEdit *, QLineEdit *,QTextEdit *);
    void run();
​
    void open_file();//打开本地文件
    void send_filedata();//发送文件
signals:
    void connect_sucess();//发出信号,连接成功
    void connect_failed();//发出信号连接失败
    void disconnect_success();//发出信号已经断开连接
    void remind_send_file();//提醒界面可以启动线程run函数发文件了
    void  send_progress(int);
​
public slots:
    void connect_server();//执行连接
    void discinnect();//断开连接
    void choose_filename();//选择文件
    void send_data();//发送较长消息
    void send_filename();  //打包文件名,并发送
    void recv_data();//接收信息
​
​
private:
    QTcpSocket *tcp;
    QTextEdit *te1;
    QLineEdit *le;
    QTextEdit *te2;
    QStringList *filepaths;
    QFile *file;
    int read_num;//传输次数
    int tail_num;//最后一次字节数
    int d=1;
    QMessageBox *box;
​
};
​
#endif // MYTHREAD_H
​

;