C++文件下载实战:libcurl从入门到断点续传多线程
干了这么多年C你迟早会遇到这种需求程序里要下载一个文件从更新包到数据文件从模型权重到资源包跑在服务器上、跑在用户电脑上跨平台还得稳定。命令行里curl一条命令就搞定的事轮到自己在代码里实现却不知道从哪下手。如果你搜过资料应该已经见过无数次这个名字libcurl。它就是C/C生态里最成熟的网络传输库没有之一。这篇文章就用最短的路径把libcurl做文件下载这件事讲透从最小可运行代码讲到断点续传、多线程分片全程可直接抄作业。这篇文章写给谁刚入门C、想给程序加上下载功能的新手以及用C做工具开发、想快速搞定网络传输的工程师。不需要你有多少网络编程基础但建议你先掌握结构体、指针、函数指针这三个C基础概念它们就是理解libcurl的关键。1. 为什么偏偏是libcurl方案选型分析1.1 自己手写HTTP下载为什么不现实很多人第一反应是下载文件不就是发个GET请求、收个响应吗我直接用socket手写不就行了理论上确实可行。但手写HTTP下载意味着你要自己处理TCP建立连接、HTTP报文解析、chunked编码、重定向跳转、HTTPS的TLS握手、证书验证、断点续传时的Range头、服务器返回的各种状态码……这个列表还能继续列下去。等你把这些全部实现完并测稳定大概率已经过了两三个星期而且只支持了HTTP换个HTTPS又得接入OpenSSL。我见过不少项目从手写socket开始最后要么换回libcurl要么维护着一堆只能在特定环境运行的黑魔法代码。结论很直接网络传输这件事除非你的目标是学习协议本身否则永远不要自己造轮子。libcurl就是那个被无数项目验证过、你不需要重复发明的轮子。1.2 主流的几套方案横向对比把当前C做下载的常见选择放在一起看差别非常明显方案跨平台协议支持上手成本维护现状libcurlWindows/Linux/macOS全支持HTTP/HTTPS/FTP/SFTP等几十种低API设计简单极其活跃C语言API稳定WinINet/WinHTTP仅WindowsHTTP/HTTPS为主中API风格偏Windows微软维护更新慢Qt Network全平台HTTP/HTTPS/FTP中需引入Qt框架跟随Qt发布节奏boost.beast全平台HTTP/WebSocket高需要懂Asio异步模型活跃但学习曲线陡自己手写socket取决于自己一般只有HTTP极高完全靠自己维护从表里能很清楚地看到libcurl几乎是唯一一个在“跨平台、协议丰富、上手快”三个维度同时拿到高分的选项。Qt Network其实也很好但为了一个下载功能引入整套Qt框架对不依赖Qt的项目来说成本太高。1.3 libcurl到底强在哪说几个实际体验层面的东西你就明白为什么它在C/C圈子里地位这么稳。第一它支持HTTP、HTTPS、FTP、SFTP、SMTP、IMAP等几十种协议意味着你写一套代码既能下载HTTP资源也能在需要的时候下载FTP文件甚至还能发邮件、上传文件API风格完全一致。第二它的API是纯C写的这反而是最大的优点。C可以直接调用C语言项目也能用甚至其他语言Python、PHP、Java等的很多网络库底层都是libcurl。这意味着它的社区极其庞大你遇到的任何问题基本上都能搜到现成的解法。第三它本身就是命令行curl所用的核心引擎。你平时在终端里用curl下载文件、测试接口时底层跑的就是libcurl。所以你可以先在命令行里用curl -I url验证一个链接是否可达、返回什么头部信息再回到代码里写下载逻辑排查问题的路径非常顺畅。说句实在话如果你只想做文件下载这一个事不搞复杂的异步高性能场景libcurl的easy interface就够用了而且API设计得相当友好。2. 环境准备与第一个下载Demo五分钟跑通2.1 三分钟搞定libcurl开发环境先用最短的方式把开发环境搭起来这里给三种方案按推荐顺序排列。方案一vcpkg Visual StudioWindows环境实测最省心git clone https://github.com/microsoft/vcpkg.git cd vcpkg .\bootstrap-vcpkg.bat .\vcpkg install curl:x86-windows .\vcpkg integrate install如果你用的是VS2022integrate install之后新建项目就能直接#include curl/curl.h链接和头文件路径全部自动配好基本零成本。Linux或macOS环境下也可以用curl这个包名来安装x64的triplet换成x64-linux或arm64-osx即可。方案二直接下载预编译包从curl官网的下载页拿对应的Windows预编译库解压后手动在VS里配置包含目录、库目录和附加依赖项。这种方式适合不喜欢包管理器的朋友但要注意选对架构x86/x64和运行时/MD、/MT。方案三Linux直接用系统包sudo apt install libcurl4-openssl-devUbuntu/Debian系一行命令搞定头文件在/usr/include/x86_64-linux-gnu/curl/curl.h链接的时候加-lcurl就行。CentOS/RHEL系对应的是libcurl-devel。2.2 完整的最小下载程序环境好了之后直接上第一个完整例子。这个例子做的事情很简单从指定URL下载文件保存到本地。#include curl/curl.h #include cstdio // 回调函数libcurl收到数据时就调用这个函数 static size_t WriteFileCallback(void* contents, size_t size, size_t nmemb, void* userp) { size_t totalSize size * nmemb; FILE* fp static_castFILE*(userp); return fwrite(contents, 1, totalSize, fp); } int main() { // 1. 全局初始化整个程序只需做一次 curl_global_init(CURL_GLOBAL_DEFAULT); // 2. 创建一个easy handle CURL* curl curl_easy_init(); if (!curl) { fprintf(stderr, curl_easy_init() 失败\n); return -1; } // 3. 打开本地文件wb模式表示以二进制写入 FILE* fp fopen(dataset.zip, wb); if (!fp) { fprintf(stderr, 无法打开本地文件\n); curl_easy_cleanup(curl); return -2; } // 4. 设置URL和写文件回调 curl_easy_setopt(curl, CURLOPT_URL, https://example.com/dataset.zip); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteFileCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); // 5. 自动跟随重定向301/302等 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // 6. 验证服务器证书生产环境请保持开启 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); // 7. 执行传输 CURLcode res curl_easy_perform(curl); if (res ! CURLE_OK) { fprintf(stderr, 下载失败: %s\n, curl_easy_strerror(res)); } else { printf(下载完成\n); } // 8. 清理 fclose(fp); curl_easy_cleanup(curl); curl_global_cleanup(); return 0; }这段代码就是libcurl下载一个文件的最小骨架。核心只有三步curl_easy_init()创建handlecurl_easy_setopt()设置选项curl_easy_perform()阻塞执行下载。整个下载过程中数据会源源不断地进入回调函数回调函数负责把它写到文件里。2.3 编译链接的几个坑很多新手卡在了编译这一步这里提前说清楚。Windows下用Visual Studio的话如果是vcpkg方式直接编译即可。手动配置的话需要保证附加依赖项里加上了libcurl.lib如果你用的是带SSL的静态库版本可能还需要加上ws2_32.lib、wldap32.lib、crypt32.lib这些是Windows下的系统库依赖。Linux下编译命令是g download.cpp -lcurl -o download注意-lcurl不能漏。如果编译时提示找不到头文件检查是否安装了libcurl4-openssl-dev。还有一个容易踩的坑如果你的程序以后要发给别人运行尽量选择动态链接的方式或者在项目里一并带上对应的DLL。静态链接虽然省事但遇到不同版本的VC运行时可能会在别人的机器上报奇怪的运行库错误。3. 吃透libcurl的核心机制回调、进度与错误处理3.1 为什么libcurl要设计成回调驱动第一次接触libcurl的人多少会对CURLOPT_WRITEFUNCTION这个回调感到不习惯——为什么下载数据不直接在curl_easy_perform里返回给我原因很简单libcurl不知道你想把数据写到哪。可能是文件、可能是内存、可能是网络socket、可能是数据库接口甚至可能直接丢弃只为了测速。与其为每一种目标定制API不如把“拿到数据后怎么处理”的决定权交给你。这个设计思路贯穿了整个libcurl的API你设置的是行为而不是固定的功能。回调函数的签名是固定的size_t (*)(char* buffer, size_t size, size_t nitems, void* userdata)其中buffer是收到的数据size * nitems是数据总字节数userdata就是你在CURLOPT_WRITEDATA里设置的任意指针。回调的返回值是“我实际处理了多少字节”如果返回值小于传入的字节数libcurl会认为出错并中止传输。用生活类比的话回调函数就是你给快递员指定的收货地址。快递员libcurl只负责把包裹送到指定位置至于你拿到包裹后是放仓库、拆了用还是转寄给别人那是收件人你的代码自己的事。3.2 把文件下载到内存里的写法写文件和写内存的回调逻辑是一样的只是存储目标不同。这个写法非常常用比如你要下载JSON配置、下载小体积的接口响应不想落盘。struct MemoryBuffer { char* data nullptr; size_t size 0; }; static size_t WriteMemoryCallback(void* contents, size_t size, size_t nmemb, void* userp) { size_t totalSize size * nmemb; MemoryBuffer* buf static_castMemoryBuffer*(userp); char* newData static_castchar*(realloc(buf-data, buf-size totalSize 1)); if (!newData) { // 内存不足返回0让libcurl中止传输 return 0; } buf-data newData; memcpy(buf-data buf-size, contents, totalSize); buf-size totalSize; buf-data[buf-size] \0; // 自动补个结尾符方便当字符串处理 return totalSize; }注意几个容易被坑的点realloc每次追加数据时指针可能发生变化所以要用newData接收返回值并更新buf-data。额外分配1个字节存\0是为了方便后面用字符串函数处理。但如果下载的是二进制数据这一步不是必须的而且要注意你的数据本身可能就包含\0所以处理二进制时永远不要依赖\0判断长度一定以size为准。回调返回0会中断传输这在“下载到一半发现数据不对、想要提前终止”的场景里特别好用直接return非零即可。3.3 进度显示与下载测速的实现下载大文件时用户看进度条才知道程序是不是卡死了。libcurl同样提供了进度回调机制配合CURLOPT_XFERINFOFUNCTION来使用。struct DownloadProgress { double lastPercent -1; }; static int ProgressCallback(void* clientp, curl_off_t dltotal, curl_off_t dlnow, curl_off_t ultotal, curl_off_t ulnow) { DownloadProgress* progress static_castDownloadProgress*(clientp); if (dltotal 0) { double percent static_castdouble(dlnow) / static_castdouble(dltotal) * 100.0; if (percent - progress-lastPercent 1.0) { // 每变化1%才刷新一次 progress-lastPercent percent; printf(\r下载进度: %.2f%% (%lld / %lld), percent, static_castlong long(dlnow), static_castlong long(dltotal)); fflush(stdout); } } return 0; // 返回非0会中止下载 }启用它只需要两行curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); // 默认是1不开进度回调 curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallback); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progress);想计算实时下载速度也很简单在回调里记录当前时间戳和dlnow用(当前字节数 - 上次字节数) / (当前时间 - 上次时间)就能算出来。这个方法比很多第三方测速脚本都准因为数据是直接从传输层拿到的没有磁盘写入干扰。实际经验进度回调里别做太重的操作比如写日志、刷UI。下载速度很快时它会被高频调用重操作会直接影响下载性能。真要做日志控制频率每秒钟最多写一次就足够了。3.4 错误处理返回码、错误缓冲与HTTP状态码libcurl的错误分两层新手经常混淆。第一层是传输层错误也就是curl_easy_perform返回的CURLcode。CURLE_OK代表传输成功其他值代表各种失败原因比如CURLE_COULDNT_CONNECT无法连接、CURLE_OPERATION_TIMEDOUT超时、CURLE_PARTIAL_FILE文件只下载了一部分。拿到错误码后用curl_easy_strerror(res)转成可读字符串输出即可。第二层是HTTP状态码比如404、500、403。这些不是传输层错误curl_easy_perform照样返回CURLE_OK因为从TCP角度来说请求是成功的。要获取HTTP状态码必须单独查询long httpCode 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, httpCode); if (httpCode ! 200) { // 按业务逻辑处理非200响应 }另外还可以用CURLOPT_ERRORBUFFER设置一个错误缓冲区拿到更详细的错误信息char errbuf[CURL_ERROR_SIZE] {0}; curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf);3.5 封装一个可以直接抄作业的下载函数把上面的组件拼装起来封装成一个干净的函数以后所有项目都能直接复用。#include string #include functional bool DownloadFile(const std::string url, const std::string localPath, const std::functionvoid(double, double) progressCallback nullptr) { FILE* fp fopen(localPath.c_str(), wb); if (!fp) return false; CURL* curl curl_easy_init(); if (!curl) { fclose(fp); return false; } struct ProgressData { std::functionvoid(double, double) cb; } progressData; progressData.cb progressCallback; curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteFileCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L); if (progressCallback) { curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, ProgressCallback); curl_easy_setopt(curl, CURLOPT_XFERINFODATA, progressData); } CURLcode res curl_easy_perform(curl); bool ok (res CURLE_OK); if (ok) { long httpCode 0; curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, httpCode); ok (httpCode 200); } curl_easy_cleanup(curl); fclose(fp); return ok; }从这段代码你应该能感觉到libcurl的封装思路基本是一致的设置好选项执行检查结果清理资源。之后你只会往里面加东西比如超时、限速、重试逻辑而不会推翻这个框架。4. 实战进阶断点续传、多线程分片与HTTPS细节4.1 断点续传掉线了也不怕重头来下载大文件最怕什么下了80%网络断了整个重新来。libcurl原生支持断点续传核心就一个选项CURLOPT_RESUME_FROM_LARGE。实现逻辑很简单检查本地已经下载了多大文件大小。设置CURLOPT_RESUME_FROM_LARGE为本地文件大小告诉libcurl从服务器请求这个偏移量之后的数据。服务器收到带Range头的请求后返回206 Partial Content从指定位置继续发送数据。下载完成后把数据追加写到文件的末尾。// 获取本地文件大小 FILE* fp fopen(localPath.c_str(), ab); fseek(fp, 0, SEEK_END); long long localSize ftell(fp); if (localSize 0) { curl_easy_setopt(curl, CURLOPT_RESUME_FROM_LARGE, static_castcurl_off_t(localSize)); } curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteFileCallback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); CURLcode res curl_easy_perform(curl);代码很简单但有两个坑必须注意。坑一服务器必须支持Range请求。大部分文件服务器都支持但也有少数不支持。如果服务器不支持它会对Range请求直接返回200 完整内容这时如果你用“追加写”模式文件就损坏了。所以严谨的做法是在收到响应后检查HTTP状态码如果返回200而不是206就说明服务器不支持续传此时应该清空文件重新下载。坑二文件本地已经有内容但服务器上的文件已经变了。比如你下载的是一个每天更新的数据包本地留了一半结果服务器端文件更新了。这时候续传会把新旧版本数据拼在一起。严谨的方案是本地保存一个版本标记或最后修改时间Last-Modified或ETag续传前先确认服务器文件没变。我曾在实际项目中因为忽略了这个坑导致了一批数据文件拼接损坏排查了半天才发现是服务器那边更新过文件。4.2 多线程分片下载的原理与实现下载速度跑不满文件大、传输距离远时单线程TCP传输确实可能受限于带宽延迟积。多线程分片下载的原理是给服务器发多个带不同Range头的请求每个线程下载文件的一个区间最后把所有分片按顺序合成一个完整文件。HTTP的Range头可以指定字节区间比如请求bytes0-1048575拿第一兆bytes1048576-2097151拿第二兆。libcurl里直接设置CURLOPT_RANGE即可char rangeHeader[64]; snprintf(rangeHeader, sizeof(rangeHeader), %lld-%lld, startPos, endPos); curl_easy_setopt(curl, CURLOPT_RANGE, rangeHeader);我通常不手写完整的分片下载器而是把它拆成三个环节发一个HEAD请求获取文件总大小用curl_easy_setopt(curl, CURLOPT_NOBODY, 1L)。根据总大小和线程数计算每片的[start, end]区间开N个线程各下载一片到临时文件。所有线程完成后按顺序把分片文件拼接成目标文件再删掉分片。多线程分片下载的收益和瓶颈都在服务器和网络环境上。本地局域网内下载多线程经常能跑满带宽效果明显但对单文件服务器或者瓶颈不在本地带宽的场景提升有限。分片数量建议控制在4到8个太多会触发服务器连接数限制反而更慢。这里必须提醒一句很多HTTP服务器对并发连接数有限制比如Nginx默认的keepalive_requests分片线程太多容易被服务器拒绝连接或者限流。我用过少量线程提升不明显、大量线程反而被服务器限制速度的案例最稳妥的做法是先拿4个线程实测对比。4.3 HTTPS证书开发环境与生产环境的正确姿势关于HTTPS证书很多新手为了“能跑通”直接关掉证书验证curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // 不推荐 curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); // 不推荐结果就是中间人攻击风险暴露无遗。证书验证是HTTPS安全性的基石关掉它等于裸奔。生产环境绝对不要这样做。正确的做法是正式环境保持VERIFYPEER1和VERIFYHOST2让libcurl使用系统自带的CA证书库。Windows上它使用系统证书库Linux上需要系统装有ca-certificates包。开发环境遇到自签名证书把自签名证书通过CURLOPT_CAINFO显式传给它而不是关掉验证。curl_easy_setopt(curl, CURLOPT_CAINFO, /path/to/my-ca.crt);这个习惯跟我之前在Golang、Python里学到的其实是一样的永远先想办法安装证书而不是关闭验证。4.4 超时、限速与自动重试做一个稳重的下载器一个生产环境可用的下载器光有基础下载逻辑是不够的还得有超时控制、限速和自动重试。超时设置有两个选项// 连接超时秒连不上就尽快失败 curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); // 整个传输的最大耗时秒防止服务端挂了无限等待 curl_easy_setopt(curl, CURLOPT_TIMEOUT, 0L); // 0表示不限制限速对大文件下载尤其有用比如后台下载任务不想占满整个网络带宽// 限定最大接收速度为 1MB/s curl_easy_setopt(curl, CURLOPT_MAX_RECV_SPEED_LARGE, static_castcurl_off_t(1024 * 1024));自动重试则要配合循环来实现。我常用的模式是最多重试3次每次失败后按指数退避策略等待第1次等2秒第2次等4秒第3次等8秒同时判断如果是断点续传就利用当前已下载的大小接着下。int maxRetry 3; int retryCount 0; while (retryCount maxRetry) { CURLcode res curl_easy_perform(curl); if (res CURLE_OK) break; retryCount; sleepSeconds 2 (retryCount - 1); // 2, 4, 8 printf(第 %d 次下载失败: %s%d 秒后重试...\n, retryCount, curl_easy_strerror(res), sleepSeconds); Sleep(sleepSeconds * 1000); }超时、限速和重试这三个特性组合起来就完成了一个下载器从“能用”到“稳”的跨越。5. 常见问题排查与避坑清单5.1 一张表看清新手最容易踩的坑症状可能原因解决办法下载的文件打不开/损坏使用了文本模式写文件没有FOLLOWLOCATION导致下载了错误页用wb打开文件开启重定向跟随总是收到HTTPS证书错误证书链不全、系统CA过期、自签名未导入检查系统CA证书自签名证书通过CAINFO指定下载到一半程序崩溃回调里访问了无效的userprealloc失败未处理检查userp生命周期处理 malloc/realloc 失败中文文件名的URL下载失败中文URL没有做URL编码用curl_easy_escape对中文部分编码进度回调不生效忘了NOPROGRESS置0设置CURLOPT_NOPROGRESS, 0L大文件超过2GB异常32位long溢出使用CURLOPT_RESUME_FROM_LARGE等带LARGE后缀的选项服务器拒绝连接多线程分片并发连接数超过服务器限制减少线程数或降低分片数量5.2 逐个展开说症状、原因、解决办法上面表格里有一半的坑我都踩过逐个细说。最阴险的坑文件打不开。下载下来的文件明明有大小但打开提示损坏。这通常有两个原因一是用w模式而不是wb模式打开文件Windows下换行符会被自动转换二进制文件就废了二是链接是301跳转到另一个地址但你没用CURLOPT_FOLLOWLOCATION实际下载的是服务器返回的跳转提示HTML页面。这个我之前还真花了很长时间排查后来打印HTTP状态码和响应内容才发现问题。最容易忽略的坑回调返回值和实际写入不一致。你的回调函数必须在成功处理完数据后返回实际处理的字节数。如果返回0libcurl会立即终止传输并且报CURLE_WRITE_ERROR。从网络层拿到数据后在写入本地文件时如果磁盘满了、写入失败fwrite的返回值会小于传入字节数这种时候要主动返回0中断传输而不是忽略错误继续。断点续传里偷懒的代价文件变异。前面说到了服务器文件更新导致的拼接损坏这个问题在自动更新场景里尤其麻烦。稳妥的做法是每次续传前发HEAD请求对比Content-Length和Last-Modified确认一致再继续。一个很隐蔽的问题内存版本的下载数据可能不完整。下载到内存时如果用realloc频繁扩展缓冲区性能会越来越差。大文件建议指定初始大小或者在下载前从Content-Length头直接分配好空间。也不是什么大问题但数据量上去之后你能明显感觉到程序变卡。还有一个容易忽略的线程安全问题curl_global_init不是线程安全的。如果程序里有多个线程同时调用它可能导致崩溃。规范的做法是在程序启动阶段单线程时调用一次curl_global_init然后所有线程各自创建自己的easy handle执行下载。curl_easy_perform本身就是线程安全的多个线程各用一个handle互不干扰。关于“下载的时候提示保留删除文件”这类现象很多下载工具在下载过程中会生成一个.part临时文件下载完成后才重命名成正式文件。这种设计的好处是下载半截的文件不会被其它程序误用下次续传时也能直接基于.part文件继续。我自己做下载器时也采用了同样的模式先下载到目标文件名.part完成后原子重命名。这个习惯能避免很多因“下载不完整但文件已存在”导致的问题。5.3 关于回收资源的一个忠告libcurl虽然是C API但资源管理完全靠手动。每次curl_easy_init创建的handle用完后必须curl_easy_cleanup否则就会内存泄漏。虽然一个进程里创建的handle数量通常不多但在常驻服务里反复创建不释放内存迟早会被吃光。我习惯的做法是封装一个RAII风格的类来管理CURL*的生命周期析构函数里自动清理这样就算逻辑分支复杂也不会漏掉清理。class CurlHandle { public: CurlHandle() : handle_(curl_easy_init()) {} ~CurlHandle() { if (handle_) curl_easy_cleanup(handle_); } CURL* get() const { return handle_; } // 禁用拷贝允许移动 private: CURL* handle_ nullptr; };类似地curl_slist、curl_form等分配的资源也要记得释放。做网络编程资源管理这条线一定要理清楚。写在最后从一个下载函数到一套下载器根据我多年写下载功能的经验不要一上来就追求花哨的功能先把最简单的下载流程跑通再逐步加固。优先顺序是这样的先实现基础下载和错误处理再考虑进度显示然后是超时重试最后才是断点续传和多线程分片。每一步都经过实测再叠加下一层这样即使出了问题也知道该往哪一层排查。还有一个经验分享给做实际项目的朋友下载功能做好之后最好加一个“完整性验证”的环节。下载完成后对比文件大小是否符合Content-Length或者对下载的文件做一次校验和比对。这个步骤能帮你揪出一大半“莫名损坏”的问题。库的版本管理也值得多一句嘴。libcurl的API虽然有很强的向后兼容性但不同版本对SSL后端的支持有差异。项目里锁定一个经过验证的版本不要随便升级。我见过线上环境升级libcurl之后证书校验行为变化、导致下载全部失败的案例。到今天libcurl还在持续更新但它那套easy interface从二十多年前到现在几乎没什么变化这是它最宝贵的地方一个稳定的、可靠的、不会过时的工具比一个功能繁多但年年重写的工具更值得依赖。做下载这件事你用它就是走一条成熟的路少踩弯路。