深入理解flask框架(1):WSGI与路由
flask是一个小而美的微框架,主要依赖于Werkezug 和 Jinja2, Flask 只建立 Werkezug 和 Jinja2 的桥梁,前者实现一个合适的 WSGI 应用,后者处理模板。 Flask 也绑定了一些通用的标准库包,比如 logging 。其它所有一切取决于扩展。
本文主要分析了flask是在Werkezug基础上如何构建WSGI接口与路由系统的。
WSGI是什么?
WSGI的本质是一种约定,是Python web开发中 web服务器与web应用程序之间数据交互的约定。
网关协议的本质是为了解耦,实现web服务器和web应用程序的分离,WSGI就是一个支持WSGI的web服务器与Python web应用程序之间的约定。
为了支持WSGI,服务器需要做什么?
在一起写一个 Web 服务器(2)
中给出了一个支持WSGI的服务器实现,下面的代码以他为例。
一个WSGI服务器需要实现两个函数:
1.解析http请求,为应用程序提供environ字典
def get_environ(self): env = {} env['wsgi.version'] = (1, 0) env['wsgi.url_scheme'] = 'http' env['wsgi.input'] = StringIO.StringIO(self.request_data) env['wsgi.errors'] = sys.stderr env['wsgi.multithread'] = False env['wsgi.multiprocess'] = False env['wsgi.run_once'] = False env['REQUEST_METHOD'] = self.request_method # GET env['PATH_INFO'] = self.path # /hello env['SERVER_NAME'] = self.server_name # localhost env['SERVER_PORT'] = str(self.server_port) # 8888 return env
2.实现start response函数
def start_response(self, status, response_headers, exc_info=None): # Add necessary server headers server_headers = [ ('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'), ('Server', 'WSGIServer 0.2'), ] self.headers_set = [status, response_headers + server_headers] # To adhere to WSGI specification the start_response must return # a 'write' callable. We simplicity's sake we'll ignore that detail # for now. # return self.finish_response
WSGI服务器调用Python应用程序
这里展示了服务器与应用程序交互的过程:
1.从客户端获取到请求
2.通过get_env获得envir变量
3.调用应用程序,传入env和start_response函数,并获得响应
4.将响应返回给客户端
def handle_one_request(self): self.request_data = request_data = self.client_connection.recv(1024) print(''.join( '< {line}\n'.format(line=line) for line in request_data.splitlines() )) self.parse_request(request_data) env = self.get_environ() result = self.application(env, self.start_response)//调用应用程序 self.finish_response(result)
Python 应用程序需要做什么?
在上述这个过程中,Python应用程序需要做什么呢?
主要工作就是根据输入的environ字典信息生成相应的http报文返回给服务器。
from wsgiref.simple_server import make_server def simple_app(environ, start_response): status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [u"This is hello wsgi app".encode('utf8')] httpd = make_server('', 8000, simple_app) print "Serving on port 8000..." httpd.serve_forever()
flask中如何实现WSGI接口
1.通过__call__
方法将Flask对象变为可调用
def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`.""" return self.wsgi_app(environ, start_response)
2.实现wsgi_app函数处理web服务器转发的请求
def wsgi_app(self, environ, start_response): ctx = self.request_context(environ) error = None try: try: ctx.push() response = self.full_dispatch_request() except Exception as e: error = e response = self.handle_exception(e) except: error = sys.exc_info()[1] raise return response(environ, start_response) finally: if self.should_ignore_error(error): error = None ctx.auto_pop(error)
路由是什么?
在web开发中,路由是指根据url分配到对应的处理程序。
在上面的应用中,一个包含了url路径信息的environ对应一个视图函数,问题就在于他只能处理一个固定的路由,如果我们需要根据我们的需求自由的绑定url和视图函数,这就需要我们自己建立一个新的间接层,这就是web框架的路由系统。
flask的路由是如何实现的?
Flask类中支持路由功能的数据结构,在__init__
函数中初始化:
url_rule_class = Rule self.url_map = Map() self.view_functions = {}
Map和Rule是werkzeug中实现的映射类和路由类。
>>> m = Map([ ... # Static URLs ... Rule('/', endpoint='static/index'), ... Rule('/about', endpoint='static/about'), ... Rule('/help', endpoint='static/help'), ... # Knowledge Base ... Subdomain('kb', [ ... Rule('/', endpoint='kb/index'), ... Rule('/browse/', endpoint='kb/browse'), ... Rule('/browse/<int:id>/', endpoint='kb/browse'), ... Rule('/browse/<int:id>/<int:page>', endpoint='kb/browse') ... ]) ... ], default_subdomain='www')
这里我们注意到Map类先建立了url到endpoint的映射,这样做的目的是为了实现动态路由功能。
而view_functions是一个字典,它负责建立endpoint和视图函数之间的映射关系。
下面是一个小实验,证明我们所说的映射关系
>>> from flask import Flask >>> app = Flask(__name__) >>> @app.route('/') ... def index(): ... return "hello world" ... >>> app.url_map Map([<Rule '/' (HEAD, GET, OPTIONS) -> index>, <Rule '/static/<filename>' (HEAD, GET, OPTIONS) -> static>]) >>> app.view_functions {'index': <function index at 0x7f6ced14c840>, 'static': <bound method _PackageBoundObject.send_static_file of <Flask '__main__'>>}
这里我们可以看到从<Rule '/' (HEAD, GET, OPTIONS) -> index>,'index': <function index at 0x7f6ced14c840>
,通过endpoint这个中间量,我们让把路由和函数建立了映射关系。
要注意一下,为什么会有'/static/<filename>'
这个路由呢,这是应为在初始化时flask调用了add_url_rule函数做了如下绑定:
if self.has_static_folder: assert bool(static_host) == host_matching, 'Invalid static_host/host_matching combination' self.add_url_rule( self.static_url_path + '/<path:filename>', endpoint='static', host=static_host, view_func=self.send_static_file )
注册路由
在flask中注册路由有两种方式,一种是用route装饰器,如上所示,另一种是直接调用add_url_rule函数绑定视图类,但是本质上二者都是调用add_url_rule函数,下面我们来看一下add_url_rule函数的实现。
在Flask的add_url_rule函数很长,但是核心的代码为以下几行:
self.url_map.add(rule) rule = self.url_rule_class(rule, methods=methods, **options) self.view_functions[endpoint] = view_func
1.装饰器
def route(self, rule, **options): def decorator(f): endpoint = options.pop('endpoint', None) self.add_url_rule(rule, endpoint, f, **options) return f return decorator
2.视图类
class CounterAPI(MethodView): def get(self): return session.get('counter', 0) def post(self): session['counter'] = session.get('counter', 0) + 1 return 'OK' app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))
注册路由之后,flask就需要分发路由,调用相应的视图函数。
def dispatch_request(self): req = _request_ctx_stack.top.request if req.routing_exception is not None: self.raise_routing_exception(req) rule = req.url_rule if getattr(rule, 'provide_automatic_options', False) \ and req.method == 'OPTIONS': return self.make_default_options_response() return self.view_functions[rule.endpoint](**req.view_args)