☰
FTP断点续传与断点上传源码解析:REST、APPE与偏移量语义
2026/10/5 4:06:38 网站建设 项目流程

简介:这是一份面向Linux网络编程学习者与课程设计者的FTP完整实现方案,基于C/S模式与socket编程,覆盖客户端与服务端双端代码,可帮助读者理解文件传输协议的核心机制与工程落地方式。压缩包共5个文件,以3个C源文件为主体,分别对应客户端、服务端及服务端状态处理逻辑,另附1份使用指导txt与1份实验报告pdf,整体约4.96MB,便于直接编译运行与对照阅读。功能层面支持上传、下载、删除、添加等常规操作,并实现断点续传、多用户登录与错误日志记录,适合作为网络编程课程实验、毕业设计参考或自学练手项目。目前已有262人学习关注,读者可从中获取可运行的源码框架、协议交互流程、断点续传实现思路以及实验报告中的设计分析与测试记录,是Linux环境下理解FTP通信与socket编程的实用材料。

1. 从一份 FTP_linux 源码包说起:断点续传到底难在哪

拿到FTP_linux.rar这类源码包的人,十有八九不是想重写一个 FTP 客户端,而是被同一个问题卡住:几十 GB 的镜像、日志归档、数据库备份,传到一半断了,重传一次要等几个小时。断点续传和断点上传这两个词,本质是同一件事的两面——客户端记住「已经传了多少」,服务端确认「从哪个字节接着写」。听起来简单,但真去翻 Linux 下的 FTP 源码,会发现难点根本不在 socket 收发,而在 REST 命令的语义、偏移量的字节对齐、以及服务端对已存在文件是覆盖还是追加的判断。

这份源码包适合两类人:一类是要在嵌入式 Linux 或国产 Linux 环境里自己裁剪一个轻量 FTP 模块的工程师,另一类是运维出身、想搞清楚ftp命令行背后到底发了什么、为什么有时候reput能续、有时候直接从头开始。下面按「协议怎么约定 → 源码怎么落地 → 参数怎么调 → 坑在哪」的顺序拆开讲,能照着复现,也能看清边界。

2. FTP 断点续传的协议底座:REST、APPE 与偏移量语义

2.1 REST 命令不是「请求续传」,而是「设置文件指针」

很多人第一次读 FTP 源码会误以为断点续传是客户端发一个「我要从第 N 字节继续」的请求,服务端回一个「好的」。实际协议里干这件事的是REST(Restart)命令,它的参数是一个字节偏移量,语义是「把接下来数据传输的起始文件指针移到这个位置」。它本身不传输任何数据,只是一个状态设置。真正触发传输的还是RETR(下载)或STOR(上传)。

关键点在于:REST设置的偏移量是相对于文件开头的绝对字节位置,不是相对于上次传输的块。所以客户端必须自己维护一个「已成功写入的字节数」计数器,断线后把这个数作为REST的参数发出去。服务端收到REST 10485760后,下一次RETR就从第 10485760 字节开始读,下一次STOR就从该位置开始写。

# 用系统自带 ftp 客户端手动验证 REST 语义 ftp -p 192.168.1.50 # 登录后 ftp> binary # 必须先切二进制,ASCII 模式下偏移量会错乱 ftp> rest 10485760 # 设置续传起点为 10MB ftp> get bigfile.img # 从 10MB 处开始下载

上面这段交互里,binary那一步是血泪经验:ASCII 模式会做换行符转换,服务端和客户端对「第 N 字节」的理解不一致,续传出来的文件必然损坏。-p参数是开启被动模式,避免部分网络环境下主动模式连不上数据端口。

2.2 APPE 与 STOR 的分工:追加还是覆盖

上传方向的续传,协议给了两个命令:STOR和APPE。STOR默认从文件开头写,如果文件已存在通常直接覆盖;APPE则是追加到文件末尾。但断点上传不能简单用APPE,因为APPE是「追加到当前末尾」,它不认偏移量——如果服务端文件已经比客户端记录的断点长(比如上次其实写成功了但确认包丢了),APPE会导致重复数据。

正确做法是:上传续传用REST + STOR。先REST <offset>把写指针移到断点,再STOR,服务端从该偏移量开始覆盖写入。这样即使服务端文件比断点长,超出部分会被新数据覆盖,不会产生重复。APPE只适合「确定服务端文件就是上次的完整结果、现在纯追加」的场景,比如日志轮转。

命令组合适用场景风险
REST + RETR下载续传偏移量必须与已落盘字节严格一致
REST + STOR上传续传服务端需支持 REST 作用于 STOR
APPE纯追加日志无法处理重复写入
STOR 无 REST全新上传断线即从头开始

2.3 服务端支持度差异:不是所有 FTP 都认 REST

协议归协议,落地时最大的变数是服务端实现。vsftpd 默认支持REST用于RETR,但对STOR的 REST 支持要看版本和配置;ProFTPD 支持较完整;一些嵌入式 FTP 服务端(尤其是裁剪过的 busybox ftpd)干脆不实现REST,收到就回500。所以源码里必须有降级逻辑:先发REST,如果收到500或502,就退回整文件重传,并给用户一个明确提示,而不是默默从头传让人以为续传生效了。

# 探测服务端是否支持 REST 作用于 STOR 的最小逻辑 from ftplib import FTP def probe_rest_stor(ftp: FTP, remote_path: str) -> bool: try: # 发送 REST 0,观察返回码 resp = ftp.sendcmd("REST 0") # 220/350 表示接受,5xx 表示不支持 return resp.startswith("350") or resp.startswith("220") except Exception: return False

这段代码只做能力探测,不实际传输。REST 0是安全的,不会改变文件内容。返回350表示「需要更多信息」,是 FTP 里常见的中间响应码;如果服务端直接回500 Unknown command,说明它根本不认 REST,后续就得走整传分支。参数上remote_path目前没用到,但保留它是为了将来做「按文件类型决定是否尝试续传」的扩展。

3. 把 FTP_linux 源码跑起来:编译、裁剪与最小客户端改造

3.1 源码目录结构与编译入口

拿到FTP_linux.rar解压后,典型结构是src/放核心传输逻辑,include/放协议常量,Makefile或CMakeLists.txt在根目录。第一步不是急着改代码,而是先确认它能编过、能连上。Linux 下常见依赖是gcc、make、libssl-dev(如果带 FTPS)。编译命令一般长这样:

# 解压后进入源码根目录 unrar x FTP_linux.rar cd FTP_linux # 查看构建方式,优先用 Makefile ls Makefile CMakeLists.txt 2>/dev/null # 典型编译 make clean && make # 如果报缺头文件,补依赖 sudo apt-get install build-essential libssl-dev

编译通过后先别改逻辑,用./ftp_client --help或直接跑一个匿名连接测试,确认基础收发正常。这一步的意义是建立基线:后面改断点续传,如果出问题,能快速判断是「原本就编不过」还是「改坏了」。参数上make clean不能省,源码包里经常残留上次编译的.o文件,架构不一致时会链接失败。

3.2 定位传输循环:断点续传要改的那几行

断点续传的改造点集中在「传输循环」和「断线重连」两处。在源码里搜RETR、STOR、sendcmd、recv这些关键字,通常能找到一个ftp_transfer()或do_upload()函数。核心逻辑是:打开本地文件 → 记录已传字节数 → 循环读写 socket → 出错时保存偏移量 → 重连后发REST。

// 改造前的典型上传循环(简化) while ((n = fread(buf, 1, sizeof(buf), fp)) > 0) { if (send_data(sock, buf, n) != n) { // 原逻辑:直接报错退出 return -1; } total_sent += n; }

改造后要做的第一件事,是把total_sent在每次成功send后落盘或至少保存在内存里,断线重连时用它构造REST:

// 改造后:断线时保留偏移量,重连后续传 long offset = load_checkpoint(local_path); // 从断点文件读取 if (offset > 0) { char cmd[64]; snprintf(cmd, sizeof(cmd), "REST %ld", offset); if (ftp_sendcmd(ctrl_sock, cmd) >= 400) { offset = 0; // 服务端不支持,退回整传 } } fseek(fp, offset, SEEK_SET); // 本地文件指针同步 total_sent = offset;

这里load_checkpoint是新增的,通常用一个同名.part文件或独立的.offset文件存偏移量。fseek那一步不能漏,否则本地读的位置和服务端写的位置对不上,续传出来的文件中间会缺一段。ftp_sendcmd返回码判断是降级逻辑的关键,>= 400就放弃续传。

3.3 断点信息的持久化:存哪里、什么时候写

断点信息写得太频繁会拖慢传输,写得太少断线时丢得多。常见做法是每传完一个数据块(比如 64KB 或 1MB)更新一次内存计数,每传完 10MB 或每 5 秒落一次盘。落盘内容至少包含:远端路径、本地路径、已传字节数、文件总大小、最后修改时间。最后修改时间用来校验「服务端文件是不是被换过了」——如果服务端文件 mtime 变了,续传就没意义,必须整传。

# 断点信息的最小 JSON 结构 { "remote": "/backup/db.sql.gz", "local": "/data/db.sql.gz.part", "offset": 1073741824, "total": 5368709120, "remote_mtime": 1710000000 }

offset是已成功写入服务端的字节数,total用于进度显示和判断是否传完,remote_mtime是续传前的校验依据。这个文件建议放在本地临时目录,文件名带远端路径的哈希,避免不同任务互相覆盖。

4. 参数调优与传输稳定性:块大小、超时、并发怎么定

4.1 数据块大小:64KB 不是万能值

FTP 数据连接的块大小直接影响吞吐和断点粒度。块太小,系统调用次数多,CPU 占用高;块太大,单次丢包重传代价大,断点偏移量更新也粗。局域网千兆环境常用 64KB~256KB,跨广域网建议 32KB~64KB。判断依据是MTU和实际 RTT:块大小最好不超过「带宽延迟积」的一个合理分数,否则一个块在途时间太长,断线时浪费大。

网络环境建议块大小断点更新间隔
局域网千兆128KB~256KB每 16MB
同城专线64KB~128KB每 8MB
跨地域广域网32KB~64KB每 4MB
弱网/移动网络16KB~32KB每 1MB

这张表是经验值,不是协议规定。实际调的时候先按环境选一档,跑一次大文件传输,看iftop或nload的吞吐曲线,如果曲线锯齿严重,说明块太大或超时太短。

4.2 超时与重连:别让控制连接先死

FTP 有两条连接:控制连接(21 端口)和数据连接。断点续传最怕的是数据连接断了、控制连接还活着,但客户端没检测到。常见配置是控制连接超时 30~60 秒,数据连接超时 10~30 秒。数据连接读写都要设SO_RCVTIMEO和SO_SNDTIMEO,否则一个recv可能永久阻塞。

// 设置数据连接超时,避免永久阻塞 struct timeval tv; tv.tv_sec = 30; tv.tv_usec = 0; setsockopt(data_sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); setsockopt(data_sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

30秒是广域网的保守值,局域网可以降到 10 秒。超时后不要立刻重连,先做一次指数退避(1s、2s、4s、8s),避免服务端刚重启就被大量重连打满。重连后第一件事是重新登录、切binary、发REST,顺序不能乱。

4.3 被动模式与端口范围:为什么续传总在数据连接上翻车

主动模式下服务端主动连客户端的数据端口,客户端在 NAT 后面时经常连不上,表现就是「控制连接正常、一到传数据就卡死」。被动模式由客户端发起数据连接,穿透性更好。源码里要确保PASV或EPSV命令正确发送并解析返回的 IP 和端口。有些服务端返回的 PASV 地址是内网 IP,客户端如果直接连会失败,需要忽略返回 IP、只用返回端口连控制连接的对端地址。

# 被动模式解析与连接(简化) resp = ftp.sendcmd("PASV") # 返回形如 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2) nums = [int(x) for x in re.findall(r"\d+", resp)[-6:]] host = ".".join(map(str, nums[:4])) port = nums[4] * 256 + nums[5] # 若 host 是内网地址,改用控制连接的对端 IP data_sock = socket.create_connection((host, port), timeout=30)

nums[4] * 256 + nums[5]是 PASV 返回端口的固定算法,不能改。host的替换逻辑是踩坑重灾区:很多 NAT 环境返回192.168.x.x,直接连必然超时,用控制连接的getpeername()拿到的地址替换才稳。

5. 避坑与排查:断点续传最常见的 5 个翻车现场

5.1 续传后文件变大或损坏

现象:续传完成,文件大小比源文件大,或者校验和不匹配。原因:ASCII 模式传输导致换行符被转换,偏移量与实际字节数错位;或者服务端REST后STOR实际是从头写,客户端却按续传追加。解决:传输前强制TYPE I(二进制),并在REST后校验服务端返回码;如果服务端不支持REST + STOR,直接整传,不要心存侥幸。

5.2 REST 返回 350 但续传没生效

现象:客户端发了REST,服务端回350,但传输还是从 0 开始。原因:350只是「需要进一步信息」的中间码,不代表服务端真的把文件指针移了。部分服务端对STOR的REST支持是假的,回码好看但不干活。解决:续传后对比服务端文件大小和预期偏移量,如果对不上,标记该服务端为「不支持上传续传」,后续走整传。

5.3 断点文件与服务端文件不一致

现象:续传成功,但文件内容中间缺一段或重复一段。原因:断点偏移量记录的是「客户端已发送字节数」,但服务端可能只写入了一部分就断线,实际落盘字节数小于客户端记录值。解决:断点偏移量应该以「服务端确认写入」为准,而不是「客户端发送完成」。可以在每次REST前用SIZE命令查服务端当前文件大小,取客户端记录值和服务端实际值的较小者作为续传起点。

5.4 控制连接空闲被服务端断开

现象:大文件传输中途,数据连接正常,但控制连接被服务端以421 Timeout断开,后续无法发REST。原因:FTP 控制连接有空闲超时,长时间只传数据不发控制命令会被判定为空闲。解决:传输过程中定期发NOOP保活,间隔小于服务端超时时间的一半。常见服务端超时 300 秒,那就每 120 秒发一次NOOP。

5.5 并发续传导致偏移量互相覆盖

现象:多个任务同时续传同一个远端文件,断点文件被互相覆盖,最终文件错乱。原因:断点文件按远端路径命名,但没加任务锁。解决:断点文件加文件锁(flock),或者按「远端路径 + 本地路径」联合哈希命名,并在续传前检查是否有其他进程正在操作同一远端文件。

6. 进阶技巧:用校验和与分块哈希把续传做「可信」

断点续传做到「能续」只是及格,做到「续完可信」才算落地。我一般会在整传完成后做一次远端和本地的校验和比对,但大文件全量哈希太慢,所以更实用的做法是分块哈希:每传完一个块,记录该块的哈希,续传时从最后一个「哈希已验证」的块边界开始,而不是从任意字节偏移开始。

import hashlib def block_hash(path: str, offset: int, size: int) -> str: h = hashlib.sha256() with open(path, "rb") as f: f.seek(offset) remaining = size while remaining > 0: chunk = f.read(min(65536, remaining)) if not chunk: break h.update(chunk) remaining -= len(chunk) return h.hexdigest()

这个函数对本地文件的指定区间算 SHA-256。offset是块起始位置,size是块大小。续传时,客户端把「最后一个已验证块的结束偏移」作为REST参数,而不是「已发送字节数」。这样即使中间有块写坏了,最多重传一个块,不会把坏数据当成已传数据跳过。代价是需要服务端也能提供对应块的哈希,或者至少在续传后由客户端重新拉取该块做比对。实际部署里,我通常只在关键备份任务上开这个模式,普通文件传输用简单的偏移量续传就够了。

另一个习惯是:每次续传成功后,不立刻删除断点文件,而是保留到下一次整传成功。这样如果续传后的文件校验失败,还能回退到上一个已知良好的断点。这个「后悔药」机制在跨地域传输里救过我好几次——网络抖动导致续传写入了半截数据,靠保留的断点文件重新对齐,省了一次全量重传。

传输这件事,协议是死的,网络是活的。把REST的语义吃透、把服务端支持度探测做在前面、把断点信息存得比你以为的更勤一点,剩下的就是让它在真实网络里跑够次数。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询