文章目录
- 一、TCP 传文件流程图
- 1. 服务器端流程
- 2. 客户端流程
- 二、TCP 传文件操作实现
- 1. 服务器端
- 2. 客户端
- 3. TCP 传文件实现现象
- 三、服务器端和客户端实现代码
- 1. 主函数 main.c
- 2. 服务器端头文件 serverwidget.h
- 3. 服务器端源文件 serverwidget.cpp
- 4. 客户端头文件 clientwidget.h
- 5. 客户端源文件 clientwidget.cpp
由于每次代码都是在原有程序上修改,因此除了新建项目,不然一般会在学完后统一展示代码。
提示:具体项目创建流程和注意事项见
QT 学习笔记(一)
提示:具体项目准备工作和细节讲解见
QT 学习笔记(二)
一、TCP 传文件流程图
- 服务器端和客户端进行正常的通信,通信成功后,在服务器端选择需要的文件发送给客户端。
1. 服务器端流程
- (1) 选择文件并且获取文件的基本信息(包括文件的名字和大小),这里假设文件大小是 1024,文件的名字是 hello。
- (2) 组包(“head#hello#1024”)发送文件信息,向客户端发送对应的文件信息。
- (3) 在组包发送完成后,服务器端开始读文件,发送数据(服务器端读多少发多少),当我发送文件大小和获取到的文件大小相等时,停止读取文件数据。
- (4) 服务器端连接信号 readyread() 来获取文件是否发送成功。
2. 客户端流程
- (1) 在服务器端组包发送文件信息,开始接收文件,获取文件信息,分析服务器端发送的组包字符串,获取文件的大小和文件的名字。
- (2) 在本地创建一个文件,以接收到的文件名字命名。
- (3) 在服务器端开始发送文件数据后,服务器端发送多少,客户端就接收多少。
- (4) 客户端将接收到的文件数据放入本地创建的文件当中。
- (5) 如果想清楚的直到客户端是否成功接收完成文件,可以在客户端真正接收完成后给服务端发送一个消息。
二、TCP 传文件操作实现
- 生成一个新的项目,具体步骤过程见提示。
- 在生成新项目的过程中,我们选择基类为 QWidget ,这是因为 QWidget 当中比较干净,不存在别的东西,而 QMainWindow 虽然也可以实现,但其中存在工具栏,菜单栏,核心控件等东西,比较复杂。
1. 服务器端
- 我们在 TCPfile.pro 当中加入 QT += network 代码,以便后续工作可以顺利开展。
- 在过程当中要使用 Lambda 表达式,因此,我们提前在 TCPfile.pro 当中加入 CONFIG += C++11 代码。
- 首先,我们在 ui 界面布置出服务器端的窗口界面,包含一个标签(用来表示标题),两个按钮(选择文件和发送文件)和一个文本编辑区(用来输入和显示),具体界面布局如下图所示。
- 在布局完成后,我们选择标签服务器,将其设置为水平居中(在右下角的 QLabel,点击 alignment,其中包含水平的和垂直的,水平的选取 AlignHCenter),将其字体选择为楷体,大小选择为 24。
- 完成 ui 界面的设计后,要及时编译,以便于 QT 可以及时地进行关键字信息的补充。
- TCP 当中,服务器端包括两个套接字,分别是监听套接字 QTcpServer 和通信套接字 QTcpSocket。
- 在完成头文件的包含和变量的定义之后,对监听套接字和通信套接字进行分配空间。
- 我们对两个按钮在 ui 界面进行转到槽函数的操作,并进行代码的编写,由于这里需要服务器端和客户端相结合才可以看到现象,故在此处不过多展示实现现象(主要在客户端展示)。
- 服务器端的实现主要分为以下几步:
- (1) 建立连接的实现
- 要进行服务器端和客户端之间的 TCP 通信,无论是传输数据还是文件,首先要建立服务器端和客户端之间的连接。
- 在连接到客户端的时候,打印出 IP 地址和端口号,并且显示在文本编辑区中,开始之前对按钮进行了一个不使能操作。也就是说,在没有连接的时候是无法点击按钮的。
- (2) 选择文件的实现
- 在建立完成服务器端和客户端之间的连接后,我们需要在服务端选择文件并且发送给客户端。
- 因此,我们需要包含头文件 QFile,并定义文件对象。随后,在 ui 界面当中的选择文件按钮进行转到槽的操作。
- 在发送文件之前先发送文件的信息,比如文件名,文件大小等。文件信息的获取我们使用 QT 中的
QFileInfo
。关于文件名和文件大小,我们需要定义对应的全局变量。 - 在获取文件信息之前,要先对文件信息进行清空,也就是文件名字 clear(),文件大小设置为 0。
- 同时,我们还需要定义一个全局变量,用以表示当前文件发送的进度。
- 在选择文件结束后,将选择文件按钮使能为 false,发送文件按钮使能为 true。
- (3) 发送文件的实现
- 在 ui 界面当中的发送文件按钮进行转到槽的操作。
- 关于发送文件,我们需要先发送文件头信息,再发生真正的文件信息。
- 在发送头部数据的过程当中,为了防止出现 TCP 连包问题,我们需要通过定时器延时 20ms。
- 在发送文件结束后,将发送文件按钮使能为 false,选择文件按钮使能为 true。
- (4) 发送文件数据的实现
- 对于数据发送为了防止头信息和数据出现相互干扰的情况,需要分开发送,在发送头信息之后,延时(使用定时器)然后发送数据,确保客户端先收到头信息,再收到数据。
- 在发送完数据后,要断开与客户端的连接。
- 最后具体实现结果如下图所示。
2. 客户端
- 对于客户端来说,主要就是获取发送的头信息,不能和数据相互连包,其余和之前的 TCP 数据传输一样。
- 客户端通过使用 QT 提供的 QTcpSocket 类可以方便的实现与服务器端的通信。
- 在完成服务器端后,我们进行客户端的编写,客户端只有一个套接字就是通信套接字 tcpsocket。
- 这里我们不重新开一个项目,而是将两部分放在一块。但是,服务器端和客户端的 ui 界面并不相同,因此,我们需要在添加一个 QT 设计师界面类,对客户端的 ui 界面进行设计。
- 界面模板默认 Widget 即可。
- 随后,我们先在 ui 界面布置出客户端的简单窗口界面,包含两个标签(分别是服务器端口和服务器 IP )以及他们对应的行编辑区和一个按钮(连接 connect)。
- 其中的服务器端口的行编辑区直接默认值是 8888(与服务器端相对应),服务器 IP 的行编辑区直接默认值是 127.0.0.1(本地连接),具体界面布局如下图所示。
- 在客户端当中,接收的信息主要分为两部分(头信息和文件信息)。
- 在接收头信息的过程中,我们需要对文件信息进行初始化,随后打开文件。
- 在接收完文件信息后,断开与服务器端的连接。
3. TCP 传文件实现现象
- 在完成全部代码的编写后,运行程序,会得到如下的具体现象。
- 我们点击 connect 按钮,服务器端与客户端会完成通信。
- 此时,服务器端的选择文件按钮会点亮,我们可以选择一个合适的文件进行传输。
- 然后,点击发送文件按钮,会得到文件发送完毕的提示,表明文件已经成功发送。
- 这里需要注意,在发送文件的过程中,是无法点击选择文件的,只有在发送完成后,服务器端与客户端重新建立连接,才可以重新选择待发送的文件。
- 发送文件成功之后,在对应的文件目录下,会生成我们发送的 test.txt 文件,具体现象如下图所示。
三、服务器端和客户端实现代码
1. 主函数 main.c
#include "serverwidget.h"
#include <QApplication>
#include "clientwidget.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
serverWidget w;
w.show();
clientwidget w2;
w2.show();
return a.exec();
}
2. 服务器端头文件 serverwidget.h
#ifndef SERVERWIDGET_H
#define SERVERWIDGET_H
#include <QWidget>
#include <QTcpServer> //监听套接字
#include <QTcpSocket> //通信套接字
#include <QFile>
#include <QTimer>
namespace Ui {
class serverWidget;
}
class serverWidget : public QWidget
{
Q_OBJECT
public:
explicit serverWidget(QWidget *parent = nullptr);
~serverWidget();
void sendData(); //发送文件数据
private slots:
void on_pushButtonfile_clicked();
void on_pushButton_2_clicked();
private:
Ui::serverWidget *ui;
QTcpServer *tcpserver; //监听套接字
QTcpSocket *tcpsocket; //通信套接字
QFile file; //文件对象
QString fileName; //文件名字
qint64 fileSize; //文件大小
qint64 sendSize; //已经发送文件的大小
QTimer timer; //定时器
};
#endif // SERVERWIDGET_H
3. 服务器端源文件 serverwidget.cpp
#include "serverwidget.h"
#include "ui_serverwidget.h"
#include <QFileDialog>
#include <QDebug>
serverWidget::serverWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::serverWidget)
{
ui->setupUi(this);
//监听套接字
tcpserver = new QTcpServer(this);
//监听
tcpserver->listen(QHostAddress::Any,8888);
//设置窗口标题
setWindowTitle("服务器端口为:8888");
//无连接的时候按钮无效
ui->pushButtonfile->setEnabled(false);
ui->pushButton_2->setEnabled(false);
//如果客户端成功和服务端连接
//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);
//成功连接后,才能按选择文件
ui->pushButtonfile->setEnabled(true);
}
);
connect(&timer,&QTimer::timeout,
[=]()
{
//关闭定时器
timer.stop();
//发送文件
sendData();
}
);
}
serverWidget::~serverWidget()
{
delete ui;
}
//选择文件按钮
void serverWidget::on_pushButtonfile_clicked()
{
QString filepath = QFileDialog::getOpenFileName(this,"选择文件","../");
if(filepath.isEmpty() == false) //如果选择文件路径有效
{
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(isok == false)
{
qDebug() << "只读方式打开文件失败";
}
//追加文件路径信息
ui->textEdit->append(filepath);
ui->pushButtonfile->setEnabled(false);
ui->pushButton_2->setEnabled(true);
}
else
{
qDebug() << "文件路径无效";
}
}
//发送文件按钮
void serverWidget::on_pushButton_2_clicked()
{
//先发送文件头信息,文件名##文件大小
QString headMessage = QString("%1##%2").arg(fileName).arg(fileSize);
//发送头部信息
quint64 len = tcpsocket->write(headMessage.toUtf8().data());
if(len > 0) //发送头信息成功
{
//发送真正的文件信息
//防止TCP连包问题
//需要通过定时器延时20ms
timer.start(20);
}
else
{
qDebug() << "头文件信息发送失败";
file.close();
ui->pushButtonfile->setEnabled(true);
ui->pushButton_2->setEnabled(false);
}
}
//发送文件数据
void serverWidget::sendData()
{
qint64 len = 0;
sendSize = 0;
do
{
//每次发送数据的大小
char buf[4*1024] = {0};
len = 0;
//往文件中读数据
len = file.read(buf, sizeof(buf));
//发送数据,读多少,发多少
len = tcpsocket->write(buf, len);
//发送的数据需要累计
sendSize += len;
}while(len > 0);
//判断数据是否发送完毕
if(sendSize == fileSize)
{
ui->textEdit->append("文件发送完毕");
file.close();
//断开客户端
tcpsocket->disconnectFromHost();
tcpsocket->close();
}
}
4. 客户端头文件 clientwidget.h
#ifndef CLIENTWIDGET_H
#define CLIENTWIDGET_H
#include <QWidget>
#include <QTcpSocket> //通信套接字
#include <QFile>
namespace Ui {
class clientwidget;
}
class clientwidget : public QWidget
{
Q_OBJECT
public:
explicit clientwidget(QWidget *parent = nullptr);
~clientwidget();
private slots:
void on_pushButton_clicked();
private:
Ui::clientwidget *ui;
QTcpSocket *tcpsocket; //通信套接字
QFile file; //文件对象
QString fileName; //文件名字
qint64 fileSize; //文件大小
qint64 recvSize; //已经接收文件的大小
bool isstart;
};
#endif // CLIENTWIDGET_H
5. 客户端源文件 clientwidget.cpp
#include "clientwidget.h"
#include "ui_clientwidget.h"
#include <QDebug>
#include <QMessageBox>
#include <QHostAddress>
clientwidget::clientwidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::clientwidget)
{
ui->setupUi(this);
tcpsocket = new QTcpSocket(this);
isstart = true;
connect(tcpsocket,&QTcpSocket::readyRead,
[=]()
{
//取出接收的内容
QByteArray buf = tcpsocket->readAll();
if(isstart == true)
{
//接收头
isstart = false;
//解析头部信息 buf = "hello##1024"
//QString str = "hello##1024#mike";
//str.section("##",0,0)
//初始化
fileName = QString(buf).section("##", 0, 0);
fileSize = QString(buf).section("##", 1, 1).toInt();
recvSize = 0;
//打开文件
file.setFileName(fileName);
bool isok = file.open(QIODevice::WriteOnly);
if(false == isok)
{
qDebug() << "WriteOnly error";
}
else //文件信息
{
qint64 len = file.write(buf);
recvSize += len;
if(recvSize == fileSize)
{
file.close();
QMessageBox::information(this,"完成","文件接收完成");
tcpsocket->disconnectFromHost();
tcpsocket->close();
}
}
}
}
);
}
clientwidget::~clientwidget()
{
delete ui;
}
void clientwidget::on_pushButton_clicked()
{
//获取服务器的IP和端口
QString ip = ui->lineEditip->text();
quint16 port = ui->lineEditport->text().toInt();
tcpsocket->connectToHost(QHostAddress(ip),port);
}