1.简述

有时在windows环境下通过远程访问操作另一台linux主机。如使用putty和xshell来实现远程终端,使用Xftp来相互传输文件。现在想自己使用Qt开发一个文件传输软件。

FTP是基于TCP/IP的一种应用层文件传输协议,通过建立FTP服务器(默认端口21)-客服端的形式在各个主机间传输文件。

SFTP是基于FTP服务和ssh协议实现的一种加密文件传输协议,数据传输更加安全,但是相比喻FTP会损失一点效率。

开发者一般通过ssh工具来访问目标linux主机,linux系统一般集成ssh服务(默认端口为22)。用来传输文件的SFTP协议是一ssh的子协议,一般有ssh服务的主机都会自动开启sftp服务。但是linux主机并不一定会安装FTP服务器,需要自行安装。

Qt5.0之后移除了QFtp类(基于FTP协议实现的一个类),使用 QNetworkAccessManager 可以实现 Ftp 的上传/下载功能,后者在性能上和稳定性上有所提升。但有些原本 QFtp 有的功能 QNetworkAccessManager 却没有提供,例如:list、cd、remove、mkdir、rmdir、rename 等,前者的功能更加完善,提供的API更多,更加便于开发者使用。最为新手,开发一般的应用,当然选择QFtp更加有利,值得庆幸的是 QFtp 一直在维护,只需要下载源码自行编译即可使用。

本人的开发环境为Qt 5.12.0,MSVC2017 64bits编译器。下面将总结QFtp在Qt5以上版本编译和使用的经历,开对官方提供的示例进行了重构,写了个Ftp客户端,最终效果见下。
在这里插入图片描述

2.QFtp编译与部署

2.1 下载

到github,下载QFtp源码,https://github.com/qt/qtftp

git clone https://github.com/qt/qtftp.git

2.2 修改

打开qt工程,修改qftp.pro文件中框选的部分,修改为下图所示。修改qftp.h文件的qurlinfo.h头文件,改为下图,该头文件路径有问题。
在这里插入图片描述

在这里插入图片描述

2.3 编译

只构建src,构建需要perl环境,如果之前装qt时一起装了就可以正常编译,如果没装会报错,去官网下载安装就行。
在这里插入图片描述

2.4 部署

编译完成后见下图,将编译出来的文件分别放到Qt安装路径对应的目录下,使得其和Qt自带的模块一样使用。放置文件的标准就是生成的bin文件夹中的文件放置到Qt对应编译器目录下的bin文件夹中,其他类似。我的是mscv2017_64。
在这里插入图片描述

  1. bin目录下的两个dll文件复制到Qt5.12.1\5.12.1\msvc2017_64\bin目录下;
  2. lib目录下的.lib文件和.prl文件复制到Qt5.12.1\5.12.1\msvc2017_64\lib目录下;
  3. include目录下的QtFtp文件夹整个复制到Qt5.12.1\5.12.1\msvc2017_64\include目录下。这里有个坑,include中的qftp.h和qurlinfo.h不是头文件实体,里面只有一句include将源码的头文件分别包含进来,因此还需要把src文件夹下qftp.h和qurlinfo.h两个文件复制覆盖到Qt5.12.1\5.12.1\msvc2017_64\include中;
  4. mkspecs\modules-inst目录下的两个.pri文件复制到Qt5.12.1\5.12.1\msvc2017_64\mkspecs\modules目录下。

通过上述四步,相当于给Qt新增了一个QtFtp的组件。第一步和第二步准备该组件动态库,第三步准备头文件,第四步相当于定义QtFtp组件,配置IDE,下面是自带的QtNetwork组件(左)和QtFtp组件的对比。
在这里插入图片描述

2.5 使用

上面配置好后,在一个独立的工程里,需要先在.pro中添加组件,然后包含头文件就行。

QT += ftp
#include <QFtp>

3.QFtp运用

3.1 Ftp客户端

参考官方提供的例子,将其独立出来,重构了一下,继承QWidget写了一个类FtpWidget,方便以后在其他的综合项目里直接提升。
先用Qt Designer绘制一个出ui,结构比较简单。
在这里插入图片描述
在这里插入图片描述
然后,头文件ftpwidget.h

#ifndef FTPWIDGET_H
#define FTPWIDGET_H

#include <QWidget>
#include <QUrl>
#include <QFtp>
#include <QTreeWidgetItem>
#include <QFile>

namespace Ui {
class FtpWidget;
}

class FtpWidget : public QWidget
{
    Q_OBJECT

public:
    explicit FtpWidget(QWidget *parent = nullptr);
    ~FtpWidget();

private:
    Ui::FtpWidget *ui;

private slots:

    void downloadFile();
    void cancelDownload();
    void connectToFtp();

    void ftpCommandFinished(int commandId, bool error);
    void addToList(const QUrlInfo &urlInfo);
    void processItem(QTreeWidgetItem *item, int column);
    void cdToParent();
    void updateDataTransferProgress(qint64 readBytes, qint64 totalBytes);
    void enableDownloadButton();

    void connectOrDisconnect();

private:
    QHash<QString, bool> isDirectory;
    QString currentPath;
    QFtp *ftp = nullptr;
    QFile *file = nullptr;
};

#endif // FTPWIDGET_H

然后,ftpwidget.cpp 源文件

#include "ftpwidget.h"
#include "ui_ftpwidget.h"

#include <QMessageBox>

FtpWidget::FtpWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::FtpWidget)
{
    ui->setupUi(this);
    this -> ui -> label_status -> setText("Please enter the name of an FTP server.");

    this -> ui -> treeWidget_fileList -> setEnabled(false);
    this -> ui -> treeWidget_fileList -> setRootIsDecorated(false);
    this -> ui -> treeWidget_fileList -> setHeaderLabels(QStringList() << tr("Name") << tr("Size") << tr("Owner") << tr("Group") << tr("Time"));
    this -> ui -> treeWidget_fileList -> header() ->setStretchLastSection(false);

    this -> ui ->pushButton_cdToParent -> setIcon(QPixmap(":/images/images/cdtoparent.png"));
    this -> ui ->pushButton_cdToParent -> setEnabled(false);

    this -> ui ->pushButton_download -> setEnabled(false);

    connect(ui->treeWidget_fileList, &QTreeWidget::itemActivated,this, &FtpWidget::processItem);
    connect(ui->treeWidget_fileList, &QTreeWidget::currentItemChanged,this, &FtpWidget::enableDownloadButton);
    connect(ui->pushButton_connect, &QPushButton::clicked, this, &FtpWidget::connectOrDisconnect);
    connect(ui->pushButton_cdToParent, &QPushButton::clicked, this, &FtpWidget::cdToParent);
    connect(ui->pushButton_download, &QPushButton::clicked, this, &FtpWidget::downloadFile);

    setWindowTitle("FTPWidget");
}

FtpWidget::~FtpWidget()
{
    delete ui;
}

void FtpWidget::connectOrDisconnect()
{
    if (ftp) {
        ftp -> abort();
        ftp -> deleteLater();
        ftp = nullptr;

        this -> ui -> treeWidget_fileList -> clear();
        this -> ui -> treeWidget_fileList -> setEnabled(false);
        this -> ui ->pushButton_cdToParent -> setEnabled(false);
        this -> ui ->pushButton_download -> setEnabled(false);
        this -> ui ->pushButton_connect -> setEnabled(true);
        this -> ui ->pushButton_connect -> setText(tr("Connect"));

        this -> ui -> label_status -> setText("Please enter the name of an FTP server.");
        return;
    }

    connectToFtp();
}


void FtpWidget::connectToFtp()
{
    ftp = new QFtp(this);
    connect(ftp, &QFtp::commandFinished,this, &FtpWidget::ftpCommandFinished);
    connect(ftp, &QFtp::listInfo,this, &FtpWidget::addToList);
    connect(ftp, &QFtp::dataTransferProgress,this, &FtpWidget::updateDataTransferProgress);

    this -> ui -> treeWidget_fileList ->clear();
    currentPath.clear();
    isDirectory.clear();

    QString ftpServer = this->ui->lineEdit_ftpServer->text();
    ftp -> connectToHost(ftpServer,21);
    ftp -> login("pi","302302302");

    this -> ui -> label_status ->setText(tr("Connecting to FTP server %1...").arg(this->ui->lineEdit_ftpServer->text()));
}

void FtpWidget::downloadFile()
{
    QString fileName = this -> ui -> treeWidget_fileList->currentItem()->text(0);

    if (QFile::exists(fileName)) {
        QMessageBox::information(this, tr("FTP"),
                                 tr("There already exists a file called %1 in "
                                    "the current directory.")
                                 .arg(fileName));
        return;
    }

    file = new QFile(fileName);
    if (!file->open(QIODevice::WriteOnly)) {
        QMessageBox::information(this, tr("FTP"),
                                 tr("Unable to save the file %1: %2.")
                                 .arg(fileName).arg(file->errorString()));
        delete file;
        return;
    }

    ftp->get(this -> ui -> treeWidget_fileList->currentItem()->text(0), file);
}

void FtpWidget::cancelDownload()
{
    ftp->abort();

    if (file->exists()) {
        file->close();
        file->remove();
    }
    delete file;
}


void FtpWidget::ftpCommandFinished(int, bool error)
{

    if (ftp->currentCommand() == QFtp::ConnectToHost)
    {
        if (error) {
            QMessageBox::information(this, tr("FTP"),
                                     tr("Unable to connect to the FTP server "
                                        "at %1. Please check that the host "
                                        "name is correct.")
                                     .arg(this->ui->lineEdit_ftpServer->text()));
            connectOrDisconnect();
            return;
        }
        else {
            this -> ui -> label_status -> setText(tr("Logged onto %1.").arg(this->ui->lineEdit_ftpServer->text()));
            this -> ui -> treeWidget_fileList-> setFocus();
            this -> ui -> pushButton_download->setDefault(true);
            this -> ui -> pushButton_connect-> setText("Disconnect");
            return;
        }
    }

    if (ftp->currentCommand() == QFtp::Login)
        ftp->list();

    if (ftp->currentCommand() == QFtp::Get) {
        if (error) {
            this->ui->label_status->setText(tr("Canceled download of %1.")
                                 .arg(file->fileName()));
            file->close();
            file->remove();
        } else {
            this->ui->label_status->setText(tr("Downloaded %1 to current directory.")
                                 .arg(file->fileName()));
            this -> ui -> progressBar -> setValue(100);
            file->close();
        }
        delete file;
        enableDownloadButton();

    } else if (ftp->currentCommand() == QFtp::List) {
        if (isDirectory.isEmpty()) {
            this -> ui -> treeWidget_fileList -> addTopLevelItem(new QTreeWidgetItem(QStringList() << tr("<empty>")));
            this -> ui -> treeWidget_fileList -> setEnabled(false);
        }
    }
}

void FtpWidget::addToList(const QUrlInfo &urlInfo)
{
    QTreeWidgetItem *item = new QTreeWidgetItem;
    item->setText(0, urlInfo.name());
    item->setText(1, QString::number(urlInfo.size()));
    item->setText(2, urlInfo.owner());
    item->setText(3, urlInfo.group());
    item->setText(4, urlInfo.lastModified().toString("MMM dd yyyy"));

    QPixmap pixmap(urlInfo.isDir() ? ":/images/images/dir.png" : ":/images/images/file.png");
    item->setIcon(0, pixmap);

    isDirectory[urlInfo.name()] = urlInfo.isDir();
    this -> ui -> treeWidget_fileList->addTopLevelItem(item);
    if (!this -> ui -> treeWidget_fileList->currentItem()) {
        this -> ui -> treeWidget_fileList->setCurrentItem(this -> ui -> treeWidget_fileList->topLevelItem(0));
        this -> ui -> treeWidget_fileList->setEnabled(true);
    }
}

void FtpWidget::processItem(QTreeWidgetItem *item, int /*column*/)
{
    QString name = item->text(0);
    if (isDirectory.value(name)) {
        this -> ui -> treeWidget_fileList->clear();
        isDirectory.clear();
        currentPath += '/';
        currentPath += name;
        ftp->cd(name);
        ftp->list();
        this-> ui -> pushButton_cdToParent->setEnabled(true);

        return;
    }
}

void FtpWidget::cdToParent()
{

    this -> ui -> treeWidget_fileList -> clear();
    isDirectory.clear();
    currentPath = currentPath.left(currentPath.lastIndexOf('/'));
    if (currentPath.isEmpty()) {
        this-> ui -> pushButton_cdToParent ->setEnabled(false);
        ftp->cd("/");
    } else {
        ftp->cd(currentPath);
    }
    ftp->list();
}


void FtpWidget::updateDataTransferProgress(qint64 readBytes,qint64 totalBytes)
{
    this -> ui -> progressBar -> setValue(static_cast<int>(readBytes/totalBytes));
}

void FtpWidget::enableDownloadButton()
{
    QTreeWidgetItem *current = this -> ui -> treeWidget_fileList->currentItem();
    if (current) {
        QString currentFile = current->text(0);
        this->ui->pushButton_download->setEnabled(!isDirectory.value(currentFile));
    } else {
        this->ui->pushButton_download->setEnabled(false);
    }
}

完整的源码分享出来放在github上,也放一份在CSDN上。

3.2 树莓派搭建FTP服务器

在树莓派上搭建一个FTP服务器用于测试
很简单,很多参考

3.3 测试效果

文章最前面的视频,目前只实现了下载功能。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐