QT 学习笔记(十六)

简介: QT 学习笔记(十六)

文章目录


一、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 传文件流程图

  • 服务器端和客户端进行正常的通信,通信成功后,在服务器端选择需要的文件发送给客户端。

bac800aed5d94aa1b8ddb8175aefb285.png


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 界面布置出服务器端的窗口界面,包含一个标签(用来表示标题),两个按钮(选择文件和发送文件)和一个文本编辑区(用来输入和显示),具体界面布局如下图所示。

cc7d894217094526ac7b8fd79edc3322.png


在布局完成后,我们选择标签服务器,将其设置为水平居中(在右下角的 QLabel,点击 alignment,其中包含水平的和垂直的,水平的选取 AlignHCenter),将其字体选择为楷体,大小选择为 24。

ade2d70146c64e11b6db721065659951.png


完成 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) 发送文件数据的实现

对于数据发送为了防止头信息和数据出现相互干扰的情况,需要分开发送,在发送头信息之后,延时(使用定时器)然后发送数据,确保客户端先收到头信息,再收到数据。

在发送完数据后,要断开与客户端的连接。

最后具体实现结果如下图所示。

22948742c0dc446890e3899b6fda70cc.png


2. 客户端


对于客户端来说,主要就是获取发送的头信息,不能和数据相互连包,其余和之前的 TCP 数据传输一样。

客户端通过使用 QT 提供的 QTcpSocket 类可以方便的实现与服务器端的通信。

在完成服务器端后,我们进行客户端的编写,客户端只有一个套接字就是通信套接字 tcpsocket。

这里我们不重新开一个项目,而是将两部分放在一块。但是,服务器端和客户端的 ui 界面并不相同,因此,我们需要在添加一个 QT 设计师界面类,对客户端的 ui 界面进行设计。

8428fe524a444f8e9ef09c0c71372578 (1).png

界面模板默认 Widget 即可。3b681abc918a4360b320fd21d3293ff6 (1).png


随后,我们先在 ui 界面布置出客户端的简单窗口界面,包含两个标签(分别是服务器端口和服务器 IP )以及他们对应的行编辑区和一个按钮(连接 connect)。

其中的服务器端口的行编辑区直接默认值是 8888(与服务器端相对应),服务器 IP 的行编辑区直接默认值是 127.0.0.1(本地连接),具体界面布局如下图所示。

b38219d364984154bef916148b782f0a.png


  • 在客户端当中,接收的信息主要分为两部分(头信息和文件信息)。
  • 在接收头信息的过程中,我们需要对文件信息进行初始化,随后打开文件。
  • 在接收完文件信息后,断开与服务器端的连接。


3. TCP 传文件实现现象

  • 在完成全部代码的编写后,运行程序,会得到如下的具体现象。

453822af47524673ac630a06ddb391c6.png


我们点击 connect 按钮,服务器端与客户端会完成通信。

此时,服务器端的选择文件按钮会点亮,我们可以选择一个合适的文件进行传输。

然后,点击发送文件按钮,会得到文件发送完毕的提示,表明文件已经成功发送。

这里需要注意,在发送文件的过程中,是无法点击选择文件的,只有在发送完成后,服务器端与客户端重新建立连接,才可以重新选择待发送的文件。b2966975eec54c3bb6e0012c34af4dc9.png


b2966975eec54c3bb6e0012c34af4dc9.pngb2966975eec54c3bb6e0012c34af4dc9.png

发送文件成功之后,在对应的文件目录下,会生成我们发送的 test.txt 文件,具体现象如下图所示。

496240cb46f241f299eb6be12d3fe32a.png


三、服务器端和客户端实现代码

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);
}








相关文章
|
3月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 消息对话框QMessageBox
【Qt 学习笔记】Qt窗口 | 标准对话框 | 消息对话框QMessageBox
608 4
【Qt 学习笔记】Qt窗口 | 标准对话框 | 消息对话框QMessageBox
|
3月前
|
开发者
【Qt 学习笔记】Qt系统相关 | Qt事件 | 事件的介绍及基本概念
【Qt 学习笔记】Qt系统相关 | Qt事件 | 事件的介绍及基本概念
206 4
|
3月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 文件对话框QFileDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 文件对话框QFileDialog
727 4
|
3月前
|
数据安全/隐私保护
【Qt 学习笔记】Qt窗口 | 对话框 | 模态与非模态对话框的创建
【Qt 学习笔记】Qt窗口 | 对话框 | 模态与非模态对话框的创建
377 4
|
3月前
|
搜索推荐 C++
【Qt 学习笔记】Qt窗口 | 对话框 | 创建自定义对话框
【Qt 学习笔记】Qt窗口 | 对话框 | 创建自定义对话框
73 4
|
3月前
|
API UED
【Qt 学习笔记】Qt窗口 | 状态栏 | QStatusBar的使用及说明
【Qt 学习笔记】Qt窗口 | 状态栏 | QStatusBar的使用及说明
396 4
|
3月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 输入对话框QInputDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 输入对话框QInputDialog
257 3
|
3月前
|
数据可视化
【Qt 学习笔记】Qt窗口 | 标准对话框 | 字体对话框QFontDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 字体对话框QFontDialog
81 3
|
3月前
【Qt 学习笔记】Qt窗口 | 标准对话框 | 颜色对话框QColorDialog
【Qt 学习笔记】Qt窗口 | 标准对话框 | 颜色对话框QColorDialog
535 3
|
3月前
【Qt 学习笔记】Qt窗口 | 对话框 | Qt对话框的分类及介绍
【Qt 学习笔记】Qt窗口 | 对话框 | Qt对话框的分类及介绍
128 3