Flask 0.1 源码解析

Dive into flask 0.1

Posted by Bryan on September 2, 2018

理解 Flask 0.1

为什么会挑选flask 0.1作为源码阅读的目标,主要原因在于简洁,可以比较清晰看到完整逻辑。还有一部分原因在于:整体的框架与思路与最新的代码都是一致的。因此,此次就以flask 0.1 源码作为介绍的目标,看看flask是如何实现优雅实现一个服务器的,具体注释的源码可以查看Flask 0.1

理解flask的源码需要一些基础,首先是需要python的基础,flask中无处不在的magic method,需要对此有一些概念;其次是wsgi(Web Server Gateway Interface),flask0.1 只有几百行代码,不可能实现一个完整的服务器,flask只是按照特定的wsgi协议,封装了服务器端核心的部分,让用户不需要关心服务器端具体协议,只需要关心应用代码即可。关于Wsgi的入门代码可以参考cizixs的博客;最后是需要实际使用flask,因为只有实际用过,才会理解flask的具体的操作,要不然就很难建立相关的基础概念。

完整流程

启动应用

使用过Flask的小伙伴们应该知道,启动Flask时的代码如下所示:

app = Flask(name)
app.run()

看起来第一步是创建Flask对象,第二步app.run()就是将对象运行起来,我们可以追踪进去,看看run()方法中究竟是什么

def run(self, host='localhost', port=5000, **options):
	from werkzeug import run_simple
   return run_simple(host, port, self, **options)

删除非核心的代码就只是运行了werkzeug中的run_simple() 方法,可以看到此方法将self作为参数传递,事实上,此方法将当前启动的对象作为网络请求的处理函数,后续所有的网络请求都会调用当前对象进行处理,调用的处理的代码为: application_iter = app(environ, start_response),其中的app就是run_simple中传递进去的self。因此会调用当前对象的__call__()方法进行请求的处理。

请求处理

def __call__(self, environ, start_response):
    return self.wsgi_app(environ, start_response)

可以看到网络请求实际执行的是wsgi_app()方法进行处理,那么我们再继续看看这个方法是如何处理的呢:

def wsgi_app(self, environ, start_response):
    with self.request_context(environ):
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
        response = self.make_response(rv)
        response = self.process_response(response)
        return response(environ, start_response)

在实际网络请求的处理方法中,首先执行preprocess_request()执行网络请求处理前的回调函数,然后执行dispatch_request()进行请求的处理,得到处理的结果rv,然后调用make_response()将返回值转换为Response类型,然后调用process_response()方法执行处理后的回调函数以及session处理,最后返回数据。因此我们需要关注的核心就是dispatch_request()方法,看看是如何处理网络请求的呢

def dispatch_request(self):
	endpoint, values = self.match_request()
   return self.view_functions[endpoint](**values)

去除异常处理后,实际代码就只有如下所示的两行,调用match_request()看起来是匹配请求,得到end_point与values,根据实际执行self.view_functions[endpoint](**values)推测,应该是通过匹配找到处理的方法self.view_functions[endpoint],然后调用处理请求的方法,并将请求对应的参数values传递进去,并最终返回请求处理的结果。

路由规则

那么回过头来思考,服务器怎么会知道请求应该由哪个方法来处理,比如发起一个创建任务的请求,服务器怎么知道调用哪个方法去处理。这种业务相关的事情,明显是需要业务方来指定,再联想到使用flask中我们经常会使用app.route()装饰器指定网络请求的处理,比如flask官网给出的最小的应用:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello_world():
    return 'Hello World!'

if __name__ == '__main__':
    app.run()

运行此任务后,访问127.0.0.1/ 会得到返回值’Hello world’,从这个例子中可以看到当服务器收到网络请求时,事实上是通过我们指定的处理方法进行处理。看起来的确是通过@app.route()绑定请求处理的方法,那么我们来看看是具体的代码:

def route(self, rule, **options):
    def decorator(f):
        self.add_url_rule(rule, f.__name__, **options)
        self.view_functions[f.__name__] = f
        return f
    return decorator
    
def add_url_rule(self, rule, endpoint, **options):
    options['endpoint'] = endpoint
    options.setdefault('methods', ('GET',))
    self.url_map.add(Rule(rule, **options))

代码是标准的装饰器写法,用来装饰特定的方法f,可以看到self.view_functions[f.__name__] = f了解到self.view_functions为dict类型,存储的是方法名与方法的映射关系,而根据上面的最小应用实例知道,rule就是对应的请求的url,从而推测出self.add_url_rule(rule, f.__name__, **options)此方法用于存储url到方法名的映射关系,查看add_url_rule实现的代码可以看到,flask中利用rule和endpoint构建成werkzeug中的Rule类型,然后存储为至url_map中,方便后续查找。

从而理顺思路,当请求到达时,我们可以根据url从url_map中获取到方法名(endpoint)与参数,然后根据方法名从self.view_functions中获取处理请求的方法,执行此方法用于处理网络请求。

按照这个思路我们去查看dispatch_request可以看到先调用match_request()方法得到处理的endpoint和values,而此endpoint就是上面说的方法名,values就是请求的参数,然后执行self.view_functions[endpoint](**values)获取处理函数并执行,从而验证了我们最初的推测。

url与endpoint的映射关系一般情况下的思路是直接采用普通dict存储,查找时就可以使用url作为key直接找到了。但是这种方式灵活性不足,而且合法的等价url可能无法找到对应的endpoint,可以看到flask中是将url与endpoint用于构建Rule类型并存储至url_map中,那么具体查找时是如何做的呢,具体的代码应该是在match_request()方法中

def match_request(self):
   rv = _request_ctx_stack.top.url_adapter.match()
   request.endpoint, request.view_args = rv
   return rv

通过上面的代码可以看到,最终是调用了请求的match()方法来获取到endpoint和参数,而调用者url_adapter = url_map.bind_to_environ(environ),也就是说实际获取endpoint与参数是通过调用url_map.bind_to_environ(environ).match()来获取的。通过前面的介绍我们已经知道,url_map中存储的是url与endpoint之间的映射关系,这种映射关系是通过@app.route()进行指定的。而environ为单次请求信息,内部包含请求的url。可以理解为存储信息的对象url_map绑定特定的请求信息environ,然后进行匹配match(),即可得到请求对应的endpoint和参数value。

请求信息

前面的介绍可以看到获取当前请求的信息是从 _request_ctx_stack.top中获取出来的,也就是说请求会被加入请求栈中,栈顶的就是当前请求。可以看一下这个请求栈_request_ctx_stack的定义:

_request_ctx_stack = LocalStack()

通过定义可以看到,确实是一个请求栈,而且是一个多线程隔离的请求中。关于LocalStack的具体信息可以参考之前的博客,在这边我们简单理解LocalStack就是一个多线程安全的栈,提供push,pop,top的方法。而栈中元素必然就是单个请求了,元素类型为_RequestContext

class _RequestContext(object):
    def __init__(self, app, environ):
        self.app = app
        self.url_adapter = app.url_map.bind_to_environ(environ)
        self.request = app.request_class(environ)
        self.session = app.open_session(self.request)
        self.g = _RequestGlobals()
        self.flashes = None

    def __enter__(self):
        _request_ctx_stack.push(self)

    def __exit__(self, exc_type, exc_value, tb):
        if tb is None or not self.app.debug:
            _request_ctx_stack.pop()

看到单个请求使用app和environ进行初始化,其中app就是Flask创建的实例,environ为单次请求具体信息。其中就包含url_adapter属性,前面已经介绍过,就是通过url_adapter.match()进行匹配后获取到endpoint和value的,从而获取到请求处理的方法的,从而与前面的解释相互印证。那么现在还剩下一个问题,flask是什么时候将_RequestContext加入到_request_ctx_stack中的呢?让我们回头看一下wsgi_app()方法,使用with进行调用:

def wsgi_app(self, environ, start_response):
    with self.request_context(environ):
        rv = self.preprocess_request()
        if rv is None:
            rv = self.dispatch_request()
        response = self.make_response(rv)
        response = self.process_response(response)

def request_context(self, environ):
    return _RequestContext(self, environ)

可以看到调用了request_context()方法,此方法创建了一个_RequestContext对象,然后使用with的调用方式,会执行_RequestContext__enter__()魔术方法,即会发现_request_ctx_stack.push(self),将创建的_RequestContext 加入请求栈_request_ctx_stack中,然后在执行处理结束的时候,执行__exit__()方法,将请求从请求栈中移除。至此,一切豁然开朗。

总结

在flask启动中,需要创建一个Flask对象,然后调用此对象的run()方法启动应用,在run()方法中,会将当前的Flask对象注册为网络请求处理对象,后续的网络请求都会调用当前对象的__call__()方法进行处理,而__call__()实际调用的是wsgi_app()方法进行处理。在处理请求时,会利用请求的信息environ创建单个请求对象_RequestContext,此对象会存储单个请求相关的所有信息,然后将请求对象加入到_request_ctx_stack请求栈中,在处理过程中,可以通过_request_ctx_stack.top获取本次请求对象。在请求对象_RequestContext 中存在url_adapter属性,此属性是存储了url到endpoint映射关系的url_map绑定请求信息environ后创建的对象,在处理请求中,可以调用url_adapter.match()方法得到当前请求对象的endpoint,默认情况下endpoint是处理请求的方法名,利用endpoint可以通过view_functions[endpoint]得到处理请求的具体方法, 此方法是提前通过@app.route()进行注册指定的。 执行处理方法即可得到请求处理的结果,将请求处理的结果转换为标准的Response对象,然后返回给客户端,就完成单次网络请求的处理。