python 实现web框架simfish
本文主要记录本人利用python实现web框架simfish的过程。源码github地址:simfish
WSGI HTTP Server
wsgi模块提供了简单的simple_server,
wsgiref.simple_server.make_server(host, port, app, server_class=WSGIServer, handler_class=WSGIRequestHandler)
官方提供的例子,
from wsgiref.simple_server import make_server, demo_app httpd = make_server('', 8000, demo_app) print "Serving HTTP on port 8000..." # Respond to requests until process is killed httpd.serve_forever() # Alternative: serve one request, then exit httpd.handle_request()
访问http://127.0.0.1:8000来检查是否正常运行。
因此,有了wsgi的帮助,我们只需要实现我们自己的demo_app了。
demo_app
demo_app接受两个参数environ和start_response,其中environ包含包含所有cgi和wsgi的参数变量,start_response是一个函数,参数为status和headers,返回结果为列表或__iter__的可迭代实例。
def demo_app(environ,start_response): from StringIO import StringIO stdout = StringIO() print >>stdout, "Hello world!" print >>stdout h = environ.items(); h.sort() for k,v in h: print >>stdout, k,'=', repr(v) start_response("200 OK", [('Content-Type','text/plain')]) return [stdout.getvalue()]
实现自己的hello_app,替换demo_app即可。
def hello_app(environ, start_response): status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return ['Hello world! ']
更多的基础细节请参考:http://www.tuicool.com/articles/aYBRBz
simfish的实现
web框架其实并不复杂,仅仅负责请求的分发和处理,让web后台开发变得简单、规范的一种方法。
本框架主要参考了bottle和webpy的源码。
路由
使用过web框架的人对路由并不陌生,路由就是用来记录url和callback function的映射关系。
在simfish中实现了三种添加路由的方法(装饰器、一次加载、随时添加),源码如下(具体内容参考注释):
# Route class Routes: """FrameWork Routes""" ROUTES = {} #存储所有的url到callback function的映射 @classmethod def add(cls, url, handler): """add route and handler to ROUTES""" if not url.startswith('/'): url = '/' + url if re.match(r'^/(w+/)*w*$', url): #这里需要使用re模块来获取正确的url cls.ROUTES[url] = handler @classmethod def match(cls, url): #用来寻找url对象的处理函数 """match url in ROUTES""" if not url: return None url = url.strip() route = cls.ROUTES.get(url,None) #从ROUTES中查找结果 return route @classmethod def load_urls(cls, urls): #用于类似webpy中urls加载的方式 for item in urls: cls.add(item[0], item[1]) def route(url, **kargs): #这是一个装饰器,@route('/') """Decorator for request handler. Same as Routes.route(url, handler).""" def wrapper(handler): Routes.add(url, handler, **kargs) return handler return wrapper
具体使用方法参考:routing
封装request
在demo_app中的参数environ(它是一个字典啊!!!)中包含了request中需要的所有信息,那么我们需要把environ添加到request类中,
class Request(threading.local): """Represents a single request using thread-local namespace""" def bind(self, environ): """Bind the enviroment""" self._environ = environ
添加获取请求方方法的方法,使用@propery让method方法可以直接调用,注意保持方法的大写(GET/POST)
@property def method(self): """Returns the request method (GET,POST,PUT,DELETE,...)""" return self._environ.get('REQUEST_METHOD', 'GET').upper()
如果获取请求参数呢?在django中使用如下的方法,
request.GET.get('param', '') request.POST.get('param', '')
那么,我们需要把get和post的参数全部添加到一个字典中,在environ中"QUERY_STRING"包含了get的所有参数,而post的参数需要通过"wsgi.input"获取。
@property def GET(self): """Returns a dict with GET parameters.""" if self._GET is None: raw_dict = parse_qs(self.query_string, keep_blank_values=1) self._GET = {} for key, value in raw_dict.items(): if len(value) == 1: self._GET[key] = value[0] else: self._GET[key] = value return self._GET
其中,parse_qs是解析get参数的,推荐使用urlparse.parse_qs 和 urlparse.parse_qsl,目前cgi的已经废弃但保留是为了向后兼容。
与get请求不同之处,在post请求中需要调用cgi模块的FieldStorage来解析post请求参数。
raw_data = cgi.FieldStorage(fp=self._environ['wsgi.input'], environ=self._environ)
具体参考源码:simfish.py
封装response
在这里主要实现了response-header和response-status的封装。
class Response(threading.local): """Represents a single response using thread-local namespace.""" def bind(self): """Clears old data and creates a brand new Response object""" self.status = 200 self.header = HeaderDict() #继承dict的header类 self.header['Content-type'] = 'text/plain'
继承自threading.local可以保证每个每个线程拥有自己的request和response,并不会相互影响。
实现template
这里使用bottle默认的模板,使用方法参考:template
发送文件
文件的发送与普通的string返回并不相同。首先,需要判断文件的权限,
if not filename.startswith(root): response.status = 401 return "Access denied." if not os.path.exists(filename) or not os.path.isfile(filename): response.status = 404 return "File does not exist." if not os.access(filename, os.R_OK): response.status = 401 return "You do not have permission to access this file."
获取文件的类型,这里需要使用mimetypes模块,
if mimetype: response.header['Content-type'] = mimetype elif guessmime: guess = mimetypes.guess_type(filename)[0] if guess: response.header['Content-type'] = guess
最后返回文件对象
if mimetype == 'application/octet-stream' and "Content-Disposition" not in response.header: response.header["Content-Disposition"] = "attachment;filename=%s"%name elif 'Last-Modified' not in response.header: ts = time.gmtime(stats.st_mtime) ts = time.strftime("%a, %d %b %Y %H:%M:%S +0000", ts) response.header["Content-Length"] = stats.st_size response.header['Last-Modified'] = ts return open(filename, 'r')
跳转
# Redirect to another url def redirect(url, code=307): """ Aborts execution and causes a 307 redirect """ response.status = code response.header['Location'] = url raise SimFishException("")
异常处理
# Exceptions class SimFishException(Exception): """A base class for exception""" pass class HTTPError(SimFishException): """Jump out to error handler""" def __init__(self, status, text): self.output = text self.http_status = status def __str__(self): return self.output class BreakSimFish(SimFishException): """Jump out of execution""" def __init__(self, text): self.output = text
路由分发
在上面已经有了如何添加路由,接下来就要实现路由的分发。
class Simfish: def __init__(self, environ, start_response): self.environ = environ self.start = start_response request.bind(environ) #绑定request和response response.bind() def __iter__(self): path = request.path handler = Routes.match(path) #Routes类中的match方法,获取处理的callback function result = "" if not handler: response.status = 404 result = "not Found" else: try: result = handler(request) except SimFishException,output: #捕获异常情况 result = output if isinstance(result, tuple) and len(result) == 2: #返回(response_string, mimetype),自定义返回类型 response.header['Content-type'] = result[1] result = result[0] status = '%d %s' % (response.status, HTTP_CODES[response.status]) #获取返回status self.start(status, list(response.header.items())) #调用start_response if hasattr(result, 'read'): #用于返回文件 if 'wsgi.file_wrapper' in self.environ: return self.environ['wsgi.file_wrapper'](result) else: return iter(lambda: result.read(8192), '') return iter(lambda: result.read(8192), '') elif isinstance(result, basestring): return iter([result]) else: return iter(result)
实例
#!/usr/bin/env python # encoding:utf8 from simfish import application, route @route('/') def hello(request): return "hello world" app = application(port=8086) app.run()
参考
http://www.tuicool.com/articles/aYBRBz
http://www.bottlepy.org/docs/dev/index.html
http://webpy.org/