用Python标准库实现局域网大文件高速点对点传输工具
发布时间:2026/10/23 14:09:37 锦皓数字建站

这个项目的起因特别朴素同事让我给他传一个 4GB 的日志压缩包我翻了半天抽屉没找到空闲 U 盘打开聊天软件的传输功能又担心文件过期被清理网盘上传更是慢到让人怀疑人生。折腾一圈之后我决定花一个下午自己写一个文件传输软件解决这种“大文件临时互传”的痛点。这个软件本质上就是两个 Python 脚本一个负责收一个负责发。跑通的是局域网内点对点的 TCP 文件传输支持中文文件名、大文件分块读写、SHA256 完整性校验还带实时进度提示。没有花哨的界面没有多余的第三方依赖纯标准库实现能在任意一台装了 Python 的机器上直接运行。如果你也是那种不想为了传一次文件装一堆全家桶的人或者正在学网络编程、想知道一个文件到底是怎么从一台机器“流”到另一台机器的这篇文章应该对你有用。我把协议设计、核心代码、踩坑实录都摊开来讲你照着复现一遍基本就能跑出自己的版本。1. 项目缘起为什么放着现成工具不用偏要自己写一个1.1 现有方案的三个痛点先说结论市面上能传文件的工具非常多但当你真的身处一个“两台电脑临时互传一个大文件”的场景时大部分方案都差点意思。第一个痛点是物理介质。U 盘、移动硬盘这种方案看着简单实际上要求你手边刚好有设备还得考虑文件系统格式和单文件大小限制。日志压缩包、数据库备份、虚拟机镜像这类文件动辄好几个 GB格式不对根本写不进去。就算写进去了来回插拔也费时间传完还得确认两边文件是不是一致整个流程非常啰嗦。第二个痛点是聊天软件的传输功能。临时传文件给人第一反应确实是打开聊天工具拖过去但这类功能普遍有大小限制免费情况下大文件要么被拒收要么被转码压缩。更糟的是它的链路设计明明是同一间办公室里两台机器互传数据却要先上公网服务器再绕回来走完全程的吞吐量比局域网直连差了一个数量级。第三个痛点是网盘和共享文件夹。网盘理论上能传任意大小的文件但上传和下载都受公网带宽限制还要处理账号、权限、隐私一系列问题。局域网共享文件夹其实能解决大部分问题但配置访问权限、处理跨系统差异、临时开放端口都让“我就传一次文件”这件事变得异常沉重。1.2 我当时的真实使用场景我这次的触发场景很具体两台机器在同一个办公网络里。一台是长期运行的服务器上面攒了几个月日志和中间产物大概 4GB另一台是我的工作电脑需要把这些文件拷回去分析。两边都没有公网 IP也不方便插物理介质用聊天软件传 4GB 基本等于放弃网盘绕路不说还经常连不上去。这个场景大概是很多开发者的日常不是没有工具而是现成工具的重心都不在“局域网内快速点对点传文件”这个小需求上。与其花半小时找工具、注册账号、调权限不如直接写一个几十行的脚本用完就扔留着下次继续用也行。1.3 需求边界先做减法再做加法任何项目动手之前最值得做的一件事是把需求边界划清楚。我的 v1 版只做四件事把单个文件完整地从 A 机器传到 B 机器支持任意大小不做整文件内存加载而是流式读写传完以后能校验文件没有损坏给一个看得懂的进度反馈免得传 4GB 文件时心里没底。明确砍掉的是文件夹批量传输、压缩打包、断点续传、图形界面、多客户端并发、加密。这些功能确实有用但 v1 的目的只是打通一条可靠的通路。先把链路跑通再谈优化这是我做很多小工具的通用原则单点功能做到可信比铺开一堆半成品有意义得多。2. 方案选型与协议设计动手写代码前先想清楚的三件事2.1 选 TCP 还是 UDP文件传输第一个绕不开的决策是传输层协议。TCP 和 UDP 的差别用一句话说就是TCP 是挂号信UDP 是平信。挂号信有回执、有编号、丢了会重发、顺序不会乱平信扔出去就完了能不能收到全看运气。文件传输对可靠性的要求是刚性的文件是二进制数据中间丢一个字节、顺序乱一段整个文件就废了。所以我一开始就没考虑直接用 UDP 裸传。UDP 不是不能用像视频流、语音通话这类允许少量丢包的应用UDP 配合专门的协议才是主流方案游戏位置同步、直播推流也大量用 UDP。但文件传输需要逐字节完整复原TCP 已经把丢包重传、拥塞控制、流量控制这些脏活累活全处理好了是成本最低的路线。自己基于 UDP 做可靠传输不是不行要处理的事情非常多几乎等于把 TCP 已经解决了十几年的事情再重新发明一遍。对比如下对比维度TCPUDP可靠性有确认和重传数据完整有序不保证送达可能丢包、乱序传输效率有握手和拥塞控制链路利用率高无握手开销低适用场景文件、网页、数据库连接音视频流、游戏同步、日志上报实现复杂度直接用 socket 即可可靠传输需自实现一堆机制这张表看完选 TCP 基本没有悬念。真正需要花心思的是应用层的协议设计。2.2 应用层协议一条 JSON 头加上二进制流TCP 是字节流没有天然的“消息边界”这是新手最容易卡住的地方。你在客户端写了一次 sendall服务端并不能假定一次 recv 就能拿全反之文件数据可能被拆成很多个 TCP 分段到达也可能多个分段粘在一起。所以应用层必须自己约定一段数据从哪里开始、到哪里结束。我用的方案很朴素先用换行符作为分隔边界传一行 JSON 元数据紧接着传原始二进制文件流。JSON 里放三个字段filename文件名filesize文件大小字节数checksum文件的 SHA256 摘要这个设计有三个好处。第一JSON 无状态、可读调试时随手 print 就能看到双方约定了什么。第二以换行符结尾客户端发完头部后不用额外告诉服务端“头部有多长”服务端读到换行就停止。第三元数据和文件数据分离开头部用文本编码数据保持二进制原样互不干扰。有人会问为什么不把头部做成固定长度固定长度头部的问题是文件名一长就装不下短文件名又浪费带宽还要定义编码、处理字节对齐。JSON 加换行的方案在灵活性上明显胜出付出的代价不过是几十个字节的头部开销对 GB 级的文件传输来说可以忽略不计。2.3 分块读写与校验的必要性确定协议之后还要解决一个工程问题怎么处理大文件。如果直接把整个文件读进内存再 sendall一个 4GB 的文件就能把机器内存吃穿。正确做法是分块读写客户端每次从文件里读一小段通过 TCP 发出去服务端每次也只读一小段写入磁盘。我选的块大小是 64KB这是实测里比较稳的折中值。太小会导致系统调用开销变大、CPU 空转太大又会在内存里囤积过多数据遇到慢速网络时容易造成缓冲区堆积反而拖慢发送速度。校验方面我对每个文件发送前计算一次 SHA256把摘要放进头部服务端收完后对落盘文件重新计算 SHA256 做比对。文件传输最怕的不是速度慢而是“传完了但文件是坏的”。没有校验环节坏文件会带着假象进入下游工作流那才是真正的灾难。3. 核心代码实现服务端与客户端的完整落地3.1 服务端精确读字节是基本功服务端代码看起来不多但最核心的难点在一个函数recv_exact。Python 的 socket.recv(n) 语义是“尽力读最多 n 个字节”它可能只返回一小段。很多初学网络编程的人在这里踩坑写一个 recv(1024) 就以为对方发来的 1024 字节一次全收到了实际上可能只收到 200 字节。所以必须先实现一个保证“读满指定长度”的封装不断循环 recv直到凑够为止。# server.py import hashlib import json import socket HOST 0.0.0.0 # 监听所有网卡 PORT 8765 # 与客户端约定好的端口 CHUNK_SIZE 64 * 1024 # 64KB 分块 def recv_exact(conn, length): data b while len(data) length: packet conn.recv(length - len(data)) if not packet: raise ConnectionError(连接意外断开) data packet return data def recv_until_newline(conn): buf b while not buf.endswith(b\n): ch conn.recv(1) if not ch: raise ConnectionError(头部读取失败) buf ch return buf def handle_client(conn, addr): print(f[连接] {addr}) try: header recv_until_newline(conn) meta json.loads(header.decode(utf-8)) filename meta[filename] filesize meta[filesize] checksum meta[checksum] received 0 with open(filename, wb) as f: while received filesize: chunk recv_exact(conn, min(CHUNK_SIZE, filesize - received)) f.write(chunk) received len(chunk) print(f\r[接收] {received}/{filesize} bytes, end) print() local_checksum hashlib.sha256() with open(filename, rb) as f: while True: chunk f.read(CHUNK_SIZE) if not chunk: break local_checksum.update(chunk) if local_checksum.hexdigest() checksum: print(f[校验] {filename} SHA256 一致) else: print(f[校验] {filename} SHA256 不一致文件损坏) except Exception as e: print(f[错误] {addr} - {e}) finally: conn.close() def main(): srv socket.socket(socket.AF_INET, socket.SOCK_STREAM) srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) srv.bind((HOST, PORT)) srv.listen(5) print(f接收服务已启动监听 {PORT} 端口) while True: conn, addr srv.accept() handle_client(conn, addr) if __name__ __main__: main()这里有一个容易被忽略的细节接收文件时的 while 循环条件是 received filesize每次读取长度取 min(CHUNK_SIZE, filesize - received)。这样最后一个不完整的分块能被正确处理既不会多读也不会因为简单地把 CHUNK_SIZE 当固定值而读超。3.2 客户端发送头部、数据与进度客户端的逻辑和服务端镜像对称先算好元数据和校验值连接后发头部再流式发送文件数据。# client.py import hashlib import json import os import socket import sys CHUNK_SIZE 64 * 1024 def file_sha256(path): h hashlib.sha256() with open(path, rb) as f: while True: chunk f.read(CHUNK_SIZE) if not chunk: break h.update(chunk) return h.hexdigest() def send_file(server_ip, port, path): filesize os.path.getsize(path) filename os.path.basename(path) checksum file_sha256(path) client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((server_ip, port)) header json.dumps({ filename: filename, filesize: filesize, checksum: checksum, }) \n client.sendall(header.encode(utf-8)) sent 0 with open(path, rb) as f: while sent filesize: chunk f.read(CHUNK_SIZE) if not chunk: break client.sendall(chunk) sent len(chunk) print(f\r[发送] {sent}/{filesize} bytes, end) print() client.close() print(f[完成] {filename} 已发送SHA256: {checksum[:16]}...) if __name__ __main__: if len(sys.argv) ! 4: print(用法: python client.py 服务端IP 端口 文件路径) sys.exit(1) send_file(sys.argv[1], int(sys.argv[2]), sys.argv[3])客户端最需要记住的 API 是 sendall不是 send。send 不能保证一次把数据全部发出去它只返回实际发送的字节数而 sendall 内部会循环发送直到全部完成或者直接抛异常。可以把 sendall 理解成“发送端的 recv_exact”两边思路完全对称。进度条用 \r 回车符做原地刷新。注意 print 默认的 end 参数是换行这里手动改成空字符串让同一行被反复覆盖终端上看起来就是原地跳动的数字。3.3 校验闭环发送前算一次接收后算一次整个过程里同一个文件会被计算两次 SHA256。发送端在读取文件发送前算一次接收端在落盘后算一次只有两个值一致整个传输链路才算闭环。SHA256 的计算是流式的用 hashlib 对象反复 update 分块数据而不是把整个文件一次性塞进内存。这和文件传输本身的分块策略是配套的。对大文件来说一次性 update 和流式 update 结果完全一样但内存占用天差地别。这个习惯也延续到我后来写的所有工具里凡是处理大文件一律默认流式绝不做整文件载入。4. 实操全记录从启动到拿到第一个文件4.1 环境准备确认 IP 和端口动手之前要做两件小检查。第一确认两台机器在同一局域网能互相 ping 通第二查一下服务端机器的内网 IP。命令行里执行 ipconfig 或者 ifconfig 就能看到结果一般 192.168.x.x 开头的就是局域网的地址。端口需要两端约定一致。我随机挑了 8765避开常见的 80、443 和其他容易被占用的端口。如果 8765 被占换成 8000-9999 之间的任意值都可以只要两端一致就行。为了减少麻烦最好固定写好别临时起意改。4.2 开两个终端跑起来我用两台机器试一遍。第一台机器接收端先启动服务端python server.py 接收服务已启动监听 8765 端口第二台机器发送端执行python client.py 192.168.1.100 8765 ./big_log.tar.gz [发送] 2104534523/2104534523 bytes [完成] big_log.tar.gz 已发送SHA256: 3f6a9b...同一时刻接收端的终端会打印出连接来源、接收的字节数以及校验结果[连接] (192.168.1.100, 54321) [接收] 2104534523/2104534523 bytes [校验] big_log.tar.gz SHA256 一致整个过程没有任何多余依赖Python 自带的标准库就完成了全部功能。文件越大越能体会到这个方案比聊天软件和网盘省心的地方数据不绕路速度由局域网带宽决定。4.3 确认结果不只看日志还要亲手验证程序打印校验一致不代表万事大吉我还会在接收端额外做一次文件对比。最朴素的办法是看文件大小是否一致更稳妥的是手工算一次哈希sha256sum big_log.tar.gz如果两边独立算出的值一样传输才算终极闭环。这一步是我的个人习惯宁可多花几秒钟验证也不把错误带进后面的分析流程。真实工作里文件损坏引发的排查成本远高于这几次校验的耗时。5. 常见问题与排查实录5.1 文件传一半就卡住进度数字不动第一次拿真实的大文件测试时我撞上过进度条卡住的情况。排查下来有两个常见原因。第一个原因是发送端把 sendall 想当然了以为一次调用就能把全部数据推给内核。实际上 TCP 的发送窗口和接收端的接收窗口是动态的网络拥堵时 sendall 内部会阻塞等待这是正常现象不代表程序死了需要多一点耐心观察。第二个原因才是真正的环境问题如果两边用的是无线网络信号差或者信道干扰会让 TCP 重传退避大文件的尾部经常出现长时间无进展。这个不是代码能根治的优先建议换有线连接测试。同样的文件有线网络跑和无线网络跑体感差非常多。还有一个常见误操作是服务端写 recv(filesize) 想一次性读完整个大文件。TCP 不是这么工作的超大参数的 recv 一样可能只返回一部分数据丢了数据就会卡在读不满的循环里。必须走 recv_exact 的循环封装。5.2 中文文件名保存出来是乱码第一次跨环境传文件接收端保存的中文文件名显示成乱码。原因很简单源系统默认以本地编码处理文件名目标系统以 UTF-8 解码 JSON两边约定不一致。解决办法是把文件名强制统一。客户端发送前用 os.path.basename 拿到名字后直接放进 JSON 字符串不要做任何额外的手工编码转换服务端读取头部时统一用 UTF-8 解码。JSON 本身支持 Unicode只要两端都用 UTF-8 来编解码整个头部中文名就不会出问题。顺带提醒源文件和目标文件的存储路径也尽量保持在同样的编码环境里避免后续文件系统层面的二次乱码。5.3 接收端保存出来的文件比源文件小这个现象通常意味着文件数据没有完全到达。常见原因有两个。一是 recv_exact 里没有处理连接异常的情况。对方中途断开会返回空字节如果检测不到就会陷入死循环或者提前返回不完整的数据。解决方式是每次读到空字节都抛异常宁可失败也要让调用方知道链路断了。二是客户端在发完文件后立刻调用 close()而 TCP 是全双工的close 之前必须保证所有数据都已经成功写入内核缓冲区。正常情况下 sendall 完成后 close 没有问题但如果中间有任何一步出错被吞掉文件就会悄悄少一截。我的建议是客户端要把每个系统调用的异常都暴露出来别静默吞掉错误。“传错了”不可怕可怕的是“传着传着悄悄不吭声”。5.4 报错端口被占用服务端第二次启动时报 address already in use但明明上一个进程已经退出了。原因是 TCP 连接关闭后端口会进入 TIME_WAIT 状态过一会儿才彻底释放。解决办法是在服务端代码里加一行srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)这行设置允许端口在 TIME_WAIT 期间内被重用开发调试阶段几乎必加。不加这行每次重启服务端都可能要等几十秒体验非常差。5.5 防火墙拦截客户端 connect 超时局域网环境下最常见的连接失败原因是防火墙。服务端程序首次监听端口时系统会弹出网络权限提示如果没放行客户端的 connect 会一直超时。排查方法是先在本机测试回环地址 127.0.0.1确认服务端本身没问题然后从客户端机器上测试一下服务端的 IP 和端口telnet 192.168.1.100 8765能通说明网络路径没问题不通就要去检查防火墙规则。在受控的实验网络里测试时可以先临时放行对应端口跑通后再把规则细化。把这一阶段踩过的坑汇总成一张表方便以后快速定位现象可能原因快速解法connect 超时防火墙拦截、IP 错误、端口不一致本机 127.0.0.1 测试再 telnet 对端端口文件小一截recv 循环不完整 / 异常被吞用 recv_exact 并抛出连接异常中文名乱码编码约定不一致头部统一 UTF-8二次启动报错端口 TIME_WAIT加上 SO_REUSEADDR进度条长时间不动无线网络不稳定、TCP 退避换有线网络再试这五行是我踩坑经历的浓缩版每次换新环境部署这个脚本我都会先扫一遍再做大面积传输。6. 优化方向这版做完还能怎么改6.1 多客户端并发接收现在的服务端是串行处理的一个连接没处理完后面的连接都要排队。改进方式非常直接把 handle_client 丢进线程池就行from concurrent.futures import ThreadPoolExecutor executor ThreadPoolExecutor(max_workers4) while True: conn, addr srv.accept() executor.submit(handle_client, conn, addr)注意两点。一是文件写入路径要避免并发冲突不同客户端传同名文件时最好加会话前缀或时间戳。二是线程池的大小不要盲目开大文件传输属于 IO 密集型任务4 到 8 个线程已经非常够用开太多反而增加调度开销。6.2 断点续传断点续传是文件传输工具里最实用的进阶功能。实现思路是客户端先向服务端发一个查询请求探测目标目录里是否已有同名文件、已有多少字节服务端返回已有长度后客户端从源文件的偏移位置开始读取头部带上 offset 字段服务端则把文件打开方式改成 rb从已有位置继续写。这里有个隐性问题只知道大小并不能证明已有的内容是完整的。所以断点续传通常要配合分块校验比如把文件切成固定大小的块逐块计算哈希服务端比对后只请求缺失或损坏的那部分。这个复杂度比 v1 大很多但也正是真正生产环境的传输工具必备的能力。6.3 自动发现对端设备现在还需要手工填服务端 IP在地址经常变化的办公网里确实不够方便。可以加一个 UDP 组播发现机制服务端启动时周期性地向约定好的组播地址发一条“我在这里端口 8765”的广播消息客户端在局域网监听这个消息就能自动列出可用的接收端。注意这个 UDP 组播只是做发现真正传文件仍然走 TCP。把“控制面”和“数据面”分开是网络软件里很常见的设计思想控制面要求轻、快、容错好数据面要求稳、准、不丢。这个思路在很多系统里都适用。6.4 加密传输如果文件要穿越公网或者走不受信的网络建议在 TCP 之上再套一层加密。Python 标准库里的 ssl 模块可以把现有 socket 包装成 TLS 连接改造量不大服务端用 SSLContext 加载证书客户端用同样的上下文连接即可。唯一麻烦的是证书管理小规模场景可以考虑自签证书加双方提前交换证书指纹用指纹做身份校验防止中间人攻击。加密会带来一定性能损耗但在公网传输场景下数据安全远比速度重要。局域网内部使用可以不开跨网络部署建议默认打开。最后再说一点我在这个项目里最深的体会工具类软件的价值不在于功能堆得有多全而在于它在关键时刻能稳定地完成一件小事。我给这个传输脚本保留了最朴素的命令行界面没有任何花活但之后每次需要跨机器传几个 GB 的文件时它都稳稳地完成了任务从不掉链子。如果你也想写一个类似的小工具我的建议是第一次动手就把“异常处理”和“校验闭环”想清楚。这两个东西在 demo 里可以省掉在真实的文件传输里却会直接决定工具可不可信。代码写得再漂亮传出来的文件是坏的一切都是白费。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。