FTP断点续传与断点上传源码解析:REST/APPE命令实战
2026/9/23 12:15:54 网站建设 项目流程

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

1. 从一份 FTP_linux 源码说起:断点续传到底在解决什么问题

传一个 4GB 的 Linux 镜像到内网 FTP 服务器,传到 3.2GB 时网络抖了一下,连接断了。如果客户端不支持断点续传,你只能从头再来一遍;如果支持,重新连上后从 3.2GB 处接着传,前面那 80% 的流量不白费。这就是 FTP_linux 这类源码里断点续传和断点上传功能存在的全部理由。

FTP_linux 通常指一套在 Linux 环境下运行的 FTP 客户端/服务端源码实现,核心卖点就是断点续传(下载续传)和断点上传(上传续传)。它适合三类人:需要把 FTP 能力嵌进自己系统的后端工程师、要维护老旧文件传输链路的运维、以及想搞懂 REST/APPE 命令怎么落地的人。下面从协议原理讲到源码改造,再到实际部署时那些让人翻车的细节,一步步拆开。

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

2.1 断点续传靠的不是“记住进度”,而是 REST 命令

很多人以为断点续传是客户端在本地记了一个“传到哪了”的进度文件,重连后告诉服务器“从第 N 字节开始”。方向对,但机制比这精确。FTP 协议(RFC 959 及后续扩展)里,真正干这件事的是REST(Restart)命令。

下载续传的流程是:客户端先发REST N,N 是要跳过的字节数,服务器回复350 Restarting at N,然后客户端发RETR filename,服务器就从第 N 字节开始往数据连接里吐数据。上传续传则用APPE(Append)命令,把新数据追加到服务器已有文件末尾,或者配合REST在支持它的服务器上做精确偏移写入。

关键点在于:偏移量 N 必须由客户端自己算出来。客户端怎么知道已经传了多少?通常是本地临时文件的实际大小。所以断点续传的可靠性,一半取决于协议,一半取决于客户端怎么管理那个临时文件和偏移量。

2.2 下载续传与上传续传的源码实现差异

在 FTP_linux 这类源码里,下载和上传的续传逻辑往往写在两个不同的函数分支里,因为它们的命令序列不一样。

下载续传的典型命令序列:

REST 3221225472 350 Restarting at 3221225472 RETR linux-image.iso 150 Opening BINARY mode data connection ... 数据传输 ... 226 Transfer complete

上传续传的典型命令序列(以 APPE 为例):

APPE linux-image.iso 150 Opening BINARY mode data connection ... 追加数据传输 ... 226 Transfer complete

注意APPE不保证服务器一定从“正确的位置”追加——它只是追加到文件末尾。如果服务器上的文件因为某种原因比客户端以为的短,追加就会出错。所以更稳妥的做法是上传前先SIZE filename查服务器端文件大小,再决定用REST还是APPE

下面是一段用 Python 的ftplib演示下载断点续传的核心逻辑,这段代码可以直接抄去改:

import ftplib import os def download_with_resume(host, user, passwd, remote_path, local_path): # 本地临时文件用于记录已下载的字节数 temp_path = local_path + ".part" offset = os.path.getsize(temp_path) if os.path.exists(temp_path) else 0 ftp = ftplib.FTP(host) ftp.login(user, passwd) ftp.voidcmd("TYPE I") # 二进制模式,断点续传必须用二进制 # 查询服务器端文件总大小,用于校验 total_size = ftp.size(remote_path) if offset >= total_size: print("本地文件已完整,无需续传") ftp.quit() return # 关键一步:发送 REST 命令设置偏移量 ftp.voidcmd(f"REST {offset}") # 以追加模式打开本地临时文件 with open(temp_path, "ab") as f: def callback(data): f.write(data) # RETR 会从 REST 指定的偏移量开始传输 ftp.retrbinary(f"RETR {remote_path}", callback) ftp.quit() # 传输完成后重命名为正式文件 os.rename(temp_path, local_path) print(f"下载完成,共 {total_size} 字节")

逻辑说明:os.path.getsize(temp_path)拿到本地已下载的字节数作为偏移量;ftp.voidcmd(f"REST {offset}")把这个偏移量告诉服务器;retrbinary内部发RETR,服务器就从偏移量处开始传。参数上,TYPE I不能省,ASCII 模式下偏移量会因为换行符转换而错位,这是血泪经验。

上传续传的对应逻辑:

def upload_with_resume(host, user, passwd, local_path, remote_path): ftp = ftplib.FTP(host) ftp.login(user, passwd) ftp.voidcmd("TYPE I") local_size = os.path.getsize(local_path) # 查询服务器端已有文件大小 try: remote_size = ftp.size(remote_path) except ftplib.error_perm: remote_size = 0 # 文件不存在,从头上传 if remote_size >= local_size: print("服务器端文件已完整") ftp.quit() return # 以读取模式打开本地文件,定位到服务器已有大小处 with open(local_path, "rb") as f: f.seek(remote_size) # APPE 追加到服务器文件末尾 ftp.storbinary(f"APPE {remote_path}", f) ftp.quit() print(f"上传续传完成,从 {remote_size} 字节处继续")

这里用APPE而不是STOR,因为STOR会覆盖服务器上的文件。f.seek(remote_size)把本地文件指针移到服务器已有数据的末尾,只传剩下的部分。如果服务器不支持APPE,就得改用REST+STOR的组合,但并非所有 FTP 服务端都支持上传方向的REST,这是选型时要确认的第一件事。

2.3 偏移量校验:为什么“续传成功”有时是假的

断点续传最隐蔽的坑是:命令都成功了,文件大小也对,但内容坏了。原因通常是偏移量对不上——本地临时文件大小和服务器端实际接收的字节数不一致。

常见场景:客户端在REST N之后、RETR之前连接断了,重连后客户端以为偏移量还是 N,但服务器那边可能已经因为超时清理了会话状态。或者上传时用了APPE,但服务器端文件在两次上传之间被别的进程改过。

解决办法是在续传前后都做校验。下载方向,续传完成后比对本地文件大小和SIZE返回的总大小;上传方向,续传完成后用SIZE查服务器端文件大小,和本地文件大小比对。更严格的做法是算 MD5,但 FTP 协议本身不传校验值,得靠额外的SITE命令或事后手动比对。

3. 把 FTP_linux 源码跑起来:编译、配置与最小验证

3.1 源码编译前先确认这三样东西

拿到一份 FTP_linux 源码,别急着make。先确认三件事:编译器版本、依赖库、以及源码里默认的配置路径。

常见做法是先看READMEINSTALL文件,但很多老源码的文档已经过时。我一般直接看Makefile里的CCCFLAGSLIBS变量,确认它期望的编译器和链接库。如果源码里用了openssl做 TLS,还得确认libssl-dev装了没有。

一个典型的编译流程:

# 解压源码包 tar -xvf FTP_linux.rar # 进入源码目录 cd FTP_linux # 查看 Makefile 里的编译配置 head -30 Makefile # 如果依赖 openssl,先装开发库 sudo apt-get install libssl-dev # 编译 make # 编译产物通常在 bin/ 或当前目录下 ls -la ftp_client ftp_server

参数说明:make默认用Makefile里的规则,如果报错找不到头文件,多半是依赖库没装全。CFLAGS里如果有-Wall -Werror,编译警告会被当成错误,老代码在新编译器上很容易触发,可以临时去掉-Werror先跑通。

3.2 服务端配置:匿名登录、端口和被动模式

FTP_linux 服务端跑起来之前,配置文件里有几个参数必须改。最常见的是禁止匿名登录、设置被动模式端口范围、以及指定用户根目录。

一个典型的配置文件片段(不同源码字段名可能不同,但逻辑类似):

# 禁止匿名登录 anonymous_enable=NO # 本地用户登录 local_enable=YES # 被动模式端口范围,防火墙要放行这段 pasv_min_port=30000 pasv_max_port=30100 # 用户登录后的根目录 local_root=/data/ftp # 允许断点续传 restart_enable=YES

pasv_min_portpasv_max_port是被动模式的关键。FTP 的主动模式(PORT)在客户端有防火墙时经常失败,被动模式(PASV)更通用,但需要服务端开放一段端口范围。如果只开 21 端口,数据传输会卡在425 Use PORT or PASV first这类错误上。

启动服务端:

# 前台启动,方便看日志 ./ftp_server -c /etc/ftp_linux.conf -d # -d 表示 debug 模式,输出详细日志 # -c 指定配置文件路径

3.3 用命令行客户端验证断点续传是否真的生效

服务端跑起来后,别急着写代码,先用系统自带的ftplftp验证续传功能。lftp对断点续传的支持比传统ftp命令好,推荐用它做验证。

# 用 lftp 连接 lftp -u username,password 192.168.1.100 # 下载一个大文件,中途 Ctrl+C 中断 get linux-image.iso # 再次执行同样的 get 命令 get linux-image.iso # lftp 会自动检测本地已有文件,从断点处续传

如果第二次get时看到REST命令被发送,并且传输从中间开始,说明服务端的断点续传支持是正常的。如果每次都从头开始,检查服务端配置里restart_enable是否打开,以及客户端是否用了二进制模式。

上传方向的验证类似,用put命令传一个大文件,中断后再put同一个文件,观察是否发送APPEREST

4. 断点上传的工程化改造:从能用到好用

4.1 分块上传与断点记录的持久化

直接用APPE做断点上传有个硬伤:如果上传过程中客户端崩溃,本地没有记录“传到哪了”,下次启动只能靠比对服务器端文件大小来猜。更工程化的做法是把大文件切成固定大小的块(比如 4MB),每传完一块就在本地记一条记录,下次从最后一条成功记录之后继续。

分块上传的伪代码逻辑:

CHUNK_SIZE = 4 * 1024 * 1024 # 4MB def upload_chunked(ftp, local_path, remote_path, progress_file): # 读取已上传的块索引 uploaded_chunks = load_progress(progress_file) file_size = os.path.getsize(local_path) total_chunks = (file_size + CHUNK_SIZE - 1) // CHUNK_SIZE with open(local_path, "rb") as f: for i in range(total_chunks): if i in uploaded_chunks: continue # 跳过已上传的块 f.seek(i * CHUNK_SIZE) data = f.read(CHUNK_SIZE) # 每个块用独立的临时文件名上传,成功后重命名 chunk_remote = f"{remote_path}.chunk{i}" ftp.storbinary(f"STOR {chunk_remote}", io.BytesIO(data)) # 记录进度 uploaded_chunks.add(i) save_progress(progress_file, uploaded_chunks) # 所有块传完后,在服务端合并 # 这一步通常需要服务端支持 SITE 命令或额外的合并脚本

参数说明:CHUNK_SIZE不宜太小,否则 FTP 命令开销占比过高;也不宜太大,否则单块失败重传成本高。4MB 到 16MB 是常见区间。progress_file建议用 JSON 或 SQLite,记录每个块的索引和校验值。

这种方案的好处是:即使客户端崩溃,重启后读progress_file就知道哪些块已经传完,不用重新比对服务器端文件。代价是需要服务端配合做块合并,或者客户端在全部块传完后用APPE按顺序拼接。

4.2 断点上传时的文件锁与并发问题

多个客户端同时往同一个远程文件做断点上传,结果一定是灾难。FTP 协议本身没有文件锁机制,APPE也不保证原子性。工程上的做法是在应用层加锁:上传前先创建一个.lock文件,上传完成后删除。其他客户端看到.lock文件就等待或报错。

def acquire_lock(ftp, remote_path): lock_file = remote_path + ".lock" try: # 尝试创建锁文件,如果已存在会抛异常 ftp.storbinary(f"STOR {lock_file}", io.BytesIO(b"locked")) return True except ftplib.error_perm: return False # 锁已被占用

这个锁机制不完美——如果客户端创建锁之后崩溃了,锁文件会一直留着。所以还需要一个超时清理逻辑:锁文件的修改时间超过一定阈值就视为失效,允许其他客户端强制获取。

4.3 传输速度与超时参数的调优

FTP 断点续传在实际网络里经常因为超时参数不合理而失败。默认的data_connection_timeout往往只有 30 秒,大文件传输时如果网络抖动超过 30 秒,数据连接会被服务端断开,客户端得重新发起续传。

在服务端配置里调整这几个参数:

# 数据连接超时,单位秒 data_connection_timeout=300 # 控制连接超时 control_connection_timeout=600 # 空闲超时 idle_session_timeout=900

客户端侧也要设置对应的超时。以 Pythonftplib为例:

ftp = ftplib.FTP(host, timeout=300) # 连接超时 ftp.sock.settimeout(300) # 数据连接超时

注意timeout设太大也不好——如果连接真的断了,客户端会傻等很久才重试。我一般把数据连接超时设在 120 到 300 秒之间,根据实际网络质量调整。

5. 断点续传的避坑与排查:那些让你怀疑人生的错误

5.1 现象:REST 命令返回 500,续传直接失败

原因:服务端不支持REST命令,或者只在下载方向支持、上传方向不支持。很多轻量级 FTP 服务端(尤其是嵌入式设备上的)只实现了 RFC 959 的基础命令,REST是可选扩展。

解决:先手动发REST 0看服务端返回什么。如果返回500502,说明不支持。下载方向可以改用lftppget -c,它内部会尝试多种续传策略;上传方向只能退回到分块上传 + 服务端合并的方案。

5.2 现象:续传后文件大小对了,但解压报错

原因:偏移量错位。最常见的是客户端在 ASCII 模式下计算了偏移量,但传输时用了二进制模式,或者反过来。ASCII 模式会把\n转成\r\n,字节数会变,偏移量自然对不上。

解决:断点续传全程强制TYPE I(二进制模式)。在代码里,REST之前和RETR/STOR之前都要确认当前是二进制模式。有些 FTP 库在每次数据传输后会重置模式,所以不能只在连接时设一次。

5.3 现象:被动模式下数据连接超时,控制连接正常

原因:服务端的pasv_min_portpasv_max_port范围没有在防火墙放行,或者服务端返回的 PASV 地址是内网 IP,客户端从外网连不上。

解决:检查防火墙规则,确保被动模式端口范围全部放行。如果服务端在 NAT 后面,需要在配置里指定外部 IP 或者让服务端返回正确的 PASV 地址。有些 FTP 服务端有pasv_address配置项,填公网 IP。

5.4 现象:上传续传时 APPE 成功,但文件内容重复

原因:客户端在APPE之前没有正确seek到服务器端文件大小处,导致把已经传过的数据又追加了一遍。

解决:上传前必须用SIZE命令查服务器端文件大小,然后f.seek(remote_size)。如果服务器端文件大小和本地记录不一致,以服务器端为准,重新计算偏移量。不要信任本地进度文件里的偏移量,每次续传前都重新查一次。

5.5 现象:传输大文件时内存暴涨

原因:一次性把整个文件读进内存再发送。storbinary如果传入的是BytesIO对象,整个文件都在内存里。

解决:用文件对象而不是BytesIO,让ftplib分块读取。storbinary内部会按块发送,只要传入的是文件句柄,内存占用就是常数级别。分块上传时,每块的大小也要控制,不要一次读整个文件。

6. 进阶:用 REST 偏移量做秒传校验与增量同步

断点续传的偏移量机制,其实可以反过来用:不传文件,只传偏移量和校验值,实现“秒传”判断。思路是客户端先算本地文件的 MD5,然后问服务器“你有没有这个 MD5 的文件”,如果有就直接跳过传输。FTP 协议本身不支持这个,但可以通过SITE扩展命令或者在上传前用一个小的元数据文件来传递 MD5。

更实用的进阶用法是增量同步:只传文件中发生变化的部分。比如日志文件每天追加内容,可以用REST从上次同步的偏移量开始,只传新增的字节。这比全量传输快得多。

def incremental_sync(ftp, local_path, remote_path, last_sync_offset): """只同步 last_sync_offset 之后的新增内容""" local_size = os.path.getsize(local_path) if local_size <= last_sync_offset: return last_sync_offset # 没有新内容 with open(local_path, "rb") as f: f.seek(last_sync_offset) # 用 APPE 追加新增部分 ftp.storbinary(f"APPE {remote_path}", f) return local_size # 返回新的偏移量,供下次使用

这个模式适合日志收集、数据库 binlog 同步等场景。关键是要把last_sync_offset持久化到本地,每次同步前读取,同步后更新。如果服务器端文件被截断或重建,偏移量会失效,所以每次同步前最好用SIZE校验一下服务器端文件大小是否和last_sync_offset一致。

验证增量同步是否生效,可以在本地文件末尾追加一行内容,然后跑一次同步,看 FTP 日志里是否只发送了新增的那几行对应的字节数。如果发送了整个文件,说明APPE没生效或者偏移量没传对。

我自己维护这类传输链路时养成了一个习惯:每次续传成功后,不只看文件大小,一定再算一次 MD5 比对。这个习惯帮我逮到过好几次“大小对但内容错”的玄学问题,省下了事后排查的时间。希望帮到你。

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

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

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

立即咨询