第4章 4.3 静态文件服务
上一章我们学会了路由,知道如何根据不同 URL 返回不同内容。但你有没有想过:当你在浏览器输入
http://localhost:3000/index.html时,服务器是怎么找到并返回那个.html文件的?
这,就是静态文件服务要解决的问题。
🎯 开场 3 分钟:为什么要学这个?
场景还原:你写了一个页面 index.html,里面有 CSS 样式和 JavaScript 脚本。你双击打开它,浏览器显示正常。但当你用 http://localhost:3000/index.html 访问时——样式没了,脚本也报错了。
为什么?因为浏览器请求 style.css 时,它实际在请求 http://localhost:3000/style.css。如果你的服务器没"告诉"浏览器去哪里找这个文件,浏览器就找不到了。
痛点:
- 静态资源(图片、CSS、JS)返回 404
- 不知道怎么处理 MIME 类型(浏览器不知道这是图片还是文本)
- 大文件传输慢,不知道怎么优化
学完本文你能:搭建一个能自动serve图片、CSS、JS文件的服务器,还能玩点缓存和断点续传的骚操作。
🧱 基础 25 分钟:核心概念
4.3.1 什么是静态文件服务?
生活类比:想象你开了一家快递站。用户说"帮我取个包裹",你需要知道:
1. 包裹放在哪个架子上(文件路径)
2. 这个包裹是什么类型(易碎品?文件?生鲜?)—— MIME 类型
3. 怎么打包给用户(流式传输 vs 整个加载)
静态文件服务就是:服务器收到请求后,去服务器磁盘上找到对应文件,读取它,返回给客户端。
4.3.2 最简单的文件服务
import http.server
import socketserver
PORT = 8000
# 直接使用 Python 内置的 SimpleHTTPRequestHandler
# 它会自动把当前目录的文件 serve 出去
Handler = http.server.SimpleHTTPRequestHandler
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"服务器运行在 http://localhost:{PORT}")
httpd.serve_forever()
保存为 server.py,运行后浏览器访问 http://localhost:8000,你会看到当前文件夹里的所有文件。
一行解释:SimpleHTTPRequestHandler 是 Python 自带的静态文件处理器,你不需要自己写读取文件、判断类型这些脏活累活。
4.3.3 手动实现:返回任意文件
有时候你想更灵活地控制,比如限制只能访问某个目录:
import http.server
import os
from pathlib import Path
class CustomHandler(http.server.SimpleHTTPRequestHandler):
# 设置 serve 的根目录
base_directory = "/Users/apple/static_files"
def translate_path(self, path):
"""把 URL 路径转成真实文件路径"""
# 去掉开头的 /
path = path.split('?', 1)[0]
path = path.split('#', 1)[0]
path = path.lstrip('/')
# 拼接成完整路径
return os.path.join(self.base_directory, path)
def do_GET(self):
"""处理 GET 请求"""
# 获取请求的文件路径
filepath = self.translate_path(self.path)
if os.path.isfile(filepath):
# 文件存在,读取并返回
try:
with open(filepath, 'rb') as f:
content = f.read()
self.send_response(200)
# 自动识别 MIME 类型
self.send_header('Content-type', self.guess_type(filepath))
self.send_header('Content-Length', len(content))
self.end_headers()
self.wfile.write(content)
except Exception as e:
self.send_error(500, str(e))
else:
# 文件不存在,返回 404
self.send_error(404, "File not found")
if __name__ == "__main__":
PORT = 8080
with socketserver.TCPServer(("", PORT), CustomHandler) as httpd:
print(f"静态文件服务启动:http://localhost:{PORT}")
httpd.serve_forever()
核心流程图:

4.3.4 MIME 类型是什么?
生活类比:快递员看到"生鲜"标签就知道要用冷链车,看到"易碎"就知道要轻拿轻放。MIME 类型就是文件的"标签",告诉浏览器怎么解析这个文件。
常见 MIME 类型:
| 文件类型 | MIME 类型 | 例子 |
|---|---|---|
| HTML | text/html | .html |
| CSS | text/css | .css |
| JavaScript | application/javascript | .js |
| JSON | application/json | .json |
| 图片 PNG | image/png | .png |
| 图片 JPG | image/jpeg | .jpg |
| application/pdf |
Python 的 mimetypes 模块能自动识别:
import mimetypes
# 注册一些额外的类型
mimetypes.add_type('application/javascript', '.js')
filepath = "/path/to/style.css"
mime_type, _ = mimetypes.guess_type(filepath)
print(mime_type) # 输出: text/css
4.3.5 流式传输:大文件不用一次性读入内存
痛点:一个 1G 的视频文件,如果一次性读入内存,内存直接爆掉。正确做法是流式传输——像水管一样,一段一段地送。
import http.server
import os
class StreamingHandler(http.server.SimpleHTTPRequestHandler):
def stream_file(self, filepath):
"""流式传输文件,不占用大量内存"""
file_size = os.path.getsize(filepath)
self.send_response(200)
self.send_header('Content-Type', self.guess_type(filepath))
self.send_header('Content-Length', file_size)
self.end_headers()
# 分块读取,每次读 8KB
chunk_size = 8 * 1024
with open(filepath, 'rb') as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
self.wfile.write(chunk)
def do_GET(self):
filepath = self.translate_path(self.path)
if os.path.isfile(filepath):
self.stream_file(filepath)
else:
self.send_error(404)
关键点:不是 content = f.read() 一次性读完,而是用循环 chunk = f.read(8192) 分块读,边读边发。
🔥 实战 35 分钟:3 个递进项目
项目 1(5 分钟):一个能显示图片的相册服务器
需求:搭建一个服务器,能浏览文件夹里的图片。
完整代码:
#!/usr/bin/env python3
import http.server
import os
import socketserver
from pathlib import Path
PORT = 9000
PHOTO_DIR = "./photos"
# 确保目录存在
os.makedirs(PHOTO_DIR, exist_ok=True)
class PhotoHandler(http.server.SimpleHTTPRequestHandler):
# 自定义根目录
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=PHOTO_DIR, **kwargs)
if __name__ == "__main__":
print(f"📷 相册服务器启动!")
print(f" 把图片放到 ./photos 文件夹")
print(f" 访问 http://localhost:{PORT}")
with socketserver.TCPServer(("", PORT), PhotoHandler) as httpd:
httpd.serve_forever()
预期输出:
📷 相册服务器启动!
图片放到 ./photos 文件夹
问 http://localhost:9000
把几张 .jpg 或 .png 图片扔进 photos 文件夹,刷新浏览器就能看到缩略图列表。
项目 2(15 分钟):带缓存控制的资源服务器
需求:网站图片更新了,但用户看到的是旧图(缓存问题)。实现一个带缓存控制的服务器。
完整代码:
#!/usr/bin/env python3
import http.server
import os
import time
import hashlib
from datetime import datetime, timedelta
PORT = 9001
STATIC_DIR = "./static"
os.makedirs(STATIC_DIR, exist_ok=True)
class CacheableHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=STATIC_DIR, **kwargs)
def do_GET(self):
filepath = self.translate_path(self.path)
if not os.path.isfile(filepath):
self.send_error(404)
return
# 生成文件的 ETag(基于修改时间和大小)
stat = os.stat(filepath)
etag = hashlib.md5(f"{stat.st_mtime}{stat.st_size}".encode()).hexdigest()
# 检查浏览器是否带了 If-None-Match(缓存验证)
client_etag = self.headers.get('If-None-Match')
if client_etag == etag:
# 文件没变,告诉浏览器用缓存
self.send_response(304)
self.end_headers()
return
# 文件有变化,返回新内容
try:
with open(filepath, 'rb') as f:
content = f.read()
self.send_response(200)
self.send_header('Content-Type', self.guess_type(filepath))
self.send_header('Content-Length', len(content))
self.send_header('ETag', etag)
# 缓存 1 小时
self.send_header('Cache-Control', 'max-age=3600')
self.send_header('Last-Modified', self.date_time_string(stat.st_mtime))
self.end_headers()
self.wfile.write(content)
except Exception as e:
self.send_error(500, str(e))
if __name__ == "__main__":
print(f"🚀 带缓存的静态服务器启动!")
print(f" 资源目录: ./{STATIC_DIR}")
print(f" 访问 http://localhost:{PORT}")
with socketserver.TCPServer(("", PORT), CacheableHandler) as httpd:
httpd.serve_forever()
关键代码解读:
- ETag:文件的"指纹",文件变了指纹就变
- Cache-Control: max-age=3600:告诉浏览器缓存 1 小时
- 304 Not Modified:文件没变,浏览器直接用本地缓存

项目 3(15 分钟):断点续传的多线程下载服务器
需求:下载一个大文件,中途断了不用从头开始(断点续传)。
完整代码:
#!/usr/bin/env python3
import http.server
import os
import socketserver
PORT = 9002
DOWNLOAD_DIR = "./downloads"
os.makedirs(DOWNLOAD_DIR, exist_ok=True)
class RangeHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=DOWNLOAD_DIR, **kwargs)
def do_GET(self):
filepath = self.translate_path(self.path)
if not os.path.isfile(filepath):
self.send_error(404)
return
file_size = os.path.getsize(filepath)
# 检查是否请求部分内容(Range 请求)
range_header = self.headers.get('Range')
if range_header:
# 解析 Range: bytes=start-end
try:
range_spec = range_header.replace('bytes=', '')
start_str, end_str = range_spec.split('-')
start = int(start_str) if start_str else 0
end = int(end_str) if end_str else file_size - 1
# 确保范围合法
if start >= file_size or end >= file_size:
self.send_error(416, "Range Not Satisfiable")
return
self.send_response(206) # Partial Content
self.send_header('Content-Type', self.guess_type(filepath))
self.send_header('Content-Length', end - start + 1)
self.send_header('Content-Range', f'bytes {start}-{end}/{file_size}')
self.send_header('Accept-Ranges', 'bytes')
self.end_headers()
with open(filepath, 'rb') as f:
f.seek(start)
remaining = end - start + 1
chunk_size = 8192
while remaining > 0:
to_read = min(chunk_size, remaining)
chunk = f.read(to_read)
self.wfile.write(chunk)
remaining -= len(chunk)
except Exception as e:
self.send_error(400, str(e))
else:
# 完整文件下载
with open(filepath, 'rb') as f:
content = f.read()
self.send_response(200)
self.send_header('Content-Type', self.guess_type(filepath))
self.send_header('Content-Length', file_size)
self.send_header('Accept-Ranges', 'bytes')
self.end_headers()
self.wfile.write(content)
if __name__ == "__main__":
print(f"📥 支持断点续传的下载服务器!")
print(f" 把要分享的文件放到 ./{DOWNLOAD_DIR}")
print(f" 访问 http://localhost:{PORT}")
with socketserver.TCPServer(("", PORT), RangeHandler) as httpd:
httpd.serve_forever()
断点续传原理:浏览器下载到一半断了,再请求时带上 Range: bytes=500000- 告诉服务器"我从第 50 万字节开始要"。服务器返回 206 Partial Content,只发这一段。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:路径穿越漏洞 ❌ → ✅
错误示例(危险!):
# 用户请求 /../../etc/passwd 会读取到系统文件!
filepath = os.path.join(self.base_directory, self.path)
正确示例:
from pathlib import Path
def safe_path(self, url_path):
# 使用 Path 解析并确保在基础目录内
base = Path(self.base_directory).resolve()
requested = (base / url_path.lstrip('/')).resolve()
# 防止路径穿越:确保解析后的路径在 base 目录下
if not str(requested).startswith(str(base)):
raise ValueError("路径穿越攻击!")
return str(requested)
坑 2:文件路径有中文会报错 ❌ → ✅
import urllib.parse
def translate_path(self, path):
# URL 解码处理中文路径
path = urllib.parse.unquote(path)
# ...
坑 3:忘记以二进制模式打开文件 ❌ → ✅
# ❌ 错误:文本模式会在 Windows 上坏掉
with open(filepath, 'r') as f:
content = f.read()
# ✅ 正确:二进制模式
with open(filepath, 'rb') as f:
content = f.read()
坑 4:大文件直接 read() 爆内存 ❌ → ✅
# ❌ 错误:1G 文件直接读进内存
with open(filepath, 'rb') as f:
content = f.read() # 内存爆炸!
# ✅ 正确:流式传输
self.stream_file(filepath)
坑 5:静态文件服务器不要开 debug ❌ → ✅
# ❌ 线上环境开 debug 会暴露信息
Handler = http.server.SimpleHTTPRequestHandler
Handler.log_message = print # 不要这样!
# ✅ 生产环境用成熟的 nginx 做静态文件服务
性能小贴士:静态资源用 nginx
Python 的静态文件服务器适合开发调试,生产环境建议用 nginx:
server {
listen 80;
server_name example.com;
location /static/ {
alias /var/www/static/;
# nginx 会自动处理缓存、压缩、断点续传
expires 7d; # 缓存 7 天
gzip on; # 压缩传输
}
}
调试技巧:用 logging 记录请求
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(message)s'
)
class DebugHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self):
logging.info(f"请求: {self.path} 来自 {self.client_address}")
super().do_GET()
✏️ 练习题
练习 1(2 分钟):改端口
- 输入:把相册服务器的端口从 9000 改成 8888
- 预期输出:服务器启动信息显示 http://localhost:8888
- 提示:改一个数字就行
练习 2(3 分钟):限制文件类型
- 输入:在项目 1 基础上,只允许显示 .jpg 和 .png 文件
- 预期输出:访问 .txt 文件返回 403 禁止访问
- 提示:在 do_GET 里加个 if 判断后缀
练习 3(5 分钟):添加访问日志
- 输入:给项目 2 添加日志,每请求一个文件打印 "访问: filename"
- 预期输出:终端显示 访问: logo.png
- 提示:在 do_GET 开头加 print() 或 logging.info()
练习 4(10 分钟):组合缓存+断点续传
- 输入:把项目 2 的缓存控制和项目 3 的断点续传合并
- 预期输出:同时支持 ETag 缓存验证和 Range 请求
- 提示:两个项目的代码都要用到
练习 5(5 分钟):分析报错
- 输入:用户访问返回 "File not found",但文件明明存在
- 预期输出:说出可能原因并修复
- 提示:检查路径拼接和 translate_path 方法
作业:做一个「静态文件浏览器工具」
- 需求:实现一个带 Web 界面的文件浏览器,能浏览、切换目录、预览图片
- 功能点:
1. 显示当前目录的文件列表(名字、大小、修改时间)
2. 点击文件夹可以进入,点击文件可以预览(图片直接显示,其他文件显示内容或下载)
3. 顶部显示当前路径,可点击返回上级目录 - 加分项:
1. 支持按文件名搜索过滤
2. 显示图片缩略图 - 验收标准:能跑起来 + 目录切换正常 + 图片能预览
- 提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学到的 3 个核心点:
1. 静态文件服务 = 读取磁盘文件 + 返回给浏览器,Python 有现成的 SimpleHTTPRequestHandler
2. MIME 类型告诉浏览器怎么解析文件,用 mimetypes.guess_type() 自动识别
3. 大文件要用流式传输,缓存用 ETag + Cache-Control,断点续传用 Range 请求
延伸学习资源:
- Python官方文档:http.server模块 —— 最权威的参考
- 《HTTP权威指南》—— 理解 Web 底层必备
- MDN:HTTP缓存 —— 缓存机制详解
互动钩子:你在项目里用过静态文件服务吗?有没有遇到过缓存刷不掉或者文件找不到的坑?评论区聊聊,老粉优先回复!
📝 下章预告:学会了静态文件服务,但你发现原生
http.server太基础了——路由要自己写、错误处理要自己加……下一章我们来认识一个更强大的框架:Express,用它写 Web 服务就像搭积木一样简单!

评论(0)