curl结合Shell脚本打造自动断点续传下载器 📅 发布时间:2026/9/17 9:18:07 👁 浏览次数: 干运维的朋友应该都有过这种体验下载一个大文件好不容易等进度条走到 80%网络一抖进度归零一切从头再来。做开发的朋友也绕不开这个坑——你在官网找好了 JDK 安装包、从 Maven 仓库拉个二进制包、或者备份服务器上的日志压缩包一条curl -O下去下到一半断了curl 吐出一句transfer closed with outstanding read data remaining文件是个半成品只能删掉重新下。curl 作为 Linux 下最常用的下载工具本身其实支持断点续传-C -参数就是干这个的但裸 curl 不会自动重试、自动续传、自动校验。这篇文章要分享的就是怎么用 curl 结合一个轻量 shell 脚本把它变成一个“断了就自己续、失败就自动重试、下完还能校验一下”的自动断点续传下载器。无论你是运维、开发还是经常在服务器上搬运大文件的人这套思路都能直接落地。1. 为什么需要“自动断点续传”需求与场景拆解1.1 curl 裸下载的三个痛点先说清楚我为什么要写这个脚本。平时用 curl 下载东西最容易遇到三个问题。第一个痛点是半截文件不可续。curl 默认行为是发起一个完整的 HTTP 请求一直读到连接关闭。如果中途断网、服务器超时、SSH 会话被切断curl 就带着错误码退出了本地只留下一个已经写了部分数据的文件。你看着这个文件心里清楚里面大部分数据可能还是好的但重新执行一次 curl 的话它默认会从头开始写根本不理会已经下载的部分。更麻烦的是有些 CDN 对已经传输了一半的文件并不友好第二次连接可能给你分配了另一个节点文件版本都可能对不上。第二个痛点是没有重试机制。curl 不是 wget它默认只尝试一次失败就退。你做自动化脚本的时候想让它断了之后自动再试还得自己在外面包一层while true循环。单纯写个while也能用但怎么判断这次失败到底该不该重试、重试多少次、间隔多久这些都是需要自己设计的而且处理不好很容易搞成僵尸进程反复打服务器。第三个痛点是缺少完整性确认。很多人下载完只看退出码是不是 0但 curl 退出码为 0 只能说明 HTTP 会话正常结束了不代表文件一定是完整的。服务器如果提前断开、返回了错误页、或者下载过程中网络层静默丢包你得到的可能是一个长度不对的文件。生产环境里拉安装包、拉模型文件、拉数据库备份这种“假成功”比失败还坑人。这三个痛点叠加在一起就是我写这个自动断点续传脚本的直接动因我需要一个能在无人值守情况下反复尝试并且最终能确认文件完整性的下载器。1.2 断点续传到底“续”的是什么很多人把断点续传理解成“程序记住下载到哪了”这个说法不够准确。断点续传的本质是字节级别的范围请求。HTTP 协议里有一个 Range 头客户端可以在请求里带上Range: bytes1024-意思就是“我从第 1024 个字节开始给我传”。服务器收到之后如果支持范围请求就会返回206 Partial Content状态码同时响应头里带上Content-Range: bytes 1024-2047/2048这样的信息告诉客户端目前传的是哪一段、文件总大小是多少。curl 的-C -做的事情很简单先看一下本地已有文件有多少字节然后自动在请求里加上对应的 Range 头从断点位置继续拉数据。关键点在于这个“续传”不是基于文件名、不是基于时间戳而是纯粹基于本地已写入的字节数。所以脚本设计的核心逻辑也很清楚每次启动时探测本地文件有多大然后让 curl 从这个大小继续拉。有一点要注意Range 请求有一个特例如果本地文件大小已经等于远程文件总大小服务器会返回416 Requested Range Not Satisfiable意思是“你要的范围不合法”。这种情况通常是本地残留了一个完整但内容损坏的文件或者是上次异常退出导致文件大小和真实内容对不上。脚本里必须对这个状态做特殊处理否则就会陷入死循环。2. 核心原理与关键技术点2.1 自己动手验证Range 请求到底长什么样在你写脚本之前我强烈建议先手动做一次实验亲眼看看 Range 请求的原理。这一步能帮你省掉后面大量的排查时间。先准备一个测试文件然后直接手动加 Range 头发起请求# 随便找个图片或者压缩包 curl -s -H Range: bytes0-99 -D - -o /tmp/part http://example.com/bigfile.zip加-D -是为了把响应头打印到终端。正常情况下你能看到类似这样的输出HTTP/1.1 206 Partial Content Content-Length: 100 Content-Range: bytes 0-99/104857600206和Content-Range两个信息出现说明服务器明确支持范围请求。如果服务器不支持 Range响应头里给你的是200 OK而且Content-Length是整个文件的大小这属于很老的服务器或者某些特殊的静态文件服务遇到这种服务器断点续传能力基本就废了脚本也只能退化成“每次都从头下”。另外一个值得验证的场景是416。你可以故意让本地文件大小超过远程文件touch local.bin truncate -s 104857601 local.bin curl -C - -o local.bin http://example.com/bigfile.zip这时候 curl 会报错响应头大概率是416 Requested Range Not Satisfiable。这个实验做完你就会明白为什么脚本里一旦检测到“本地文件大小 远程文件大小”但是下载又没有成功就直接删掉本地文件重新拉而不是傻乎乎地继续重试。2.2 curl 断点续传核心参数解析脚本里用到的几个 curl 参数每一个都有讲究这里逐个说清楚。-C -是整个断点续传的核心。-C后面跟数字就是指定偏移量比如-C 1024表示从第 1024 字节开始。但更常用的是-C -这个写法让 curl 自己去 stat 本地文件自动算出偏移量。注意-C和-o是配合使用的你指定了输出文件curl 才知道去 stat 哪个文件。-L表示跟随重定向。现在很多下载链接都不是直链比如你从官网点下载按钮服务器先返回一个 302 跳转到 CDN。如果不加-Lcurl 只会下载到一个跳转提示页文件自然就是坏的。脚本里必须带上。--fail让 curl 在收到 HTTP 4xx/5xx 状态码时以非零状态退出。不加这个参数的话服务器返回 404 页面curl 照样会把错误页面保存成文件然后退出码还是 0这会让外层脚本误判为成功。加上--fail配合后面的退出码判断才能把“HTTP 层面失败”和“传输成功”区分开。--connect-timeout 15是连接超时防止服务器 IP 不可达时一直卡着不退出。这里我没用--max-time因为下载大文件本来就可能持续一个小时以上设一个总超时反而会把正常的长时间下载掐断得不偿失。-sI用于发送 HEAD 请求并且不打印进度条脚本里拿它来获取Content-Length。不过也要注意部分服务器不支持 HEAD会返回 405 或者干脆用 GET 的响应头冒充所以脚本里做了降级处理。2.3 “自动”两个字背后的设计逻辑幂等与状态机这一段是这个脚本的精华所在。我说它“自动”不是简单写个while循环重试而是要让整个脚本具备两个很关键的特征幂等性和明确的状态机。幂等性是什么意思同一个脚本在任意时刻被执行不管之前发生了什么它的结果都应该是一致的。换句话说你把脚本跑一半 CtrlC 杀掉然后再跑一次它不应该把已经下载的数据推翻重建而应该接着上次的位置继续。这个能力来自-C -和脚本对本地文件大小的探测逻辑。只要本地文件存在脚本启动时的第一个动作就是看这个文件有多大然后从那里继续而不是无脑从头下载。状态机则是用来管理整个下载流程的。我把脚本运行过程抽象成几个状态未开始、下载中、下载失败待重试、异常残留文件待处理、已完成。while 循环里每跑一次 curl就相当于状态机的一次流转。根据 curl 的退出码和文件大小比对结果脚本决定是继续重试、删除残留文件重下还是确认完成退出。这样看着简单的循环逻辑上是闭环的不会出现“重试一百次都是同样错误”的傻循环。另外还有一点脚本的退出码设计也很重要。如果所有重试次数都耗尽脚本要以非 0 状态退出这样如果你把它挂在 cron 定时任务里或者放在 CI/CD 流水线里外部调用方就能通过退出码感知到下载失败从而触发告警或后续处理。这一点很容易被忽略但自动化场景里非常关键。3. 脚本设计与实操实现3.1 最小可用版先跑通再增强如果你只想尽快解决问题下面这个三行的最小可用版本就够了while ! curl -C - -L --fail -o bigfile.zip $URL; do sleep 3 done这个版本的核心思路就是用curl的退出码作为循环条件失败就等 3 秒再试。它的优点是极简、直观几秒钟就能写出来。缺点是完全没有日志、没有重试上限、没法处理 416 这种异常残留文件。如果下载连续失败它会无限循环下去这在无人值守场景下是很危险的。所以最小可用版只适合你在前台手动执行、盯着看的情况。真正要落地到生产环境或者长时间挂机下载还是需要用完整版。3.2 完整版脚本可以直接抄作业下面这个脚本是我实际在用的版本注释已经写得很详细直接保存成auto_resume_download.sh就能用。#!/usr/bin/env bash # # curl 自动断点续传下载脚本 # 用法: ./auto_resume_download.sh -u URL -o OUTPUT [-r 重试次数] [-d 重试间隔秒] [-k] # # 功能: # 1. 自动从本地已有文件大小处续传 (-C -) # 2. 失败自动重试, 默认最多 10 次, 每次间隔 3 秒 # 3. 下载完成后校验本地文件大小与服务器 Content-Length 是否一致 # 4. 已完整的文件直接跳过, 不会重复下载 # 5. 兼容 Linux 和 macOS set -o pipefail URL OUTPUT RETRY_TIMES10 RETRY_DELAY3 CURL_EXTRA_OPTS() usage() { cat EOF 用法: $0 -u URL -o OUTPUT [-r 重试次数] [-d 重试间隔秒] [-k] -u, --url 下载地址 (必填) -o, --output 保存文件名 (必填) -r, --retry 失败重试次数, 默认 10 -d, --delay 重试间隔秒数, 默认 3 -k, --insecure 跳过 SSL 证书校验 (不推荐生产环境使用) -h, --help 显示帮助 EOF } while [[ $# -gt 0 ]]; do case $1 in -u|--url) URL$2; shift 2 ;; -o|--output) OUTPUT$2; shift 2 ;; -r|--retry) RETRY_TIMES$2; shift 2 ;; -d|--delay) RETRY_DELAY$2; shift 2 ;; -k|--insecure) CURL_EXTRA_OPTS(-k); shift ;; -h|--help) usage; exit 0 ;; *) echo 未知参数: $1 2; usage; exit 1 ;; esac done if [[ -z $URL || -z $OUTPUT ]]; then usage exit 1 fi # 获取远端文件大小, 优先 HEAD, 失败后尝试 Range 探测 remote_size() { local size size$(curl -sI ${CURL_EXTRA_OPTS[]} $URL 2/dev/null | tr -d \r \ | awk -F: tolower($1)content-length {s$2} END{print s0}) if [[ ${size:-0} -le 0 ]]; then size$(curl -s -r 0-0 ${CURL_EXTRA_OPTS[]} -D - -o /dev/null $URL 2/dev/null \ | tr -d \r \ | awk -F[ /] /[Cc]ontent-[Rr]ange:/ {total$3} END{print total0}) fi echo ${size:-0} } # 获取本地文件大小, 兼容 Linux 的 stat -c 和 BSD/macOS 的 stat -f local_size() { if [[ -f $OUTPUT ]]; then stat -c %s $OUTPUT 2/dev/null || stat -f %z $OUTPUT 2/dev/null || echo 0 else echo 0 fi } # 判断本地文件是否已经完整: 远端大小 0, 本地大小 远端大小 且本地大小 0 is_completed() { local rs ls rs$(remote_size) ls$(local_size) if [[ $rs -gt 0 $ls -gt 0 $ls -ge $rs ]]; then return 0 fi return 1 } echo 下载地址: $URL echo 保存文件: $OUTPUT echo 最大重试: $RETRY_TIMES 次 echo 重试间隔: ${RETRY_DELAY}s attempt0 while [[ $attempt -lt $RETRY_TIMES ]]; do attempt$((attempt 1)) # 已完成就直接退出 if is_completed; then echo [$OUTPUT] 已完整下载, 跳过。 exit 0 fi lsz$(local_size) rsz$(remote_size) echo [第 ${attempt}/${RETRY_TIMES} 次尝试] 远程 ${rsz} 字节, 本地已有 ${lsz} 字节 # 核心下载命令 curl -C - -L --fail --connect-timeout 15 \ ${CURL_EXTRA_OPTS[]} \ -o $OUTPUT $URL rc$? if [[ $rc -eq 0 ]]; then if is_completed; then echo 下载完成: $OUTPUT ($(local_size) 字节) else echo 下载完成(服务器未返回可校验的 Content-Length): $OUTPUT fi exit 0 else echo curl 下载失败, 退出码: $rc fi # 处理 416 类问题: 本地残留文件异常 lsz$(local_size) rsz$(remote_size) if [[ $rsz -gt 0 $lsz -gt 0 $lsz -ge $rsz ]]; then echo 检测到本地文件异常(可能触发 416), 删除后重新下载 rm -f $OUTPUT fi if [[ $attempt -lt $RETRY_TIMES ]]; then echo 等待 ${RETRY_DELAY} 秒后重试... sleep $RETRY_DELAY fi done echo 重试次数用尽, 下载失败。 2 echo 已下载的部分保存在 $OUTPUT 中, 再次执行本脚本会继续断点续传。 2 exit 13.3 脚本各段逻辑详解为什么这么写先说参数解析这一段。我选择用-u、-o、-r、-d这种带短横线的命令行参数而不是直接在脚本顶部改变量是因为命令行参数的可复用性更强。同一个脚本今天下载 JDK明天下载 Maven后天备份数据库只需要在命令行里换 URL 和文件名不需要打开文件改代码。这在自动化流程里尤其方便脚本本身是只读的每次调用传参就行。再来看remote_size()函数。为什么优先用 HEAD 请求因为 HEAD 请求只返回响应头不返回响应体网络开销最小对服务器压力也小。但有些服务器对 HEAD 处理得不对返回的 Content-Length 可能不是文件真实大小所以脚本里在这一层做了兜底如果 HEAD 拿不到有效大小就换成 Range 探测也就是请求bytes 0-0只下第一个字节然后从Content-Range头里解析出总大小。注意Content-Range的格式是这样的bytes 0-0/104857600最后那个数字就是总大小所以我用 awk 按空格和斜杠分割取第三段。local_size()里我同时写了stat -c %s和stat -f %z两种语法。-c是 Linux 的写法-f是 BSD 和 macOS 的写法。判断文件是否存在时用-f而不是-e因为我们要下载的是一个文件如果同名路径是一个目录-e也会通过后面操作就会出错。主循环中的判断顺序也值得说一下每次循环开始先走一次is_completed判断这样设计的好处是脚本被中断后再次执行时如果文件完整就直接退出不会浪费带宽重新下载。下载完成后再次判断is_completed这是在远程服务器确实返回了 Content-Length 的前提下做的严格校验。如果服务器没返回curl 退出码为 0 也能正常结束这是实用性和严谨性之间的一个平衡。3.4 实战效果中断后如何自动续传脚本写出来到底行不行得实测。我拿一个 1GB 的测试文件做演示先正常发起下载然后在下载到 40% 多的时候故意用kill杀掉进程模拟网络中断。第一次运行的输出大概是这样$ ./auto_resume_download.sh -u http://example.com/bigfile.zip -o bigfile.zip -r 5 -d 2 下载地址: http://example.com/bigfile.zip 保存文件: bigfile.zip 最大重试: 5 次 重试间隔: 2s [第 1/5 次尝试] 远程 1073741824 字节, 本地已有 0 字节 curl: (18) transfer closed with outstanding read data remaining curl 下载失败, 退出码: 18 等待 2 秒后重试...这里出现了一个环境变量相关的小插曲。第一次测试时 curl 报了error 7提示连接 127.0.0.1 的一个端口失败。我排查了一下发现环境变量里配置了代理curl 默认会读http_proxy、https_proxy这些变量而代理对应的本地端口没有服务监听。这个问题很常见如果你遇到类似情况先执行env | grep -i proxy看下环境变量再决定是取消变量还是把代理服务拉起来。杀掉进程后马上重新执行同一条命令这次输出变成了$ ./auto_resume_download.sh -u http://example.com/bigfile.zip -o bigfile.zip -r 5 -d 2 下载地址: http://example.com/bigfile.zip 保存文件: bigfile.zip 最大重试: 5 次 重试间隔: 2s [第 1/5 次尝试] 远程 1073741824 字节, 本地已有 453284106 字节本地已有 453284106 字节说明上次中断留下的 400 多 MB 数据被正确识别了curl 从第 453284106 字节开始继续拉数据而不是从头下载。等到进度走完脚本打印出下载完成信息本地文件大小和远程大小完全一致。4. 常见问题与排查技巧实录4.1 服务器返回 416本地残留文件比远程还大这个是我实际踩过最多的坑。触发场景一般有三种本地文件是之前用别的工具下载的但那个工具中途退出只写了文件头或者上一次下载时服务器返回了错误页curl 把错误页写进了文件还有一种情况是远程服务器文件被替换了新文件变小而本地旧的半成品还留着。416 报错的核心特征就是本地文件字节数大于或等于远程文件的 Content-Length但下载没有成功。脚本里对应的处理是在每次 curl 失败后重新获取远端大小和本地大小一旦发现“本地大小 远端大小”就认定是残留文件异常直接把本地文件删掉重下。这里有一点值得注意不能一上来就删。如果你辛辛苦苦下到 99%只是因为网络抖动失败了这时候远端大小和本地大小其实是一致的也在lsz rsz的范围里直接删掉会浪费之前 99% 的进度。所以判断逻辑里我加了额外条件这个残留文件删除操作只在 curl 失败之后触发。如果 curl 退出码为 0 且文件大小校验通过说明下载已经完成根本不会走到删除分支。4.2 curl 常见退出码速查表脚本里的重试机制依赖 curl 的退出码看懂这些退出码能帮你快速定位问题。下面是我遇到最频繁的几种情况退出码含义常见场景与处理建议0传输完成不一定代表文件完整配合响应码和文件大小校验7连接服务器失败目标主机不可达、端口不通、代理端口没监听先检查网络和代理18数据传输中途关闭服务器主动断连或网络闪断经典断点续传场景重试即可22HTTP 返回码 400文件不存在(404)、权限不足(403)、或者 416需具体看 HTTP 状态码28操作超时连接建立了但服务器长时间无响应加长--connect-timeout或检查带宽33服务器不支持 Range老式静态文件服务器或某些第三方接口断点续传不可用只能整文件重下35SSL 连接错误服务器证书问题、TLS 版本不匹配常见于内网自签名证书51服务器证书无法验证证书过期、域名不匹配测试环境可临时加-k生产环境建议换可信证书56接收数据失败连接被重置常见于代理、防火墙干扰重试一次通常能恢复排查这些错误时有个通用思路先加-v参数看详细交互过程再用curl -sI单独测一次响应头把网络层问题、HTTP 层问题和文件层问题分开定位。比如error 18是纯网络层问题重试就完事error 22是 HTTP 层问题重试多少次都没用得先看看 URL 是不是变了、文件是不是被移除了。4.3 服务器不支持断点续传怎么办真实环境里总会遇到一些不按套路出牌的服务器。判断方法很简单curl -sI http://example.com/bigfile.zip | grep -i accept-ranges如果响应头里有accept-ranges: bytes说明服务器支持 Range 请求断点续传可用。如果没这个头或者显示accept-ranges: none那就意味着你每次请求都必须从头开始。这种情况下脚本会退化成一个普通的重试下载器每次失败后不是从断点继续而是整文件重新下载。效率确实低但至少比手动操作强。另外部分服务器虽然不返回accept-ranges头但实际支持 Range 请求脚本里-C -发出的 Range 请求如果收到了 206还是会走续传逻辑所以不用过度担心。4.4 关于多线程下载和限速的补充有朋友看了这个脚本后问我curl 能不能像某些下载工具一样“多线程同时下载”我说明一下单条 curl 命令本身不支持单连接多线程但你可以开多个 curl 进程每个负责下载不同 Range 段最后再拼起来这就是 aria2c 的-x参数做的事情。如果你的主要诉求是“大文件下得快”我建议直接用 aria2c用它来做多连接加速更成熟如果你的诉求是“下载过程稳定可靠、能自动续传”那这个脚本已经够了。下载大文件还要注意网络带宽占用。如果你是在生产服务器上下载不想让下载任务挤占业务带宽可以在 curl 命令里加一个--limit-rate参数比如--limit-rate 5M表示限速 5MB/s。脚本的CURL_EXTRA_OPTS数组本来就支持追加参数想开启限速的话在调用时手动加一个参数或者直接在脚本里追加即可。5. 脚本扩展方向从“能下”到“下得明白”脚本做好能自动续传之后我还有三个扩展习惯分享给你都是实际工作中加进去之后非常受益的。第一个是校验和校验。文件下载完成只能说明“传输过程没报错”不能说明“文件内容没问题”。如果你下载的是安装包、系统镜像或者数据库备份我强烈建议下载完成后额外做一次 SHA256 校验。做法很简单先从官网或者其他可靠渠道拿到文件的 SHA256 值然后执行echo 期望的sha256值 文件名 | sha256sum -c -把这段校验逻辑追加到脚本的exit 0之前能拦截掉一大部分“假成功”的文件。第二个是日志输出到文件。脚本现在是直接往终端打印信息的但如果你把它挂到 cron 里定时执行终端输出是看不到的。可以在调用时加上重定向或者干脆在脚本开头加一行日志记录LOG_FILE${OUTPUT}.log exec $LOG_FILE 21这样每次执行记录都会追加到日志文件后面排查问题有据可依。第三个是结合邮箱或 IM 机器人通知。下载是一个耗时的任务尤其是大文件你不可能一直在旁边盯着。脚本执行结束后可以根据退出码判断结果然后调用curl往企业微信、钉钉或 Slack 的 Webhook 发一条消息。这样下载完成或者失败重试耗尽你都能第一时间知道。这一步做下来整个下载流程就真正具备“无人值守自动化”的能力了。再分享一个操作习惯下载执行类的脚本安装包时不要直接curl xxx | sh。很多软件官网提供的一键安装脚本都是类似curl url | sh的用法我个人的习惯是先通过断点续传脚本把脚本文件完整下到本地看一眼内容确认没有恶意操作再手动执行。这多花的几分钟在某些场景下能替你挡掉很大的安全风险。最后回到这个脚本本身。它不复杂核心就是-C -参数加一个重试循环但加上文件大小校验、416 异常处理、退出码设计和幂等判断之后它就成了一个能真正在各种下载场景里扛得住的小工具。我从最早写那个三行最小可用版到后来不断加校验逻辑前后也迭代了好几轮。如果你在实际使用中遇到这个脚本没有覆盖到的边界情况欢迎按自己的使用场景继续往里加判断逻辑——这类工具脚本的价值往往就是在一次次真实的下载失败中打磨出来的。