49黑马QT笔记之利用TCP传输文件
前提:
黑马视频的代码第二次不能传输的原因是因为:客户端在第一次传输完成后,isStart=false。即第二次无法接收文件信息并打开文件,所以他会出现"write:device not open"。我们在文件接收完毕后,即文件相等那里重置isStart=true即可。
1 服务端流程:
1) 正常连接通信。
2) 选择文件按钮,弹出对话框选择文件,在该槽函数初始化文件信息info。包括文件名,大小,文件对象,已发送数据大小等等。
3) 发送文件按钮,先组包发送一次文件信息。(发送成功就利用定时器延时一定秒数,再发送完整的文件数据。实现延时的操作就是把原本要写的内容写在timeout信号的槽函数中。)
4) 发送完整的文件数据—发送数据先file.read读出来,再write发送过去,循环读和发送。最后再判断发送的数据和文件大小是否相同即可。
注:
1)服务端是发送了两次文件信息,第一次只是文件信息;第二次包括文件信息和数据。延时是为了防止黏包。
2)文件头与文件信息不对等,文件头是文件前54字节,包括文件各种信息。这里的例子文件信息只是自定义指文件名和文件大小。
2 客户端流程:
1 网络连接(tcpsocket,connect按钮)。
2 读文件信息和整个文件:
– 1)若为文件信息 —初始化接收文件的信息并打开接收文件。
– 2)若为整个文件 —写进文件对象file(不用while,因为有readyread),当recvSize==fileSize时,用信息框提示文件接收完成并重置isStart,为下一次连接传输作准备。
注:接收文件信息和整个文件用isStart标志位区分。默认接收文件路径在当前文件夹,不同环境可能不同。
3 代码:
1)服务端头文件:
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include<QTcpServer>
#include<QTcpSocket>
#include<QFile>
#include<QTimer>
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
void sendData(); //自己封装一个发送文件数据的函数
private slots:
void on_buttonFile_clicked();
void on_buttonsend_clicked();
private:
Ui::Widget *ui;
QTcpServer *tcpServer;
QTcpSocket *tcpSocket;
QFile file; //文件对象
QString fileName; //文件名字
qint64 fileSize; //文件大小
qint64 sendSize; //已经发生文件的大小
QTimer timer; //定时器
};
#endif // WIDGET_H
2)服务端实现文件:
#include "widget.h"
#include "ui_widget.h"
#include<QFileDialog>
#include<QIODevice>
#include<qDebug>
//1 TCP连接
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
//1监听套接字
tcpServer=new QTcpServer(this);
//2监听
tcpServer->listen(QHostAddress::Any,8888);
setWindowTitle("服务器端口为:8888");
//没连接之前两个按钮都不能按
ui->buttonFile->setEnabled(false);
ui->buttonsend->setEnabled(false);
//3如果客户端成功和服务器连接
//tcpServer会自动触发newConnection()
connect(tcpServer,&QTcpServer::newConnection,
[=]()
{
//取出建立好连接的套接字
tcpSocket=tcpServer->nextPendingConnection();
//获取对方的ip和端口号
QString ip=tcpSocket->peerAddress().toString();
quint16 port=tcpSocket->peerPort();
QString str=QString("[%1:%2] 成功连接").arg(ip).arg(port);
ui->textEdit->setText(str); //1 以上三步为了在文本编辑区显示客户端 ip和端口
//成功连接后,才能选择文件
ui->buttonFile->setEnabled(true);
}
);
connect(&timer,&QTimer::timeout,
[=]()
{
//关闭定时器
timer.stop();
//发送文件
sendData();
}
);
}
Widget::~Widget()
{
delete ui;
}
//2 选择文件按钮---初始化
void Widget::on_buttonFile_clicked()
{
QString filePath=QFileDialog::getOpenFileName(this,"open","../");
if(false==filePath.isEmpty()) //如果选择文件路径有效
{
fileName.clear();
fileSize=0;
//获取文件信息
QFileInfo info(filePath);
fileName=info.fileName();
fileSize=info.size();
sendSize=0;
//只读方式打开
//指定文件的名字
file.setFileName(filePath);
//打开文件
bool isOk=file.open(QIODevice::ReadOnly);
if(false==isOk)
{
qDebug() <<"只读方式打开文件失败 75";
return;
}
//提示打开文件的路径
ui->textEdit->append(filePath);
ui->buttonFile->setEnabled(false);
ui->buttonsend->setEnabled(true);
}
else
{
qDebug()<<"选择文件路径出错 82";
return;
}
}
//3 发送文件按钮--文件头
void Widget::on_buttonsend_clicked()
{
//按下发送给它不能再按 因为发送一次就断开一次连接
ui->buttonsend->setDisabled(true);
//先发送文件头信息 文件名##文件大小
QString head=QString("%1##%2").arg(fileName).arg(fileSize);
//发送头部信息
qint64 len=tcpSocket->write(head.toUtf8().data());
if(len>0) //头部信息成功发生
{
//发生真正的文件信息
//防止TCP粘包文件
//需要通过定时器延时20ms
//实现延时的操作就是把原本要写的内容写在timeout信号的槽函数中。
timer.start(20);
}
else
{
qDebug()<<"头部信息大小:"<<len;
qDebug()<<"头部信息发生失败 137";
file.close();
// tcpSocket->disconnectFromHost();
// tcpSocket->close(); //2 不用关连接,只需要关闭文件让他在选择文件按钮 发送文件(会跳回send按钮)就好
ui->buttonFile->setEnabled(true);
ui->buttonsend->setEnabled(false);
}
}
//4 开始发送文件--也包括文件头54个字节和真正数据
// 前面发送文件信息是为了给客户端要接收多大的文件
void Widget::sendData()
{
qint64 len=0;
do
{
//每次发送数据的大小
char buf[4096]={0};
len=0;
//往文件中读数据,返回实际读到的字节数
len=file.read(buf,sizeof(buf)); //3 读只有这一种,写有单参数和双参数两种
//发送数据,读多少,发多少
len=tcpSocket->write(buf,len); //4 与发送头部信息的单参数有区别,其实上面也可用两个参数的
//发送的数据需要累积
sendSize+=len;
}while(len>0); //5 如果写过去的数据为0,则表示文件写完,do while()结束
//是否发送文件完毕
if(sendSize==fileSize) //6 其实这个判断我感觉多余了,因为已经结束了,提示发送完毕然后直接关闭文件和连接就好了,保险起见吧
{
ui->textEdit->append("!!!文件发送完毕!!!");
file.close();
//把客户端断开
tcpSocket->disconnectFromHost(); //7 这次要全部断开了
tcpSocket->close();
}
}
3)客户端头文件:
#ifndef CILENT_H
#define CILENT_H
#include <QWidget>
#include<QTcpSocket>
#include<QFile>
namespace Ui {
class cilent;
}
class cilent : public QWidget
{
Q_OBJECT
public:
explicit cilent(QWidget *parent = 0);
~cilent();
private slots:
void on_pushButton_clicked(); //connect按钮,忘记改名了
private:
Ui::cilent *ui;
QTcpSocket *tcpSocket;
QFile file; //文件对象
QString fileName; //文件名字
qint64 fileSize; //文件大小
qint64 recvSize; //已经接收文件的大小
bool isStart; //用于区分接收文件头和文件数据
};
#endif // CILENT_H
4)客户端实现文件:
#include "cilent.h"
#include "ui_cilent.h"
#include<QIODevice>
#include<QMessageBox>
#include<QHostAddress>
cilent::cilent(QWidget *parent) :
QWidget(parent),
ui(new Ui::cilent)
{
ui->setupUi(this);
tcpSocket=new QTcpSocket(this);
isStart=true;
//等待对方发送数据---构造时不会执行 只有readyRead触发才会执行--所以connect你可以认为不是顺序执行
connect(tcpSocket,&QTcpSocket::readyRead,
[=]()
{
//1)取出接收的内容
QByteArray buf=tcpSocket->readAll();
//接收文件头
if(true==isStart)
{
//下一次接收整个文件
isStart=false;
//初始化
fileName=QString(buf).section("##",0,0);
file.setFileName(fileName);
fileSize=QString(buf).section("##",1,1).toInt();
recvSize=0;
//打开文件
bool isOk=file.open(QIODevice::WriteOnly);
if(false==isOk)
{
qDebug()<<"WriteOnly error 37";
}
}
//2)接收文件信息
else
{
//file.read(buf); //写进file应用write 读进buf应用read
qint64 len=file.write(buf); //写buf里的数据到设备file
recvSize+=len;
if(recvSize==fileSize) //相等则用信息框提示接收完成 然后关闭文件和连接
{
file.close();
QMessageBox::information(this,"完成","文件接收完成");
tcpSocket->disconnectFromHost();
tcpSocket->close();
//使第二次仍能打开 解决"write:device not open"
isStart=true;
}
}
});
}
cilent::~cilent()
{
delete ui;
}
//connect按钮只用于连接
void cilent::on_pushButton_clicked()
{
//获取服务器ip和端口号
QString ip=ui->lineEdit->text();
quint16 port=ui->lineEdit_2->text().toInt();
tcpSocket->connectToHost(QHostAddress(ip),port);
}
4 服务端与客户端的ui界面: