• libcurl与分片传输、断点续传相关研究


    分片传输、断点续传相关研究

    场景,构建一个下载类组件,基于libcurl,达到正常下载、分片传输、断点续传等功能,同时保证组件的健壮性、对极限情况的兼容性、对上层业务回抛信息的完善

    对本次任务的前置校验操作

    分片传输+断点的实现对本次任务有要求限制,一般来说:针对大文件传输服务端都会进行该功能的配置

    1. 通过服务端请求拿到文件总长度
      给出一个demo例子,具体需要根据业务场景添加
    bool getFileLength(const std::string url, curl_off_t& fileLength) {
        bool retValue = false;
        for (int retrytime = 0; retrytime < 5; retrytime++) {
            CURL* curlHandle = curl_easy_init();
            curl_easy_setopt(curlHandle, CURLOPT_SHARE, sharednsHandle);
            curl_easy_setopt(curlHandle, CURLOPT_DNS_CACHE_TIMEOUT, 60 * 5);
            curl_easy_setopt(curlHandle, CURLOPT_CUSTOMREQUEST, "GET");
            curl_easy_setopt(curlHandle, CURLOPT_URL, url.c_str());
            curl_easy_setopt(curlHandle, CURLOPT_HEADER, 1);
            curl_easy_setopt(curlHandle, CURLOPT_NOBODY, 1);
            curl_easy_setopt(curlHandle, CURLOPT_TIMEOUT, MM_TIMEOUT);
            curl_easy_setopt(curlHandle, CURLOPT_NOSIGNAL, 1L);
            CURLcode code = curl_easy_perform(curlHandle);
            if (code == CURLE_OK) {
             	curl_easy_getinfo(curlHandle,
                                      CURLINFO_CONTENT_LENGTH_DOWNLOAD_T,
                                      &fileLength);
                retValue = true;
                break;
            }
    	}
    	return retValue;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    1. 通过请求得到的HEADERDATA中是否含有Content-Range: bytes、Accept-Ranges: bytes确定该下载是否支持分段传输
    bool useMutilDownload(std::string url) {
        bool ret = false;
    
        for (int retryTime = 0; retryTime < 5; retryTime++) {
            CURL* curlHandle = curl_easy_init();
            curl_easy_setopt(curlHandle, CURLOPT_SHARE, sharednsHandle);
            curl_easy_setopt(curlHandle, CURLOPT_DNS_CACHE_TIMEOUT, 60 * 5);
            curl_easy_setopt(curlHandle, CURLOPT_URL, url.c_str());
            curl_easy_setopt(curlHandle, CURLOPT_HEADER, 1);
            curl_easy_setopt(curlHandle, CURLOPT_NOBODY, 1);
            std::string strHeader;
            curl_easy_setopt(curlHandle, CURLOPT_HEADERDATA, &strHeader);
            curl_easy_setopt(curlHandle, CURLOPT_HEADERFUNCTION,
                             &LibcurlMultiThread::headerInfo);
            curl_easy_setopt(curlHandle, CURLOPT_RANGE, "0-");
            curl_easy_setopt(curlHandle, CURLOPT_TIMEOUT, MM_TIMEOUT);
            curl_easy_setopt(curlHandle, CURLOPT_NOSIGNAL, 1L);
            CURLcode code = curl_easy_perform(curlHandle);
            if (code == CURLE_OK) {
                ret =
                    ((strHeader.find("Content-Range: bytes") !=
                      std::string::npos) ||
                     (strHeader.find("Accept-Ranges: bytes") != std::string::npos));
                break;
            }
        }
        return ret;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    针对多任务下载及断点传输文件的设计

    注:所有的操作,保证使用curl_off_t(__int64)

    • . 使用.dltmp文件进行过度,用内存映射创建临时文件,大小为前置工作中已获取的长度+分片传输自定义信息

    文件设计

    beginPos:该块开始的位置,blockSize:该块下载内容大小,recvSize:该块下载已完成的大小

    最优线程数计算:min(文件长度/最优下载块大小,自定义最大线程数量)

    例:文件长度为2001byte,将分为5个任务下载,临时信息长度5 * 3 * sizeof(curl_off_t)+sizeof(curl_off_t);5块分别下载大小400、400、400、400、401,则第一块的信息是,beginPos 0,blockSize 400,recvSize 0,第二块beginPos 400,blcokSize 400,recvSize 400…

    实际开发中,可自定义数据结构,用于存储若干个任务块的信息。同时在开始传输前,将临时文件尾部的信息进行准确填写,具体操作为_fseeki64配合已知的文件长度,定位到待写入的位置,信息依次写入。注意:一旦某个字节出错,则会导致全部下载失败!

    • . 使用curl_multi_init()替换多线程

    7.9.6版本后引入的multi接口,可以一次针对多个easy_curl句柄进行操作,类似多路复用IO。个人的设计中,将多线程替换为该方式,可以很大程度上降低复杂度。

    libcurl的multi interface,7.9.6之后引入。个人分析为:easy_curl在开始执行后会阻塞当前线程,因此通常使用多线程来实现下载,而multi interface可以对多个句柄共同操作,同时这个操作可以是异步的,避免了主线程阻塞。但对于任务的结束,仍需要遍历任务堆栈。

    思路:

    1. n个线程变为获取n个easy_curl任务句柄,配合分片传输中的beginPos、blockSize、recvSize信息,确定该句柄执行的下载任务范围;
    2. 若干句柄使用同一个写入文件回调函数来避免同步的问题
    3. 将若干句柄加入multi中,异步启动,不阻塞主线程;
    • 文件写入回调函数

    该回调函数是分片传输的核心,具体分为以下几个步骤

    1. 多个任务要使用同一个回调来避免IO等问题
    2. 根据自定义的数据结构(主要存放每个任务的信息,包括beginPos,blockSize,recvSize等),将文件句柄_fseeki64重新定位,值为beginPos+recvSize,再将本次得到的buffer进行写入操作;
    3. 重新定位到该次写入任务对应的块位置,为达到这个目的,也许需要在自定义的数据结构中加入相关信息实现,当_fseeki64定位到块的位置后,将beginPos、blockSize、recvSize进行值更新

    断点流程

    • 根据临时文件的信息判断是否可续传
    1. 首先得到文件长度,同从服务端请求的长度比对,若不同则可能文件已更新或损坏,需要重新构建下载任务
    2. 根据文件长度,得到临时信息相关的位置,将文件句柄定位至该处,读取每一个块的信息(beginPos、blockSize、recvSize),更新自定义数据结构
    3. 根据读取的内容,构建本次下载任务对应块的偏移量(beginPos,blockSize-recvSize),将多个easy_curl句柄加入mutil_curl,断点传输

    核心流程总结梳理

    1. 本次下载的前置操作,包括请求本次下载文件长度、是否启动分片传输
    2. 临时文件的构建,使用内存映射创建临时文件格式为.dltmp,大小为请求的文件长度+分片传输信息长度(sizeof(cur_off_t))+n* 3 * sizeof(cur_off_t)
    3. 填充自定义的数据结构,并将内容写入临时文件的后缀信息中
    4. 构建若干个cur_easy句柄任务,设置每个句柄下载任务区间,将其加入到multi句柄
    5. 为若干句柄构建同一个文件写入回调,在回调中,重定向文件写入指针位置,写入本次buffer数据;再次重新定位文件句柄到对应任务的尾部信息位置,更新数据结构和文件尾部信息内容(recvSize);
    6. 当断点续传时,首先读取尾部文件确定文件是否可用,后根据尾部的块信息定义本次下载任务每个块对应的区间
    7. 利用mutil_curl进行任务异步进行

    待解决

    1. 健壮性,任何一个字节的出错将导致整个文件的出错,而实际应用中网络环境复杂、机器IO性能差异化明显,因此该设计使用需要构建完善的边界极限处理机制。
    2. 上层业务的感知性,libcurl的easy_curl可以得到错误码,只针对本块传输的下载和写入,而对于所有的读取、临时信息解析、临时信息写入、IO等操作,需要构建一套可回抛给上层业务的错误码机制
    3. 异步问题,虽然使用mutil代替线程已经很大程度降低了复杂度,但获取当前的下载状态信息等仍需要轮询mutil本身的消息堆栈,而该操作不能影响主线程,因此仍需要异步任务来调用curl_multi_poll或curl_multi_perform来实现,作为组件的效果不如直接对接具体业务。(组件黑盒化,对外界无感知)

    实际使用,需要从整个框架层面设计,包含主流程、网络错误处理、IO错误处理、业务层回抛信息、异步操作等内容综合设计

  • 相关阅读:
    基于深度学习网络的人员吸烟行为检测算法matlab仿真
    C语言 - 通讯录详解
    二分/树上第k短路,LeetCode2386. 找出数组的第 K 大和
    第十四章《多线程》第9节:ThreadLocal类
    面向防疫的智能导诊机器人关键技术及应用
    【高阶数据结构(八)】跳表详解
    【设计模式】Java设计模式 - 访问者模式
    CANopen扫盲
    ali内核服务器搭建Linux版本的小皮面板(微调)
    Python - PyMuPDF (fitz) 处理 PDF
  • 原文地址:https://blog.csdn.net/qq_44745063/article/details/127817033