项目概述
该 FTP 服务端与配套 FTP 客户端成对使用,基于 socketserver 多线程 TCP 网络框架实现,支持多客户端并发连接。服务端采用 ini 配置文件数据库 存储用户账号信息,使用 JSON 文件保存断点续传记录;实现用户账号管理、登录认证、磁盘配额管控、文件上传(断点续传)、文件下载、目录浏览、目录切换、日志记录等全套 FTP 服务能力。
服务端会启动独立子进程运行 TCP 服务,每一个客户端连接分配独立线程处理请求;客户端与服务端通信协议格式为:
命令|参数1|参数2...,以管道符|分隔字符串。
项目文件清单
| 文件路径 | 文件名称 | 模块作用 |
|---|---|---|
| ftpserver.py | ftpserver.py | 服务端主程序,进程管理、TCP 服务入口、请求分发 Handler |
| conf/settings.py | settings.py | 服务端全局配置:IP 端口、上传目录、日志、用户数据库、断点目录、默认磁盘配额 |
| conf/template.py | template.py | 服务端本地控制台菜单模板(管理员后台菜单) |
| modules/common.py | common.py | 公共工具:sha224 密码加密、md5 文件校验、日志、输入校验 |
| modules/users.py | users.py | Users 用户业务类:用户实例、认证、家目录、磁盘配额更新、创建用户 |
| modules/server.py | server.py | FTP 业务命令实现:auth 登录、put 上传、get 下载、show 查看目录、cd 切换目录 |
| dbhelper/dbapi.py | dbapi.py | 文件数据库操作 API:ini 用户配置读写、断点 JSON 文件增删改查 |
1. conf/settings.py 服务端配置模块
功能说明
统一管理服务端运行参数、各类存储路径,自动解析项目根目录,路径动态拼接。
| 配置项 | 说明 |
|---|---|
| FTP_SERVER_IP | 监听 IP,默认 127.0.0.1 |
| FTP_SERVER_PORT | 监听 TCP 端口,默认 9999,客户端需要与此端口一致 |
| DOWNLOAD_FILE_PATH | 服务端接收上传文件根目录,实际用户家目录存放于此 |
| LOGS | 服务端日志路径 logs/ftpserver.log,记录连接、命令、异常 |
| USER_INFO | 用户 ini 配置数据库存放目录 |
| USER_HOME_FOLDER | 所有用户家目录父目录,每个用户在此生成独立文件夹 |
| HOME_QUOTA | 新建用户默认磁盘配额,单位 MB |
| BREAK_POINT_FOLDER | 断点续传记录存储目录,每个用户单独一个 json 断点文件 |
说明:每个登录用户会在
USER_HOME_FOLDER下生成同名文件夹作为用户家目录,用户无法跳出家目录访问其他用户数据。
2. conf/template.py UI 模板模块
存放服务端本地管理员控制台打印模板。
-
MENU_START:服务端启动首页模板 -
MENU_USER_CENTER:管理员用户管理菜单:查看用户、新增用户、锁定用户
注意:该模板用于 服务端本机控制台交互,和客户端无关;客户端交互菜单在客户端的 template.py 中。
3. modules/common.py 公共工具模块
与客户端公共工具函数功能基本对齐,保证两端加密算法一致。
3.1 encry_sha(string)
sha224 加密明文密码。客户端上传加密后字符串,服务端数据库存储加密后的摘要。
3.2 encry_md5(file)
分块读取文件计算 md5 哈希值。下载文件时,服务端计算文件 md5 下发给客户端,客户端做完整性校验。
3.3 writelog(content, types)
写日志,自动拼接时间、日志级别 info/error,追加写入 ftpserver.log。
3.4 input_msg(message, limit_value=tuple(), default="")
管理员控制台输入工具;支持空输入判断、限定可选输入、设置默认值。
4. dbhelper/dbapi.py 文件数据库操作模块
底层存储:用户账号使用
configparser读写 ini 配置文件;断点续传记录使用 JSON 文件存储。
用户 ini 配置字段说明
每个用户对应 ini 中的一个 section,key 包含:
-
password:sha224 密文 -
islocked:1 锁定 / 0 正常 -
isdel:1 删除 / 0 正常 -
totalspace:总空间字节数 -
usedspace:已使用字节数
4.1 read_section_all()
读取 ini 全部 section,返回全部用户名列表。
4.2 read_section_by_name(section_name)
根据用户名读取单个用户全部配置,返回字典。
4.3 add_option(section_name, **kwargs)
新增用户,写入 ini 配置。
4.4 modify_option(section_name, **kwargs)
修改用户信息:密码、锁定状态、删除标记、磁盘总空间、已使用空间,覆盖写回 ini 文件。
断点续传 JSON 相关函数
断点文件存放路径:
BREAK_POINT_FOLDER/用户名,json 结构:{md5 值: {"filepath": "文件路径", "totalsize": 总大小, "recvsize": 已接收字节}}
4.5 write_breakpoint(filemd5, filesize, recved_size, filepath, userobj)
当客户端上传中途断开,写入断点记录,保存文件 md5、总大小、已经接收字节、文件保存路径。
4.6 check_breakpoint(filemd5, userobj)
根据用户对象 + 文件 md5 查询是否存在断点记录:
-
存在:返回
[已接收字节, 文件路径] -
不存在:返回
[0, 用户当前工作目录]
4.7 del_breakpoint(filemd5, userobj)
文件完整上传完成后,删除该 md5 对应的断点记录,清除续传标记。
5. modules/users.py Users 用户类
每一个登录客户端实例化一个 Users 对象,保存该登录用户全部状态。
对象属性
| 属性 | 含义 |
|---|---|
| username | 用户名 |
| exists | 布尔,用户是否存在数据库 |
| password | sha224 加密密码 |
| islocked | 0 正常 / 1 账户锁定 |
| isdel | 0 未删除 / 1 已删除 |
| totalspace | 用户总磁盘配额,字节单位 |
| usedspace | 已经使用磁盘大小,字节单位 |
| homepath | 用户家目录绝对路径,登录后不可跳出该目录 |
| currpath | 用户当前工作目录,执行 cd 会修改此属性 |
方法详情
5.1 init(username)
构造函数:初始化全部属性,调用 __check_users() 检查用户是否存在。
5.2 __check_users()
读取 ini 用户列表,如果用户名存在,设置 exists=True,加载用户信息,初始化当前路径为家目录。
5.3 __load_user_info()
读取 ini 配置,把数据库用户信息赋值到对象属性。
5.4 user_auth(password)
登录密码校验:传入 sha224 加密密码,和对象 password 比对;账户锁定 / 删除直接返回 False。返回 True=认证成功,False 认证失败。
5.5 create_user()
新建用户:调用 dbapi 写入 ini 配置,调用 __create_folder() 创建用户家目录文件夹。
5.6 __create_folder()
创建用户专属家目录文件夹。
5.7 update_quota(filesize)
上传新文件后更新 usedspace += filesize,调用 dbapi 同步修改 ini 配置文件的已用空间。
6. modules/server.py FTP 业务命令处理模块
接收来自 ftpserver.py 分发的网络请求,实现各个 FTP 命令业务逻辑。
6.1 auth(client_socket, args) 用户登录认证
入参 args 格式字符串:auth|用户名|sha224密码
-
分割解析用户名和加密密码,实例化 Users 对象
-
判断用户是否存在,账户是否锁定、删除,密码是否匹配
-
返回状态码给 socket:
| 返回码 | 含义 |
|---|---|
| "0" | 认证成功;继续发送 `总字节 |
| "1" | 用户不存在 / 已删除 |
| "2" | 账户被锁定 |
| "3" | 用户名密码错误 |
- 返回用户对象
client_user给上层 handler。
6.2 show(client_socket, client_user, recv_data) 查看目录列表
命令:show|
-
获取用户对象
currpath当前工作目录;os.listdir()列出目录全部内容。 -
遍历目录,区分文件 / 文件夹;获取修改时间、文件大小,组装返回字符串。返回格式:
文件总数量|文件列表文本 -
通信协议流程
-
先把返回字符串编码后的字节长度发送给客户端
-
等待客户端回复
ready -
发送完整目录文本数据
-
-
客户端收到总长度后循环接收字节,避免 TCP 粘包问题。
6.3 cd(client_socket, client_user, recv_data) 切换目录
命令格式 cd|目录名,支持 cd|.. 返回上级目录。
逻辑:
-
如果参数是
..:-
当前路径等于用户家目录:返回状态码
0|目录名(提示已经根目录) -
不是家目录:修改
client_user.currpath为父目录,返回状态码1|目录名切换成功
-
-
如果普通文件夹名称:拼接完整路径;判断是否文件夹
-
是文件夹:修改
currpath属性,返回1|文件夹名 -
不是文件夹,返回
2|文件夹名切换失败
-
-
发送结果字符串
状态码|目录名称给客户端。
限制:用户无法切换到自己 homepath 上级目录,保证安全隔离。
6.4 put(client_socket, client_user, recv_data) 文件上传接收
客户端发送命令元数据格式:put|文件名|filesize|filemd5
-
解析文件名、文件总大小、文件 md5;调用
dbapi.check_breakpoint()查询断点记录。 -
无断点:
break_status=0 recv_size=0,保存路径为当前工作目录,调用client_user.update_quota()累加占用磁盘。 -
有断点:
break_status=1 recv_size=已经接收字节,沿用上次未传完文件路径。 -
向客户端返回应答:
状态码|已接收字节,客户端据此执行全新上传或者 seek 断点续传。 -
以追加二进制模式打开文件,seek 跳转到已接收偏移,循环每次接收 2048 字节写入本地。
-
异常处理:客户端断开、发生异常,把当前已经接收大小写入断点记录。
-
文件全部接收完毕:如果是断点续传,删除断点 json 记录。
6.5 get(client_socket, client_user, recv_data) 文件下载发送
命令格式 get|文件名
-
拼接路径:
currpath + filename,判断文件是否存在-
文件不存在:发送
0标识给客户端,结束 -
文件存在:发送
1标识告知客户端文件存在
-
-
等待客户端回复
ok;获取文件大小,计算本地 md5,发送字符串filesize|filename|md5 -
等待客户端发送
ready;分块读取本地文件,每次 2048 字节发送二进制数据给客户端。
md5 由服务端计算下发,客户端下载完成后本地计算 md5 做完整性比对。
7. ftpserver.py 服务端主程序入口
7.1 Myserver(socketserver.BaseRequestHandler) 请求处理类
每一个新 TCP 连接,实例化 Myserver 对象,handle 方法在独立线程运行。
handle() 执行流程:
-
获取连接 socket 对象
self.request、客户端地址client_address,写入日志记录客户端接入。 -
向客户端发送
OK字符串(客户端 connect 等待这个 OK,作为连接成功标记)。 -
定义变量
client_user = None,代表当前连接登录用户对象,未登录为 None。 -
while 死循环持续接收客户端命令(TCP 长连接):
-
recv 读取 100 字节命令头;收到空字节代表客户端断开连接,关闭 socket 退出循环。
-
分割字符串取出命令
cmd(auth / put / get / show / cd)。 -
如果 cmd == auth:调用
server.auth()执行登录,返回赋值client_user用户对象。 -
其他业务命令:必须用户登录成功,使用 反射
getattr(server, cmd)调用 server 模块对应业务函数,传入 socket、用户对象、原始命令字符串。 -
捕获执行异常,记录 error 日志,关闭 socket 连接。
-
反射设计:新增业务命令,只需要在 server.py 新增函数,不需要修改主 handler 循环。
7.2 doprocess()
初始化 ThreadingTCPServer 多线程 TCP 服务,绑定 IP 端口,调用 serve_forever() 永久监听。
ThreadingTCPServer:每一个客户端连接生成独立线程,实现多用户并发。
7.3 start()
创建 Process 子进程运行 doprocess,返回进程对象。
服务运行在独立子进程,不和管理员控制台主线程抢占。
整体通信交互完整流程(客户端 ↔ 服务端)
-
客户端 socket 发起 TCP 连接;服务端 accept,新开线程,回复
OK。客户端收到 OK → 连接成功。 -
客户端输入账号密码,密码 sha224 加密,发送
auth|user|sha224passwd。 -
服务端执行 auth 认证,返回状态码;认证成功下发
总空间|已用空间字节数据。客户端保存用户空间信息。 -
登录完成,进入客户端交互循环。
-
put 上传 客户端发送元数据
put|name|size|md5服务端查询断点,回复0|0新文件 /1|已收字节断点续传 客户端 seek 偏移发送二进制文件;服务端接收;中途断开写入断点;全部完成清除断点。 -
get 下载 客户端发送
get|filename;服务端判断文件是否存在返回 1/0。 文件存在:服务端发送size|name|md5;客户端回复 ready;服务端分块发送文件二进制流;客户端本地保存,计算 md5 校验。 -
show 查看目录 客户端发送
show|;服务端发送返回字符串总长度;客户端回复 ready;服务端传输目录文本。 -
cd 切换目录 客户端发送
cd|xxx;服务端修改用户 currpath,返回切换结果。
-
-
客户端输入
quit退出客户端;服务端 recv 得到 b'',记录客户端断开日志,关闭 socket。
关键技术点
-
多客户端并发 :
socketserver.ThreadingTCPServer,每个连接独立线程。 -
进程隔离:FTP TCP 服务运行在独立子进程,管理员控制台为主进程。
-
用户隔离:每个用户独立家目录,cd 命令不能跳出家目录,不同用户文件互相隔离。
-
磁盘配额管控:服务端 Users 类记录总空间与已使用空间;上传新文件自动更新 ini 数据库。
-
断点续传:md5 作为文件唯一标识,JSON 记录断点;客户端断开下次上传同一个 md5 文件自动恢复。
-
完整性校验:服务端计算文件 md5 下发给客户端,下载后客户端校验,检测传输损坏。
-
反射调用:命令字符串直接映射函数,业务命令解耦。
-
文件数据库:configparser ini 存储账号;JSON 存储断点;不依赖 MySQL 等外部数据库。
-
日志系统:客户端连接断开、每条执行命令、异常全部持久化日志。
现存缺陷与改进建议
-
socket
recv(固定字节)存在 TCP 粘包风险,协议没有消息边界,大数据高并发场景容易解析出错。 -
断点文件逻辑:同一 md5 多个文件会冲突;每个用户只保存一条断点记录。
-
上传时磁盘配额 仅客户端做前置校验,服务端 put 虽然更新配额,但没有二次校验,存在超配额上传风险。
-
没有实现主动关闭 socket;客户端 quit 退出,服务端依靠 recv 空字节感知断开。
-
dbapi 中
check_breakpoint函数调用common.write(),源码方法名错误,应为common.writelog(),运行会抛异常。 -
ini 数据库并发写:多个线程同时修改同一个用户配置,会出现 ini 配置文件内容错乱。
-
用户删除标记 isdel 仅有判断,没有提供完整删除文件目录逻辑。