深入理解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)

相关推荐