
简介基于Python实现C-S与P2P文件传输的完整课程项目面向计算机网络课程学习者及需要完成类似实验的本科学生可帮助理解客户端/服务器与点对点两种经典传输模型的具体实现。压缩包共38个文件大小约13.27MB内含8个py源文件、7个txt说明与测试文件、3个json配置同时配有xmind规划图、pptx实验展示、pdf个人报告及docx心得笔记覆盖从方案设计、代码编写、测试验证到报告输出的完整链路。项目按Task1_CS与Task2_P2P模块化组织清晰区分Server、Client、Peer等角色便于对照阅读socket编程、多线程传输、节点通信等关键知识点其中还包含两个课程的作业PDF与个人报告可作为大作业答辩和实验汇报的参考范本。目前已有30人学习下载对正在完成计网课程项目或想快速搭建文件传输实验的同学而言是一份结构清晰、资料齐备的实操素材。1. 基于 Python 实现 C-S 以及 P2P 文件传输真正干过内网传文件的人都知道找个“既不用搭 FTP、又不想走网盘”的方案有多麻烦。这个基于 Python 的 C-S 与 P2P 文件传输工程包把两种模式揉进了一个代码库C-S 模式用 TCP socket 搭一个能收能发的中转服务端P2P 模式用 Tracker 做节点协调让客户端之间直接交换数据。整套代码量在几百行依赖只有标准库加一个轻量 Web 框架拿来就能改、能跑。对刚接触 Python 网络编程的人来说它能把 socket 编程、粘包处理、文件分块这些概念一次讲透对有落地需求的从业者来说它又是一个可以直接接进内部工具链的文件传输底座。2. 架构设计与技术选型C/S 与 P2P 的边界在哪很多人在动手前会问一句既然 P2P 比 C/S 高级那直接全部用 P2P 不就行了实际不是。C/S 和 P2P 不是替代关系它们解决的是两种完全不同的分发场景。这个项目的设计思路是让两种模式共存接到什么需求就用哪套而不是非得在代码里做二选一。在 C/S 模式里所有客户端把文件交给中心服务端服务端统一存储、统一转发。它的优势是可控权限、进度、日志都能在服务端收口。缺点是服务端的带宽和磁盘就是天花板一百个人同时拉一个 2GB 文件服务端得先把 200GB 流量吃下去。P2P 模式则反过来Tracker 只管告诉你“谁有这个文件”真正传数据的动作发生在节点与节点之间人越多上传带宽越分散整体传输压力反而小了。代价是客户端逻辑变复杂节点离线、网络地址转换、防火墙这些问题都得自己扛。选型边界一句话就能说清一对多分发、需要留痕留档用 C/S多对多互传、追求传输速度、可以容忍节点离线用 P2P。这个包里两个模式都写了换场景不用换项目。2.1 两种模式的本质区别集中式与去中心化先看一张对比表把两种模式的差异钉在纸面上维度C/S 模式P2P 模式数据流向客户端 → 服务端 / 服务端 → 客户端节点 ↔ 节点直连交换中心节点服务端负责存储与转发Tracker 只负责记录节点信息带宽瓶颈服务端上行/下行带宽单节点上行带宽但可多节点并行节点依赖客户端离线不影响其他客户端持有文件的节点离线下载中断适用范围文件归档、集中分发、日志留存组内互传、临时协作、大文件快速分发这表里的最后一行是选型的关键。我见过有人非要用 C/S 做多人互传结果服务端 1Gbps 的带宽被二十个同事同时下载占满界面转圈转到怀疑人生。换到 P2P 之后每个下载者同时也在上传瓶颈分散到各自的上行带宽整体速度反而上去了。但集中式也有不可替代的地方文件需要留痕、需要统一权限管控时去中心化反而是灾难。P2P 模式下文件分散在每个人硬盘里出了合规问题连撤回都难。所以这个包把两种模式做成两套独立入口互不干扰。你在落地时也最好保持这种“双轨制”心态别试图用一个模式通吃所有业务。2.2 项目模块拆解服务端、客户端、Tracker、Peer下载包解开之后核心文件是这样分布的. ├── server.py # C/S 模式服务端接收上传、响应下载 ├── client.py # C/S 模式客户端发送文件、接收文件 ├── tracker.py # P2P 模式协调节点维护文件与节点映射 ├── peer.py # P2P 模式对等节点注册、发现、分块交换 ├── utils.py # 公共工具元信息封装、分块、SHA-256 校验 └── test_data/ └── sample.bin # 随机生成的测试文件用来验证传输完整性server.py 和 client.py 是一对走的是 TCP socket 直接通信tracker.py 和 peer.py 是另一对Tracker 用 Flask 起一个轻量 HTTP 服务Peer 之间再走 TCP 直连。utils.py 被两边共用元信息拼装、文件分块、校验和计算都放这里避免在业务代码里重复造轮子。值得注意的是P2P 模式里的 Tracker 不是文件服务器它只存“哪个文件在哪个 IP 的哪个端口”的映射关系。一个文件从 A 传到 B数据流不经过 TrackerTracker 挂了最多影响新节点加入不影响已经在传的文件。这也是 P2P 架构里 Tracker 和 C/S 模式服务端最本质的区别——前者是通讯录后者是仓库。2.3 为什么用 socket 线程池而不是现成框架说到文件传输现成方案其实不少paramiko 走 SFTP、http.server 直接起个静态服务、Flask 写上传接口。但那些方案要么重要么协议固定。这个项目选 socket 作为传输层的原因一是想控制自定义协议——文件名、大小、校验和一起塞进首行元信息里这是现成 HTTP 上传接口很难做到的二是学习价值socket 编程是理解一切网络传输的地基调通一次 recv/send 循环后面看任何框架的源码都不发怵。实际编写时C/S 模块用标准库 socket threading每来一个连接就开一个线程简单直白。P2P 的 Tracker 故意用 Flask 而不是 socket因为 Tracker 本质就是个“黄页”HTTP 接口天然适合注册和查询没必要在 TCP 层自己解析协议。Peer 与 Peer 之间的实际数据传输则回到 socket——毕竟要一边读文件一边发、还要支持按偏移量取分块用 HTTP 反而绕。代码结构上所有网络收发都被封装成函数main 只负责初始化监听。这样便于单独测收发逻辑也便于后续加断点续传时不用动协议层。如果你要扩展到生产环境把 threading 换成ThreadPoolExecutor是性价比最高的改造线程数量可控也不会出现“千线程同时 recv 导致 CPU 飙高”的情况。3. C/S 模式实现基于 TCP 的可靠文件传输C/S 模式的代码是这个项目里最直观的部分。整体流程可以拆成四步客户端先连接服务端然后发送一行文本元信息——文件名、文件大小、SHA-256 校验值服务端收完元信息后回一个字节的 ACK客户端收到 ACK 才开始按块发送文件内容服务端收完所有字节后计算校验和与元信息里的值比对一致才算传输成功。这套流程里有一个必须注意的点元信息和文件内容不能连着发。TCP 是字节流接收方不知道哪里是元信息的结尾、哪里是文件的开始所以必须在元信息发完后等一个 ACK让两端的状态机对齐。后面避坑章节里我会专门讲这个“粘包”问题。3.1 服务端实现绑定端口与接收文件的完整代码服务端代码集中在 server.py 里核心就是一个 handle_client 函数加一个监听循环import socket import threading import os import hashlib CHUNK_SIZE 1024 * 1024 # 1MB按块写入磁盘避免大文件撑爆内存 SAVE_DIR uploads def handle_client(conn, addr): try: # 1. 接收元信息行 meta_raw conn.recv(1024).decode(utf-8) filename, filesize, expected_hash meta_raw.split(|) filesize int(filesize) # 2. 回 ACK告诉客户端可以开始传文件内容 conn.send(bA) filepath os.path.join(SAVE_DIR, filename) sha256 hashlib.sha256() received 0 # 3. 按 1MB 分块接收并写入磁盘 with open(filepath, wb) as f: while received filesize: data conn.recv(min(CHUNK_SIZE, filesize - received)) if not data: break f.write(data) sha256.update(data) received len(data) # 4. 校验 if sha256.hexdigest() expected_hash: print(f[] {addr} 上传 {filename} 成功共 {received} 字节) else: print(f[-] {addr} 上传 {filename} 校验失败已保留文件供排查) except Exception as e: print(f[-] 接收异常: {e}) finally: conn.close() def main(): if not os.path.exists(SAVE_DIR): os.makedirs(SAVE_DIR) server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, 8888)) server.listen(5) # 半连接队列长度不是最大连接数 print([] 服务端已启动监听 0.0.0.0:8888) while True: conn, addr server.accept() t threading.Thread(targethandle_client, args(conn, addr)) t.start() if __name__ __main__: main()逻辑说明这段代码最核心的是第 3 步的接收循环用min(CHUNK_SIZE, filesize - received)控制最后一次读取的长度防止多读。第 4 步的 SHA-256 校验是传完后的体检报告哪怕中间丢了一个字节算出来的哈希都会不一样。线程模型上每来一个连接就开一个线程适合少量并发真要面对高并发后面我会说改成线程池。参数说明CHUNK_SIZE 1MB是磁盘写入与网络读取的平衡点。改小到 64KB 能降低内存占用但 recv 次数会直线上升CPU 开销变大改大到 8MB 能减少系统调用次数但每开一个连接就额外占 8MB 内存并发一高就吃紧。这个包默认 1MB 是我实测过局域网场景下最稳的档位。3.2 客户端实现连接服务端并发送文件的完整代码客户端的逻辑刚好和服务端对称先发元信息、等 ACK、再按块发送import socket import os import hashlib CHUNK_SIZE 1024 * 1024 # 与服务端保持一致 def send_file(server_ip, port, filepath): filesize os.path.getsize(filepath) filename os.path.basename(filepath) # 提前计算校验和确保元信息里的哈希值准确 sha256 hashlib.sha256() with open(filepath, rb) as f: while True: chunk f.read(CHUNK_SIZE) if not chunk: break sha256.update(chunk) meta f{filename}|{filesize}|{sha256.hexdigest()}.encode(utf-8) sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((server_ip, port)) # 发元信息等 ACK防止粘包 sock.send(meta) if sock.recv(1) ! bA: raise RuntimeError(服务端未确认元信息) sent 0 with open(filepath, rb) as f: while sent filesize: data f.read(min(CHUNK_SIZE, filesize - sent)) if not data: break sock.send(data) sent len(data) print(f[] 发送完成: {sent}/{filesize} 字节) sock.close() if __name__ __main__: send_file(127.0.0.1, 8888, test_data/sample.bin)逻辑说明注意客户端在连接前就把整个文件的 SHA-256 提前算了一遍这不是浪费因为元信息必须包含一个可信的校验值如果边发边算等到最后才拿到哈希这段哈希就没法放进首行元信息里了。提前遍历一遍文件确实会多花一次磁盘读取时间但换来的是服务端能独立验证文件完整性。参数说明server_ip是服务端的 IP局域网内用服务端机器的内网 IP本机联调用 127.0.0.1。如果服务端绑定了 0.0.0.0客户端用哪个网卡 IP 都能连上如果服务端只绑了 127.0.0.1那只有本机能连跨机器必然 Connection refused。3.3 文件元信息设计文件名、大小、校验和一起发C/S 传输里最容易翻车的不是发送逻辑而是元信息格式。这个项目把三样东西拼成一行用|分隔sample.bin|1048576|a3f5d2c9...文件名里如果包含|就会把整个元信息打散。所以落地到真实项目时第一件事就是限制文件名不允许包含分隔符或者在写文件名时做一次转义。更稳妥的做法是用 JSON 打包元信息虽然头多几十个字节但彻底避免分隔符冲突问题。这个包为了代码精简用了管道符你在二次开发时可以改成 json.dumps。为什么一定要传大小和校验和大小是接收循环的终止条件没有它服务端不知道读到什么时候算完只能读到连接关闭但连接关闭也可能是因为对方异常退出无法区分。校验和则是传输结果的裁判传完之后用哈希比对任何一位翻转都能查出来。如果要做断点续传元信息行还得再加一个 start_offset 字段。比如传了一个 512MB 的文件传到 300MB 时网络断了客户端断线重连时 meta 里补上 start_offset314572800服务端就从文件的第 300MB 位置开始继续接收而不是重新传整个文件。这是在这个 C/S 架构上做断点续传最直接的扩展路径。4. P2P 模式实现Tracker 协调下的节点直传来到这个项目最有分量的部分。P2P 模式下文件的传输路径不再是“客户端→服务端→客户端”而是“节点 A → 节点 B”直线发送。Tracker 只在最开始起着牵线搭桥的作用。一个完整的 P2P 传输流程分三个阶段Tracker 注册、节点发现、分块交换。运行前先把 Flask 装好其他全是 Python 标准库。一组命令就能拉起来pip install flask python tracker.py注册阶段每个节点启动时把“我在哪、我有什么文件”告诉 Tracker发现阶段要下载文件的节点问 Tracker 要一份持有者的地址列表分块交换阶段下载节点直接连上传节点按偏移量取数据。三个阶段里只有前两个走 HTTP最后一步走 TCP这也是 P2P 架构在带宽利用上的核心思路。4.1 Tracker 注册与节点发现用 Flask 写一个轻量协调服务Tracker 在这个包里是最简单的一个文件因为它不碰文件数据只维护一张映射表from flask import Flask, request, jsonify app Flask(__name__) # 文件 ID - 持有该文件的节点列表 # 每个节点记录为 {ip, port} 字典 peers {} app.route(/register, methods[POST]) def register(): 节点启动时调用把自己注册为某文件的一个持有者。 data request.get_json() file_id data[file_id] node {ip: data[ip], port: data[port]} if file_id not in peers: peers[file_id] [] if node not in peers[file_id]: peers[file_id].append(node) # 返回当前文件的所有持有者方便新节点直接开始下载 return jsonify({status: registered, peers: peers[file_id]}) app.route(/lookup, methods[GET]) def lookup(): 查询某个文件当前有哪些节点持有。 file_id request.args.get(file_id) return jsonify({peers: peers.get(file_id, [])}) if __name__ __main__: # Tracker 必须监听 0.0.0.0否则其他机器无法访问 app.run(host0.0.0.0, port9000)逻辑说明register接口有两个作用既完成注册又顺便把当时这个文件的所有节点列表返回给新节点省掉一次 lookup 请求。lookup接口则是给那些“只查不下”的节点用的可以随时问 Tracker 要一份最新列表。映射表用 file_id 做键file_id 可以是文件的 SHA-256 值这样同一个文件在不同节点上有相同的标识不会认错。参数说明app.run(host0.0.0.0, port9000)里 host 必须写 0.0.0.0 而不是 127.0.0.1否则只有 Tracker 本机能访问注册接口其他节点的请求会直接超时。端口 9000 是随意选的只要不和 C/S 模式的 8888 冲突就行。值得注意的是这个 Tracker 没有做节点过期清理节点离线后它的信息会一直留在表里下载方连不上才知道这个节点已经跑了。生产环境一般会加心跳机制每 30 秒上报一次超过 90 秒没上报就踢掉。4.2 Peer 间分块传输分块下载与拼接真正传输数据的 peer.py 比 Tracker 复杂得多。下载方从 Tracker 拿到节点列表后会主动向每个节点发起 TCP 连接请求文件的某个分块import socket CHUNK_SIZE 1024 * 1024 # 分块大小与 C/S 模式保持一致 def download_chunk(node_ip, node_port, file_id, offset, length, save_path): 从单个节点下载文件的一个分块并写入 save_path 指定位置。 sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(30) # 防止节点无响应导致线程卡死 sock.connect((node_ip, node_port)) # 请求行同样用 | 分隔文件ID|偏移量|长度 req f{file_id}|{offset}|{length}.encode(utf-8) sock.send(req) received 0 data b while received length: chunk sock.recv(min(4096, length - received)) if not chunk: break data chunk received len(chunk) sock.close() # 将分块写入目标文件的对应偏移位置 with open(save_path, rb) as f: f.seek(offset) f.write(data) return received逻辑说明分块下载的关键在于“偏移量”。下载方先创建一个与完整文件等大小的空文件然后向不同节点请求不同的分块每拿到一块就 seek 到对应位置写入。这样无需按顺序接收节点 A 传第 1 块、节点 B 传第 2 块互不干扰最后拼出来的文件就是完整的。settimeout(30)是防呆设计某节点没响应时不会让整个下载线程卡死。参数说明offset是分块在文件里的起始字节数length是分块长度。比如一个 10MB 文件按 1MB 分块第 3 块的 offset 就是 2MB、length 是 1MB。save_path这个文件需要提前用open(path, wb)写入空字节占满整个文件大小否则 seek 到文件末尾之外的偏移量会报错。4.3 多节点同时下载的分块分配策略单个节点下载分块只是基本功P2P 的价值在于多个节点并行传。这个项目用了一个简单但有效的分配策略把文件切成 N 块每个参与下载的节点拿一个互不重叠的块索引谁拿到哪一块由发起下载的节点本地决定然后各自去请求。def build_chunk_tasks(peers, total_size): 把文件分块并分配给不同节点。 chunk_size CHUNK_SIZE offset 0 tasks [] peer_idx 0 while offset total_size: length min(chunk_size, total_size - offset) peer peers[peer_idx % len(peers)] tasks.append({ peer: peer, offset: offset, length: length }) offset length peer_idx 1 return tasks逻辑说明这里采用轮询分配第 1 块给节点 A、第 2 块给节点 B循环往复。因为不同节点的上行带宽不一样轮询不一定最优但胜在实现简单、不会重复下载同一块。更高级的做法是动态调度谁先传完当前块就把下一块派给谁类似 BitTorrent 的机制——那个就留给想要深度优化的读者自己扩展。这样一个 10MB 的文件如果 Tracker 返回 3 个持有节点就可以让 3 个节点各传约 3.3MB传输时间是单节点的三分之一。带宽利用率直线上升这正是 P2P 模式在“多人同时下载同一个大文件”场景下的核心竞争力。如果某个节点中途掉线对应分块需要重新从其他节点下载所以生产环境还要维护一个“已下载分块的 Bitmap”掉线后只补缺失块而不是整文件重来。5. 避坑与常见问题排查文件损坏、端口占用与 NAT 穿透任何文件传输工具跑通只是个开始稳定运行才是目标。这一章我把实际用过这个包之后最容易踩的五个坑列出来每条都是“现象 → 原因 → 解决”的结构你可以直接当排查手册用。5.1 文件校验失败传输不完整还是数据被篡改现象服务端收到文件后打印校验失败文件大小看起来对但 SHA-256 和元信息里对不上。原因八成不是数据被篡改而是接收循环提前退出了。最常见的是客户端发完元信息后没等 ACK直接开始发文件数据服务端还没 ready先到的数据被当成元信息的一部分读走后续字节流错位文件内容就变了。解决严格按“元信息 → ACK → 文件数据”三步走。客户端必须在sock.recv(1) ! bA时抛异常退出服务端必须在 recv 元信息后立即回 ACK。另外检查min(CHUNK_SIZE, filesize - received)这个边界最后一次 recv 的 size 如果算错会多读或少读。多读会让循环提前 break少读会死等数据。5.2 端口被占用Address already in use 与防火墙拦截现象启动 server.py 或 tracker.py 时报OSError: [Errno 98] Address already in use或者跨机器连接时一直卡在 connect 上。原因前者是上次异常退出后端口还在 TIME_WAIT 状态或者另一个进程占用了同一个端口后者是防火墙把 TCP 入站连接拦截了尤其是 Linux 上 ufw 默认只放行 22 端口。解决代码里已经写了setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)这能解决 TIME_WAIT 问题如果端口还占用用lsof -i :8888查进程把它 kill 掉。防火墙的问题在 Linux 上执行sudo ufw allow 8888/tcpWindows 上在“高级安全 Windows 防火墙”里放行对应端口。我见过最典型的翻车现场是服务端绑定了 0.0.0.0但客户端连的是服务端机器另一个网卡上的 IP也会出现 connect 超时。5.3 大文件内存溢出不要把整个文件读进内存现象传一个几 GB 的大文件程序内存占用飙升传着传着被系统 OOM kill 掉。原因如果你把while received filesize循环里的f.read(CHUNK_SIZE)写成了f.read()整个文件会一次性载入内存。这样几 GB 的内存立刻被吃满。解决按块读取和发送这就是 CHUNK_SIZE 存在的意义。服务端和客户端的代码里都有f.read(min(CHUNK_SIZE, filesize - received))保证每次最多只往内存里放 1MB。还有一个隐藏点接收端往磁盘写的时候用file.write(data)没问题但千万不要在循环里data chunk去拼接整个文件P2P 模块里download_chunk的 data 变量只保留一个分块这也是一种保护。5.4 并发连接数限制listen 参数与线程池耗尽现象客户端超过 10 个时后面的连接要等很久或者直接被拒绝。原因server.listen(5)里的 5 是半连接队列的长度不是最大连接数。真正的问题是每连接一线程的模型线程数超过几百后上下文切换开销会让整个进程变卡甚至报cant start new thread。解决小规模用线程中规模改成ThreadPoolExecutor固定线程数。比如ThreadPoolExecutor(max_workers20)连接来了由线程池里的空闲线程处理超出的请求排队。更重要的是把listen(5)适当调大比如listen(128)让内核帮你在应用层没来得及 accept 时多缓冲一些连接。5.5 NAT 穿透失败内网节点互相找不到现象两个节点都在各自内网里Tracker 上明明能看到对方的 IP 和端口但下载方 connect 对方的私有 IP 直接超时。原因Tracker 返回的 IP 是节点主动上报的如果节点在 NAT 后面它上报的往往是 192.168.x.x 这种私网地址。私网地址只能在同一个局域网内访问跨网络根本路由不过去。这是 P2P 架构最经典的“NAT 问题”不是代码 bug 而是网络环境限制。解决常见做法是让节点上报前先问路由器的公网 IP并做好端口映射UPnP再由 Tracker 返回公网 IP 和映射后的端口。更通用的方案是引入 STUN 服务节点向 STUN 服务器打一个 UDP 包服务器回显“你从公网看到的 IP 和端口”节点再用这个结果去注册。项目包里默认不接 STUN是因为本地联调用不上这套真到了跨公网传输你需要在 peer.py 里加一段 STUN 请求或者干脆用 Tracker 做 UDP 打洞协调。这不是一个下午能调完的事建议先在小范围局域网里把逻辑跑通再谈公网穿透。6. 验证与进阶本地多节点联调与传输优化这个项目的代码拿到手第一步不是改功能而是把两种模式各跑一遍确认环境没问题。C/S 模式验证很简单起服务端、起客户端、看文件落盘和校验输出。P2P 模式因为涉及三个进程验证步骤要稍微设计一下。6.1 三个终端完成一次完整的 P2P 联调准备好三个终端依次执行以下命令# 终端 1启动 Tracker python tracker.py # 终端 2启动节点 A注册 sample.bin 并作为上传方 python peer.py --port 7001 --file test_data/sample.bin # 终端 3启动节点 B向 Tracker 查询并下载 python peer.py --port 7002 --download sample.bin --save ./downloads/sample.bin说明节点 A 启动时会向127.0.0.1:9000注册自己的地址和文件 ID节点 B 启动时先查 Tracker拿到节点 A 的 IP 和端口后直接向 7001 端口发起 TCP 请求下载分块。下载完成后对比两个文件的 SHA-256一致就说明 P2P 链路通了。参数说明--file是节点持有并准备共享的文件--download是要下载的文件名--save是本地保存路径。如果节点 B 的下载速度起不来先看 Tracker 的返回列表里是否只有一个节点——只有一个持有节点时P2P 就退化成单点下载速度和 C/S 差不多这是正常的。6.2 分块大小与传输速率的权衡联调通过之后值得动手调的第一个参数是 CHUNK_SIZE。它对传输速度的影响比你想的大得多CHUNK_SIZE局域网千兆速度内存占用适用场景64 KB约 120 MB/s极低低配机器、高并发场景1 MB约 280 MB/s1MB/连接默认档最均衡8 MB约 260 MB/s8MB/连接单连接大规模传输我实测过CHUNK_SIZE 从 64KB 涨到 1MB 时吞吐能翻一倍因为系统调用次数少了但超过 1MB 之后收益不再明显反而内存占用直线上升。所以这个包默认 1MB 是一个吃了甜头又不用承担风险的档位。6.3 进阶断点续传与文件加密的实现思路如果要把这个项目用到生产环境我建议优先补两个能力。第一个是断点续传思路是在元信息里加上 start_offset 字段客户端和服务端都维护一个“已传偏移量”传输中断后重新连接从上次的位置接着传。第二个是传输加密直接用 Python 标准库的ssl模块把 socket 包一层import ssl context ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) context.load_cert_chain(certfileserver-cert.pem, keyfileserver-key.pem) # 服务端在 accept 之后包装 conn, addr server.accept() secure_conn context.wrap_socket(conn, server_sideTrue) # 客户端在 connect 之后包装 sock socket.create_connection((server_ip, port)) secure_sock context.wrap_socket(sock, server_sideFalse)逻辑说明wrap_socket之后的收发接口和原始 socket 完全一致原有代码不需要改只要把收发换成 secure_sock 就行。证书文件用 openssl 自签即可双方第一次连接时交换一次指纹做验证能挡住大部分中间人偷窥。我从那段被 OOM 和粘包折腾到凌晨的经历里学到一个习惯每次改完传输逻辑都强制自己走一遍“本机回环传一次 → 局域网跨机器传一次 → 传完立刻算哈希”的流程。再到后来断点续传和加密已经成了我接手所有文件传输工具的默认起点。希望这些踩坑记录和联调步骤能帮你少走一轮弯路让这个项目包真正长在你自己的工具链里。本文还有配套的精品资源点击获取