Qt 实现 FTP 客户端完整教程:上传下载、断点续传与报错解决方案

Qt实现FTP客户端与断点续传

Qt 实现 FTP 客户端完整教程:上传下载、断点续传与报错解决方案

在跨平台文件传输场景中,FTP(文件传输协议)因兼容性强、部署简单,被广泛应用于设备配置文件同步、日志上传等场景。本文基于 Qt 5.15+ 框架,从零实现一个功能完整的 FTP 客户端,支持文件上传、下载、断点续传、进度监控,并针对开发中常见的“连接超时”“权限拒绝”“断点续传失败”等问题提供解决方案,附带可直接运行的完整代码。

一、核心依赖与环境准备

1.1 开发环境

  • 开发工具:Visual Studio 2019/2022 或 Qt Creator 11+

  • Qt 版本:Qt 5.15 及以上(推荐 5.15.2,LTS 版本稳定性更佳)

  • FTP 服务器:FileZilla Server(Windows 本地测试)或 Linux 内置 vsftpd(生产环境)

  • 依赖模块:Qt Network(核心网络模块,无需额外第三方库)

1.2 Qt 项目配置

在 Qt 项目的 .pro 文件中添加网络模块依赖,确保 FTP 相关类(如 QFtpQUrl)可正常使用:

QT += core gui widgets network  # 必须包含 network 模块

# 调试与发布模式配置
CONFIG(debug, debug|release) {
    DEFINES += FTP_DEBUG  # 调试模式启用详细日志
} else {
    DEFINES += QT_NO_DEBUG_OUTPUT  # 发布模式关闭调试输出
    QMAKE_CXXFLAGS += -O2  # 启用优化
}

# 支持中文路径
DEFINES += QT_DEPRECATED_WARNINGS
QMAKE_CXXFLAGS += -fexec-charset=GBK -finput-charset=UTF-8

1.3 FTP 服务器配置(本地测试用)

以 FileZilla Server 为例,快速搭建测试环境:

  1. 安装 FileZilla Server 后,创建本地用户(如用户名 ftp_test,密码 123456);

  2. 设置共享目录(如 D:\FTP_Share),并授予“读取”“写入”权限;

  3. 服务器端口保持默认 21,关闭“被动模式”(新手测试避免防火墙问题,生产环境需配置被动模式端口范围);

  4. 通过 FileZilla 客户端验证服务器可用性(IP 填 localhost,端口 21,输入上述账号密码)。

二、核心功能设计:FtpClient 类

封装独立的 FtpClient 类,实现 FTP 连接、上传、下载、断点续传等核心功能,通过信号槽机制反馈进度和状态,确保与 UI 线程解耦。

2.1 头文件 FtpClient.h

#ifndef FTPCLIENT_H
#define FTPCLIENT_H

#include <QObject>
#include <QFtp>
#include <QFile>
#include <QUrl>
#include <QFileInfo>
#include <atomic>

class FtpClient : public QObject
{
    Q_OBJECT
public:
    explicit FtpClient(QObject *parent = nullptr);
    ~FtpClient();

    // 初始化 FTP 连接(主机、端口、账号、密码)
    void initFtp(const QString& host, int port = 21, 
                const QString& username = "", const QString& password = "");

    // 文件上传(本地文件路径、远程目录,支持断点续传)
    void uploadFile(const QString& localFilePath, const QString& remoteDir, 
                   bool enableResume = true);

    // 文件下载(远程文件路径、本地保存目录,支持断点续传)
    void downloadFile(const QString& remoteFilePath, const QString& localDir, 
                     bool enableResume = true);

    // 取消当前操作(上传/下载)
    void cancelOperation();

    // 断开 FTP 连接
    void disconnectFtp();

signals:
    // 操作进度(已完成字节数,总字节数)
    void progressChanged(qint64 bytesDone, qint64 bytesTotal);
    // 上传成功(远程文件完整路径)
    void uploadSuccess(const QString& remoteFilePath);
    // 下载成功(本地文件完整路径)
    void downloadSuccess(const QString& localFilePath);
    // 操作失败(错误信息)
    void operationFailed(const QString& errMsg);
    // 连接状态变化(是否连接成功)
    void connectStateChanged(bool isConnected);

private slots:
    // FTP 命令执行完成回调
    void onFtpCommandFinished(int id, bool error);
    // 下载数据接收回调(用于断点续传)
    void onDownloadReadyRead();
    // 上传数据发送回调(用于断点续传)
    void onUploadBytesWritten(qint64 bytes);
    // 连接状态变化回调
    void onFtpStateChanged(int state);

private:
    // 检查远程文件是否存在(返回文件大小,-1 表示不存在)
    qint64 checkRemoteFileExist(const QString& remoteFilePath);
    // 构建远程文件完整路径(处理目录拼接)
    QString buildRemoteFilePath(const QString& remoteDir, const QString& fileName);
    // 构建本地文件完整路径(处理目录拼接)
    QString buildLocalFilePath(const QString& localDir, const QString& fileName);
    // 创建本地目录(递归创建,确保保存路径存在)
    bool createLocalDir(const QString& localDir);
    // 清理资源(关闭文件、重置状态)
    void cleanupResource();

private:
    // FTP 核心对象
    QFtp* m_ftp;
    // 文件操作对象(上传/下载时使用)
    QFile* m_file;
    // 操作状态标志
    std::atomic<bool> m_isOperating;   // 是否正在执行上传/下载
    std::atomic<bool> m_isCanceled;    // 是否取消操作
    // 断点续传相关
    qint64 m_resumeStartPos;            // 断点续传起始位置
    bool m_enableResume;                // 是否启用断点续传
    // 当前操作的命令 ID(用于匹配回调)
    int m_currentCmdId;
    // 远程/本地路径缓存
    QString m_remoteFilePath;
    QString m_localFilePath;
};

#endif // FTPCLIENT_H

2.2 源文件 FtpClient.cpp(完整实现)

#include "FtpClient.h"
#include <QDebug>
#include <QDir>
#include <QMessageBox>

// 调试日志宏(仅调试模式输出)
#ifdef FTP_DEBUG
#define FTP_LOG(qmsg) qDebug() << "[FTP_DEBUG]" << qmsg
#else
#define FTP_LOG(qmsg)
#endif

FtpClient::FtpClient(QObject *parent)
    : QObject(parent),
      m_ftp(new QFtp(this)),
      m_file(nullptr),
      m_isOperating(false),
      m_isCanceled(false),
      m_resumeStartPos(0),
      m_enableResume(false),
      m_currentCmdId(-1)
{
    // 连接 FTP 信号槽
    connect(m_ftp, SIGNAL(commandFinished(int,bool)), 
            this, SLOT(onFtpCommandFinished(int,bool)));
    connect(m_ftp, SIGNAL(readyRead()), 
            this, SLOT(onDownloadReadyRead()));
    connect(m_ftp, SIGNAL(bytesWritten(qint64)), 
            this, SLOT(onUploadBytesWritten(qint64)));
    connect(m_ftp, SIGNAL(stateChanged(int)), 
            this, SLOT(onFtpStateChanged(int)));
}

FtpClient::~FtpClient()
{
    cancelOperation();
    disconnectFtp();
    delete m_ftp;
}

void FtpClient::initFtp(const QString& host, int port, 
                       const QString& username, const QString& password)
{
    // 断开现有连接
    if (m_ftp->state() != QFtp::Unconnected) {
        m_ftp->abort();
    }

    // 配置 FTP 连接参数
    m_ftp->connectToHost(host, port);
    if (!username.isEmpty()) {
        m_ftp->login(username, password);
    } else {
        m_ftp->login(); // 匿名登录
    }

    FTP_LOG(QString("初始化 FTP 连接:%1:%2,用户:%3").arg(host).arg(port).arg(username));
}

void FtpClient::uploadFile(const QString& localFilePath, const QString& remoteDir, 
                          bool enableResume)
{
    // 检查是否正在执行其他操作
    if (m_isOperating) {
        emit operationFailed("当前有未完成的操作,请先取消");
        return;
    }

    // 检查本地文件是否存在
    QFileInfo localFileInfo(localFilePath);
    if (!localFileInfo.exists() || !localFileInfo.isFile()) {
        emit operationFailed(QString("本地文件不存在或不是有效文件:%1").arg(localFilePath));
        return;
    }

    // 初始化操作状态
    m_isOperating = true;
    m_isCanceled = false;
    m_enableResume = enableResume;
    m_resumeStartPos = 0;
    m_localFilePath = localFilePath;

    // 构建远程文件路径
    QString fileName = localFileInfo.fileName();
    m_remoteFilePath = buildRemoteFilePath(remoteDir, fileName);
    FTP_LOG(QString("准备上传:本地=%1,远程=%2").arg(localFilePath).arg(m_remoteFilePath));

    // 检查是否支持断点续传
    if (m_enableResume) {
        // 获取远程文件大小(-1 表示文件不存在)
        m_resumeStartPos = checkRemoteFileExist(m_remoteFilePath);
        if (m_resumeStartPos >= 0) {
            // 远程文件存在,检查大小是否匹配本地文件
            if (m_resumeStartPos > localFileInfo.size()) {
                emit operationFailed("远程文件大小大于本地文件,无法断点续传");
                m_isOperating = false;
                return;
            } else if (m_resumeStartPos == localFileInfo.size()) {
                emit uploadSuccess(m_remoteFilePath);
                m_isOperating = false;
                return;
            }
            FTP_LOG(QString("启用断点续传,起始位置:%1 字节").arg(m_resumeStartPos));
        }
    }

    // 打开本地文件(断点续传时从指定位置开始读取)
    m_file = new QFile(localFilePath);
    if (!m_file->open(QIODevice::ReadOnly)) {
        emit operationFailed(QString("打开本地文件失败:%1").arg(m_file->errorString()));
        cleanupResource();
        return;
    }

    // 断点续传:移动文件指针到起始位置
    if (m_resumeStartPos > 0) {
        if (!m_file->seek(m_resumeStartPos)) {
            emit operationFailed("移动文件指针失败,无法断点续传");
            cleanupResource();
            return;
        }
    }

    // 执行上传命令(断点续传时使用 put 并指定起始位置)
    if (m_resumeStartPos > 0) {
        m_currentCmdId = m_ftp->put(m_file, m_remoteFilePath, QFtp::Append);
    } else {
        m_currentCmdId = m_ftp->put(m_file, m_remoteFilePath);
    }
}

void FtpClient::downloadFile(const QString& remoteFilePath, const QString& localDir, 
                            bool enableResume)
{
    if (m_isOperating) {
        emit operationFailed("当前有未完成的操作,请先取消");
        return;
    }

    // 检查远程文件路径有效性
    if (remoteFilePath.isEmpty()) {
        emit operationFailed("远程文件路径不能为空");
        return;
    }

    // 初始化操作状态
    m_isOperating = true;
    m_isCanceled = false;
    m_enableResume = enableResume;
    m_resumeStartPos = 0;
    m_remoteFilePath = remoteFilePath;

    // 构建本地文件路径
    QFileInfo remoteFileInfo(remoteFilePath);
    QString fileName = remoteFileInfo.fileName();
    m_localFilePath = buildLocalFilePath(localDir, fileName);
    FTP_LOG(QString("准备下载:远程=%1,本地=%2").arg(remoteFilePath).arg(m_localFilePath));

    // 创建本地目录
    if (!createLocalDir(localDir)) {
        emit operationFailed(QString("创建本地目录失败:%1").arg(localDir));
        m_isOperating = false;
        return;
    }

    // 检查断点续传
    if (m_enableResume) {
        QFileInfo localFileInfo(m_localFilePath);
        if (localFileInfo.exists()) {
            // 本地文件存在,获取文件大小作为续传起始位置
            m_resumeStartPos = localFileInfo.size();
            // 验证远程文件大小是否大于本地文件
            qint64 remoteFileSize = checkRemoteFileExist(remoteFilePath);
            if (remoteFileSize == -1) {
                emit operationFailed("远程文件不存在,无法断点续传");
                cleanupResource();
                return;
            }
            if (m_resumeStartPos >= remoteFileSize) {
                emit downloadSuccess(m_localFilePath);
                m_isOperating = false;
                return;
            }
            FTP_LOG(QString("启用断点续传,起始位置:%1 字节").arg(m_resumeStartPos));
        }
    }

    // 打开本地文件(断点续传时以追加模式打开)
    m_file = new QFile(m_localFilePath);
    QIODevice::OpenMode openMode = QIODevice::WriteOnly;
    if (m_resumeStartPos > 0) {
        openMode |= QIODevice::Append;
    }
    if (!m_file->open(openMode)) {
        emit operationFailed(QString("打开本地文件失败:%1").arg(m_file->errorString()));
        cleanupResource();
        return;
    }

    // 执行下载命令(断点续传时指定起始位置)
    if (m_resumeStartPos > 0) {
        m_ftp->rawCommand(QString("REST %1").arg(m_resumeStartPos)); // 发送续传起始位置命令
    }
    m_currentCmdId = m_ftp->get(remoteFilePath, m_file);
}

void FtpClient::cancelOperation()
{
    if (!m_isOperating) return;

    m_isCanceled = true;
    if (m_ftp->state() != QFtp::Unconnected) {
        m_ftp->abort(); // 终止当前 FTP 命令
    }
    FTP_LOG("操作已取消");
}

void FtpClient::disconnectFtp()
{
    cancelOperation();
    if (m_ftp->state() != QFtp::Unconnected) {
        m_ftp->close();
        FTP_LOG("FTP 连接已断开");
    }
}

void FtpClient::onFtpCommandFinished(int id, bool error)
{
    // 只处理当前操作的命令回调
    if (id != m_currentCmdId && m_currentCmdId != -1) {
        return;
    }

    // 处理取消操作
    if (m_isCanceled) {
        emit operationFailed("操作已被用户取消");
        cleanupResource();
        return;
    }

    // 处理命令错误
    if (error) {
        QString errMsg = QString("FTP 命令执行失败:%1(命令 ID:%2)")
                         .arg(m_ftp->errorString()).arg(id);
        FTP_LOG(errMsg);
        emit operationFailed(errMsg);
        cleanupResource();
        return;
    }

    // 区分上传/下载操作并反馈结果
    if (m_currentCmdId == m_ftp->commands().last()) {
        if (m_file->openMode() & QIODevice::ReadOnly) {
            // 上传操作
            emit uploadSuccess(m_remoteFilePath);
            FTP_LOG(QString("上传成功:%1").arg(m_remoteFilePath));
        } else if (m_file->openMode() & QIODevice::WriteOnly) {
            // 下载操作
            emit downloadSuccess(m_localFilePath);
            FTP_LOG(QString("下载成功:%1").arg(m_localFilePath));
        }
    }

    cleanupResource();
}

void FtpClient::onDownloadReadyRead()
{
    if (!m_file || !m_file->isOpen()) return;

    // 读取下载数据并写入本地文件
    QByteArray data = m_ftp->readAll();
    qint64 bytesWritten = m_file->write(data);
    if (bytesWritten > 0) {
        // 计算总进度(续传起始位置 + 已写入字节数)
        qint64 totalDone = m_resumeStartPos + m_file->size();
        // 这里假设已知远程文件大小(实际可通过 SIZE 命令获取)
        emit progressChanged(totalDone, m_ftp->size());
        FTP_LOG(QString("下载进度:%1/%2 字节").arg(totalDone).arg(m_ftp->size()));
    }
}

void FtpClient::onUploadBytesWritten(qint64 bytes)
{
    if (!m_file || !m_file->isOpen()) return;

    // 计算上传进度(续传起始位置 + 已写入字节数)
    qint64 totalDone = m_resumeStartPos + bytes;
    qint64 totalSize = m_file->size() + m_resumeStartPos;
    emit progressChanged(totalDone, totalSize);
    FTP_LOG(QString("上传进度:%1/%2 字节").arg(totalDone).arg(totalSize));
}

void FtpClient::onFtpStateChanged(int state)
{
    bool isConnected = (state == QFtp::Connected);
    emit connectStateChanged(isConnected);

    if (isConnected) {
        FTP_LOG("FTP 连接成功");
    } else if (state == QFtp::Unconnected) {
        FTP_LOG("FTP 连接已断开");
        if (m_isOperating) {
            emit operationFailed("FTP 连接意外断开");
            cleanupResource();
        }
    }
}

qint64 FtpClient::checkRemoteFileExist(const QString& remoteFilePath)
{
    // 使用 SIZE 命令获取远程文件大小(不存在则返回 -1)
    int sizeCmdId = m_ftp->rawCommand(QString("SIZE %1").arg(remoteFilePath));
    m_ftp->waitForDone(); // 等待命令执行完成

    // 解析 SIZE 命令返回结果
    QString response = m_ftp->readAll();
    if (response.startsWith("213")) { // 213 是 SIZE 命令的成功响应码
        qint64 fileSize = response.split(' ').last().toLongLong();
        FTP_LOG(QString("远程文件大小:%1 字节").arg(fileSize));
        return fileSize;
    } else {
        FTP_LOG(QString("远程文件不存在:%1,响应:%2").arg(remoteFilePath).arg(response));
        return -1;
    }
}

QString FtpClient::buildRemoteFilePath(const QString& remoteDir, const QString& fileName)
{
    // 处理 FTP 远程路径(统一使用 '/' 分隔符)
    QString fixedRemoteDir = remoteDir;
    fixedRemoteDir.replace('\\', '/');
    if (!fixedRemoteDir.isEmpty() && !fixedRemoteDir.endsWith('/')) {
        fixedRemoteDir += '/';
    }
    return fixedRemoteDir + fileName;
}

QString FtpClient::buildLocalFilePath(const QString& localDir, const QString& fileName)
{
    // 处理本地路径(适配 Windows 分隔符)
    QDir dir(localDir);
    return dir.filePath(fileName);
}

bool FtpClient::createLocalDir(const QString& localDir)
{
    QDir dir(localDir);
    if (dir.exists()) return true;
    // 递归创建目录(包括父目录)
    return dir.mkpath(localDir);
}

void FtpClient::cleanupResource()
{
    // 关闭文件
    if (m_file) {
        if (m_file->isOpen()) {
            m_file->close();
        }
        delete m_file;
        m_file = nullptr;
    }

    // 重置状态
    m_isOperating = false;
    m_isCanceled = false;
    m_resumeStartPos = 0;
    m_currentCmdId = -1;
    m_remoteFilePath.clear();
    m_localFilePath.clear();

    FTP_LOG("资源清理完成");
}

三、核心功能解析

3.1 FTP 连接初始化

通过 initFtp 方法配置 FTP 服务器参数,内部调用 connectToHostlogin 完成连接。连接状态通过 stateChanged 信号反馈,方便 UI 显示“已连接”或“断开”状态。

3.2 文件上传(支持断点续传)

  1. 本地文件校验:检查本地文件是否存在,避免无效路径;

  2. 远程路径构建:统一使用 '/' 作为 FTP 路径分隔符,避免 Windows 分隔符 '\' 导致的路径错误;

  3. 断点续传逻辑: 通过 SIZE 命令获取远程文件大小,判断文件是否已存在;

  4. 若远程文件存在且大小小于本地文件,使用 QFtp::Append 模式追加上传,实现断点续传;

  5. 若远程文件大小与本地文件一致,直接返回上传成功,避免重复上传。

  6. 进度反馈:通过 bytesWritten 信号实时计算上传进度,反馈给 UI。

3.3 文件下载(支持断点续传)

  1. 本地目录创建:通过 mkpath 递归创建本地保存目录,确保路径存在;

  2. 断点续传逻辑: 检查本地是否存在同名文件,若存在则以文件大小作为续传起始位置;

  3. 发送 REST 命令告知 FTP 服务器续传起始位置,再执行 get 命令下载;

  4. 本地文件以追加模式打开,避免覆盖已下载内容。

  5. 数据写入:通过 readyRead 信号接收下载数据,实时写入本地文件,同时计算进度。

3.4 异常处理与资源清理

通过 cleanupResource 方法统一管理资源,确保在操作完成、失败或取消时:

  • 关闭已打开的本地文件,避免文件句柄泄露;

  • 重置操作状态标志,确保后续操作可正常执行;

  • 清空路径缓存,避免残留数据影响下一次操作。

四、常见报错及解决方案

FTP 开发中容易遇到连接、权限、路径等问题,以下是高频报错的解决方法:

报错 1:“连接超时”或“无法连接到主机”

可能原因

  • FTP 服务器未启动或 IP/端口配置错误;

  • 服务器防火墙阻止了 21 端口的访问;

  • Qt 项目未添加 network 模块,导致网络功能失效。

解决方案

  1. 通过 ping 命令验证服务器 IP 可达性,用 Telnet 测试端口(telnet 192.168.0.10 21);

  2. 在服务器防火墙中开放 21 端口(Windows 需在“高级安全 Windows 防火墙”中添加入站规则);

  3. 确认 .pro 文件中已添加 QT += network,并重新构建项目。

报错 2:“权限被拒绝”(550 Permission denied)

可能原因

  • FTP 服务器用户未被授予写入/读取权限;

  • 远程目录路径错误,指向了无权限的目录;

  • 上传的文件在服务器上已存在且被锁定。

解决方案

  1. 在 FTP 服务器管理界面(如 FileZilla Server)中,给用户授予“读取”“写入”“删除”权限;

  2. 通过 cd 命令验证远程目录路径(如 m_ftp->cd("/upload")),确保路径存在且有权限;

  3. 先删除服务器上的同名文件(或启用断点续传),避免文件锁定导致的权限问题。

报错 3:“远程文件不存在”(550 File not found)

可能原因

  • 远程文件路径拼接错误(如使用 Windows 分隔符 '\');

  • 文件名包含特殊字符(如中文、空格),未正确编码;

  • 执行下载前未切换到正确的远程目录。

解决方案

  1. 使用 buildRemoteFilePath 方法统一处理路径,将 '\' 替换为 '/';

  2. 确保项目配置支持中文编码(QMAKE_CXXFLAGS += -fexec-charset=GBK);

  3. 下载前先执行 m_ftp->cd(remoteDir) 切换到目标目录,再执行下载命令。

报错 4:断点续传失败,重新开始上传/下载

可能原因

  • FTP 服务器不支持 SIZEREST 命令(老式服务器常见);

  • 本地文件被修改,大小与远程文件不匹配;

  • 续传时文件指针移动失败。

解决方案

  1. 在服务器端启用命令支持(如 vsftpd 需配置 allow_raw_commands=YES);

  2. 续传前校验本地文件 MD5,确保文件未被修改;

  3. 优化 seek 方法调用,增加错误判断(如本文代码中“移动文件指针失败”的处理)。

报错 5:发布模式下中文路径文件上传失败

可能原因

  • 发布模式下编码不匹配,中文路径被解析为乱码;

  • Qt 字符串转 FTP 命令时未使用正确的编码格式。

解决方案

  1. .pro 文件中添加编码配置(-fexec-charset=GBK -finput-charset=UTF-8);

  2. 将中文路径转换为 UTF-8 编码后再执行 FTP 命令,示例: QString utf8RemotePath = m_remoteFilePath.toUtf8(); m_currentCmdId = m_ftp->put(m_file, utf8RemotePath);

五、UI 集成与使用示例

FtpClient 集成到 Qt UI 项目中,实现可视化的上传下载功能,核心代码如下:

#include "MainWindow.h"
#include "FtpClient.h"
#include <QMessageBox>
#include <QFileDialog>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    // 初始化 UI(省略按钮、输入框等控件创建代码)
    initUI();

    // 初始化 FTP 客户端
    m_ftpClient = new FtpClient(this);
    connect(m_ftpClient, &FtpClient::progressChanged, this, &MainWindow::onProgressChanged);
    connect(m_ftpClient, &FtpClient::uploadSuccess, this, &MainWindow::onUploadSuccess);
    connect(m_ftpClient, &FtpClient::downloadSuccess, this, &MainWindow::onDownloadSuccess);
    connect(m_ftpClient, &FtpClient::operationFailed, this, &MainWindow::onOperationFailed);
    connect(m_ftpClient, &FtpClient::connectStateChanged, this, &MainWindow::onConnectStateChanged);
}

// 连接 FTP 服务器(按钮点击事件)
void MainWindow::on_btnConnect_clicked()
{
    QString host = ui->leHost->text(); // 服务器 IP
    int port = ui->lePort->text().toInt(); // 端口
    QString user = ui->leUser->text(); // 用户名
    QString pwd = ui->lePwd->text(); // 密码

    m_ftpClient->initFtp(host, port, user, pwd);
}

// 选择本地文件并上传(按钮点击事件)
void MainWindow::on_btnUpload_clicked()
{
    QString localFilePath = QFileDialog::getOpenFileName(this, "选择上传文件");
    if (localFilePath.isEmpty()) return;

    QString remoteDir = ui->leRemoteDir->text(); // 远程目录(如 "/upload")
    bool enableResume = ui->cbResume->isChecked(); // 是否启用断点续传

    m_ftpClient->uploadFile(localFilePath, remoteDir, enableResume);
}

// 下载文件(按钮点击事件)
void MainWindow::on_btnDownload_clicked()
{
    QString remoteFilePath = ui->leRemoteFile->text(); // 远程文件路径(如 "/upload/config.txt")
    QString localDir = QFileDialog::getExistingDirectory(this, "选择保存目录");
    if (localDir.isEmpty()) return;

    bool enableResume = ui->cbResume->isChecked();
    m_ftpClient->downloadFile(remoteFilePath, localDir, enableResume);
}

// 进度更新(更新进度条)
void MainWindow::onProgressChanged(qint64 bytesDone, qint64 bytesTotal)
{
    if (bytesTotal <= 0) return;
    int progress = static_cast<int>((bytesDone * 100.0) / bytesTotal);
    ui->progressBar->setValue(progress);
    ui->lbProgress->setText(QString("进度:%1/%2 字节(%3%)")
                           .arg(bytesDone).arg(bytesTotal).arg(progress));
}

// 上传成功提示
void MainWindow::onUploadSuccess(const QString& remoteFilePath)
{
    QMessageBox::information(this, "成功", QString("文件上传成功!远程路径:%1").arg(remoteFilePath));
    ui->progressBar->setValue(0);
}

// 操作失败提示
void MainWindow::onOperationFailed(const QString& errMsg)
{
    QMessageBox::critical(this, "失败", "操作失败:" + errMsg);
    ui->progressBar->setValue(0);
}

// 连接状态更新(更新按钮状态)
void MainWindow::onConnectStateChanged(bool isConnected)
{
    if (isConnected) {
        ui->btnConnect->setText("断开连接");
        ui->btnUpload->setEnabled(true);
        ui->btnDownload->setEnabled(true);
    } else {
        ui->btnConnect->setText("连接 FTP");
        ui->btnUpload->setEnabled(false);
        ui->btnDownload->setEnabled(false);
    }
}

六、生产环境优化建议

本文提供的代码已实现核心功能,若用于生产环境,建议补充以下优化:

  1. 连接池管理:避免频繁创建/断开 FTP 连接,通过连接池复用会话,提升性能;

  2. 文件校验:上传/下载完成后,通过 MD5 或 SHA256 校验文件完整性,避免传输损坏;

  3. 被动模式支持:生产环境中 FTP 服务器常启用被动模式,需在代码中添加被动模式配置(m_ftp->setPassive(true));

  4. 日志持久化:将 FTP 操作日志写入本地文件,便于问题排查;

  5. 超时重连:添加连接超时检测和自动重连机制,提升稳定性;

  6. 批量操作:扩展支持多文件批量上传/下载,通过队列管理任务。

七、总结

本文基于 Qt Network 模块实现了完整的 FTP 客户端,覆盖从环境搭建、核心功能开发到报错解决的全流程。核心亮点在于:

  • 封装独立的 FtpClient 类,接口简洁,易于集成到任意 Qt 项目;

  • 完整支持断点续传,解决大文件传输中断的痛点;

  • 针对开发中高频报错提供详细解决方案,降低调试成本;

  • 代码兼容 Debug/Release 模式,支持中文路径和跨平台部署。

读者可基于本文代码,根据实际业务需求扩展功能,如集成到设备管理系统实现配置文件同步,或开发桌面端 FTP 工具用于日常文件传输。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值