最近有个小需求:把本机上的资料临时共享给外地的同事,但手头没有云服务器,办公室也没有公网IP。以前遇到这种情况我都是直接把文件丢到某网盘,可一旦文件零散、数量多,网盘打包下载要么限速,要么链接隔天就失效,折腾下来比干活还累。后来我改用 Flask 写了个简易文件服务器,再用 NATAPP 做内网穿透,把本机 8000 端口映射到公网,别人打开一个链接就能直接下载,临时上传也可以。整个过程从零搭建不到半小时,成本为零,实测稳定跑了好几天。
这篇文章就把完整方案记录下来:Flask 怎么实现文件列表和下载、NATAPP 怎么配置隧道、内外网如何打通,以及我在实际调试中踩过的那些坑和最终的优化建议。适合有基础 Python 知识、想快速搭建个人/团队临时文件共享服务的开发者参考。如果你是第一次接触内网穿透,跟着步骤走也能顺利跑通。
1. 为什么是 Flask + NATAPP 这套组合
1.1 没有公网 IP 时的常见方案怎么选
做开发的朋友应该都有这种经历:本地服务跑得好好的,一要给外部的人演示就卡住了。没公网 IP、没云服务器、公司内网又不让随便映射端口,临时需求又不能走一遍申请流程。这时候能选的路其实就那么几条:
- 租一台云服务器,把服务部署上去,走公网访问。最正规但最重,一台最低配的轻量服务器一年也要几百块,加上备案、域名、安全加固,为了传几个文件实在不划算。
- 借用办公室已有的公网出口做端口映射。需要找网管开权限,很多公司根本不允许。
- 用网盘中转,前面说了,大量零散文件体验很差。
- 用内网穿透工具,把本地端口映射成一个公网地址。成本最低、部署最快,适合临时任务和个人项目调试。
内网穿透这条路里,实现方式又可以分成两类:一类是自己买一台有公网 IP 的服务器,部署 frp 之类的反向代理工具;另一类是直接用现成的内网穿透平台,客户端配置好 authtoken 就能跑。前者更灵活,但前提是“有服务器”,这又绕回了成本问题。后者省事,对以文件下载为核心诉求的场景完全够用。
我的选择是 NATAPP。原因很直接:它是国内访问比较稳定的在线穿透服务,免费隧道对新用户友好,一条命令就能把本地端口暴露出去,不需要自己维护服务器,配置过程十分钟内完成。当然它不是唯一选择,网上类似的服务不少,但只要搞懂隧道、authtoken 这些核心概念,换任何一家都能快速上手。
1.2 文件传输用 Flask 重不重
有人可能会问:既然只是传文件,Python 自带的http.server不就行了吗?一条python -m http.server 8000命令就能起一个文件服务,何必再装 Flask?
我实际对比过,http.server确实能应急,但它有两个问题:一是页面样式和文件排序非常朴素,多文件场景下浏览不方便;二是不太好扩展。万一后面要加上传、加访问密码、限制文件类型、记录下载日志,在http.server里折腾这些得写不少底层代码。而 Flask 天生就是干这个的:路由清晰、模板简单、扩展生态成熟,写一个文件服务总共不超过 50 行代码,逻辑却可以做得比较完整。
另一个考虑是开发调试体验。Flask 自带调试模式,改了代码自动重载,配合内网穿透时本地终端能实时看到访问日志,方便排查问题。对于一个要长期“跑着”的临时服务来说,这种可维护性比裸写标准库舒服很多。
1.3 内网穿透到底穿透的是什么
理解 NATAPP 的原理,对后面排错很有帮助。它做的事可以这样理解:本地服务监听在127.0.0.1:8000,NATAPP 客户端和它的云端服务器建立一条长连接,云服务器对外提供一个公网地址,所有到达这个公网地址的请求,都会被通过这条长连接转发到本地端口的服务上。
说白了,就是“借用”云端服务器的公网 IP,把本地的服务“挂”到上面去。免费隧道一般给的是一个随机域名,访问者打开域名,请求走 NATAPP 云服务器再经隧道转回本机,整个链路就这样打通了。
这个方案的核心价值在于:访问者并不直接连接你的本机,本机也不需要公网 IP,一条出站连接即可,所以在大部分网络环境下都能工作。需要注意,内网穿透是开发调试、个人项目演示、临时文件共享的常见工具,用途要正当,别拿来做违反平台规则的事。下面所有操作都以合法合规的日常开发场景为前提。
2. 动手前的准备和项目结构规划
2.1 环境要求
- Python 3.8 及以上版本。
- pip 包管理器。
- 一个 NATAPP 账号,用于获取 authtoken。
强烈建议用虚拟环境装 Flask,避免把包装到系统 Python 里污染环境。打开终端执行:
mkdir flask-file-server cd flask-file-server python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install flask装完执行python -c "import flask; print(flask.__version__)",能输出版本号说明环境没问题。
2.2 目录结构设计
我的目录结构很简单,但每一层都有明确职责:
flask-file-server/ ├── app.py # Flask 入口和全部路由 ├── files/ # 存放共享文件的地方 └── templates/ # 可选,不复杂时可以不用files目录需要在启动前手动建好,或者让代码自动创建。我推荐代码里写自动创建逻辑,避免每次换环境部署都忘掉。
import os BASE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "files") os.makedirs(BASE_DIR, exist_ok=True)这样不管从哪个目录启动脚本,BASE_DIR永远指向项目下的files文件夹,不会出现“明明文件放进去了可页面看不到”的尴尬。
2.3 选 NATAPP 免费隧道还是付费隧道
NATAPP 的免费隧道和付费隧道的核心差别在于域名和稳定性。免费隧道域名每次启动会变化,适合短期应急;付费隧道可以绑定自定义域名,域名固定、带宽和稳定性更有保障,适合需要长时间对外提供的服务。
如果只是给同事传一两天文件,免费隧道可以完成任务。但要注意免费隧道有流量和带宽限制,下载大文件时会比较慢。真要长期用,建议花点小钱升级,或者直接用自己申请的免费域名解析搭 frp。
我的建议是:先免费跑通流程,确认这套方案符合需求,再按需付费。没必要一上来就买,等实验成功了再说。
3. Flask 文件服务器核心代码实现
3.1 路由设计
整个服务只需要三个核心路由:首页展示文件列表、下载文件、上传文件(可选项)。
/:读取files目录下的所有文件,渲染成 HTML 列表。/download/<文件名>:触发浏览器下载。/upload:处理 POST 请求,保存用户上传的文件到files目录。
看似简单,但实际写的时候有很多细节会影响安全和稳定性,逐个说。
3.2 完整代码及说明
下面是我实跑通过的版本,加了文件大小显示和中文文件名处理:
import os from flask import Flask, render_template_string, send_from_directory, abort, request, redirect, url_for from werkzeug.utils import secure_filename app = Flask(__name__) BASE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "files") os.makedirs(BASE_DIR, exist_ok=True) ALLOWED_EXTENSIONS = {"txt", "pdf", "png", "jpg", "jpeg", "zip", "tar", "gz", "docx", "xlsx"} def allowed_file(filename): return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS def human_size(num): for unit in ["B", "KB", "MB", "GB"]: if num < 1024: return f"{num:.1f} {unit}" num /= 1024 return f"{num:.1f} TB" @app.route("/") def index(): files = [] for name in os.listdir(BASE_DIR): full_path = os.path.join(BASE_DIR, name) if os.path.isfile(full_path): files.append({ "name": name, "size": human_size(os.path.getsize(full_path)), "mtime": os.path.getmtime(full_path), }) files.sort(key=lambda x: x["mtime"], reverse=True) html = """ <!doctype html> <title>文件共享</title> <h1>文件列表</h1> <form method="post" action="/upload" enctype="multipart/form-data"> <input type="file" name="file" required> <button type="submit">上传</button> </form> <ul> {% for f in files %} <li> <a href="/download/{{ f.name }}">{{ f.name }}</a> ({{ f.size }}) </li> {% endfor %} </ul> """ return render_template_string(html, files=files) @app.route("/download/<path:filename>") def download(filename): # 使用 basename 防止目录穿越 safe_name = os.path.basename(filename) return send_from_directory(BASE_DIR, safe_name, as_attachment=True) @app.route("/upload", methods=["POST"]) def upload(): file = request.files.get("file") if file and allowed_file(file.filename): filename = secure_filename(file.filename) file.save(os.path.join(BASE_DIR, filename)) return redirect(url_for("index")) abort(400) if __name__ == "__main__": app.run(host="0.0.0.0", port=8000)3.3 代码里的几个关键细节
第一,os.path.basename这个处理不能省。如果不做这一步,攻击者构造一个/download/../../etc/passwd这样的路径,send_from_directory可能会把服务器上任意文件暴露出去。加上basename后,路径被截断到只看最后一级,目录穿越直接失效。这是我给所有文件服务类项目加的第一道保险。
第二,secure_filename在上传时很重要。它会把中文名、特殊字符处理成安全的 ASCII 文件名,比如“项目报告2025.docx”可能变成“xiang-mu-bao-gao2025.docx”。有点丑,但胜在安全。如果不喜欢改名,可以用werkzeug.utils的另一个思路:只保留原始文件名里的中文字符和白名单字符,但这个实现起来要更小心,建议新手先用secure_filename保平安。
第三,host="0.0.0.0"是必须的。如果写成默认的127.0.0.1,NATAPP 隧道连接进来时访问的是本机回环地址,网络层会直接把请求拒掉,穿透基本等于失效。这一点排错时最容易忽略,后面问题排查部分会再强调。
第四,as_attachment=True让浏览器以附件形式下载文件,而不是在标签页里直接打开文本或图片。如果是想在线预览,去掉这个参数即可。
3.4 上传功能要不要加
我在列表页加了上传入口,方便别人不光是下载,还能把文件传回来。但要注意,给任何人都开放上传,意味着任何人都能往你的服务器写文件,这在公网环境下是有风险的,很容易被塞入垃圾文件,甚至被用来挂恶意内容。
所以我在代码里做了两层限制:一是只允许特定扩展名,二是文件类型不在白名单里直接返回 400。如果服务要对公网开放更长时间、更多人使用,建议再加一层简单密码认证,这个留到第 6 章的扩展部分说明。
4. NATAPP 隧道配置与内外网打通实操
4.1 注册并获取 authtoken
NATAPP 的使用第一步是注册账号,注册后进入后台,在个人信息或令牌管理页面可以找到自己的 authtoken。这是一串类似 UUID 的字符串,相当于你登录 NATAPP 客户端的凭证,不要泄露给别人。
获取到 authtoken 后,下载对应操作系统的客户端压缩包并解压。不同系统运行文件不一样,Windows 上是natapp.exe,macOS/Linux 上是一个可执行二进制文件。放到项目同级目录下方便记。
4.2 创建隧道并分配本地端口
在 NATAPP 后台的“隧道管理”页新建隧道,需要填几个关键参数:
| 参数 | 我的配置 | 说明 |
|---|---|---|
| 隧道类型 | HTTP | 选 HTTP 方便浏览器直接访问,也方便在后台看网络请求日志 |
| 本地地址 | 127.0.0.1 | NATAPP 客户端所在机器,也就是运行 Flask 的本机 |
| 本地端口 | 8000 | 必须和 Flask 的app.run(port=8000)保持一致 |
| 绑定域名 | 留空 | 免费隧道会分配随机域名 |
这里有一个容易忽略的点:本地地址填127.0.0.1,但 Flask 的host要设置成0.0.0.0,两者不矛盾。NATAPP 客户端连接的是本机的回环地址,因为它就在本机运行;而 Flask 监听0.0.0.0是为了让所有本机网络接口上的连接都能进得来,包括 NATAPP 客户端发起的回环连接和其他局域网请求。这两层搞明白了,网络配置基本不会出错。
4.3 启动和验证
先启动 Flask:
python app.py看到Running on http://0.0.0.0:8000说明服务起来了。接着在浏览器打开http://127.0.0.1:8000,确认本地能访问,能看到文件列表页面。
然后启动 NATAPP 客户端:
./natapp -authtoken=你的authtokenWindows 下是:
natapp.exe -authtoken=你的authtoken启动成功后终端会显示一个映射的公网地址,类似http://xxxx.natappfree.cc。把这个地址发给同事,对方就能打开文件列表页面直接下载了。
我在实测中遇到过一个问题:NATAPP 已经显示在线,访问公网地址却一直转圈。后来发现是 Windows 防火墙默认拦截了 NATAPP 客户端的入站/出站连接,在防火墙设置里放行它的网络权限立刻就好了。macOS 和 Linux 下基本没有这个问题。
4.4 一个必看的验证小技巧
为了确认文件传输链路完整无损,建议第一次跑通后传一个稍大的文件(比如 50MB 以上),下载下来对比哈希:
# 在本地和对方机器分别计算 md5sum 文件名如果两边哈希一致,说明 NATAPP 穿透链路没有额外丢包或改包问题,可以放心用。这个习惯我每次搭完穿透服务都会做,别嫌麻烦,一次验证能挡掉后面无数个问号。
5. 常见问题与排查技巧实录
5.1 公网打不开页面,但本地正常
优先检查 NATAPP 客户端日志。如果显示Tunnel established说明隧道通了,问题大概率出在防火墙;如果显示连接失败,检查 authtoken 是否正确、网络能否访问 NATAPP 官方域名。按优先级排查:
- Flask 启动时绑定的是不是
0.0.0.0。 - NATAPP 隧道的本地端口是不是 8000。
- 本机系统防火墙有没有放行 NATAPP 客户端。
- 企业办公网络是否封禁了 NATAPP 使用的协议和端口。
5.2 下载文件名中文乱码
send_from_directory在返回Content-Disposition头时,浏览器对中文文件名的解析存在历史兼容问题。最稳妥的办法是给文件名加 URL 编码:
from urllib.parse import quote filename = quote(safe_name) response = send_from_directory(BASE_DIR, safe_name, as_attachment=True) response.headers["Content-Disposition"] = f"attachment; filename*=UTF-8''{filename}"用filename*=UTF-8''是 RFC 标准做法,现代浏览器都认,也不会被中间环节错误转码。
5.3 大文件下载中断、速度慢
免费隧道受带宽限制,几十 MB 以上的文件传输速度确实起伏。排查思路是:先下载一次看是否每次都断在同一个位置,如果是,大概率是隧道出口带宽问题;如果随机断,则可能是本地网络不稳定,NATAPP 客户端默认 TCP 长连接被重置。
缓解手段有两个:一是把大文件压缩分卷,比如拆成 100MB 一份再传到files目录;二是升级到付费隧道获得更高带宽。实在不想花钱,可以用aria2这种多线程下载工具配合断点续传,体验会好一些。
5.4 上传返回 400 的几种原因
上传报 400 基本都是allowed_file拦截:
- 文件没有扩展名,被拦。
- 文件类型不在白名单,比如传了一个
.apk。 - 文件名为全中文,
secure_filename处理之后变成空字符串,导致保存路径为空。
第三种情况最常见,也最坑。解决办法是在保存前判断filename是否为空,为空就自动生成一个带时间戳的默认文件名。
import time if not filename: filename = f"upload_{int(time.time())}.dat"另外,request.files.get("file")取到的file.filename可能是浏览器原始文件名,也可能是空字符串。空字符串同样需要兜底处理。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 本地正常,公网打不开 | 隧道未建立或防火墙拦截 | 检查 NATAPP 日志、放行客户端 |
| 下载的文件损坏 | 中间链路断开重传 | 对比哈希,换有线网络或提升带宽 |
| 中文名文件名变成乱码 | Content-Disposition 编码问题 | 按filename*=UTF-8''方式设置响应头 |
| 上传后列表不显示 | 保存目录不存在或权限不足 | 确认BASE_DIR存在,且有写权限 |
| 服务一启动就崩溃 | 8000 端口被占用 | 执行lsof -i:8000查看占用进程,换端口或结束进程 |
| 手机访问排版错乱 | 页面没有做响应式布局 | 加一段简单 viewport meta,或换 Bootstrap |
6. 进阶扩展:让文件服务更可靠更安全
6.1 加简单登录认证
文件服务器一暴露到公网,总会有爬虫和扫描器来敲门。我后来给这个服务加了一道最简单的认证:在 Flask 里实现一个 Basic Auth 中间件。
from functools import wraps from flask import request, Response USERNAME = "admin" PASSWORD = "换成你自己的密码" def check_auth(username, password): return username == USERNAME and password == PASSWORD def requires_auth(f): @wraps(f) def decorated(*args, **kwargs): auth = request.authorization if not auth or not check_auth(auth.username, auth.password): return Response( "需要认证", 401, {"WWW-Authenticate": "Basic realm='Login Required'"} ) return f(*args, **kwargs) return decorated然后在首页和上传路由上挂@requires_auth装饰器即可。浏览器的表现是打开链接弹一个账号密码框,简单直接。缺点是不适合机器对机器的访问场景,但有总比裸奔强。
6.2 日志和访问统计
NATAPP 后台已经提供简单的请求记录,但如果想看更详细的信息,比如谁在什么时间下载了哪个文件,可以在 Flask 里自己记一份日志。写一个装饰器或用after_request钩子,把时间、IP、路径、状态码写进 CSV 文件,几百行代码的改动却能带来完整的可追溯性。对内部共享场景来说,这个信息比想象中有用,至少能看出文件到底有没有被人下载走。
6.3 HTTPS 和断点续传
免费 NATAPP 隧道自带一个随机域名的 HTTPS 地址,浏览器访问时证书是有效的,这点比自建 frp 方便。不用额外配置,直接把公网地址里的http换成https试试即可。
关于断点续传,send_from_directory底层基于 Flask 的静态文件处理机制,实际上支持Range请求头,也就是下载工具和多线程下载器可以正常断点续传。如果发现某个浏览器下大文件下载总是失败,多半是浏览器自身对下载管理的限制,换下载工具反而更稳。
6.4 万一 NATAPP 宕机了怎么办
任何第三方服务都有不可用的可能。我在一次分享文件时遇到过 NATAPP 临时维护,那个随机域名直接打不开。后来留了个心眼:重要文件共享时,同时准备一条备用穿透通道,比如用另一家平台的临时隧道,或者干脆把files目录打成包传到网盘作为兜底。
不要把鸡蛋放在一个篮子里。对于临时文件共享这种需求,永远有个秒级可用的备选方案,才是真正从容。
做了几次之后,我个人最大的体会是:内网穿透类工具的价值不只是“把内网服务发出去”,而是把本地开发的自由度又拉回来了一截。很多临时想到的小工具、小页面,不需要先买服务器、走流程,本地写好,一条隧道就能让需要的人用上。这个 Flask 文件服务后来被我改了很多个版本,加过 Web 界面美化、加过上传校验、加过下载限速,但核心思路始终没变:能少依赖外部资源就少依赖,能快速上线就快速上线。如果你也需要在临时场景下分享文件,照这套方案走一遍,大概率不会失望。