目录
一、关键技术点
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