哎呀,听到你想自己搞一个HTTP服务器来处理文件上传下载,我简直太兴奋了!这听起来像是一个大工程,对吧?其实吧,Python简直就是为这种任务而生的。它内置了极其强大的http模块,你根本不需要去装什么重型框架,就像你用一把瑞士军刀切水果一样,既精准又优雅。咱们今天就一步步把这事儿捋清楚,我会把那些让人头疼的跨域问题和断点续传的坑都给你填平。
你知道吗,在开始写代码之前,咱们得先明白几个核心概念。HTTP服务器说白了就是个快递员,客户端(浏览器或下载工具)发出请求,服务器接收并处理,然后返回数据。文件上传下载呢,就是快递员在搬运东西。但是呢,浏览器有个很严格的“同源策略”,简单说就是:如果你的服务器端口和页面端口不一样,浏览器就会竖起高高的围墙,挡住数据往来。这就是跨域问题的根源。别担心,咱们有办法把它拆掉。
好啦,废话不多说,咱们直接切入正题。我会先给你一个最基础的文件服务器模板,然后再一点点加入上传、断点续传和跨域处理的“神技”。你可以跟着我的步骤,亲手敲一遍代码,保证你能把这套东西彻底玩明白。
基础搭建:一个最简单的HTTP服务器
咱们先来个简单的。你要知道,Python的http.server模块里有两个核心类:BaseHTTPRequestHandler和HTTPServer。前者处理具体的请求逻辑,后者负责监听端口。咱们先写一个能响应GET请求、返回简单HTML页面的服务器,作为咱们的起点。
想象一下,你坐在电脑前,打开Python,创建一个叫server.py的文件。第一行,咱们导入必要的模块:http.server、socketserver和urllib.parse。这些是咱们的小帮手。然后,咱们定义一个处理类,继承自BaseHTTPRequestHandler。在这个类里,咱们主要关注两个方法:do_GET和do_POST。GET用来下载,POST用来上传。
在do_GET方法里,咱们要先解析用户请求的URL路径。比如用户输入了http://localhost:8080/index.html,咱们就要从路径中提取出index.html。然后,咱们去服务器的文件目录里找这个文件。如果找到了,咱们就用send_response告诉客户端“嘿,200 OK,文件在这里”,用send_header告诉客户端这是HTML类型,最后用end_headers结束头部,并用wfile.write把文件内容发送出去。如果没找到文件呢?咱们就得返回404错误,告诉客户端“抱歉,文件不存在”。
这个逻辑听起来是不是挺清晰的?咱们试着把它写成代码。你看,代码其实不长,但每行都有它的作用。
import http.server
import socketserver
import os
from urllib.parse import unquote
PORT = 8080
DIRECTORY = "./public" # 这是存放文件的目录
class SimpleHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
# 解析URL,去掉开头的斜杠,并处理可能的URL编码
path = unquote(self.path.strip('/'))
# 确保路径不超出目录范围,防止路径穿越攻击
if '..' in path or path.startswith('/'):
self.send_error(400, "Invalid path")
return
filepath = os.path.join(DIRECTORY, path)
if os.path.exists(filepath) and os.path.isfile(filepath):
with open(filepath, 'rb') as f:
self.send_response(200)
self.send_header('Content-Type', self.guess_content_type(filepath))
self.send_header('Content-Length', os.path.getsize(filepath))
self.end_headers()
self.wfile.write(f.read())
else:
self.send_error(404, "File not found")
def guess_content_type(self, path):
# 根据文件扩展名猜测MIME类型,返回对应的字符串
if path.endswith('.html'):
return 'text/html'
elif path.endswith('.css'):
return 'text/css'
elif path.endswith('.js'):
return 'application/javascript'
elif path.endswith('.png'):
return 'image/png'
elif path.endswith('.jpg') or path.endswith('.jpeg'):
return 'image/jpeg'
else:
return 'application/octet-stream'
with socketserver.TCPServer(("", PORT), SimpleHTTPRequestHandler) as httpd:
print(f"Serving at port {PORT}")
httpd.serve_forever()
你看,这段代码是不是相当简洁?guess_content_type方法只是简单地根据文件后缀返回对应的MIME类型,这是HTTP协议规定的一种“身份证”,告诉浏览器这个文件该用什么方式解析。比如HTML文件就用text/html,图片就用image/png。这样浏览器拿到文件后就能正确地显示它了。
当然,这段代码还有些小问题,比如没有处理跨域,也没有断点续传功能。咱们一步一步来,先把这个基础跑通。你运行这段代码,然后在浏览器访问http://localhost:8080/,看看能不能看到一个简单的页面或者文件列表。如果能看到,说明咱们的基础搭建已经成功了!
跨域问题:拆掉浏览器的那堵墙
现在,咱们来解决那个让人头疼的跨域问题。什么是跨域呢?简单说,就是当你的前端页面(比如一个HTML文件)和你的后端服务器(比如这个Python服务器)不在同一个“源”时,浏览器就会阻止数据交互。这个“源”包括协议、域名和端口。比如,你的页面在http://localhost:3000,而服务器在http://localhost:8080,虽然域名一样,但端口不同,这就是跨域了。
浏览器这么做是为了安全,防止恶意网站偷偷访问你的数据。但是,有时候咱们就是想跨域通信呢?比如咱们的前后端分离开发,或者咱们只是想从其他网站获取资源。这时候,咱们就需要一种机制来告诉浏览器:“嘿,这个跨域请求是合法的,允许它通过。”这个机制就是CORS,全称是Cross-Origin Resource Sharing(跨域资源共享)。
CORS的工作原理挺巧妙的。当浏览器发送一个跨域请求时,它会自动在请求头里加上一些额外的信息,比如Origin,告诉服务器“我来自哪里”。服务器收到请求后,可以检查一下这个Origin是否在允许的范围内。如果允许,服务器就在响应头里加上Access-Control-Allow-Origin,告诉浏览器“这个跨域请求是OK的”。浏览器看到这个头信息,就会放行。
在咱们的Python服务器里,实现CORS非常简单。咱们只需要在发送响应头的时候,加上一些特定的CORS头信息就行了。比如,咱们要允许所有来源的请求,就可以设置Access-Control-Allow-Origin: *。如果咱们只想允许特定的来源,就可以设置成具体的域名,比如Access-Control-Allow-Origin: http://localhost:3000。
除了这个头信息,咱们还要处理一些复杂的请求,比如使用DELETE、PUT方法的请求,或者携带自定义头信息的请求。这些请求会被浏览器称为“预检请求”(Preflight Request),它们会先发送一个OPTIONS请求给服务器,询问是否允许跨域。如果服务器回复允许,浏览器才会发送真正的请求。
所以,咱们得在do_OPTIONS方法里处理这种预检请求。咱们要回复一个200状态码,并且加上一些CORS头信息,告诉浏览器允许哪些方法、哪些头信息。
咱们来看看怎么修改咱们的代码。咱们可以在SimpleHTTPRequestHandler类里添加一些辅助方法来处理CORS。
class CORSHTTPRequestHandler(SimpleHTTPRequestHandler):
def set_cors_headers(self, origin='*'):
self.send_header('Access-Control-Allow-Origin', origin)
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
self.send_header('Access-Control-Max-Age', '86400')
def do_OPTIONS(self):
# 处理预检请求
self.send_response(200)
self.set_cors_headers()
self.end_headers()
def do_GET(self):
# 在原有GET逻辑基础上,添加CORS头
self.set_cors_headers()
super().do_GET()
def do_POST(self):
# 在原有POST逻辑基础上,添加CORS头
self.set_cors_headers()
super().do_POST()
你看,是不是很简单?咱们只是添加了几个方法,然后在原有的请求处理方法里调用了一下set_cors_headers。这样,每次响应都会带上CORS头信息,浏览器就会允许跨域请求了。
不过,这里有个小细节要注意。Access-Control-Allow-Origin可以设置为*,表示允许所有来源。但是,如果咱们的请求需要携带认证信息(比如Cookie或者Token),就不能设置为*了,必须设置为具体的来源。这时候,咱们就得根据请求头里的Origin来判断,动态设置Access-Control-Allow-Origin的值。
咱们来优化一下set_cors_headers方法,让它能动态设置Access-Control-Allow-Origin。
def set_cors_headers(self):
origin = self.headers.get('Origin', '*')
self.send_header('Access-Control-Allow-Origin', origin)
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization')
self.send_header('Access-Control-Max-Age', '86400')
这样,咱们就根据请求里的Origin来设置允许的源了。如果请求里没有Origin,就默认为*。
好啦,跨域问题就解决了。咱们现在可以试试,用另一个端口的网页来访问咱们的服务器,看看能不能成功获取数据。如果成功了,说明咱们的CORS配置是有效的!
文件上传:让数据“进”来
现在,咱们来处理文件上传。文件上传和下载有点像,但是方向相反。下载是从服务器读取文件发送给客户端,上传则是客户端发送文件给服务器,服务器接收并保存到本地。
在HTTP协议里,文件上传通常使用POST请求,并且数据格式是multipart/form-data。这种格式可以把文件数据和表单数据一起发送,类似于在邮件里同时发送文字附件和附件文件。
咱们的任务就是在do_POST方法里解析这种格式的数据,提取出文件内容,然后保存到服务器的指定目录里。
解析multipart/form-data有点复杂,因为数据里夹杂着边界字符串,用来分隔不同的字段。不过,Python的email模块里有一个Parser类,可以帮咱们解析这种格式的数据。咱们可以读取整个请求体,然后用Parser来解析。
咱们来看看怎么实现。
import email
from email import policy
from email.parser import BytesParser
class FileUploadHandler(CORSHTTPRequestHandler):
def do_POST(self):
content_length = int(self.headers['Content-Length'])
post_data = self.rfile.read(content_length)
# 解析multipart/form-data数据
msg = BytesParser(policy=policy.default).parsebytes(post_data)
# 遍历所有部分
for part in msg.walk():
if part.get_content_disposition() and part.get_filename():
# 这是一个文件上传部分
filename = part.get_filename()
# 清理文件名,防止路径穿越
filename = os.path.basename(filename)
if not filename:
continue
filepath = os.path.join(DIRECTORY, filename)
# 读取文件内容并保存
with open(filepath, 'wb') as f:
payload = part.get_payload(decode=True)
f.write(payload)
# 返回成功响应
self.send_response(200)
self.set_cors_headers()
self.send_header('Content-Type', 'text/plain')
self.end_headers()
self.wfile.write(f"File {filename} uploaded successfully.".encode('utf-8'))
return
# 如果没有找到文件,返回错误
self.send_error(400, "No file uploaded")
# 保留原有的do_GET和do_OPTIONS方法
你看,咱们在do_POST方法里,首先读取整个请求体,然后用BytesParser来解析。解析后的msg对象是一个邮件消息对象,咱们可以遍历它的所有部分。每个部分可能是一个文件或一个普通的表单字段。咱们通过get_content_disposition()和get_filename()来判断这个部分是不是文件。如果是文件,咱们就提取出文件名,然后读取文件内容,保存到服务器的DIRECTORY目录下。
这里有个小细节,咱们用了os.path.basename(filename)来清理文件名,防止路径穿越攻击。比如,如果用户上传的文件名是../../etc/passwd,咱们只取passwd部分,避免用户恶意写入系统文件。
另外,咱们还需要处理上传成功和失败的响应。如果上传成功,咱们返回200状态码,并且告诉客户端文件上传成功了。如果上传失败,咱们返回400错误。
现在,咱们有了一个能处理文件上传的服务器。你可以写一个简单的HTML页面,用一个表单来测试上传功能。
<!DOCTYPE html>
<html>
<body>
<form action="http://localhost:8080/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="submit" value="Upload" />
</form>
</body>
</html>
把这个HTML页面放到另一个端口,比如http://localhost:3000,然后打开它,选择一个文件,点击上传。看看咱们的服务器能不能成功接收并保存文件。如果成功了,说明咱们的上传功能也搞定了!
断点续传:让下载“慢”下来再“快”起来
现在,咱们来解决最后一个问题:断点续传。断点续传是个啥呢?简单来说,就是当你下载一个大文件时,如果网络断了或者你主动暂停了,下次下载可以从断开的地方继续,而不是重新开始。这就像你看视频时,网络卡了一下,你点一下继续,视频就能从断开的地方接着播放,而不是从头开始。
断点续传的原理其实挺巧妙的。HTTP协议里有一个Range请求头,用来告诉服务器“我只想要文件的某一部分”。比如,如果你想从文件的第1000字节开始下载,你可以发送一个请求,头信息里加上Range: bytes=1000-。服务器收到这个请求后,就会只发送从第1000字节开始的数据,并且返回一个206状态码,表示“部分成功”。
咱们来看看怎么在Python里实现断点续传。咱们需要修改do_GET方法,让它能处理Range请求头。
class ResumableHandler(FileUploadHandler):
def do_GET(self):
path = unquote(self.path.strip('/'))
if '..' in path or path.startswith('/'):
self.send_error(400, "Invalid path")
return
filepath = os.path.join(DIRECTORY, path)
if os.path.exists(filepath) and os.path.isfile(filepath):
file_size = os.path.getsize(filepath)
# 检查Range头
range_header = self.headers.get('Range')
if range_header:
# 解析Range头,比如"bytes=1000-"
try:
start = int(range_header.split('=')[1].split('-')[0])
end = range_header.split('=')[1].split('-')[1]
if end == '':
end = file_size - 1
else:
end = int(end)
except:
self.send_error(400, "Invalid range")
return
# 发送部分响应
self.send_response(206)
self.send_header('Content-Range', f"bytes {start}-{end}/{file_size}")
self.send_header('Accept-Ranges', 'bytes')
self.send_header('Content-Length', end - start + 1)
self.set_cors_headers()
self.send_header('Content-Type', self.guess_content_type(filepath))
self.end_headers()
with open(filepath, 'rb') as f:
f.seek(start)
self.wfile.write(f.read(end - start + 1))
else:
# 发送完整文件
self.send_response(200)
self.send_header('Content-Length', file_size)
self.send_header('Accept-Ranges', 'bytes')
self.set_cors_headers()
self.send_header('Content-Type', self.guess_content_type(filepath))
self.end_headers()
with open(filepath, 'rb') as f:
self.wfile.write(f.read())
else:
self.send_error(404, "File not found")
你看,咱们在do_GET方法里,首先检查Range头是否存在。如果存在,咱们就解析出起始和结束位置,然后只发送那一部分数据,并且返回206状态码。如果不存在Range头,咱们就发送完整文件,返回200状态码。
这里有个小细节,咱们发送了Accept-Ranges: bytes头信息,告诉客户端“我支持断点续传”。这样,客户端(比如浏览器或下载工具)就知道可以请求文件的某一部分了。
现在,咱们的服务器就支持断点续传了。你可以用浏览器的开发者工具,或者用curl命令来测试一下断点续传功能。
# 下载整个文件
curl -o file.zip http://localhost:8080/file.zip
# 从第1000字节开始下载
curl -H "Range: bytes=1000-" -o file.zip http://localhost:8080/file.zip
这样,你就能体验到断点续传的威力了。如果网络断了,你可以从断开的地方继续下载,而不是重新开始。
整合与优化:打造完美的HTTP服务器
好啦,咱们现在有了基础的文件服务器、跨域支持、文件上传和断点续传功能。但是,作为一个完整的HTTP服务器,咱们还得考虑一些优化和问题。
首先,咱们得考虑安全性。咱们的服务器得防止一些常见的攻击,比如路径穿越攻击、大文件攻击、恶意请求等。咱们已经在文件名清理和路径检查上做了一些工作,但还可以做得更细致。
比如,咱们可以设置一个文件大小上限,防止用户上传
