简易版
python自带了SimpleHTTPServer
模块,可以快速拉起一个文件服务器,通过web方式即可对服务器上的文件进行下载
使用方式也很简单python -m SimpleHTTPServer 10080
然后浏览器里输入<server_ip>:10080
即可对服务器上的文件进行下载
进阶版
如果需要一些自定义的功能,比如:
- 文件下载完成后就关闭文件服务器,不仅能回收端口,还能避免忘记关闭的时候,服务器上的文件长时间暴露在外
- 需要携带简单的auth认证,否则返回401
这时候简易版的可能就无法满足需求了,因此我们需要对SimpleHTTPServer
进行二次开发,参考:
import sys
import socket
import random
import shutil
import mimetypes
import urllib
import string
from urlparse import urlparse
import cgi
import os
import tempfile
import requests
import re
import time
try:
import threading
except ImportError:
import dummy_threading as threading
import posixpath
import atexit
import BaseHTTPServer
import SocketServer
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
FILE_PREFIX='/data'
FILES='^/data|/data/abc|/data/bcd'
LOG_FILES='abc|bcd'
AUTH_KEY='auth'
SKIP_PATH='/favicon.ico'
LATEST_TIME=0
COPY_SIZE=64*1024
EVENT=None
class MyHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
# 参考SimpleHTTPServer
# 尝试改进1:从请求参数中提取auth,检查auth是否匹配,不匹配直接401
# 尝试改进2:进行文件拷贝的时候,更新LATEST_TIME,防止下载大文件时下载时间过长,server被主动关闭
# 尝试改进3:下载完成后EVENT进行通知,以便recycle线程去主动关闭server
def send_head(self, req_auth):
# 参考SimpleHTTPServer
# 尝试改进1:对正则不匹配的路径直接403返回
def list_directory(self, path, req_auth):
# 参考SimpleHTTPServer
# 尝试改进1:仅展示正则匹配上的那些目录
# 尝试改进2:每一个文件链接的URL上都要拼接auth参数,否则会401
def translate_path(self, path):
# 参考SimpleHTTPServer
def guess_type(self, path):
# 参考SimpleHTTPServer
class httpThread(threading.Thread):
def __init__(self, name, httpd):
threading.Thread.__init__(self)
self.name = name
self.httpd = httpd
def run(self):
print "Starting " + self.name
global LATEST_TIME
global EVENT
global INITED
EVENT = threading.Event()
LATEST_TIME=int(time.time())
INITED = True
self.httpd.serve_forever()
print "Exiting " + self.name
class recycleThread(threading.Thread):
def __init__(self, name, httpd, t1):
threading.Thread.__init__(self)
self.name = name
self.httpd = httpd
self.t1 = t1
def run(self):
print "Starting " + self.name
global LATEST_TIME
global EVENT
global INITED
while not INITED:
time.sleep(1)
print "Into loop"
print "Latest time: " + str(LATEST_TIME)
while not EVENT.is_set():
now=int(time.time())
print "Sub: " + str(now - LATEST_TIME)
if now - LATEST_TIME >= 15*60:
break
print "Waiting..."
EVENT.wait(60*1)
print "Outer loop"
self.httpd.shutdown()
self.t1.join()
self.httpd.server_close()
print "Exiting " + self.name
#find unuse port
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 0))
addr=(s.getsockname())
port=(addr[1])
s.close()
# random auth
slen=16
auth=''.join(random.sample(string.ascii_letters + string.digits, slen))
print "Url:" + str(port) + "/?auth=" + auth + ":lrU"
#path to list
web_dir=(sys.argv[1])
os.chdir(web_dir)
HandlerClass = MyHTTPRequestHandler
ServerClass = BaseHTTPServer.HTTPServer
Protocol = "HTTP/1.0"
server_address = ('0.0.0.0', port)
HandlerClass.protocol_version = Protocol
httpd = ServerClass(server_address, HandlerClass)
# new thread
t1=httpThread("Server", httpd)
t1.start()
t2=recycleThread("Recycle", httpd, t1)
t2.start()
上面主要参考SimpleHTTPServer
重新实现了一版满足需要的文件下载服务器,简要说明:
-
为了避免指定端口冲突,我们添加了找随机端口的代码逻辑
-
为了能够在脚本启动时能够指定需要下载的目录,我们添加了启动参数,然后list该目录
-
为了添加简单的auth认证,我们生成了一个随机字符串,必须携带此随机字符串才能当问
-
为了保护服务器上的某些目录,我们通过正则指定了一批目录,并在返回时候忽略这些目录,这样通过浏览器访问的时候就看不到这些目录了
-
为了能够在文件下载完成后,正常退出文件下载服务器,我们引入了两个线程,其中一个线程
httpThread
用于启动文件下载服务器;另一个线程用于关闭文件下载服务器,关闭场景包括:- 长时间没有操作的,将在15分钟后自动关闭文件下载服务器
- 文件下载完成后,自动关闭文件下载服务器
终极版
上面的进阶版,一般场景下就已经可以很好的工作了;但是在某些特殊场景下就有点小问题了;比如你想要把文件下载页面嵌入到你自己项目的前端页面里的时候,你会发现这时候在你的前端界面里点击的时候文件服务器响应非常慢,每次点击都要等一分多钟才会响应,很奇葩的问题;单独在浏览器里打开页面的时候就很流畅,直接curl 访问也很流畅,只有嵌入的页面里影响很慢
在一顿抓包分析之后,发现嵌入的页面里会同时建立两个TCP链接,由于浏览器的keep alive机制,这两个TCP链接都需要等待一段时间后才会断开;而上述进阶版的文件下载服务器是单线程模型的,如果同时建立两个TCP链接,两个TCP链接会互相阻塞,直到链接释放;
那么就需要我们再把文件服务器改成多线程模型的,好在python已经内置了两个:
- ServerClass = SocketServer.ThreadingTCPServer
- ServerClass = SocketServer.ForkingTCPServer
两个的区别在于,ForkingTCPServer
会为每个请求都单独建立一个进程来处理;而ThreadingTCPServer
会为每个请求单独建立一个线程来处理;
由于我们进阶版的实现中,需要线程之间共享变量的,因此这里我们只能选择ThreadingTCPServer
,而不能选择ForkingTCPServer
,因为在我们这个示例里进程之间是无法贡献变量的
网友评论