借助libcurl实现ftp文件上传,断点续传demo梳理。

news2024/11/16 13:49:59

公司业务,需要实现一个ftp大文件上传的功能,简单搭建一个ftp服务器,首先研究demo,以及断点上传的功能。

1:首先了解文件上传相关协议,ftp,sftp或者基于http,其他自己实现等。

2:确定基于ftp实现,搭建简单的ftp服务器并用工具确定服务器正常。

3:基于现有的服务器,了解相关ftp开源库,使用代码实现文件上传最基本的功能。

4:考虑到大文件的传输,首先考虑断点续传功能,后期用线程池/多线程方案进行适配优化。

5:考虑使用场景,适配在windows上进行测试,基于qt。

1:在linux上安装ftp服务器 vsftpd 了解sftp和ftp的区别。

​ sftp基于ssh进行远程传输,基于ssh通道进行数据传输,默认使用22端口。
​ ftp需要单独配置ftp服务器,比如linux上安装vsftpd,windows上安装FileZilla Server,默认使用20/21端口传输
​ 21端口是控制信息端口,20端口一般数据传输端口,可以协商。
​ 主动方式 ftp客户端开放端口给服务器,连接上21端口后,由服务器主动连接开放端口,可能被防火墙阻塞。
​ 被动方式 服务器开放另外的数据端口给客户端,客户端直接连接进行数据传输。

2:ubuntu环境安装ftp服务器后,测试使用。

​ 这里我ubuntu环境使用vsftpd进行安装,安装后正常启动,需要关注配置文件中,/etc/vsftpd.conf,下面的配置,否则一直上传有报错。
​ local_enable=YES
​ write_enable=YES
​ 这里我使用xftp连接我的ftp服务器,21端口,使用ftp进行上传(sftp基于ssh,原理不一样),这次测试成功,即基于工具测试ftp服务器成功。

#ftp服务器上  服务进程已经正常启动
ubuntu@ubuntu:~/test/test1$ ps afx|grep vsftpd
  84952 pts/4    S+     0:00  |           \_ grep --color=auto vsftpd
  83278 ?        Ss     0:00 /usr/sbin/vsftpd /etc/vsftpd.conf

3:分析ftp上传时的端口交互。

​ 要使用代码实现ftp的相关功能,需要 借助相关开源代码 libcurl 或者libftp libssh2 (sftp)
​ 需要根据业务场景,结合上传的文件个数,大小,选择适应的协议进行上传 ftp还是sftp

4:代码实现ftp上传功能测试

因为我比较熟悉linux,首先参考网络,或者libcurl开源库下的example中测试用例,实现最基本的上传文件成功。

这里遇到的问题是,参考网络时,url都是域名,使用自己搭建的ftp时,需要注意url的设置正确,以及这里登陆用户是我自己的账号,根目录也就是/home/用户名,否则会报错。

#include <iostream>
#include <curl/curl.h>

int main() {
    CURL* curl;
    CURLcode res;
    
    // 初始化CURL对象
    curl_global_init(CURL_GLOBAL_ALL);
    curl = curl_easy_init();
    
    if (curl) {
        // 设置远程服务器地址、用户名和密码
        std::string url = "ftp://192.168.189.132/";
        std::string username = "ubuntu";
        std::string password = "123456";
        
        // 设置本地文件路径
        std::string localFile = "/home/ubuntu/ftp_test/1.cpp";
        
        // 配置CURL选项
        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
        curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
        curl_easy_setopt(curl, CURLOPT_USERNAME, username.c_str());
        curl_easy_setopt(curl, CURLOPT_PASSWORD, password.c_str());
        
        FILE* file = fopen(localFile.c_str(), "rb");
        
        if (file) {
            // 上传文件
            curl_easy_setopt(curl, CURLOPT_READDATA, file);
            
            res = curl_easy_perform(curl);
            
            if (res == CURLE_OK) {
                std::cout << "文件上传成功" << std::endl;
            } else {
                std::cout << "文件上传失败: " << curl_easy_strerror(res) << std::endl;
            }
            
            fclose(file);
        }
        
        // 清理CURL资源
        curl_easy_cleanup(curl);
    }
    
    // 全局清理
    curl_global_cleanup();
}

5:基于libcurl实现文件上传功能

经过了解,libcurl是最好实现该方案的开源库,暂时确定以该库入手实现功能。

5.1 首先获取libcurl开源库源码,编译完成后,了解相关目录架构,参考example目录下相关demo,了解相关基本接口。
5.2 获取libcurl的windows版本,这里直接从官网获取,基于qt进行测试
5.2.1 首先,获取到release版本后,直接解压在对应目录,需要在qt项目中,链接对应的头文件和lib库,使代码编译能通过。
#在测试的qt项目中 pro文件中增加libcurl库的路径,这里我的路径如下
INCLUDEPATH += E:/curl-8.5.0_3-win32-mingw/include
LIBS += -LE:/curl-8.5.0_3-win32-mingw/lib -lcurl

同时: 代码实现时能正常调用到libcurl的库,但是运行无反应。

需要把libcurl对应的链接库拷贝到qt 项目debug/release运行目录下,取libcurl release版本目录下bin目录下libcurl.dll。

5.2.2 实现简单的界面,测试服务器连接正常,

在这里插入图片描述

服务器连接测试demo:

void MainWindow::on_pb_check_connect_clicked()
{
    QString ftp_server_addr = ftp_addr + ftp_dir;

    CURL *curl;
    CURLcode res;
    curl_global_init(CURL_GLOBAL_DEFAULT);
    curl = curl_easy_init();
    if (curl) {
        // 设置FTP服务器地址和端口
        curl_easy_setopt(curl, CURLOPT_URL, ftp_addr.toStdString().c_str());
        // 设置用户名和密码
        curl_easy_setopt(curl, CURLOPT_USERNAME, ftp_username.toStdString().c_str());
        curl_easy_setopt(curl, CURLOPT_PASSWORD, ftp_passwd.toStdString().c_str());
        // 发起连接请求
        res = curl_easy_perform(curl);

        // 检查连接状态
        if (res == CURLE_OK) {
            ui->Display_Edit->appendPlainText("ftp 测试连接成功 : "+QString(curl_easy_strerror(res)));
        } else {
            ui->Display_Edit->appendPlainText("ftp 测试连接失败 : "+QString(curl_easy_strerror(res)));
        }
        curl_easy_cleanup(curl);
    }
    curl_global_cleanup();
}
5.2.3 测试单个文件上传正常
//用户自定义数据指针  下载总估计值 已下载   上传总值  已上传
static int progressCallback(void *p, double dltotal, double dlnow, double ult,
                   double uln)
{
    Q_UNUSED(p);
    Q_UNUSED(dltotal);
    Q_UNUSED(dlnow);
    double process = (double)uln / ult * 100;
    qDebug()<<"progressCallback :"<<process;
    return 0;
}

//开始一个文件的上传测试  注意进度的打印
void MainWindow::on_pb_start_one_clicked()
{
    ui->Display_Edit->appendPlainText("开始上传一个文件:"+ ftp_file);
    //这里实际是基于上面测试连接的基础上  加上真正的上传。
    CURL *curl;
    curl_global_init(CURL_GLOBAL_DEFAULT);
    curl = curl_easy_init();
    if(curl == nullptr)
    {
        ui->Display_Edit->appendPlainText("创建句柄失败,请检查!");
        curl_global_cleanup();
        return;
    }

    FILE* hd_src = fopen(ftp_file.toStdString().c_str(), "rb");
    if (!hd_src) {
        ui->Display_Edit->appendPlainText("打开文件失败:"+ftp_file);
        curl_global_cleanup();
        return;
    }

    //这里是真正的上传目的 注意文件名的拼接
    QString ftp_server_addr = ftp_addr + ftp_dir +"/test";

    curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
    curl_easy_setopt(curl, CURLOPT_URL, ftp_server_addr.toStdString().c_str());
    curl_easy_setopt(curl, CURLOPT_USERPWD, QString(ftp_username+":"+ftp_passwd).toStdString().c_str());

    fseek(hd_src, 0L, SEEK_END);
    long fileSize = ftell(hd_src);
    fseek(hd_src, 0L, SEEK_SET);

    curl_easy_setopt(curl, CURLOPT_READFUNCTION, read_callback);  //读文件的回调
    curl_easy_setopt(curl, CURLOPT_READDATA, hd_src); //设置要上传的文件的指针
    curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)fileSize);

    // 设置CURLOPT_NOPROGRESS为0,以启用进度回调函数
    // 设置CURLOPT_PROGRESSFUNCTION为progressCallback函数指针,用于获取上传进度
    curl_easy_setopt(curl,CURLOPT_NOPROGRESS , 0L);
    curl_easy_setopt(curl,CURLOPT_PROGRESSFUNCTION , progressCallback);

    CURLcode res;
    res = curl_easy_perform(curl);

    // 检查连接状态
    if (res == CURLE_OK) {
        ui->Display_Edit->appendPlainText("ftp 上传文件成功 : "+ftp_file);
    } else {
        ui->Display_Edit->appendPlainText("ftp 上传文件失败 : "+ftp_file);
    }
    curl_easy_cleanup(curl);
    fclose(hd_src);

    curl_global_cleanup();
}

5.2.4 如果服务器上已经有该文件,并且上传一半,测试断点续传功能正常。

===》这里我阻塞一段时间,参考example下的resume代码,一直无法实现,最后发现获取服务器上文件名称定义的CURL 实例,需要和真正上传文件时CURL实例

static size_t getcontentlengthfunc(void *ptr, size_t size, size_t nmemb,  void *stream)
{
    int r;
    long len = 0;

    char *pptr = (char*)ptr;
    r = sscanf(pptr, "Content-Length: %ld\n", &len);
    if(r)
        *((long *) stream) = len;

    return size * nmemb;
}

static size_t discardfunc(void *ptr, size_t size, size_t nmemb, void *stream)
{
    char * cptr = (char*) ptr;
    qDebug()<<QString::fromUtf8(cptr);
  (void)ptr;
  (void)stream;
  return size * nmemb;
}

static size_t readfunc(char *ptr, size_t size, size_t nmemb, void *stream)
{
    FILE *f = static_cast<FILE *>(stream);
    size_t n;

    if(ferror(f))
        return CURL_READFUNC_ABORT;

    n = fread(ptr, size, nmemb, f) * size;

    return n;
}

//单个文件的断点续传测试 构造断点续传场景
//发现断点续传,这种方案并不可靠,分开构造curl分别获取服务端文件大小,进行续传处理
void MainWindow::on_pb_restart_one_clicked()
{
    CURL *curl = nullptr;
    curl_global_init(CURL_GLOBAL_ALL);
    curl = curl_easy_init();
    if(curl == nullptr)
    {
        ui->Display_Edit->appendPlainText("创建句柄失败,请检查!");
        curl_global_cleanup();
        return;
    }

    FILE* hd_src = fopen(ftp_file.toStdString().c_str(), "rb");
    if (!hd_src) {
        ui->Display_Edit->appendPlainText("打开文件失败:"+ftp_file);
        curl_global_cleanup();
        return;
    }
    //设置上传   url  用户名和密码 默认端口
    QString ftp_server_addr = ftp_addr + ftp_dir +"/test";
    curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
    curl_easy_setopt(curl, CURLOPT_URL, ftp_server_addr.toStdString().c_str());
    curl_easy_setopt(curl, CURLOPT_USERPWD, QString(ftp_username+":"+ftp_passwd).toStdString().c_str());

    long uploaded_len = 0;
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, getcontentlengthfunc);  //相应头部的回调函数
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, &uploaded_len);             //从头部获取到目标远程文件的大小


    CURLcode res = CURLE_GOT_NOTHING;
    for(int i=0; (i<3)  && (res!= CURLE_OK); ++i)
    {
        curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);  //只获取响应头信息  而不实际下载响应体
        curl_easy_setopt(curl, CURLOPT_HEADER, 1L);  //响应头信息包含在返回的数据中 和上面的读数据一致

        res = curl_easy_perform(curl); //这里获取远程服务器文件的大小 
        if(res != CURLE_OK)
            continue;
        curl_easy_cleanup(curl); //获取后,先清理,再重新进行必要的设置,上传成功了。

        curl = curl_easy_init();
        curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L);
        curl_easy_setopt(curl, CURLOPT_URL, ftp_server_addr.toStdString().c_str());
        curl_easy_setopt(curl, CURLOPT_USERPWD, QString(ftp_username+":"+ftp_passwd).toStdString().c_str());

        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, discardfunc);            //这是获取下载的数据?
        curl_easy_setopt(curl, CURLOPT_READFUNCTION, readfunc);
        curl_easy_setopt(curl, CURLOPT_READDATA, hd_src);             //发送请求中回调函数的指针

        curl_easy_setopt(curl, CURLOPT_FTPPORT, "-");   //默认端口
        curl_easy_setopt(curl, CURLOPT_ACCEPTTIMEOUT_MS, 7000L);
        curl_easy_setopt(curl, CURLOPT_FTP_CREATE_MISSING_DIRS, 1L);  //自动创建缺失目录
        curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);

        ui->Display_Edit->appendPlainText("获取到服务器文件大小为:"+QString::number(uploaded_len));
        curl_easy_setopt(curl, CURLOPT_NOBODY, 0L);  //重新设置
        curl_easy_setopt(curl, CURLOPT_HEADER, 0L);

        fseek(hd_src, uploaded_len, SEEK_SET); //把hd_src从开始位置偏移uploaded_len长度
        curl_easy_setopt(curl, CURLOPT_APPEND, 1L); //远程文件存在  则追加
    }

    if(res != CURLE_OK)
    {
        curl_easy_setopt(curl, CURLOPT_APPEND, 0L);
    }

    res = curl_easy_perform(curl); //真正的数据上传

    if(res == CURLE_OK)
        ui->Display_Edit->appendPlainText("断点续传文件成功 !");
    else
        ui->Display_Edit->appendPlainText("断点续传文件失败 ! "+QString(curl_easy_strerror(res)));

    fclose(hd_src);
    curl_easy_cleanup(curl);
    curl_global_cleanup();
}

6:阻塞问题

1:断点续传一直不生效,发现是设置CURLOPT_NOBODY 和CURLOPT_HEADER 后,就不会触发上传。

===》解决方案是获取服务器上对应文件大小后,先清理对应的CURL * 再进行设置就好

2:模拟断点续传时,手动在linux上把目标文件进行部分内容的删除。

===》断点续传再次触发后,发现和源文件相比,两次上传之间有个换行。

===》解决方案,发现是linux环境手动删除时,自带一个换行符号,把该文件传到window环境上删除换行符后正常。

只是第一版初步的探索demo,技术点已经攻克,下一步优化代码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1352818.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

图像分割-漫水填充法 floodFill

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 本文的C#版本请访问&#xff1a;图像分割-漫水填充法 floodFill (C#&#xff09;-CSDN博客 FloodFill方法是一种图像处理算法&#…

最新Java详细安装教程

Java详细安装教程 JDK与JRE区别java官网链接java安装配置环境验证java安装成功 文章参加https://zhuanlan.zhihu.com/p/612846156 各位小伙伴想要博客相关资料的话关注公众号&#xff1a;chuanyeTry即可领取相关资料&#xff01; JDK与JRE区别 对于安装Java的新手&#xff0c;…

Hive09_函数

HIVE函数 系统内置函数 1&#xff09;查看系统自带的函数 hive> show functions;2&#xff09;显示自带的函数的用法 hive> desc function upper;3&#xff09;详细显示自带的函数的用法 hive> desc function extended upper;hive函数分类 1、UDF&#xff1a;用…

理解 RPC 与 Protobuf:完整指南

一、Protobuf 数据格式简析 Protobuf 是什么&#xff1f; 在数据密集型应用领域&#xff0c;Google 开发的 Protobuf 作为一种高效数据编码方式而广受欢迎。它胜任于 JSON 及 XML 对比&#xff0c;不仅在体积和速度上表现出色&#xff0c;而且其结构化方式优化了网络传输中的…

组织权限收集表

在组织角色收集过程中&#xff0c;主要分为两个重要环节&#xff1a;用户信息的收集和角色定义。其中&#xff0c;用户信息的收集相对简单&#xff0c;而角色定义则更为复杂。 在我们的项目中&#xff0c;权限涉及页面权限、按钮权限和数据权。为了确保每个角色具备适当的权限…

数字信号处理期末复习——计算小题(二)

个人名片&#xff1a; &#x1f981;作者简介&#xff1a;一名喜欢分享和记录学习的在校大学生 &#x1f42f;个人主页&#xff1a;妄北y &#x1f427;个人QQ&#xff1a;2061314755 &#x1f43b;个人邮箱&#xff1a;2061314755qq.com &#x1f989;个人WeChat&#xff1a;V…

SQL 在已有表中修改列名的方法

文章目录 1. MySQL2. SQL Server3. Oracle / PostgreSQL Question&#xff1a; 假设有一张表 StudentInfo&#xff0c;表中有一个列名是 Student_Name &#xff0c;想要把这个列名改成 StudentName 应该如何操作&#xff1f; 建表语句如下&#xff1a; --建表 if object_id(S…

零知识证明(zk-SNARK)- groth16(一)

全称为 Zero-Knowledge Succinct Non-Interactive Argument of Knowledge&#xff0c;简洁非交互式零知识证明&#xff0c;简洁性使得运行该协议时&#xff0c;即便 statement 非常大&#xff0c;它的 proof 大小也仅有几百个bytes&#xff0c;并且验证一个 proof 的时间可以达…

算法28:力扣64题,最小路径和------------样本模型

题目&#xff1a; 给定一个二维数组matrix&#xff0c;一个人必须从左上角出发&#xff0c;最后到达右下角 。沿途只可以向下或者向右走&#xff0c;沿途的数字都累加就是距离累加和 * 返回累加和最小值 思路&#xff1a; 1. 既然是给定二维数组matrix&#xff0c;那么二维数…

LDD学习笔记 -- Linux内核模块

LDD学习笔记 -- 内核模块 简介LKM类型Static Linux Kernel ModuleDynamic Linux Kernel ModuleLKM编写语法 syntax详细描述内核头文件用户空间头文件Module Initialization FunctionModule Cleanup FunctionKeyword & Tag宏 __init __exitLKM入口注册Module Metadate&#…

使用Matplotlib模拟绘制北京上海气温变化折线图

02 模拟北京上海气温变化折线图 通过本练习&#xff0c;可以掌握如何在一个坐标系中展示多个折线图&#xff0c;以及如何修改折线图的颜色和样式&#xff0c;以及如何设置和显示图例。 在一个坐标系中绘制两条折线 要在一个坐标系中绘制两条这些&#xff0c;我们只需要进行两…

Python open函数详解:打开指定文件与 readline和readlines函数:按行读取文件

Python open函数详解&#xff1a;打开指定文件 掌握了各种操作目录字符串或目录的函数之后&#xff0c;接下来可以准备读写文件了。在进行文件读写之前&#xff0c;首先要打开文件。 Python 提供了一个内置的 open() 函数&#xff0c;该函数用于打开指定文件。 open() 函数的…

Matplotlib基础

目录&#xff1a; 一、绘制函数图像&#xff1a;二、创建图形对象&#xff1a;三、绘制多子图&#xff1a; 一、绘制函数图像&#xff1a; from matplotlib import pyplot as plt import numpy as np #生成&#xff08;-50,50&#xff09;的数组 x np.arange(-50,50) #计算因…

口罩佩戴监测识别摄像机

口罩佩戴监测识别摄像机是一种应用于公共场所的智能监控设备&#xff0c;旨在监测人们是否正确佩戴口罩。这种摄像机使用先进的图像识别技术&#xff0c;能够准确辨识出人们的面部&#xff0c;并判断是否佩戴口罩。该技术可以用于各种场所&#xff0c;如火车站、机场、商场、学…

IDEA 修改 jdk 版本

三步&#xff1a; 一 、file--setting 二、 file--Project Structure 三 、file--Project Structure

【JUC的四大同步辅助类】

文章目录 一、CountDownLatch二、CyclicBarrier三、Semaphore四、Phaser 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、CountDownLatch CountDownLatch如同火箭发射&#xff0c;计数只能不断减减&#xff0c;当到达0时即发射 场景示例&#xff1…

gitee创建仓库

描述 本文章记录了怎么在gitee上创建项目&#xff0c;以及使用vscode提代码到远程呢个仓库&#xff0c;如何创建一个新分支&#xff0c;并将新分支提交到远程仓库。 1、创建远程仓库 在创建远程仓库之前要先进行ssh密钥的设置 &#xff08;1&#xff09;打开黑窗口&#xff…

NSSCTF 简单包含

开启环境: 使用POST传flag&#xff0c;flag目录/var/www/html/flag.php 先使用post来尝试读取该flag.php 没反应: 查看一下源码index.php&#xff0c;看有什么条件 base64解密: <?php$path $_POST["flag"];if (strlen(file_get_contents(php://input)) <…

SSH

简介 SSH&#xff1a;Secure Shell Protocol&#xff0c;安全的远程登录&#xff0c;实现加密通信&#xff0c;替代传统telnet协议。 端口&#xff1a;22/tcp 软件实现&#xff1a; OpenSSH&#xff1a;ssh协议的开源实现&#xff0c;CentOS默认安装Dropbear&#xff1a;另…

Socket closed 异常解决方案:如何解决 JMeter 压测中的问题

问题描述 JMeter 压测时会报 java.net.SocketException: Socket closed java.net.SocketException: Socket closed at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) at java.ne…