之前用Qt开发时用Qt4 QFtp,在使用中发现诸多不便,比如很难处理超时,无法进行偏移下载、分片下载等等。因此萌生了用libcurl替代的想法,以下是主要的技术路线和实现细节供参考。

Qt4 QFtp与libcurl的对比

Qt4 QFtp的特点

Qt4提供的QFtp模块是Qt框架原生的FTP实现,具有以下特点:

优点

  • 与Qt框架深度集成,使用信号槽机制处理异步操作,符合Qt编程习惯
  • 无需额外依赖,作为Qt的一部分直接使用
  • 接口封装层次高,上手简单,适合快速开发
  • 支持FTP协议的基本操作(上传、下载、目录浏览等)

缺点

  • 功能相对简单,高级特性支持不足(如断点续传、多线程下载等)
  • 性能表现一般,在大文件传输场景下效率不高
  • 错误处理机制不够完善,调试难度较大
  • Qt5及以上版本已移除QFtp模块,改用QNetworkAccessManager,兼容性受限
  • 对FTP协议的一些扩展支持有限

libcurl的特点

libcurl是一个功能强大的开源网络传输库,支持包括FTP在内的多种协议:

优点

  • 支持丰富的协议(FTP、HTTP、HTTPS等),功能全面
  • 提供完善的FTP特性支持(断点续传、主动/被动模式、SSL加密等)
  • 性能优异,适合大文件传输和高并发场景
  • 活跃的社区支持和持续的更新维护
  • 跨平台性好,在各种操作系统上表现一致
  • 详细的调试信息和完善的错误处理机制

缺点

  • 基于C语言接口,在C++项目中需要自行封装
  • 与Qt等框架的集成需要额外工作
  • 学习曲线相对较陡,需要理解更多底层细节

因此,对于简单的FTP需求且使用Qt4开发,QFtp足以胜任;但对于需要高级特性、更好性能或跨框架的项目,libcurl是更优选择。

一、整体设计

本FTP客户端采用分层设计思想,将功能划分为三个核心层次:

  1. 底层通信层:基于libcurl库实现FTP协议的底层通信,封装CURL的初始化、配置、执行和清理流程
  2. 业务逻辑层:实现FTP核心功能(登录、目录浏览、上传下载等),处理协议细节和错误恢复
  3. UI交互层:基于Qt框架实现用户界面,通过信号槽机制与业务逻辑层交互
    核心类关系如下:
Widget(UI层) <--信号槽--> FtpThread(业务逻辑层) <--调用--> libcurl(底层通信层)

二、核心功能实现路线

1. FtpThread类设计

class FtpThread : public QObject {
    Q_OBJECT
public:
    explicit FtpThread(QObject *parent = nullptr);
    explicit FtpThread(const FtpInfo& info,QObject *parent = nullptr);
    ~FtpThread();
    ...
    int login(const FtpInfo& info);
    int dirlist(const QString& directory);
    int startMultiDownload(const QStringList &remotepath,const QStringList& localPath, bool resume = false,size_t offset=0,size_t len=0 );
    int startDownload(const QString& remotePath, const QString& localPath, bool resume = false,size_t offset=0,size_t len=0 );
    int stopDownload();
   ...
signals:
    void dirListReady(QVector<FtpEntry> entries);
    void downloadProgress(int percent,int bw);
    void downloadFinished(QString localFilePath);
    void downloadError(QString error);

private:
    CURL* curl_init(CommandCode code);
    void curl_exit(CURL*);
    static size_t listCallback(void* ptr, size_t size, size_t nmemb, std::string* data);
    static size_t downloadCallback(void* ptr, size_t size, size_t nmemb, std::ofstream* stream);
    static int downloadProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow,
                                        curl_off_t ultotal, curl_off_t ulnow);
    int performDownload(const QString& remoteUrl, const QString& localPath, bool resume,size_t offset=0,size_t len=0);
};

2. 目录列表获取与解析

目录浏览功能需要处理FTP服务器返回的列表数据,并解析为结构化信息:

    CURL* curl = curl_init();
    curl_easy_setopt(curl, CURLOPT_URL, fullUrl.toUtf8().constData());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, listCallback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &listData);
    
    CURLcode res = curl_easy_perform(curl); 
    curl_easy_cleanup(curl);

3. 文件下载功能实现

下载功能是FTP客户端的核心,本实现支持断点续传、指定范围下载和文件切割下载等高级特性。

3.1 基础下载
    curl_easy_setopt(curl, CURLOPT_URL, encodedRemoteUrl.toUtf8().constData());
    curl_easy_setopt(curl, CURLOPT_USERNAME, m_serverInfo.userName.toUtf8().constData());
    curl_easy_setopt(curl, CURLOPT_PASSWORD, m_serverInfo.passWord.toUtf8().constData());
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, downloadCallback);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &outFile);
    curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, downloadProgressCallback);
    curl_easy_setopt(curl, CURLOPT_XFERINFODATA, this);
    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
3.2 断点续传、范围下载、分片下载

通过CURLOPT_RESUME_FROM_LARGECURLOPT_RANGE实现断点续传和指定范围下载:
在特殊使用场景下,要求对文件按照特定大小进行切割下载。也是基于CURLOPT_RANGE实现

std::string FtpThread::setRange(size_t start, size_t len) {
    if (len == 0) {
        return QString("%1-").arg(start).toStdString();  // 从start到文件末尾
    } else {
        return QString("%1-%2").arg(start).arg(start + len - 1).toStdString();
    }
}

 // 设置断点续传
  if (resume) {
        qDebug()<<"resume download:"<<fileSize<<len;
        curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, fileSize);
  }
  //Range下载、分片下载
  if (range){ 
         std::string  range=setRange(offset,len);
         curl_easy_setopt(curl, CURLOPT_RANGE, range.c_str()); 
  }
3.3 多线程与批量下载

为避免UI阻塞,下载操作在独立线程中执行:

int FtpThread::startMultiDownload(const QStringList &remotePaths, const QStringList& localPaths, 
                                     bool resume, size_t offset, size_t len) {
    if (m_downloadThread.joinable()) {
        m_downloadThread.join();
    }
    m_isDownloading = true;
    m_downloadThread = std::thread([this, remotePaths, localPaths, resume, offset, len]() {
        for(int i = 0 ; i < remotePaths.size(); i++){
            int result = performDownload(url, localPaths.at(i), resume, offset, len);
            if (result == 0 && m_isDownloading) {
                emit downloadFinished(remotePaths.at(i));
            } else {
                break;
            }
        }
        m_isDownloading = false;
    });
    return 0;
}

4. 回调函数监控进度和速率

通过进度回调函数实时跟踪传输状态,并计算传输速率:

int FtpThread::downloadProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow,
                                           curl_off_t , curl_off_t ) {
    auto* worker = static_cast<FtpThread*>(clientp);
    // 计算并发送进度(每秒更新一次)
    static curl_off_t lastBytes = 0;
    static auto lastUpdate = std::chrono::steady_clock::now() - std::chrono::seconds(10);
    auto now = std::chrono::steady_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastUpdate);

    if (duration.count() >= 1000 ) {
        lastUpdate = now;
        int percent = dltotal > 0 ? static_cast<int>((dlnow * 100) / dltotal) : 0;
        
        // 计算传输速率(MB/s)
        float    speed = 1000.0 * (static_cast<long long>(dlnow - lastBytes)) / 
                    duration.count() / 1000 / 1000;
        lastBytes = dlnow;
        emit worker->downloadProgress(percent, speed);
    }

    return 0;
}

三、一些注意事项

1) Windows系统下的中文编码问题通过编码转换解决:
2) URL中的空格处理:

QString encodedRemoteUrl = remoteUrl;
encodedRemoteUrl.replace(" ", "%20");  // 空格替换为URL编码

3)通过原子变量和互斥锁确保线程安全

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐