TCP文件传输流程
今天学习TCP文件传输。下面是服务端向客户端发送文件的流程:
- 服务器向客户端发送文件,需要先选择一个文件,然后
获取文件的文件名和大小
。将文件名和文件大小组成一个文件头信息
,通过通信套接字发送给客户端。 - 客户端从服务端接收到文件头信息后,要按照一定的规则对字符串进行解析,从中获取文件的大小、文件名等信息。有了文件名,客户端就可以在本地创建一个同名的文件。
- 接着,服务端开始读文件并发送数据(读多少发多少),循环执行这个操作,一直到
已发送的文件大小
与文件实际大小
一致时,才结束。每次发送数据都要更新这个已发送的文件大小。 - 客户端这边,服务器发来多少数据,客户端就读多少数据,然后把接收到的数据写到本地文件里。
黏包问题
由于两次发送数据的时间间隔很短,因此很可能会出现应该分开发送的文件头信息和文件内容“黏”在一起,被一块发送出去的情况,这种现象就叫作“黏包”
。解决“黏包”问题有一个很简单的方法,可以用定时器来设置一段延迟,当服务端向客户端发送文件头信息后,经过一段延时,再发送文件内容。
当然,在实际的文件传输中,不仅仅在文件头信息与文件内容之间会产生黏包问题,数据报之间也会产生黏包问题,这时再用定时器解决黏包问题效率就不够高了,我们还可以利用各种各样的协议解决黏包问题。在这个小demo中,就暂时先用定时器吧-v-。
服务端的实现
在布置好基本的界面后,首先要做的是创建监听套接字QTcpServer
,接着对指定主机地址下的指定端口进行监听listen()
,等待客户端主动进行连接,连接成功后服务端自动触发newConnection()信号
,在对应的槽函数中获取通信套接字QTcpSocket
。
//创建监听套接字
p_tcpServer=new QTcpServer(this);
p_tcpSocket=NULL;
p_tcpServer->listen(QHostAddress::Any,8888);
//建立连接
connect(p_tcpServer,&QTcpServer::newConnection,this,&ServerWidget::getConnection)
复制代码
void ServerWidget::getConnection(){
//取出通信套接字
p_tcpSocket=p_tcpServer->nextPendingConnection();
//获取客户端IP和端口号
QString ip=p_tcpSocket->peerAddress().toString();
quint16 port=p_tcpSocket->peerPort();
p_label->setText(QString("[%1:%2] 接连成功!").arg(ip).arg(port));
//将按钮重新置为可用
p_select->setEnabled(true);
p_send->setEnabled(true);
}
复制代码
注意:在这里可以对“选择文件”按钮和“发送文件”按钮做一个处理,在服务端和客户端之间的连接成功建立起来之前,将两个按钮状态设置为不可用。在连接成功后才置为可用,这样能够防止在连接没有建立起来之前就将文件发送出去。
接下来,处理“选择文件”按钮。点击按钮,在槽函数中调用QFileDialog::getOpenFileName()
获取文件路径,再调用QFileInfo的fileName()
和size()
方法获取文件的文件名和文件大小。为了知道当前文件传输的进度,还要定义一个用来记录已发送的文件大小的变量,并初始化为0。然后,以只读方式打开文件。
void ServerWidget::selectFile(){
QString filePath=QFileDialog::getOpenFileName(this,"选择文件","C:/Users/MSI-NB/Desktop");
//打开文件
if(!filePath.isEmpty()){
//先清空
m_fileName.clear();
m_fileSize=0;
//获取文件名和文件大小
QFileInfo fileInfo=QFileInfo(filePath);
m_fileName=fileInfo.fileName();
m_fileSize=fileInfo.size();
//将发送文件的大小初始化为0
m_sendSize=0;
//以只读的方式打开文件
m_file.setFileName(filePath);
m_file.open(QIODevice::ReadOnly);
//提示打开文件的路径
p_label->setText(QString("文件路径为:%1").arg(filePath));
}
}
复制代码
P.S:文件对象和文件名、文件大小最好是作为类的成员数据,便于全局操作。
下面要处理的是“发送文件”按钮。为了使客户端能够在接收文件内容数据之前,先获取从服务端传过来的文件的文件名和文件大小,从而在客户端主机上创建一个同名的文件,我们需要先从服务端发送一段文件头信息
,然后再发送真正的文件内容信息,并且还要利用定时器来解决黏包问题。
假设我们以文件名##文件大小
这样的格式来组装文件头信息:
void ServerWidget::sendFile(){
//组装文件头信息
QString head=QString("%1##%2").arg(m_fileName).arg(m_fileSize);
//写入通信套接字并获取成功写入的字符个数
qint64 length=p_tcpSocket->write(head.toUtf8());
//文件头信息发送成功
if(length>0){
//用定时器解决黏包问题
//启动计时器
m_timer.start(20);
connect(&m_timer,&QTimer::timeout,this,&ServerWidget::sendData);
}
}
复制代码
文件信息头发送成功后,还需要延迟一定的时间再启动一个计时器,计时器的timeout()信号
被触发时,在相应的槽函数中发送真正的文件内容数据。
服务端发送文件的步骤是:从文件中读取指定大小的内容,再将这些内容写入通信套接字中,每次对已发送文件大小进行更新,一直循环执行直到已发送文件大小等于文件实际大小。
void ServerWidget::sendData(){
//首先关闭计时器
if(m_timer.isActive()){
m_timer.stop();
}
qint64 len=0;
char buf[1024];
do{
//先读取文件内容数据
len=m_file.read(buf,sizeof(buf));
//读多少,写多少
len=p_tcpSocket->write(buf,len);
m_sendSize+=len;
}while(len>0);
if(m_sendSize==m_fileSize){
//提示文件发送完毕并关闭文件
QMessageBox::about(this,"提示","文件发送完毕!");
//断开连接
p_tcpSocket->disconnectFromHost();
p_tcpSocket->close();
}
}
复制代码
注意:计时器在这里只启动一次,是为了延时调用sendData()方法,因此在达到目的后就要将它关闭。
完整代码:
//ServerWidget.h
#ifndef SERVERWIDGET_H
#define SERVERWIDGET_H
#pragma execution_character_set("utf-8")
#include <QWidget>
#include <QLabel>
#include <QPushButton>
#include <QTcpServer>
#include <QTcpSocket>
#include <QFile>
#include <QTimer>
class ServerWidget : public QWidget
{
Q_OBJECT
public:
ServerWidget(QWidget *parent = 0);
~ServerWidget();
private:
QLabel *p_label;
QPushButton *p_select;
QPushButton *p_send;
QTcpServer *p_tcpServer;
QTcpSocket *p_tcpSocket;
QFile m_file;
QString m_fileName;
qint64 m_fileSize;
qint64 m_sendSize;
QTimer m_timer;
protected:
void ServerWidget::selectFile();
void ServerWidget::sendFile();
void ServerWidget::getConnection();
void ServerWidget::sendData();
};
#endif // SERVERWIDGET_H
复制代码
//ServerWidget.cpp
#include "ServerWidget.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QFileDialog>
#include <QFileInfo>
#include <QTimer>
#include <QMessageBox>
ServerWidget::ServerWidget(QWidget *parent)
: QWidget(parent)
{
//界面
this->resize(640,480);
QVBoxLayout *p_vlayout=new QVBoxLayout(this);
QHBoxLayout *p_hlayout=new QHBoxLayout();
p_label=new QLabel("暂无文件",this);
p_label->setAlignment(Qt::AlignCenter);
p_select=new QPushButton("选择文件",this);
p_send=new QPushButton("发送文件",this);
p_vlayout->addWidget(p_label);
p_vlayout->addLayout(p_hlayout);
p_hlayout->addWidget(p_select);
p_hlayout->addStretch();
p_hlayout->addWidget(p_send);
p_select->setEnabled(false);
p_send->setEnabled(false);
//创建监听套接字
p_tcpServer=new QTcpServer(this);
p_tcpSocket=NULL;
p_tcpServer->listen(QHostAddress::Any,8888);
//建立连接
connect(p_tcpServer,&QTcpServer::newConnection,this,&ServerWidget::getConnection);
//按钮的信号与槽
connect(p_select,&QPushButton::clicked,this,&ServerWidget::selectFile);
connect(p_send,&QPushButton::clicked,this,&ServerWidget::sendFile);
}
ServerWidget::~ServerWidget()
{
}
void ServerWidget::selectFile(){
QString filePath=QFileDialog::getOpenFileName(this,"选择文件","C:/Users/MSI-NB/Desktop");
//打开文件
if(!filePath.isEmpty()){
//先清空
m_fileName.clear();
m_fileSize=0;
//获取文件名和文件大小
QFileInfo fileInfo=QFileInfo(filePath);
m_fileName=fileInfo.fileName();
m_fileSize=fileInfo.size();
//将发送文件的大小初始化为0
m_sendSize=0;
//以只读的方式打开文件
m_file.setFileName(filePath);
m_file.open(QIODevice::ReadOnly);
//提示打开文件的路径
p_label->setText(QString("文件路径为:%1").arg(filePath));
}
}
void ServerWidget::sendFile(){
//组装文件头信息
QString head=QString("%1##%2").arg(m_fileName).arg(m_fileSize);
//写入通信套接字并获取成功写入的字符个数
qint64 length=p_tcpSocket->write(head.toUtf8());
//文件头信息发送成功
if(length>0){
//用定时器解决黏包问题
//启动计时器
m_timer.start(20);
connect(&m_timer,&QTimer::timeout,this,&ServerWidget::sendData);
}
}
void ServerWidget::getConnection(){
//取出通信套接字
p_tcpSocket=p_tcpServer->nextPendingConnection();
//获取客户端IP和端口号
QString ip=p_tcpSocket->peerAddress().toString();
quint16 port=p_tcpSocket->peerPort();
p_label->setText(QString("[%1:%2] 接连成功!").arg(ip).arg(port));
//将按钮重新置为可用
p_select->setEnabled(true);
p_send->setEnabled(true);
}
void ServerWidget::sendData(){
//首先关闭计时器
if(m_timer.isActive()){
m_timer.stop();
}
qint64 len=0;
char buf[1024];
do{
//先读取文件内容数据
len=m_file.read(buf,sizeof(buf));
//读多少,写多少
len=p_tcpSocket->write(buf,len);
m_sendSize+=len;
}while(len>0);
if(m_sendSize==m_fileSize){
//提示文件发送完毕并关闭文件
QMessageBox::about(this,"提示","文件发送完毕!");
//断开连接
p_tcpSocket->disconnectFromHost();
p_tcpSocket->close();
}
}
复制代码
客户端的实现
在布置好基本的界面后,由客户端主动向服务端发起连接,在连接的过程中来创建与服务端对应的通信套接字QTcpSocket
:
//与服务端连接
connect(p_connectButton,&QPushButton::clicked,this,&ClientWidget::getConnection);
void ClientWidget::getConnection(){
//获取服务端ip和端口号
QString ip=p_ipEdit->text();
quint16 port=p_portEdit->text().toInt();
//发起连接
p_tcpSocket->connectToHost(QHostAddress(ip),port);
}
复制代码
然后等待服务端往通信套接字中写入数据从而触发客户端的readyRead()信号
,在对应的槽函数中,解析文件头信息:
//收到从服务端发来的消息
connect(p_tcpSocket,&QTcpSocket::readyRead,this,&ClientWidget::recvFile);
void ClientWidget::recvFile(){
//先取出读取到的内容
QByteArray buf=p_tcpSocket->readAll();
//判断是否是文件头信息
//如果是,解析文件头,获取文件大小和文件名,并创建文件
//否则,读取文件内容数据
if(isHead){
//说明还未解析头信息
isHead=false;
//解析文件头,获取文件名和文件大小
m_fileName=QString(buf).section("##",0,0);
m_fileSize=QString(buf).section("##",1,1).toInt();
m_recvSize=0;
//打开文件
m_file.setFileName(m_fileName);
m_file.open(QIODevice::WriteOnly);
}else{
//将数据写入本地文件并返回成功写入的字符个数,这里不需要用到循环是因为这是服务器调用write时被动触发的,服务器循环调用write,此函数也会被循环调用
qint64 len=m_file.write(buf);
m_recvSize+=len;
if(m_recvSize==m_fileSize){
//文件传输完毕则关闭文件
m_file.close();
QMessageBox::about(this,"提示","文件接收成功!");
p_tcpSocket->disconnectFromHost();
p_tcpSocket->close();
}
}
}
复制代码
客户端接收文件的步骤是:调用readAll()
从文件中读取内容,接着判断是否已经对文件头进行解析,如果未解析,则进行初始化工作并解析文件头,从文件头信息中获取文件的文件名和文件大小,再将真正的文件内容数据写入通信套接字中,每次对记录已发送文件大小的变量进行更新,直到已发送文件大小等于文件实际大小。
实现效果:
在当前工程的build-文件夹下出现了从服务端传过来的文件。
进度条的实现
在文件传输的过程中,为了使程序更加人性化,进度条以及对应的文字提示是必不可少的。要使用进度条,需要先引入<QProgressBar>
头文件。进度条一般需要设置三个属性值,分别是最小值、最大值和当前值
,对应的方法是setMinimum()、setMaximum()、setValue()
。此外,因为文件的大小一般能达到KB级以上,而我们用fileSize()获取的文件大小是以字节(Byte)为单位的,如果文件太大,用qint64的变量来接收文件大小就很可能越界,所以最好将文件大小 / 1024,转化成KB级的大小。
//设置进度条
void ClientWidget::recvFile(){
//先取出读取到的内容
QByteArray buf=p_tcpSocket->readAll();
//判断是否是文件头信息
//如果是,解析文件头,获取文件大小和文件名,并创建文件
//否则,读取文件内容数据
if(isHead){
//说明还未解析头信息
isHead=false;
//解析文件头,获取文件名和文件大小
m_fileName=QString(buf).section("##",0,0);
m_fileSize=QString(buf).section("##",1,1).toInt();
m_recvSize=0;
//打开文件
m_file.setFileName(m_fileName);
m_file.open(QIODevice::WriteOnly);
//设置进度条
p_progressBar->setMinimum(0);
p_progressBar->setMaximum(m_fileSize/1024);
p_progressBar->setValue(0);
}else{
//将数据写入本地文件并返回成功写入的字符个数,这里不需要用到循环是因为这是服务器调用write时被动触发的,服务器循环调用write,此函数也会被循环调用
qint64 len=m_file.write(buf);
m_recvSize+=len;
p_progressBar->setValue(m_recvSize/1024);
if(m_recvSize==m_fileSize){
m_file.close();
p_tcpSocket->write("done");
QMessageBox::about(this,"提示","文件接收成功!");
p_tcpSocket->disconnectFromHost();
p_tcpSocket->close();
}
}
}
复制代码
实现效果:
完整代码:
//ServerWidget.h
#ifndef SERVERWIDGET_H
#define SERVERWIDGET_H
#pragma execution_character_set("utf-8")
#include <QWidget>
#include <QLabel>
#include <QPushButton>
#include <QTcpServer>
#include <QTcpSocket>
#include <QFile>
#include <QTimer>
class ServerWidget : public QWidget
{
Q_OBJECT
public:
ServerWidget(QWidget *parent = 0);
~ServerWidget();
private:
QLabel *p_label;
QPushButton *p_select;
QPushButton *p_send;
QTcpServer *p_tcpServer;
QTcpSocket *p_tcpSocket;
QFile m_file;
QString m_fileName;
qint64 m_fileSize;
qint64 m_sendSize;
QTimer m_timer;
protected:
void ServerWidget::selectFile();
void ServerWidget::sendFile();
void ServerWidget::getConnection();
void ServerWidget::sendData();
};
#endif // SERVERWIDGET_H
复制代码
//ServerWidget.cpp
#include "ServerWidget.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QFileDialog>
#include <QFileInfo>
#include <QTimer>
#include <QMessageBox>
#include <QDebug>
ServerWidget::ServerWidget(QWidget *parent)
: QWidget(parent)
{
//界面
this->resize(640,480);
this->setWindowTitle("服务端:8888");
QVBoxLayout *p_vlayout=new QVBoxLayout(this);
QHBoxLayout *p_hlayout=new QHBoxLayout();
p_label=new QLabel("暂无文件",this);
p_label->setAlignment(Qt::AlignCenter);
p_select=new QPushButton("选择文件",this);
p_send=new QPushButton("发送文件",this);
p_vlayout->addWidget(p_label);
p_vlayout->addLayout(p_hlayout);
p_hlayout->addWidget(p_select);
p_hlayout->addStretch();
p_hlayout->addWidget(p_send);
p_select->setEnabled(false);
p_send->setEnabled(false);
//创建监听套接字
p_tcpServer=new QTcpServer(this);
p_tcpSocket=NULL;
p_tcpServer->listen(QHostAddress::Any,8888);
//建立连接
connect(p_tcpServer,&QTcpServer::newConnection,this,&ServerWidget::getConnection);
//按钮的信号与槽
connect(p_select,&QPushButton::clicked,this,&ServerWidget::selectFile);
connect(p_send,&QPushButton::clicked,this,&ServerWidget::sendFile);
}
ServerWidget::~ServerWidget()
{
}
void ServerWidget::selectFile(){
QString filePath=QFileDialog::getOpenFileName(this,"选择文件","C:/Users/MSI-NB/Desktop");
//打开文件
if(!filePath.isEmpty()){
//先清空
m_fileName.clear();
m_fileSize=0;
//获取文件名和文件大小
QFileInfo fileInfo=QFileInfo(filePath);
m_fileName=fileInfo.fileName();
m_fileSize=fileInfo.size();
//将发送文件的大小初始化为0
m_sendSize=0;
//以只读的方式打开文件
m_file.setFileName(filePath);
m_file.open(QIODevice::ReadOnly);
//提示打开文件的路径
p_label->setText(QString("文件路径为:%1").arg(filePath));
}
}
void ServerWidget::sendFile(){
//组装文件头信息
QString head=QString("%1##%2").arg(m_fileName).arg(m_fileSize);
//写入通信套接字并获取成功写入的字符个数
qint64 length=p_tcpSocket->write(head.toUtf8());
//文件头信息发送成功
if(length>0){
//用定时器解决黏包问题
//启动计时器
m_timer.start(20);
connect(&m_timer,&QTimer::timeout,this,&ServerWidget::sendData);
}
}
void ServerWidget::getConnection(){
//取出通信套接字
p_tcpSocket=p_tcpServer->nextPendingConnection();
//获取客户端IP和端口号
QString ip=p_tcpSocket->peerAddress().toString();
quint16 port=p_tcpSocket->peerPort();
p_label->setText(QString("[%1:%2] 接连成功!").arg(ip).arg(port));
//将按钮重新置为可用
p_select->setEnabled(true);
p_send->setEnabled(true);
}
void ServerWidget::sendData(){
//首先关闭计时器
if(m_timer.isActive()){
m_timer.stop();
}
qint64 len=0;
char buf[1024];
p_label->setText(p_label->text().append("\n文件正在发送……"));
do{
//先读取文件内容数据
len=m_file.read(buf,sizeof(buf));
//读多少,写多少
len=p_tcpSocket->write(buf,len);
m_sendSize+=len;
}while(len>0);
if(m_sendSize==m_fileSize){
//提示文件发送完毕并关闭文件
p_label->setText(p_label->text().append("\n文件发送完毕!"));
//断开连接
p_tcpSocket->disconnectFromHost();
p_tcpSocket->close();
}
}
复制代码
//ClientWidget.h
#ifndef CLIENTWIDGET_H
#define CLIENTWIDGET_H
#pragma execution_character_set("utf-8")
#include <QWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QFile>
#include <QFileInfo>
#include <QTcpSocket>
#include <QProgressBar>
class ClientWidget : public QWidget
{
Q_OBJECT
public:
explicit ClientWidget(QWidget *parent = nullptr);
private:
QLineEdit *p_ipEdit;
QLineEdit *p_portEdit;
QPushButton *p_connectButton;
QPushButton *p_closeButton;
QTcpSocket *p_tcpSocket;
QProgressBar *p_progressBar;
QFile m_file;
QString m_fileName;
qint64 m_fileSize;
qint64 m_recvSize;
bool isHead;
protected:
void ClientWidget::getConnection();
void ClientWidget::closeConnection();
void ClientWidget::recvFile();
signals:
public slots:
};
#endif // CLIENTWIDGET_H
复制代码
//ClientWidget.cpp
#include "ClientWidget.h"
#include <QGridLayout>
#include <QLabel>
#include <QMessageBox>
#include <QProgressBar>
#include <QHostAddress>
#include <QDebug>
ClientWidget::ClientWidget(QWidget *parent) : QWidget(parent)
{
//界面
this->resize(640,480);
this->setWindowTitle("客户端");
this->move(1280,237);
isHead=true;
m_recvSize=0;
QGridLayout *p_layout=new QGridLayout(this);
p_progressBar=new QProgressBar(this);
p_progressBar->setValue(0);
p_tcpSocket=new QTcpSocket(this);
p_ipEdit=new QLineEdit(this);
p_portEdit=new QLineEdit(this);
p_connectButton=new QPushButton("连接",this);
p_closeButton=new QPushButton("关闭连接",this);
p_layout->addWidget(new QLabel("服务器IP:",this),0,0,1,1);
p_layout->addWidget(new QLabel("服务器端口:",this),1,0,1,1);
p_layout->addWidget(p_ipEdit,0,1,1,3);
p_layout->addWidget(p_portEdit,1,1,1,3);
p_layout->addWidget(p_connectButton,0,4,2,1,Qt::AlignVCenter);
p_layout->addWidget(p_progressBar,2,0,1,5);
p_progressBar->setAlignment(Qt::AlignCenter);
p_layout->addWidget(p_closeButton,3,2,1,1,Qt::AlignCenter);
//与服务端连接
connect(p_connectButton,&QPushButton::clicked,this,&ClientWidget::getConnection);
connect(p_closeButton,&QPushButton::clicked,this,&ClientWidget::closeConnection);
//收到从服务端发来的消息
connect(p_tcpSocket,&QTcpSocket::readyRead,this,&ClientWidget::recvFile);
}
void ClientWidget::getConnection(){
//获取服务端ip和端口号
QString ip=p_ipEdit->text();
quint16 port=p_portEdit->text().toInt();
//发起连接
p_tcpSocket->connectToHost(QHostAddress(ip),port);
}
void ClientWidget::closeConnection(){
p_tcpSocket->disconnectFromHost();
p_tcpSocket->close();
}
void ClientWidget::recvFile(){
//先取出读取到的内容
QByteArray buf=p_tcpSocket->readAll();
//判断是否是文件头信息
//如果是,解析文件头,获取文件大小和文件名,并创建文件
//否则,读取文件内容数据
if(isHead){
//说明还未解析头信息
isHead=false;
//解析文件头,获取文件名和文件大小
m_fileName=QString(buf).section("##",0,0);
m_fileSize=QString(buf).section("##",1,1).toInt();
m_recvSize=0;
//打开文件
m_file.setFileName(m_fileName);
m_file.open(QIODevice::WriteOnly);
//设置进度条
p_progressBar->setMinimum(0);
p_progressBar->setMaximum(m_fileSize/1024);
p_progressBar->setValue(0);
}else{
//将数据写入本地文件并返回成功写入的字符个数,这里不需要用到循环是因为这是服务器调用write时被动触发的,服务器循环调用write,此函数也会被循环调用
qint64 len=m_file.write(buf);
m_recvSize+=len;
p_progressBar->setValue(m_recvSize/1024);
if(m_recvSize==m_fileSize){
m_file.close();
p_tcpSocket->write("done");
QMessageBox::about(this,"提示","文件接收成功!");
p_tcpSocket->disconnectFromHost();
p_tcpSocket->close();
}
}
}
复制代码
P.S:如有错误,欢迎指正~