用Socket重写FTP协议:多进程模型、短连接与用户目录隔离

发布时间:2026/9/14 5:33:40

用Socket重写FTP协议:多进程模型、短连接与用户目录隔离 简介这是一个基于FTP协议、用Python实现的文件传输系统资源包面向需要学习Socket编程与FTP通信原理的Python开发者。系统采用socket实现客户端与FTP服务器间的通讯和文件传输另起Flask服务器提供浏览器端交互支持用户登录注册、查看服务器端文件以及上传、下载、删除等操作并具备多进程、多用户并发处理能力。 压缩包共19个文件约112KB包含Python源码、前端页面及相关配置5个py文件实现服务端与客户端核心逻辑3个html和4个js构成浏览器交互界面另有4个xml工程配置及3个pyc编译文件结构清晰便于直接对照学习。 目前已有494人学习下载。除完整源码外目录还梳理了clientFiles、serverFiles、static等模块配合博客说明可快速理解FTP文件传输的整体流程、多用户并发处理思路和Web交互设计适合作为课程设计或入门项目参考。1. 用 socket 重写 ftp 传输协议为什么绕开 ftplib这套 python 实现 ftp 文件传输的项目最吸引我的地方是它刻意放弃了 ftplib。python 标准库里的 ftplib 是客户端库能连别人的 ftp server但承担不了服务端职责想快速搭一个 ftp 服务器官方也没有对应的服务端实现。项目直接用 socket 从协议层把通讯和文件传输写了一遍另用 flask 做浏览器交互覆盖登录注册、目录浏览、上传下载删除并用多进程支撑多用户。它适合两类人一类是需要把局域网文件传输做成带用户体系的内部工具另一类是面试和课程设计中想展示协议解析、并发模型、数据库落库完整链路的人。全文会按服务端、客户端、数据库、测试脚本四个层次拆开讲。2. 服务端多进程模型监听、指令分发与 socket 关闭顺序2.1 监听参数绑定 0.0.0.0 与选择 2121 端口的原因服务端入口在server.py它做的事比 ftplib 更底层自己建 socket、自己 bind、自己解析请求。先看最核心的启动部分。# server.py 核心骨架 import socket import multiprocessing as mp HOST 0.0.0.0 PORT 2121 class FTPServer: def start(self): s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((HOST, PORT)) s.listen(128) while True: conn, addr s.accept() mp.Process(targetself.handle, args(conn, addr), daemonTrue).start() conn.close()HOST写成0.0.0.0而不是127.0.0.1目的就是让局域网内其他机器也能访问。默认 ftp 端口是 21但 1024 以下端口在 linux 上需要 root 权限所以这里选 2121这是做自定义服务时最常见的做法。listen(128)表示连接队列长度多用户场景下不需要调太大因为每个连接都由独立进程接管accept 循环很快就能处理完。SO_REUSEADDR让服务端重启后能立刻重新绑定端口否则会等 TIME_WAIT 超时开发期反复改代码会特别难受。2.2 指令表与响应码设计一个可解析的应用层协议服务端收到的是纯文本命令按行分隔格式统一为COMMAND 参数。响应码沿用了 ftp 风格200 表示成功230 表示登录成功501 表示参数错误550 表示文件不可用。这套简化协议虽然没有完整实现 RFC 959但胜在干净客户端好解析。核心命令如下。命令参数服务端行为成功响应USER用户名校验用户是否存在331 需要密码PASS密码校验密码并建立会话230 登录成功LIST无返回当前用户目录下的文件名列表200 JSON 数组RETR文件名先返回文件大小再传文件内容200 字节数STOR文件名接收文件内容并写入磁盘200 写入成功DELE文件名删除指定文件200 删除成功QUIT无断开连接200 再见命令分发在handle方法里完成每次recv后按空格拆分命令和参数再查字典找到对应处理函数。这里有个容易被忽视的点客户端发来的命令必须以\r\n结尾服务端先用.strip()去掉再解析否则会出现STOR filename\r\n被当成带换行符的路径导致文件名不对。来看具体的 LIST 实现。import json import os def cmd_list(self, user): if user is None: return 530 Not logged in\r\n target os.path.join(self.root, user) if not os.path.isdir(target): return 550 Directory unavailable\r\n items os.listdir(target) body json.dumps(items, ensure_asciiFalse) return f200 {body}\r\n响应体用 JSON 而不是用\n拼接好处是客户端直接json.loads就能拿数组不需要自己处理换行转义和路径里有空格的情况。ensure_asciiFalse保证中文文件名以 UTF-8 原样返回这对 windows 上访问 ftp 文件夹时常见的乱码问题很关键。self.root是服务端文件根目录每个用户都在根目录下有自己的子目录目录名就是用户名这样多用户之间天然隔离。2.3 多进程 fork 陷阱父进程没有及时 close 连接多进程部分用的是multiprocessing.Process而不是 threading。原因是文件传输是 IO 密集加磁盘读写进程模型下某个用户卡住不会影响其他用户而且进程崩溃不拖累主服务。但这里有一个很容易踩的坑就在上面start()里的conn.close()。在 linux 下mp.Process默认用 fork 创建子进程子进程会复制父进程的文件描述符表。如果父进程 accept 到conn后直接 start 子进程却不关掉自己的conn那么同一个 socket 文件描述符同时被父子进程持有引用计数是 2。即使子进程处理完退出父进程持有的那一份引用还没释放连接不会真正关闭。客户端表现是命令都执行完了但连接一直挂着QUIT 之后也不断开。所以标准做法是start()之后立刻conn.close()让父进程放弃引用把连接完全交给子进程。还有一点如果运行环境是 windows多进程默认不是 fork 而是 spawn子进程会重新导入模块所以server.py里启动服务的主流程要放在if __name__ __main__:下面否则 spawn 会无限递归启动新进程。原项目里有.idea目录说明开发环境是 PyCharm在 windows 上跑的时候这一条尤其重要。3. 客户端双通道架构flask 网页操作与 socket 数据管道的衔接3.1 flask 层职责路由和会话不碰数据链路client.py里的 flask 服务负责用户交互浏览器打开页面后所有操作都走 HTTP 请求flask 再把请求转成 socket 命令发给真正的 ftp server。这个设计把界面和传输链路彻底分开了flask 进程不监听 2121 端口也不直接读写文件它只是中间翻译层。这样做的好处是以后换前端框架不会影响传输逻辑。路由和职责对应如下。HTTP 路由方法对应 ftp 命令页面/GET无index.html 入口/registerGET/POSTUSER PASSregister.html/loginGET/POSTUSER PASSmain.html/listGETLISTmain.html 文件列表/uploadPOSTSTOR上传文件/download/nameGETRETR下载文件/delete/namePOSTDELE删除文件一个关键决策是 flask 端不维护长连接每个请求都新建 socket。这是刻意的理由有两个一是 flask 自带 dev server 是线程模型如果每个请求都去维护一个长时间占用的 ftp 控制连接线程会被拖死二是短连接天然适配服务端多进程模型每次请求对应一个新连接和新进程处理完连接关闭、进程退出资源回收干净。3.2 每次请求新建 socket短连接为什么够用看下载路由的实现就能明白整个链路的数据流。# client.py 下载路由 import socket import io from flask import send_file app.route(/download/path:name) def download(name): with socket.create_connection((192.168.1.20, 2121), timeout30) as s: s.sendall(fRETR {name}\r\n.encode(utf-8)) head s.recv(1024) size int(head.split(b )[1]) buf b while len(buf) size: chunk s.recv(65536) if not chunk: break buf chunk return send_file(io.BytesIO(buf), download_namename)注意create_connection的第一个参数是 ftp server 的地址不是 flask 自己的地址。flask 和 ftp server 是可以部署在两台机器上的。这里的核心函数是最后那个while len(buf) size循环。TCP 是流协议recv(65536)不保证一次收到完整文件可能只收到一部分也可能一次收到超过预期长度的数据所以必须循环收直到收满声明的size。常见错误是用s.recv(1024)读一次就拿去写文件小文件没事大文件必然截断。timeout30是给整个 socket 设置的超时因为浏览器可能中途断开上传或下载挂在那里没有超时的话 flask 线程会一直阻塞。超时后抛出socket.timeout前端会收到 500至少比无限挂起好排查。3.3 两个经典边界sendall 与 recv 循环上传路由和下载是对称的看上传部分。app.route(/upload, methods[POST]) def upload(): f request.files.get(file) if f is None: return jsonify({ok: False, msg: 缺少文件字段}), 400 filename f.filename.replace(/, _) with socket.create_connection((192.168.1.20, 2121), timeout30) as s: s.sendall(fSTOR {filename}\r\n.encode(utf-8)) while True: chunk f.stream.read(65536) if not chunk: break s.sendall(chunk) resp s.recv(1024) return jsonify({ok: resp.startswith(b200), msg: resp.decode(utf-8)})这里用的sendall而不是send。send不保证把整个缓冲区发出去底层 socket 的发送缓冲区满了之后会返回实际发送的字节数开发者需要自己处理剩余部分sendall内部循环发送直到全部发完虽然性能上略有损耗但能避免半包问题。65536这个分块大小是 64KB是 socket 缓冲区和文件系统块大小之间的一个均衡值再大也不会明显加速反而会增加单次sendall阻塞时间。还有一个隐藏边界flask 的f.stream.read读取的可能是临时文件而不是内存大文件上传时不会一次性把文件读进内存这就保证了内存占用和文件大小无关。很多人上来就f.read()整个文件几 GB 文件直接把进程内存打满这个项目用分块读是正确示范。4. 用户体系与目录隔离DBManager 的用户表、哈希与路径越界防护4.1 register.html 到 main.html 的注册登录链路用户相关的数据库操作集中在util/DBManager.py。注册和登录的流程是register.html表单提交用户名密码到 flask 路由flask 调用DBManager.register()写入用户表成功后重定向到main.html登录时DBManager.verify()校验密码校验通过后往 flask session 里写用户标识后续所有文件操作都从 session 读用户名。注意 flask 端不认证 ftp 层面的连接而是用 session 保持登录态因为每次 socket 请求都是新连接不可能维持 ftp 会话状态。文件页面上会展示当前用户的文件列表列表数据来自LIST命令。main.html里的上传下载按钮分别对应前面提到的/upload和/download路由删除按钮走POST /delete/name页面用 AJAX 提交后用 JS 刷新列表整体是不刷新页面的交互模式。4.2 存密码只存哈希盐的选择与校验逻辑用户表用 SQLite 落库密码不能明文存。项目里采用的方案是每个用户生成独立的随机盐存储盐和sha256(盐 密码)的哈希值。核心代码看 DBManager。# util/DBManager.py 核心逻辑 import sqlite3 import hashlib import os class DBManager: def __init__(self, dbusers.db): self.conn sqlite3.connect(db, timeout5) self.conn.execute(PRAGMA journal_modeWAL) ... def register(self, username, password): salt os.urandom(16).hex() pwd_hash hashlib.sha256((salt password).encode(utf-8)).hexdigest() try: self.conn.execute( INSERT INTO users (username, salt, password_hash) VALUES (?, ?, ?), (username, salt, pwd_hash) ) self.conn.commit() return True except sqlite3.IntegrityError: return Falseos.urandom(16).hex()生成 32 位十六进制随机盐每次注册都重新生成而不是全局固定一个盐。相同密码在不同用户下的哈希值不同这样即使数据库泄漏也无法通过彩虹表反推密码。hexdigest()输出 64 位十六进制字符串存到password_hash字段。校验时取出该用户的 salt重新计算sha256(salt password)再和库里哈希比对而不是解密哈希再用明文比较因为哈希是单向的。注意PRAGMA journal_modeWAL。SQLite 默认回滚日志模式下多进程同时写入会出现database is locked错误。WAL 模式允许读写并发读取不阻塞写入写入之间用timeout5让冲突的写操作等待而不是立刻报错。这里的多进程不是指 flask 工作进程而是服务端mp.Process可能同时调用 DBManager加上 flask 和 ftp server 共用同一个 users.db 时跨进程写并发是很现实的。如果省略 WAL高并发注册登录会有间歇性 500。4.3 多用户目录隔离与路径穿越防护用户登录后只能操作自己目录下的文件目录结构是serverFiles/用户名/。这里有一个必须处理的安全点如果文件名从外部传入而不做过滤攻击者传../../etc/passwd就能路径穿越到任意目录。项目里注册时就在serverFiles下创建以用户名为名的目录文件操作前先做一次路径归一化校验。def safe_join(root, user, filename): base os.path.realpath(os.path.join(root, user)) path os.path.realpath(os.path.join(base, filename)) if path ! base and not path.startswith(base os.sep): raise ValueError(路径越界) return pathrealpath会解析掉..和符号链接得到绝对路径然后检查规范化后的路径是否仍然以用户目录开头。base os.sep是为了防止base本身是另一个目录的前缀而误判比如files/user1和files/user1_evil之间的边界。这个函数在RETR、STOR、DELE三个命令里都要调用漏一个就是任意文件读写漏洞。多用户场景下删除命令尤其要防否则用户可以删除其他用户的文件。5. test.py 模拟并发客户端验证多进程和排查 501、传输卡死5.1 并发测试脚本的写法test.py是拿来检验服务端多进程能力的。它的思路是起多个客户端线程每个线程模拟一次完整的上传流程看服务端能不能同时处理。下载场景的验证同理把上传换成 RETR 即可。# test.py 并发上传测试 import socket import threading def one_client(idx): with socket.create_connection((127.0.0.1, 2121), timeout10) as s: s.sendall(bUSER test%d\r\n % idx) s.recv(1024) s.sendall(bPASS 123456\r\n) s.recv(1024) s.sendall(bSTOR con_%d.txt\r\n % idx) s.sendall(bA * 1024 * 1024) resp s.recv(1024) assert resp.startswith(b200), resp ts [] for i in range(10): t threading.Thread(targetone_client, args(i,)) ts.append(t) t.start() for t in ts: t.join()这段脚本验证两件事一是 10 个线程同时上传服务端所有连接都能返回 200说明多进程真的在处理并发二是s.sendall(bA * 1024 * 1024)一次性发 1MB 数据如果服务端只recv(4096)一次就开始写文件文件必然是截断的测试会阻塞在最后的recv(1024)等不到响应。服务端日志里如果打印了os.getpid()还能看到每个连接由不同 pid 处理这是验证多进程最有说服力的证据。5.2 高频排错501、能登录不能传、半个文件现象常见原因排查方向登录后命令全部返回 501命令缺少\r\n结尾服务端用repr(received)打印原始字节能登录但上传无响应数据没发完就等响应检查客户端是否用 sendall 发送分块局域网其他机器连不上服务端绑了 127.0.0.1linux 用ss -lntp确认监听地址下载文件比原文件小recv 只调用了一次改成循环 recv按 size 累计多用户同时传大文件卡死SQLite 写锁阻塞确认已启用 WAL 并设置 timeout501 是 ftp 常规错误最常见原因不是命令拼错而是命令行的换行符不对。客户端如果只发STOR file.txt不带\r\n服务端的recv会一直等到缓冲区有数据才返回但.strip()之后命令本身没问题问题往往出在参数里混入了多余字符。排查时在服务端 decode 之前先打印repr(line)能看到不可见字符。能登录不能传文件优先检查监听地址和防火墙2121 是自定义端口windows 防火墙默认不会放行需要单独加规则下载文件少字节数是典型的 recv 循环缺失对照 3.2 节的while len(buf) size改掉就正常了。压测时把并发数加到 20 以上再盯服务端日志里的 pid 分布基本就能判断出多进程模型是否存在连接泄漏。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/9/14 5:28:39

用Matlab实现资本资产定价模型:从数据清洗到滚动Beta估计

简介:一份基于Matlab的资本资产定价模型(CAPM)估计程序源码,适合金融工程、投资学、计量经济学等课程的学习者,也方便入门量化研究者理解贝塔系数的计算思路。资源以“估计资本资产定价模型”为核心,在单个…

2026/9/14 5:28:39

Simulink模型预测控制仿真:从闭环搭建到参数整定与发散排查

简介:这份基于Simulink的预测控制仿真资料包,面向控制工程、自动化及相关专业的初学者和工程师,旨在帮助读者从预测控制(MPC)的理论基础出发,在MATLAB/Simulink环境中完成动态系统建模、预测控制器设计、滚…

2026/9/14 6:28:42

游戏出海买量成本高?AI语言引擎如何破解本地化瓶颈

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/14 6:28:42

大模型隐私数据删除技术:PrivacyScalpel原理与实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/14 6:28:42

Agent五层架构实战:MCP与A2A协议工程落地指南

1. 这不是一张“技术海报”,而是一份Agent产业实操者的生存地图 如果你最近刷技术社区、看融资新闻、听行业峰会,甚至只是打开GitHub Trending,都会被“Agent”这个词反复击中——它不再是个实验室里的概念,而是正在长出牙齿、开始…

2026/9/14 2:17:50

拯救者Y7000黑屏故障排查与维修实战指南

1. 项目概述:一台黑屏的拯救者Y7000,到底卡在哪一步? 联想拯救者Y7000系列笔记本,从2018年第一代搭载i5-8300H开始,到后来的i7-9750H、i7-10750H、i5-11400H,再到2023年款的R7-7840HS,它始终是学…

2026/9/14 0:03:22

KCF目标跟踪算法与OTB工程实现:毕业设计实战解析

简介:这是一份基于KCF核相关滤波算法、融合尺度池与抗遮挡处理的目标检测跟踪MATLAB完整源码,主要面向计算机相关专业准备毕业设计、课程设计或期末大作业的学生,也适合需要项目实战练习的初学者。源码在OTB数据集上完成验证,能够…

2026/9/14 0:03:22

语音情感识别实战:Keras实现LSTM、CNN、SVM与MLP多模型对比

简介:面向语音情感识别入门与进阶开发者,这份基于Keras的项目源码完整实现了LSTM、CNN、SVM、MLP四种模型,兼容Python3.8与Keras/TensorFlow2环境。压缩包内含49个文件,大小约70.31MB,主体包括Python脚本、yaml/json配…

2026/9/12 6:29:36

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/12 14:32:17

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/13 11:18:28

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码